Commit 620c62bbd99d33af726272e89194d335bf56db66

Authored by xp.Huang
2 parents 2ec1faac caa1b75d

Merge branch 'f-dev' into 'main'

feat:设备列表新增TBox远程连接 和修改Teambition上的问题和修改设备配置相关

See merge request huang/yun-teng-iot-front!264
Showing 32 changed files with 1549 additions and 146 deletions
@@ -17,6 +17,7 @@ enum EDeviceConfigApi { @@ -17,6 +17,7 @@ enum EDeviceConfigApi {
17 DEVICE_CONFIG_EXPORT = '/deviceProfile/export', 17 DEVICE_CONFIG_EXPORT = '/deviceProfile/export',
18 DEVICE_CONFIG_IMPORT = '/deviceProfile/import', 18 DEVICE_CONFIG_IMPORT = '/deviceProfile/import',
19 SET_DEVICE_ISDEFAULT = '/deviceProfile', 19 SET_DEVICE_ISDEFAULT = '/deviceProfile',
  20 + FRP_API = '/frp/',
20 } 21 }
21 22
22 /** 23 /**
@@ -114,3 +115,18 @@ export const setDeviceProfileIsDefaultApi = (id: string, v, params?: {}) => { @@ -114,3 +115,18 @@ export const setDeviceProfileIsDefaultApi = (id: string, v, params?: {}) => {
114 } 115 }
115 ); 116 );
116 }; 117 };
  118 +
  119 +/**
  120 + * Frp内网穿透信息API
  121 + */
  122 +export const frpGetInfoApi = (proxyName: string) => {
  123 + return defHttp.get({
  124 + url: `${EDeviceConfigApi.FRP_API}${proxyName}`,
  125 + });
  126 +};
  127 +
  128 +export const frpPutInfoApi = (proxyName: string, enableRemote: number) => {
  129 + return defHttp.put({
  130 + url: `${EDeviceConfigApi.FRP_API}${proxyName}/${enableRemote}`,
  131 + });
  132 +};
@@ -60,7 +60,7 @@ @@ -60,7 +60,7 @@
60 }); 60 });
61 const emit = defineEmits(['expand']); 61 const emit = defineEmits(['expand']);
62 62
63 - const show = ref(true); 63 + const show = ref(false);
64 64
65 const { prefixCls } = useDesign('collapse-container'); 65 const { prefixCls } = useDesign('collapse-container');
66 66
@@ -9,5 +9,7 @@ export { useForm } from './src/hooks/useForm'; @@ -9,5 +9,7 @@ export { useForm } from './src/hooks/useForm';
9 export { default as ApiSelect } from './src/components/ApiSelect.vue'; 9 export { default as ApiSelect } from './src/components/ApiSelect.vue';
10 export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; 10 export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue';
11 export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'; 11 export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
  12 +export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
  13 +
12 14
13 export { BasicForm }; 15 export { BasicForm };
@@ -27,6 +27,7 @@ import { BasicUpload } from '/@/components/Upload'; @@ -27,6 +27,7 @@ import { BasicUpload } from '/@/components/Upload';
27 import { StrengthMeter } from '/@/components/StrengthMeter'; 27 import { StrengthMeter } from '/@/components/StrengthMeter';
28 import { IconPicker } from '/@/components/Icon'; 28 import { IconPicker } from '/@/components/Icon';
29 import { CountdownInput } from '/@/components/CountDown'; 29 import { CountdownInput } from '/@/components/CountDown';
  30 +import ApiRadioGroup from './components/ApiRadioGroup.vue';
30 //自定义组件 31 //自定义组件
31 import JAddInput from './jeecg/components/JAddInput.vue'; 32 import JAddInput from './jeecg/components/JAddInput.vue';
32 33
@@ -39,6 +40,7 @@ componentMap.set('InputSearch', Input.Search); @@ -39,6 +40,7 @@ componentMap.set('InputSearch', Input.Search);
39 componentMap.set('InputTextArea', Input.TextArea); 40 componentMap.set('InputTextArea', Input.TextArea);
40 componentMap.set('InputNumber', InputNumber); 41 componentMap.set('InputNumber', InputNumber);
41 componentMap.set('AutoComplete', AutoComplete); 42 componentMap.set('AutoComplete', AutoComplete);
  43 +componentMap.set('ApiRadioGroup', ApiRadioGroup);
42 44
43 componentMap.set('Select', Select); 45 componentMap.set('Select', Select);
44 componentMap.set('ApiSelect', ApiSelect); 46 componentMap.set('ApiSelect', ApiSelect);
  1 +<!--
  2 + * @Description:It is troublesome to implement radio button group in the form. So it is extracted independently as a separate component
  3 +-->
  4 +<template>
  5 + <RadioGroup v-bind="attrs" v-model:value="state" button-style="solid" @change="handleChange">
  6 + <template v-for="item in getOptions" :key="`${item.value}`">
  7 + <RadioButton v-if="props.isBtn" :value="item.value" :disabled="item.disabled">
  8 + {{ item.label }}
  9 + </RadioButton>
  10 + <Radio v-else :value="item.value" :disabled="item.disabled">
  11 + {{ item.label }}
  12 + </Radio>
  13 + </template>
  14 + </RadioGroup>
  15 +</template>
  16 +<script lang="ts">
  17 + import { defineComponent, PropType, ref, watchEffect, computed, unref, watch } from 'vue';
  18 + import { Radio } from 'ant-design-vue';
  19 + import { isFunction } from '/@/utils/is';
  20 + import { useRuleFormItem } from '/@/hooks/component/useFormItem';
  21 + import { useAttrs } from '/@/hooks/core/useAttrs';
  22 + import { propTypes } from '/@/utils/propTypes';
  23 + import { get, omit } from 'lodash-es';
  24 + import { useI18n } from '/@/hooks/web/useI18n';
  25 + type OptionsItem = { label: string; value: string | number | boolean; disabled?: boolean };
  26 +
  27 + export default defineComponent({
  28 + name: 'ApiRadioGroup',
  29 + components: {
  30 + RadioGroup: Radio.Group,
  31 + RadioButton: Radio.Button,
  32 + Radio,
  33 + },
  34 + props: {
  35 + api: {
  36 + type: Function as PropType<(arg?: Recordable | string) => Promise<OptionsItem[]>>,
  37 + default: null,
  38 + },
  39 + params: {
  40 + type: [Object, String] as PropType<Recordable | string>,
  41 + default: () => ({}),
  42 + },
  43 + value: {
  44 + type: [String, Number, Boolean] as PropType<string | number | boolean>,
  45 + },
  46 + isBtn: {
  47 + type: [Boolean] as PropType<boolean>,
  48 + default: false,
  49 + },
  50 + numberToString: propTypes.bool,
  51 + resultField: propTypes.string.def(''),
  52 + labelField: propTypes.string.def('label'),
  53 + valueField: propTypes.string.def('value'),
  54 + immediate: propTypes.bool.def(true),
  55 + },
  56 + emits: ['options-change', 'change'],
  57 + setup(props, { emit }) {
  58 + const options = ref<OptionsItem[]>([]);
  59 + const loading = ref(false);
  60 + const isFirstLoad = ref(true);
  61 + const emitData = ref<any[]>([]);
  62 + const attrs = useAttrs();
  63 + const { t } = useI18n();
  64 + // Embedded in the form, just use the hook binding to perform form verification
  65 + const [state] = useRuleFormItem(props);
  66 +
  67 + // Processing options value
  68 + const getOptions = computed(() => {
  69 + const { labelField, valueField, numberToString } = props;
  70 +
  71 + return unref(options).reduce((prev, next: Recordable) => {
  72 + if (next) {
  73 + const value = next[valueField];
  74 + prev.push({
  75 + label: next[labelField],
  76 + value: numberToString ? `${value}` : value,
  77 + ...omit(next, [labelField, valueField]),
  78 + });
  79 + }
  80 + return prev;
  81 + }, [] as OptionsItem[]);
  82 + });
  83 +
  84 + watchEffect(() => {
  85 + props.immediate && fetch();
  86 + });
  87 +
  88 + watch(
  89 + () => props.params,
  90 + () => {
  91 + !unref(isFirstLoad) && fetch();
  92 + },
  93 + { deep: true },
  94 + );
  95 +
  96 + async function fetch() {
  97 + const api = props.api;
  98 + if (!api || !isFunction(api)) return;
  99 + options.value = [];
  100 + try {
  101 + loading.value = true;
  102 + const res = await api(props.params);
  103 + if (Array.isArray(res)) {
  104 + options.value = res;
  105 + emitChange();
  106 + return;
  107 + }
  108 + if (props.resultField) {
  109 + options.value = get(res, props.resultField) || [];
  110 + }
  111 + emitChange();
  112 + } catch (error) {
  113 + console.warn(error);
  114 + } finally {
  115 + loading.value = false;
  116 + }
  117 + }
  118 +
  119 + function emitChange() {
  120 + emit('options-change', unref(getOptions));
  121 + }
  122 +
  123 + function handleChange(_, ...args) {
  124 + emitData.value = args;
  125 + }
  126 +
  127 + return { state, getOptions, attrs, loading, t, handleChange, props };
  128 + },
  129 + });
  130 +</script>
@@ -82,6 +82,7 @@ export interface ColEx { @@ -82,6 +82,7 @@ export interface ColEx {
82 82
83 export type ComponentType = 83 export type ComponentType =
84 | 'Input' 84 | 'Input'
  85 + | 'ApiRadioGroup'
85 | 'InputGroup' 86 | 'InputGroup'
86 | 'InputPassword' 87 | 'InputPassword'
87 | 'InputSearch' 88 | 'InputSearch'
@@ -77,7 +77,7 @@ @@ -77,7 +77,7 @@
77 } 77 }
78 78
79 let resStr = ''; 79 let resStr = '';
80 - let dirStr = isBefore ? t('component.time.before') : t('component.time.after'); 80 + let dirStr = isBefore ? t('component.time.after') : t('component.time.before');
81 81
82 if (diff < ONE_SECONDS) { 82 if (diff < ONE_SECONDS) {
83 resStr = t('component.time.just'); 83 resStr = t('component.time.just');
@@ -477,14 +477,14 @@ @@ -477,14 +477,14 @@
477 cursor: pointer; 477 cursor: pointer;
478 position: absolute; 478 position: absolute;
479 top: 0.85rem; 479 top: 0.85rem;
480 - left: 1.1rem; 480 + left: 1.1vw;
481 } 481 }
482 .fold-right { 482 .fold-right {
483 z-index: 1; 483 z-index: 1;
484 cursor: pointer; 484 cursor: pointer;
485 position: absolute; 485 position: absolute;
486 top: 0.85rem; 486 top: 0.85rem;
487 - left: 21.6rem; 487 + left: 18.2vw;
488 } 488 }
489 @prefix-cls: ~'@{namespace}-basic-tree'; 489 @prefix-cls: ~'@{namespace}-basic-tree';
490 490
@@ -9,9 +9,7 @@ @@ -9,9 +9,7 @@
9 import { useMessage } from '/@/hooks/web/useMessage'; 9 import { useMessage } from '/@/hooks/web/useMessage';
10 import { PlayProtocol } from '../manage/config.data'; 10 import { PlayProtocol } from '../manage/config.data';
11 11
12 - const emit = defineEmits({  
13 - success: null,  
14 - }); 12 + const emit = defineEmits(['success','register']);
15 13
16 const createFlag = ref(false); 14 const createFlag = ref(false);
17 15
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 import { ref } from 'vue'; 15 import { ref } from 'vue';
16 import { useMessage } from '/@/hooks/web/useMessage'; 16 import { useMessage } from '/@/hooks/web/useMessage';
17 import { Popconfirm } from 'ant-design-vue'; 17 import { Popconfirm } from 'ant-design-vue';
  18 + import { Authority } from '/@/components/Authority';
18 19
19 const enabledBatchDelete = ref(true); 20 const enabledBatchDelete = ref(true);
20 const [register, { reload, getSelectRowKeys }] = useTable({ 21 const [register, { reload, getSelectRowKeys }] = useTable({
@@ -51,23 +51,21 @@ export const step1Schemas: FormSchema[] = [ @@ -51,23 +51,21 @@ export const step1Schemas: FormSchema[] = [
51 }, 51 },
52 }, 52 },
53 { 53 {
54 - field: 'field7',  
55 - component: 'RadioGroup', 54 + field: 'brand',
  55 + component: 'ApiRadioGroup',
56 label: '选择厂家', 56 label: '选择厂家',
  57 + required: true,
57 colProps: { 58 colProps: {
58 span: 8, 59 span: 8,
59 }, 60 },
  61 + defaultValue: 'DIY_',
60 componentProps: { 62 componentProps: {
61 - options: [  
62 - {  
63 - label: '自定义厂家',  
64 - value: '1',  
65 - },  
66 - {  
67 - label: 'TBox边缘网关',  
68 - value: '2',  
69 - },  
70 - ], 63 + api: findDictItemByCode,
  64 + params: {
  65 + dictCode: 'DEVICE_BRAND_GATEWAY',
  66 + },
  67 + labelField: 'itemText',
  68 + valueField: 'itemValue',
71 }, 69 },
72 ifShow: ({ values }) => isGateWay(values.deviceType), 70 ifShow: ({ values }) => isGateWay(values.deviceType),
73 }, 71 },
@@ -22,6 +22,15 @@ @@ -22,6 +22,15 @@
22 <TabPane key="4" tab="子设备" v-if="deviceDetail?.deviceType === 'GATEWAY'"> 22 <TabPane key="4" tab="子设备" v-if="deviceDetail?.deviceType === 'GATEWAY'">
23 <ChildDevice :fromId="deviceDetail?.tbDeviceId" /> 23 <ChildDevice :fromId="deviceDetail?.tbDeviceId" />
24 </TabPane> 24 </TabPane>
  25 + <!-- 网关设备并且场家是TBox -->
  26 + <TabPane
  27 + key="6"
  28 + tab="TBox"
  29 + v-if="deviceDetail?.deviceType === 'GATEWAY' && deviceDetail?.brand == 'TBox'"
  30 + >
  31 + <TBoxDetail :deviceDetail="deviceDetail" />
  32 + </TabPane>
  33 + <!-- 网关设备并且是TBox -->
25 </Tabs> 34 </Tabs>
26 </BasicDrawer> 35 </BasicDrawer>
27 </template> 36 </template>
@@ -34,6 +43,7 @@ @@ -34,6 +43,7 @@
34 import RealTimeData from '../tabs/RealTimeData.vue'; 43 import RealTimeData from '../tabs/RealTimeData.vue';
35 import Alarm from '../tabs/Alarm.vue'; 44 import Alarm from '../tabs/Alarm.vue';
36 import ChildDevice from '../tabs/ChildDevice.vue'; 45 import ChildDevice from '../tabs/ChildDevice.vue';
  46 + import TBoxDetail from '../tabs/TBoxDetail.vue';
37 import CommandIssuance from '../tabs/CommandIssuance.vue'; 47 import CommandIssuance from '../tabs/CommandIssuance.vue';
38 import { getDeviceDetail } from '/@/api/device/deviceManager'; 48 import { getDeviceDetail } from '/@/api/device/deviceManager';
39 export default defineComponent({ 49 export default defineComponent({
@@ -47,6 +57,7 @@ @@ -47,6 +57,7 @@
47 Alarm, 57 Alarm,
48 ChildDevice, 58 ChildDevice,
49 CommandIssuance, 59 CommandIssuance,
  60 + TBoxDetail,
50 }, 61 },
51 emits: ['reload', 'register'], 62 emits: ['reload', 'register'],
52 setup() { 63 setup() {
@@ -53,12 +53,6 @@ @@ -53,12 +53,6 @@
53 <a-button type="primary" class="mr-4" @click="copyTbDeviceId">复制设备ID</a-button> 53 <a-button type="primary" class="mr-4" @click="copyTbDeviceId">复制设备ID</a-button>
54 <a-button type="primary" class="mr-4" @click="copyDeviceToken">复制访问令牌</a-button> 54 <a-button type="primary" class="mr-4" @click="copyDeviceToken">复制访问令牌</a-button>
55 <a-button type="primary" class="mr-4" @click="manageDeviceToken">管理设备凭证</a-button> 55 <a-button type="primary" class="mr-4" @click="manageDeviceToken">管理设备凭证</a-button>
56 - <a-button  
57 - type="primary"  
58 - v-if="deviceDetail.deviceType == 'GATEWAY'"  
59 - @click="remoteConnectiondGateway"  
60 - >远程连接边缘网关</a-button  
61 - >  
62 <ManageDeviceTokenModal @register="registerModal" /> 56 <ManageDeviceTokenModal @register="registerModal" />
63 </div> 57 </div>
64 <div v-if="deviceDetail?.deviceInfo?.address" class="mt-4"> 58 <div v-if="deviceDetail?.deviceInfo?.address" class="mt-4">
  1 +<template>
  2 + <div>
  3 + <BasicTable @register="registerTable" :dataSource="tableData">
  4 + <template #status>
  5 + <Switch
  6 + checked-children="开"
  7 + un-checked-children="关"
  8 + v-model:checked="checked"
  9 + @change="handleChange"
  10 + :disabled="enableRemoteDisabled"
  11 + />
  12 + </template>
  13 + </BasicTable>
  14 + <div style="margin-top: -54vh">
  15 + <a-button type="primary" class="mr-4" :disabled="disabled" @click="handleFrpRemote"
  16 + >远程连接</a-button
  17 + >
  18 + </div>
  19 + </div>
  20 +</template>
  21 +
  22 +<script setup lang="ts">
  23 + import { ref, nextTick } from 'vue';
  24 + import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
  25 + import { Switch } from 'ant-design-vue';
  26 + import { h } from 'vue';
  27 + import { Tag } from 'ant-design-vue';
  28 + import { frpGetInfoApi, frpPutInfoApi } from '/@/api/device/deviceConfigApi';
  29 + import { useMessage } from '/@/hooks/web/useMessage';
  30 + import { ConsoleSqlOutlined } from '@ant-design/icons-vue';
  31 +
  32 + const props = defineProps({
  33 + deviceDetail: {
  34 + type: Object,
  35 + required: true,
  36 + },
  37 + });
  38 +
  39 + const { createMessage } = useMessage();
  40 + const tableData: any = ref([]);
  41 + const checked = ref<boolean>(false);
  42 + const disabled = ref<boolean>(true);
  43 + const viewDeviceColumn: BasicColumn[] = [
  44 + {
  45 + title: 'Frp连接状态',
  46 + dataIndex: 'status',
  47 + width: 80,
  48 + customRender: ({ record }) => {
  49 + const status = record.status;
  50 + const color = status == 1 ? 'green' : 'red';
  51 + const text = status == 1 ? '在线' : '离线';
  52 + return h(Tag, { color }, () => text);
  53 + },
  54 + },
  55 + {
  56 + title: '远程连接',
  57 + dataIndex: 'enableRemote',
  58 + width: 80,
  59 + slots: { customRender: 'status' },
  60 + },
  61 + {
  62 + title: '设备SN',
  63 + dataIndex: 'proxyName',
  64 + width: 120,
  65 + },
  66 + ];
  67 + const [registerTable] = useTable({
  68 + title: 'TBox信息',
  69 + columns: viewDeviceColumn,
  70 + showIndexColumn: false,
  71 + clickToRowSelect: false,
  72 + showTableSetting: false,
  73 + bordered: true,
  74 + });
  75 + const enableRemote = ref(0);
  76 + const proxyName = ref('');
  77 + const remotePort = ref(0);
  78 + const address = ref('');
  79 + const enableRemoteDisabled = ref(false);
  80 + const getTableData = async () => {
  81 + // const res = await frpGetInfoApi('1000000061664FF');
  82 + const res = await frpGetInfoApi(props.deviceDetail.sn);
  83 + enableRemote.value = res.enableRemote;
  84 + proxyName.value = res.proxyName;
  85 + remotePort.value = res.remotePort;
  86 + address.value = res.address;
  87 + if (res.enableRemote == 1) {
  88 + checked.value = true;
  89 + disabled.value = false;
  90 + enableRemoteDisabled.value = true;
  91 + } else {
  92 + checked.value = false;
  93 + disabled.value = true;
  94 + enableRemoteDisabled.value = false;
  95 + }
  96 + if (res.status == 0) {
  97 + enableRemoteDisabled.value = true;
  98 + } else {
  99 + enableRemoteDisabled.value = false;
  100 + }
  101 + tableData.value.push({
  102 + enableRemote: res.enableRemote,
  103 + proxyName: res.proxyName,
  104 + status: res.status,
  105 + });
  106 + if (res == '') {
  107 + enableRemoteDisabled.value = true;
  108 + disabled.value = true;
  109 + }
  110 + };
  111 + const handleFrpRemote = async () => {
  112 + window.open(`${address.value}`);
  113 + };
  114 + nextTick(() => {
  115 + getTableData();
  116 + });
  117 + const handleChange = async (e) => {
  118 + if (e) {
  119 + disabled.value = false;
  120 + enableRemote.value = 1;
  121 + const res = await frpPutInfoApi(proxyName.value, enableRemote.value);
  122 + if (res.enableRemote == 1) {
  123 + createMessage.success('开启成功');
  124 + checked.value = true;
  125 + }
  126 + } else {
  127 + disabled.value = true;
  128 + enableRemote.value = 0;
  129 + const res = await frpPutInfoApi(proxyName.value, enableRemote.value);
  130 + if (res.enableRemote == 0) {
  131 + createMessage.success('关闭成功');
  132 + checked.value = false;
  133 + }
  134 + }
  135 + };
  136 +</script>
  137 +<style lang="less" scoped></style>
@@ -214,45 +214,49 @@ @@ -214,45 +214,49 @@
214 if (isUpdate.value == 1) { 214 if (isUpdate.value == 1) {
215 delete postDeviceConfogData.id; 215 delete postDeviceConfogData.id;
216 } 216 }
217 - //1 TODO 待解决OID对象唯一标识不能重复问题验证  
218 - let isMappings = false;  
219 - // let isQuerying = false; 217 + let isMappingsKey = ref(false);
  218 + let isMappingValue = ref(false);
  219 + let isQueryings = ref(false);
  220 + let isOIDRepet = ref(false);
220 postDeviceConfogData?.profileData?.transportConfiguration?.communicationConfigs?.forEach( 221 postDeviceConfogData?.profileData?.transportConfiguration?.communicationConfigs?.forEach(
221 - (f) => {  
222 - // if (f.spec == 'TELEMETRY_QUERYING' && f.queryingFrequencyMs == null) {  
223 - // isQuerying = true;  
224 - // } else {  
225 - // isQuerying = false;  
226 - // }  
227 - // if (f.spec == 'CLIENT_ATTRIBUTES_QUERYING' && f.queryingFrequencyMs == null) {  
228 - // isQuerying = true;  
229 - // } else {  
230 - // isQuerying = false;  
231 - // } 222 + (f: any) => {
  223 + if (f.spec == 'TELEMETRY_QUERYING' || f.spec == 'CLIENT_ATTRIBUTES_QUERYING') {
  224 + if (f.queryingFrequencyMs == null) {
  225 + isQueryings.value = true;
  226 + return createMessage.error('请填写查询频率');
  227 + } else {
  228 + isQueryings.value = false;
  229 + }
  230 + }
232 if (f.mappings.length == 0) { 231 if (f.mappings.length == 0) {
233 - isMappings = true; 232 + isMappingsKey.value = true;
  233 + isMappingValue.value = true;
234 } else { 234 } else {
235 f.mappings.forEach((f1) => { 235 f.mappings.forEach((f1) => {
236 const findNoneKey = Object.keys(f1).includes(''); 236 const findNoneKey = Object.keys(f1).includes('');
237 if (findNoneKey) { 237 if (findNoneKey) {
238 - isMappings = findNoneKey;  
239 - } else {  
240 - isMappings = false; 238 + isMappingsKey.value = true;
241 } 239 }
242 }); 240 });
243 f.mappings.forEach((f2) => { 241 f.mappings.forEach((f2) => {
244 const findNoneVal = Object.values(f2).includes(''); 242 const findNoneVal = Object.values(f2).includes('');
245 if (findNoneVal) { 243 if (findNoneVal) {
246 - isMappings = findNoneVal;  
247 - } else {  
248 - isMappings = false; 244 + isMappingValue.value = true;
  245 + }
  246 + });
  247 + //新增OID不能重复=====同一层级的OID不能重复
  248 + f.mappings.forEach((item, _) => {
  249 + if (f.mappings.some((citem) => citem !== item && citem.oid === item.oid)) {
  250 + isOIDRepet.value = true;
249 } 251 }
250 }); 252 });
251 } 253 }
252 } 254 }
253 ); 255 );
254 - // if (isQuerying) return createMessage.error('请填写Querying frequency,ms');  
255 - if (isMappings) return createMessage.error('请填写Date key和OID'); 256 + if (isQueryings.value) return createMessage.error('请填写查询频率');
  257 + if (isMappingsKey.value) return createMessage.error('请填写Date key和OID');
  258 + if (isMappingValue.value) return createMessage.error('请填写Date key和OID');
  259 + if (isOIDRepet.value) return createMessage.error('OID不能重复');
256 deviceConfigAddOrEdit(isEdit.value ? noEditObj : postDeviceConfogData) 260 deviceConfigAddOrEdit(isEdit.value ? noEditObj : postDeviceConfogData)
257 .then((res) => { 261 .then((res) => {
258 if (!res) return; 262 if (!res) return;
@@ -32,7 +32,7 @@ export const CoapSchemas: FormSchema[] = [ @@ -32,7 +32,7 @@ export const CoapSchemas: FormSchema[] = [
32 { 32 {
33 field: 'coapDeviceType', 33 field: 'coapDeviceType',
34 component: 'Select', 34 component: 'Select',
35 - label: 'CoAP 设备类型', 35 + label: 'CoAP设备类型',
36 defaultValue: 'DEFAULT', 36 defaultValue: 'DEFAULT',
37 componentProps: { 37 componentProps: {
38 options: [ 38 options: [
@@ -45,7 +45,7 @@ export const CoapSchemas: FormSchema[] = [ @@ -45,7 +45,7 @@ export const CoapSchemas: FormSchema[] = [
45 { 45 {
46 field: 'transportPayloadType', 46 field: 'transportPayloadType',
47 component: 'Select', 47 component: 'Select',
48 - label: 'CoAP 设备消息 Payload', 48 + label: 'CoAP设备消息Payload',
49 defaultValue: 'JSON', 49 defaultValue: 'JSON',
50 componentProps: { 50 componentProps: {
51 options: [ 51 options: [
@@ -59,7 +59,7 @@ export const CoapSchemas: FormSchema[] = [ @@ -59,7 +59,7 @@ export const CoapSchemas: FormSchema[] = [
59 { 59 {
60 field: 'powerMode', 60 field: 'powerMode',
61 component: 'Select', 61 component: 'Select',
62 - label: 'Power Saving Mode', 62 + label: '节能模式',
63 defaultValue: 'DRX', 63 defaultValue: 'DRX',
64 componentProps: { 64 componentProps: {
65 options: [ 65 options: [
@@ -76,11 +76,11 @@ export const CoapSchemas: FormSchema[] = [ @@ -76,11 +76,11 @@ export const CoapSchemas: FormSchema[] = [
76 { 76 {
77 field: 'psmActivityTimer', 77 field: 'psmActivityTimer',
78 component: 'InputNumber', 78 component: 'InputNumber',
79 - label: 'PSM Activity Timer', 79 + label: 'PSM活动计时器',
80 required: true, 80 required: true,
81 - defaultValue: '10', 81 + defaultValue: 10,
82 componentProps: { 82 componentProps: {
83 - placeholder: '请输入PSM Activity Timer', 83 + placeholder: '请输入PSM活动计时器',
84 }, 84 },
85 colProps: { span: 11 }, 85 colProps: { span: 11 },
86 ifShow: ({ values }) => isPsm(values.powerMode), 86 ifShow: ({ values }) => isPsm(values.powerMode),
@@ -104,11 +104,11 @@ export const CoapSchemas: FormSchema[] = [ @@ -104,11 +104,11 @@ export const CoapSchemas: FormSchema[] = [
104 { 104 {
105 field: 'edrxCycle', 105 field: 'edrxCycle',
106 component: 'InputNumber', 106 component: 'InputNumber',
107 - label: 'eDRX cycle', 107 + label: 'eDRX循环',
108 required: true, 108 required: true,
109 - defaultValue: '81', 109 + defaultValue: 81,
110 componentProps: { 110 componentProps: {
111 - placeholder: '请输入PSM Activity Timer', 111 + placeholder: '请输入eDRX循环',
112 }, 112 },
113 colProps: { span: 11 }, 113 colProps: { span: 11 },
114 ifShow: ({ values }) => isDrx(values.powerMode), 114 ifShow: ({ values }) => isDrx(values.powerMode),
@@ -132,11 +132,11 @@ export const CoapSchemas: FormSchema[] = [ @@ -132,11 +132,11 @@ export const CoapSchemas: FormSchema[] = [
132 { 132 {
133 field: 'pagingTransmissionWindow', 133 field: 'pagingTransmissionWindow',
134 component: 'InputNumber', 134 component: 'InputNumber',
135 - label: 'Paging Transmission Window', 135 + label: '寻呼传输窗口',
136 required: true, 136 required: true,
137 - defaultValue: '10', 137 + defaultValue: 10,
138 componentProps: { 138 componentProps: {
139 - placeholder: '请输入Paging Transmission Window', 139 + placeholder: '请输入寻呼传输窗口',
140 }, 140 },
141 colProps: { span: 11 }, 141 colProps: { span: 11 },
142 ifShow: ({ values }) => isDrx(values.powerMode), 142 ifShow: ({ values }) => isDrx(values.powerMode),
  1 +<template>
  2 + <div>
  3 + <BasicModal
  4 + v-bind="$attrs"
  5 + width="30rem"
  6 + :height="heightNum"
  7 + @register="register"
  8 + title="Add new server config"
  9 + @cancel="handleCancel"
  10 + :showOkBtn="true"
  11 + @ok="handleSubmit"
  12 + v-model:visible="visible"
  13 + >
  14 + <div style="display: flex; align-items: center; justify-content: center">
  15 + <h2>Server type:</h2>
  16 + <Select
  17 + v-model:value="selectValue"
  18 + style="width: 340px"
  19 + :options="selectOptions"
  20 + @change="emitChange"
  21 + />
  22 + </div>
  23 + </BasicModal>
  24 + </div>
  25 +</template>
  26 +<script setup lang="ts">
  27 + import { ref } from 'vue';
  28 + import { BasicModal, useModalInner } from '/@/components/Modal';
  29 + import { Select } from 'ant-design-vue';
  30 + import { SelectTypes } from 'ant-design-vue/es/select';
  31 +
  32 + const emit = defineEmits(['register', 'emitSelect']);
  33 + const [register] = useModalInner((data) => {
  34 + console.log(data);
  35 + });
  36 + const heightNum = ref(80);
  37 + const visible = ref(false);
  38 + const selectValue = ref('LwM2M');
  39 + const selectOptions = ref<SelectTypes['options']>([
  40 + {
  41 + label: 'LwM2M Server',
  42 + value: 'LwM2M',
  43 + },
  44 + {
  45 + label: 'Bootstrap Server',
  46 + value: 'Bootstrap',
  47 + },
  48 + ]);
  49 + const emitChange = (e) => {
  50 + selectValue.value = e;
  51 + };
  52 + const handleSubmit = () => {
  53 + emit('emitSelect', selectValue.value);
  54 + handleCancel();
  55 + };
  56 +
  57 + const handleCancel = () => {
  58 + visible.value = false;
  59 + };
  60 +</script>
  61 +<style lang="less" scoped></style>
@@ -236,7 +236,6 @@ export const deviceSchemas: FormSchema[] = [ @@ -236,7 +236,6 @@ export const deviceSchemas: FormSchema[] = [
236 colProps: { span: 22 }, 236 colProps: { span: 22 },
237 component: 'InputTextArea', 237 component: 'InputTextArea',
238 componentProps: { 238 componentProps: {
239 - disabled: true,  
240 autoSize: { 239 autoSize: {
241 maxRows: 50, 240 maxRows: 50,
242 }, 241 },
@@ -250,45 +249,31 @@ export const deviceSchemas: FormSchema[] = [ @@ -250,45 +249,31 @@ export const deviceSchemas: FormSchema[] = [
250 "keyName": {}, 249 "keyName": {},
251 "attributeLwm2m": {} 250 "attributeLwm2m": {}
252 }, 251 },
253 - "bootstrap": {  
254 - "servers": {  
255 - "binding": "UQ",  
256 - "shortId": 123,  
257 - "lifetime": 300,  
258 - "notifIfDisabled": true,  
259 - "defaultMinPeriod": 1  
260 - },  
261 - "bootstrapServer": {  
262 - "bootstrapServerIs": true,  
263 - "host": "0.0.0.0",  
264 - "port": 5687,  
265 - "securityHost": "0.0.0.0",  
266 - "securityPort": 5688,  
267 - "serverId": 111,  
268 - "clientHoldOffTime": 1,  
269 - "serverPublicKey": "",  
270 - "bootstrapServerAccountTimeout": 0  
271 - },  
272 - "lwm2mServer": { 252 + "bootstrap": [
  253 + {
  254 + "shortServerId": 123,
273 "bootstrapServerIs": false, 255 "bootstrapServerIs": false,
274 "host": "0.0.0.0", 256 "host": "0.0.0.0",
275 "port": 5685, 257 "port": 5685,
276 - "securityHost": "0.0.0.0",  
277 - "securityPort": 5686,  
278 - "serverId": 123,  
279 "clientHoldOffTime": 1, 258 "clientHoldOffTime": 1,
280 "serverPublicKey": "", 259 "serverPublicKey": "",
281 - "bootstrapServerAccountTimeout": 0 260 + "serverCertificate": "",
  261 + "bootstrapServerAccountTimeout": 0,
  262 + "lifetime": 300,
  263 + "defaultMinPeriod": 1,
  264 + "notifIfDisabled": true,
  265 + "binding": "U",
  266 + "securityMode": "NO_SEC"
282 } 267 }
283 - }, 268 + ],
284 "clientLwM2mSettings": { 269 "clientLwM2mSettings": {
285 "clientOnlyObserveAfterConnect": 1, 270 "clientOnlyObserveAfterConnect": 1,
286 "fwUpdateStrategy": 1, 271 "fwUpdateStrategy": 1,
287 - "swUpdateStrategy": 2,  
288 - "swUpdateResource": "coap://localhost:5685", 272 + "swUpdateStrategy": 1,
289 "powerMode": "DRX", 273 "powerMode": "DRX",
290 "compositeOperationsSupport": false 274 "compositeOperationsSupport": false
291 }, 275 },
  276 + "bootstrapServerUpdateEnable": false,
292 "type": "LWM2M" 277 "type": "LWM2M"
293 } 278 }
294 `, 279 `,
@@ -11,29 +11,44 @@ @@ -11,29 +11,44 @@
11 </TabPane> 11 </TabPane>
12 <TabPane forceRender key="2" tab="Bootstrap"> 12 <TabPane forceRender key="2" tab="Bootstrap">
13 <div> 13 <div>
14 - <Checkbox v-model:checked="bootstrapServerUpdateEnable">包括引导服务器更新</Checkbox>  
15 - <Card 14 + <Checkbox
  15 + @change="handleCheckChange($event)"
  16 + v-model:checked="bootstrapServerUpdateEnable"
  17 + >包括引导服务器更新</Checkbox
  18 + >
  19 + <CollapseContainer
16 v-for="(item, index) in dynamicBOOTSTRAP.bootstrap" 20 v-for="(item, index) in dynamicBOOTSTRAP.bootstrap"
17 :key="item" 21 :key="item"
18 - title="LwM2M Server"  
19 - style="width: 99%; margin-top: 2vh" 22 + :title="collapseTitle(item)"
  23 + class="mt-4"
20 > 24 >
21 - <template #extra>  
22 - <Button size="small" type="dashed" @click="handleRemove(index)">  
23 - <template #icon>  
24 - <MinusCircleOutlined />  
25 - </template> 25 + <template #action>
  26 + <Button
  27 + style="margin-right: 1vw"
  28 + size="small"
  29 + type="text"
  30 + @click="handleRemove(index)"
  31 + >
  32 + <template #icon> <DeleteOutlined /> </template>
26 </Button> 33 </Button>
27 </template> 34 </template>
28 - <!-- BootStrapForm表单项 -->  
29 - <BootStrapForm :ref="dynamicBindRef.BootStrapFormItemRef" :index="index" :item="item" />  
30 - </Card> 35 + <div style="border: 1px solid #d9d9d9; width: 100%">
  36 + <div style="margin: 10px 15px">
  37 + <BootStrapForm
  38 + :ref="dynamicBindRef.BootStrapFormItemRef"
  39 + :index="index"
  40 + :item="item"
  41 + />
  42 + </div>
  43 + </div>
  44 + </CollapseContainer>
31 <div style="margin-top: 2vh"> 45 <div style="margin-top: 2vh">
32 - <Button size="middle" type="dashed" @click="handleAdd"> 46 + <Button size="middle" type="text" @click="handleAdd">
33 <template #icon> 47 <template #icon>
34 <PlusCircleOutlined /> 48 <PlusCircleOutlined />
35 </template> 49 </template>
36 - Add LwM2M Server 50 + <span v-if="selectCheckStatus">Add server config</span>
  51 + <span v-else>Add LwM2M Server</span>
37 </Button> 52 </Button>
38 </div> 53 </div>
39 </div> 54 </div>
@@ -61,6 +76,7 @@ @@ -61,6 +76,7 @@
61 <BasicForm :showResetButton="false" :showSubmitButton="false" @register="registerDevice" /> 76 <BasicForm :showResetButton="false" :showSubmitButton="false" @register="registerDevice" />
62 </TabPane> 77 </TabPane>
63 </Tabs> 78 </Tabs>
  79 + <ServerConfigModal @register="registerModal" @emitSelect="acceptEmitFunc" />
64 </div> 80 </div>
65 </template> 81 </template>
66 82
@@ -70,8 +86,11 @@ @@ -70,8 +86,11 @@
70 import { BasicForm, useForm } from '/@/components/Form'; 86 import { BasicForm, useForm } from '/@/components/Form';
71 import { modelSchemas, settingsSchemas, deviceSchemas } from './index'; 87 import { modelSchemas, settingsSchemas, deviceSchemas } from './index';
72 import BootStrapForm from './cpns/BootStrapForm.vue'; 88 import BootStrapForm from './cpns/BootStrapForm.vue';
73 - import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons-vue'; 89 + import { DeleteOutlined, PlusCircleOutlined } from '@ant-design/icons-vue';
74 import { Button, Checkbox } from 'ant-design-vue'; 90 import { Button, Checkbox } from 'ant-design-vue';
  91 + import { CollapseContainer } from '/@/components/Container';
  92 + import { useModal } from '/@/components/Modal';
  93 + import ServerConfigModal from './cpns/ServerConfigModal.vue';
75 94
76 export default defineComponent({ 95 export default defineComponent({
77 name: 'index', 96 name: 'index',
@@ -80,16 +99,21 @@ @@ -80,16 +99,21 @@
80 TabPane: Tabs.TabPane, 99 TabPane: Tabs.TabPane,
81 BasicForm, 100 BasicForm,
82 BootStrapForm, 101 BootStrapForm,
83 - MinusCircleOutlined, 102 + DeleteOutlined,
84 Card, 103 Card,
85 PlusCircleOutlined, 104 PlusCircleOutlined,
86 Button, 105 Button,
87 Checkbox, 106 Checkbox,
  107 + CollapseContainer,
  108 + ServerConfigModal,
88 }, 109 },
89 setup() { 110 setup() {
  111 + const collapseTitle = (item) => {
  112 + return `LwM2M Server Short server ID: ${item.shortServerId} Security config mode: ${item.securityMode}`;
  113 + };
90 const bootstrapServerUpdateEnable = ref(false); 114 const bootstrapServerUpdateEnable = ref(false);
91 const dynamicBOOTSTRAP: any = reactive({ 115 const dynamicBOOTSTRAP: any = reactive({
92 - bootstrap: [{}], 116 + bootstrap: [{ securityMode: 'NO_SEC', shortServerId: 1234 }],
93 }); 117 });
94 const dynamicBindRef: any = { 118 const dynamicBindRef: any = {
95 BootStrapFormItemRef: ref([]), 119 BootStrapFormItemRef: ref([]),
@@ -140,9 +164,20 @@ @@ -140,9 +164,20 @@
140 span: 14, 164 span: 14,
141 }, 165 },
142 }); 166 });
  167 + const [registerModal, { openModal }] = useModal();
143 168
144 const handleAdd = () => { 169 const handleAdd = () => {
145 - dynamicBOOTSTRAP.bootstrap.push({}); 170 + //TODO 如果是server config 则只弹窗一次
  171 + if (selectCheckStatus.value) {
  172 + openModal(true, {
  173 + isUpdate: true,
  174 + });
  175 + } else {
  176 + dynamicBOOTSTRAP.bootstrap.push({
  177 + securityMode: 'NO_SEC',
  178 + shortServerId: 1234,
  179 + });
  180 + }
146 }; 181 };
147 const handleRemove = (index) => { 182 const handleRemove = (index) => {
148 dynamicBOOTSTRAP.bootstrap.splice(index, 1); 183 dynamicBOOTSTRAP.bootstrap.splice(index, 1);
@@ -240,7 +275,32 @@ @@ -240,7 +275,32 @@
240 resetDeviceValue(); 275 resetDeviceValue();
241 }); 276 });
242 }; 277 };
243 - 278 + const selectCheckStatus = ref(false);
  279 + const handleCheckChange = (e) => {
  280 + selectCheckStatus.value = e.target.checked;
  281 + if (!selectCheckStatus.value) {
  282 + const findIndex = dynamicBOOTSTRAP.bootstrap.findIndex((o) => o.type == 'Bootstrap');
  283 + if (findIndex !== -1) {
  284 + dynamicBOOTSTRAP.bootstrap.splice(findIndex, 1);
  285 + }
  286 + }
  287 + };
  288 + const acceptEmitFunc = (e) => {
  289 + switch (e) {
  290 + case 'LwM2M':
  291 + dynamicBOOTSTRAP.bootstrap.push({
  292 + securityMode: 'NO_SEC',
  293 + shortServerId: 1234,
  294 + });
  295 + break;
  296 + case 'Bootstrap':
  297 + dynamicBOOTSTRAP.bootstrap.push({
  298 + securityMode: 'NO_SEC',
  299 + shortServerId: 1234,
  300 + });
  301 + break;
  302 + }
  303 + };
244 return { 304 return {
245 currentKey, 305 currentKey,
246 currentSize, 306 currentSize,
@@ -256,6 +316,11 @@ @@ -256,6 +316,11 @@
256 handleAdd, 316 handleAdd,
257 handleRemove, 317 handleRemove,
258 bootstrapServerUpdateEnable, 318 bootstrapServerUpdateEnable,
  319 + collapseTitle,
  320 + handleCheckChange,
  321 + registerModal,
  322 + acceptEmitFunc,
  323 + selectCheckStatus,
259 }; 324 };
260 }, 325 },
261 }); 326 });
@@ -20,7 +20,7 @@ export const MqttSchemas: FormSchema[] = [ @@ -20,7 +20,7 @@ export const MqttSchemas: FormSchema[] = [
20 { 20 {
21 field: 'deviceTelemetryTopic', 21 field: 'deviceTelemetryTopic',
22 component: 'Input', 22 component: 'Input',
23 - label: '遥测数据 topic 筛选器', 23 + label: '遥测数据主题筛选器',
24 required: true, 24 required: true,
25 defaultValue: 'v1/devices/me/telemetry', 25 defaultValue: 'v1/devices/me/telemetry',
26 componentProps: { 26 componentProps: {
@@ -32,7 +32,7 @@ export const MqttSchemas: FormSchema[] = [ @@ -32,7 +32,7 @@ export const MqttSchemas: FormSchema[] = [
32 field: 'deviceAttributesTopic', 32 field: 'deviceAttributesTopic',
33 component: 'Input', 33 component: 'Input',
34 required: true, 34 required: true,
35 - label: 'Attributes topic filter', 35 + label: '属性主题过滤器',
36 defaultValue: 'v1/devices/me/attributes', 36 defaultValue: 'v1/devices/me/attributes',
37 componentProps: { 37 componentProps: {
38 placeholder: '请输入Attributes topic 筛选器', 38 placeholder: '请输入Attributes topic 筛选器',
@@ -64,7 +64,14 @@ export const MqttSchemas: FormSchema[] = [ @@ -64,7 +64,14 @@ export const MqttSchemas: FormSchema[] = [
64 colProps: { span: 23 }, 64 colProps: { span: 23 },
65 defaultValue: false, 65 defaultValue: false,
66 component: 'Checkbox', 66 component: 'Checkbox',
67 - renderComponentContent: `启用后,平台将默认使用Protobuf有效载荷格式。如果解析失败,平台将尝试使用JSON有效负载格式。用于固件更新期间的向后兼容性。例如,固件的初始版本使用Json,而新版本使用Protobuf。在设备组的固件更新过程中,需要同时支持Protobuf和JSON。兼容性模式会导致性能轻微下降,因此建议在所有设备更新后禁用此模式。`, 67 + ifShow: ({ values }) => isProtobuf(values.transportPayloadType),
  68 + renderComponentContent: '启用与其他有效负载格式的兼容性',
  69 + },
  70 + {
  71 + field: 'desc1',
  72 + component: 'InputTextArea',
  73 + label: '',
  74 + slot: 'desc1',
68 ifShow: ({ values }) => isProtobuf(values.transportPayloadType), 75 ifShow: ({ values }) => isProtobuf(values.transportPayloadType),
69 }, 76 },
70 { 77 {
@@ -73,7 +80,16 @@ export const MqttSchemas: FormSchema[] = [ @@ -73,7 +80,16 @@ export const MqttSchemas: FormSchema[] = [
73 colProps: { span: 23 }, 80 colProps: { span: 23 },
74 defaultValue: false, 81 defaultValue: false,
75 component: 'Checkbox', 82 component: 'Checkbox',
76 - renderComponentContent: `启用后,平台将使用Json有效负载格式通过以下主题推送属性和RPC:v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes, v1/devices/me/rpc/request/$request_id,v1/devices/me/rpc/response/$request_id.此设置不会影响使用新(v2)主题发送的属性和rpc订阅:v2/a/res/$request_id, v2/a, v2/r/req/$request_id, v2/r/res/$request_id. Where $request_id是一个整数请求标识符。`, 83 + renderComponentContent: '默认下行主题使用Json格式',
  84 + ifShow: ({ values }) =>
  85 + isProtobuf(values.transportPayloadType) &&
  86 + !!values.useJsonPayloadFormatForDefaultDownlinkTopics,
  87 + },
  88 + {
  89 + field: 'desc2',
  90 + component: 'InputTextArea',
  91 + label: '',
  92 + slot: 'desc2',
77 ifShow: ({ values }) => 93 ifShow: ({ values }) =>
78 isProtobuf(values.transportPayloadType) && 94 isProtobuf(values.transportPayloadType) &&
79 !!values.useJsonPayloadFormatForDefaultDownlinkTopics, 95 !!values.useJsonPayloadFormatForDefaultDownlinkTopics,
@@ -19,6 +19,28 @@ @@ -19,6 +19,28 @@
19 </p> 19 </p>
20 </div> 20 </div>
21 </template> 21 </template>
  22 + <template #desc1>
  23 + <div style="width: 47rem; margin-left: 2rem">
  24 + <p>
  25 + 启用时,默认情况下,平台将使用Protobuf有效载荷格式。如果解析失败,平台将尝试使用JSON负载格式。
  26 + 有助于固件更新期间的向后兼容性。例如,固件的初始版本使用Json,而新版本使用Protobuf。
  27 + 在设备组的固件更新过程中,需要同时支持Protobuf和JSON。兼容性模式会导致性能略有下降,
  28 + 因此建议在更新所有设备后禁用此模式。
  29 + </p>
  30 + </div>
  31 + </template>
  32 + <template #desc2>
  33 + <div style="width: 47rem; margin-left: 2rem">
  34 + <p>
  35 + 启用后,平台将使用Json负载格式通过以下主题推送属性
  36 + 和RPC:v1/devices/me/attributes/response/$request\u id、v1/devices/me/attributes、
  37 + v1/devices/me/RPC/request/$request\u id、v1/devices/me/RPC/response/$request\u id。
  38 + 此设置不会影响使用新(v2)主题发送的属性和rpc订阅:
  39 + v2/a/res/$request\u id、v2/a、v2/r/req/$request\u id、v2/r/res/$request\u id。
  40 + 其中,$request\u id是整数请求标识符。
  41 + </p>
  42 + </div>
  43 + </template>
22 </BasicForm> 44 </BasicForm>
23 </div> 45 </div>
24 </div> 46 </div>
@@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
2 <div 2 <div
3 v-for="(param, index) in dynamicInput.params" 3 v-for="(param, index) in dynamicInput.params"
4 :key="index" 4 :key="index"
5 - style="display: flex; margin-top: 0.2vh" 5 + style="display: flex; margin-top: 0.25vh"
6 > 6 >
7 <Select 7 <Select
8 v-model:value="param.dataType" 8 v-model:value="param.dataType"
@@ -17,7 +17,7 @@ @@ -17,7 +17,7 @@
17 @change="emitChange" 17 @change="emitChange"
18 /> 18 />
19 <a-input 19 <a-input
20 - placeholder="请输入OID" 20 + placeholder="请输入OID(不能重复)"
21 v-model:value="param.oid" 21 v-model:value="param.oid"
22 style="width: 38%; margin: 0 0 5px 8px" 22 style="width: 38%; margin: 0 0 5px 8px"
23 @change="emitChange" 23 @change="emitChange"
@@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
30 /> 30 />
31 </div> 31 </div>
32 <div> 32 <div>
33 - <a-button type="dashed" style="width: 38%" @click="add"> 33 + <a-button type="text" style="width: 28%; margin-top: 0.25vh" @click="add">
34 <PlusCircleOutlined /> 34 <PlusCircleOutlined />
35 Add mapping 35 Add mapping
36 </a-button> 36 </a-button>
@@ -16,10 +16,12 @@ @@ -16,10 +16,12 @@
16 > 16 >
17 <InputNumber 17 <InputNumber
18 v-if="item.spec == 'TELEMETRY_QUERYING' || item.spec == 'CLIENT_ATTRIBUTES_QUERYING'" 18 v-if="item.spec == 'TELEMETRY_QUERYING' || item.spec == 'CLIENT_ATTRIBUTES_QUERYING'"
19 - v-model:value="item.queryingFrequencyMs"  
20 - :min="5000"  
21 - :max="1000000000" 19 + v-model:value="queryingFrequencyMs"
  20 + :min="0"
  21 + :max="99999999999999999999"
  22 + style="margin-top: 0.25vh"
22 /> 23 />
  24 + <div style="margin-top: 0.65vh"></div>
23 <MappingsForm 25 <MappingsForm
24 :value="item.mappings" 26 :value="item.mappings"
25 @change="handleMappingsChange" 27 @change="handleMappingsChange"
@@ -33,7 +35,7 @@ @@ -33,7 +35,7 @@
33 style="text-align: center; line-height: 20vh" 35 style="text-align: center; line-height: 20vh"
34 :style="{ lineHeight: dynamicHeight + 'vh' }" 36 :style="{ lineHeight: dynamicHeight + 'vh' }"
35 > 37 >
36 - <Button size="small" type="dashed" @click="handleRemove(item, index)"> 38 + <Button size="small" type="default" @click="handleRemove(item, index)">
37 <template #icon> 39 <template #icon>
38 <MinusCircleOutlined /> 40 <MinusCircleOutlined />
39 </template> 41 </template>
@@ -47,6 +49,7 @@ @@ -47,6 +49,7 @@
47 import { Button, InputNumber } from 'ant-design-vue'; 49 import { Button, InputNumber } from 'ant-design-vue';
48 import { MinusCircleOutlined } from '@ant-design/icons-vue'; 50 import { MinusCircleOutlined } from '@ant-design/icons-vue';
49 import MappingsForm from './MappingsForm.vue'; 51 import MappingsForm from './MappingsForm.vue';
  52 + import { useMessage } from '/@/hooks/web/useMessage';
50 53
51 const props = defineProps({ 54 const props = defineProps({
52 item: { 55 item: {
@@ -59,7 +62,8 @@ @@ -59,7 +62,8 @@
59 }); 62 });
60 const emit = defineEmits(['removeItem']); 63 const emit = defineEmits(['removeItem']);
61 const dynamicHeight = ref(25); 64 const dynamicHeight = ref(25);
62 - 65 + const queryingFrequencyMs = ref(5000);
  66 + const { createMessage } = useMessage();
63 const handleMappingsChange = (e) => { 67 const handleMappingsChange = (e) => {
64 // eslint-disable-next-line vue/no-mutating-props 68 // eslint-disable-next-line vue/no-mutating-props
65 props.item.mappings = e; 69 props.item.mappings = e;
@@ -70,9 +74,18 @@ @@ -70,9 +74,18 @@
70 //设置回显的高度 74 //设置回显的高度
71 const setFieldsValueFunc = () => { 75 const setFieldsValueFunc = () => {
72 dynamicHeight.value = props.item.mappings.length * 3 + props.item.mappings.length + 15; 76 dynamicHeight.value = props.item.mappings.length * 3 + props.item.mappings.length + 15;
  77 + queryingFrequencyMs.value = props.item.queryingFrequencyMs;
73 }; 78 };
74 //获取表单的值 79 //获取表单的值
75 const getSnmpFormFunc = () => { 80 const getSnmpFormFunc = () => {
  81 + if (
  82 + props.item.spec == 'TELEMETRY_QUERYING' ||
  83 + props.item.spec == 'CLIENT_ATTRIBUTES_QUERYING'
  84 + ) {
  85 + if (queryingFrequencyMs.value == null) {
  86 + return createMessage.error('请填写查询频率');
  87 + }
  88 + }
76 let obj: any = {}; 89 let obj: any = {};
77 obj = { 90 obj = {
78 ...{ spec: props.item.spec }, 91 ...{ spec: props.item.spec },
@@ -80,7 +93,7 @@ @@ -80,7 +93,7 @@
80 ...{ 93 ...{
81 queryingFrequencyMs: 94 queryingFrequencyMs:
82 props.item.spec == 'TELEMETRY_QUERYING' || props.item.spec == 'CLIENT_ATTRIBUTES_QUERYING' 95 props.item.spec == 'TELEMETRY_QUERYING' || props.item.spec == 'CLIENT_ATTRIBUTES_QUERYING'
83 - ? props.item.queryingFrequencyMs 96 + ? queryingFrequencyMs.value
84 : null, 97 : null,
85 }, 98 },
86 }; 99 };
@@ -27,22 +27,14 @@ @@ -27,22 +27,14 @@
27 :options="selectOptions" 27 :options="selectOptions"
28 @change="handleChange(item.spec)" 28 @change="handleChange(item.spec)"
29 allowClear 29 allowClear
30 - >  
31 - <SelectOption  
32 - v-for="it in selectOptions"  
33 - :value="it.value"  
34 - :key="it.value"  
35 - :label="it.label"  
36 - :disabled="it.disabled"  
37 - />  
38 - </Select> 30 + />
39 </SnmpForm> 31 </SnmpForm>
40 </template> 32 </template>
41 <div 33 <div
42 v-if="dynamicSNMP.communicationConfigs.length < 4" 34 v-if="dynamicSNMP.communicationConfigs.length < 4"
43 style="margin-left: 0vw; margin-top: 2vh" 35 style="margin-left: 0vw; margin-top: 2vh"
44 > 36 >
45 - <Button size="middle" type="dashed" @click="handleAdd"> 37 + <Button size="middle" type="text" @click="handleAdd">
46 <template #icon> 38 <template #icon>
47 <PlusCircleOutlined /> 39 <PlusCircleOutlined />
48 </template> 40 </template>
@@ -60,7 +52,7 @@ @@ -60,7 +52,7 @@
60 import { BasicForm, useForm } from '/@/components/Form'; 52 import { BasicForm, useForm } from '/@/components/Form';
61 import { snmpSchemas } from './config'; 53 import { snmpSchemas } from './config';
62 import { PlusCircleOutlined } from '@ant-design/icons-vue'; 54 import { PlusCircleOutlined } from '@ant-design/icons-vue';
63 - import { Button, Select, SelectOption } from 'ant-design-vue'; 55 + import { Button, Select } from 'ant-design-vue';
64 import SnmpForm from './cpns/SnmpForm.vue'; 56 import SnmpForm from './cpns/SnmpForm.vue';
65 57
66 interface mappingsI { 58 interface mappingsI {
@@ -77,26 +69,22 @@ @@ -77,26 +69,22 @@
77 { 69 {
78 label: 'Telemetry', 70 label: 'Telemetry',
79 value: 'TELEMETRY_QUERYING', 71 value: 'TELEMETRY_QUERYING',
80 - disabled: false,  
81 }, 72 },
82 { 73 {
83 label: 'Client attributes', 74 label: 'Client attributes',
84 value: 'CLIENT_ATTRIBUTES_QUERYING', 75 value: 'CLIENT_ATTRIBUTES_QUERYING',
85 - disabled: false,  
86 }, 76 },
87 { 77 {
88 label: 'Shared attributes', 78 label: 'Shared attributes',
89 value: 'SHARED_ATTRIBUTES_SETTING', 79 value: 'SHARED_ATTRIBUTES_SETTING',
90 - disabled: false,  
91 }, 80 },
92 { 81 {
93 label: 'RPC request', 82 label: 'RPC request',
94 value: 'TO_DEVICE_RPC_REQUEST', 83 value: 'TO_DEVICE_RPC_REQUEST',
95 - disabled: false,  
96 }, 84 },
97 ]); 85 ]);
98 const selectValue = ref(''); 86 const selectValue = ref('');
99 - //解决新增选择框互斥问题和编辑回显互斥问题 87 + //Select互斥
100 const handleChange = (value: string) => { 88 const handleChange = (value: string) => {
101 selectValue.value = value; 89 selectValue.value = value;
102 selectOptions.value.forEach((ele: any) => { 90 selectOptions.value.forEach((ele: any) => {
@@ -154,12 +142,15 @@ @@ -154,12 +142,15 @@
154 }); 142 });
155 }; 143 };
156 const handleRemoveItem = (item, index) => { 144 const handleRemoveItem = (item, index) => {
157 - console.log(item.spec);  
158 - //2 TODO待解决删除时清空互斥问题 145 + selectOptions.value.forEach((ele: any) => {
  146 + if (ele.value == item.spec) {
  147 + ele.disabled = false;
  148 + }
  149 + });
159 dynamicSNMP.communicationConfigs.splice(index, 1); 150 dynamicSNMP.communicationConfigs.splice(index, 1);
160 }; 151 };
  152 +
161 //回显表单值 153 //回显表单值
162 - //TODO 采用这种方式动态绑定ref 都会造成回显数据顺序是随机的但不影响回显数据的正确性  
163 const setStepFieldsValueFunc = (v) => { 154 const setStepFieldsValueFunc = (v) => {
164 setFieldsValue({ 155 setFieldsValue({
165 timeoutMs: v.timeoutMs, 156 timeoutMs: v.timeoutMs,
@@ -168,14 +159,13 @@ @@ -168,14 +159,13 @@
168 dynamicSNMP.communicationConfigs = v.communicationConfigs; 159 dynamicSNMP.communicationConfigs = v.communicationConfigs;
169 dynamicSNMP.communicationConfigs.forEach((snmp, index: number) => { 160 dynamicSNMP.communicationConfigs.forEach((snmp, index: number) => {
170 nextTick(() => { 161 nextTick(() => {
171 - //编辑回显互斥赋值  
172 handleChange(snmp.spec); 162 handleChange(snmp.spec);
173 unref(dynamicBindRef.SnmpFormItemRef)[index]?.setFieldsValueFunc(); 163 unref(dynamicBindRef.SnmpFormItemRef)[index]?.setFieldsValueFunc();
174 }); 164 });
175 }); 165 });
176 }; 166 };
177 167
178 - //获取最终的 168 + //获取表单
179 const getSnmpForm = async () => { 169 const getSnmpForm = async () => {
180 let value = await validate(); 170 let value = await validate();
181 if (!value) return; 171 if (!value) return;
  1 +<template>
  2 + <div>
  3 + <BasicModal
  4 + v-bind="$attrs"
  5 + width="55rem"
  6 + :height="heightNum"
  7 + @register="register"
  8 + title="执行设备及属性"
  9 + @cancel="handleCancel"
  10 + :showOkBtn="false"
  11 + >
  12 + <div>
  13 + <BasicTable @register="registerTable" :dataSource="tableData" />
  14 + </div>
  15 + </BasicModal>
  16 + </div>
  17 +</template>
  18 +<script setup lang="ts">
  19 + import { ref, nextTick } from 'vue';
  20 + import { BasicModal, useModalInner } from '/@/components/Modal';
  21 + import { BasicTable, useTable } from '/@/components/Table';
  22 + import { viewDeviceColumn } from './config.data';
  23 +
  24 + const heightNum = ref(800);
  25 + const tableData: any = ref([]);
  26 + const [registerTable] = useTable({
  27 + title: '执行设备及属性',
  28 + columns: viewDeviceColumn,
  29 + showIndexColumn: false,
  30 + clickToRowSelect: false,
  31 + showTableSetting: false,
  32 + bordered: true,
  33 + });
  34 + const [register] = useModalInner((data) => {
  35 + console.log(data);
  36 + const getTableData = () => {
  37 + for (let i = 0; i < 100; i++) {
  38 + tableData.value.push({
  39 + tdDevice: `设备${i}`,
  40 + attr: `CO${i}、Temp${i}`,
  41 + });
  42 + }
  43 + };
  44 + nextTick(() => {
  45 + getTableData();
  46 + });
  47 + });
  48 + const handleCancel = () => {};
  49 +</script>
  50 +<style></style>
  1 +<template>
  2 + <BasicDrawer
  3 + v-bind="$attrs"
  4 + @register="registerDrawer"
  5 + showFooter
  6 + :title="getTitle"
  7 + width="30%"
  8 + @ok="handleSubmit"
  9 + >
  10 + <BasicForm @register="registerForm" />
  11 + </BasicDrawer>
  12 +</template>
  13 +<script lang="ts" setup>
  14 + import { ref, computed, unref, nextTick } from 'vue';
  15 + import { BasicForm, useForm } from '/@/components/Form';
  16 + import { formSchema } from './config.data';
  17 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  18 + import { createOrEditCameraManage } from '/@/api/camera/cameraManager';
  19 + import { useMessage } from '/@/hooks/web/useMessage';
  20 +
  21 + const emit = defineEmits(['success', 'register']);
  22 + const isUpdate = ref(true);
  23 + const editId = ref('');
  24 + const [registerForm, { validate, setFieldsValue, resetFields }] = useForm({
  25 + labelWidth: 120,
  26 + schemas: formSchema,
  27 + showActionButtonGroup: false,
  28 + });
  29 +
  30 + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  31 + await resetFields();
  32 + setDrawerProps({ confirmLoading: false });
  33 + isUpdate.value = !!data?.isUpdate;
  34 + if (unref(isUpdate)) {
  35 + await nextTick();
  36 + editId.value = data.record.id;
  37 + await setFieldsValue(data.record);
  38 + } else {
  39 + editId.value = '';
  40 + }
  41 + });
  42 +
  43 + const getTitle = computed(() => (!unref(isUpdate) ? '新增报表配置' : '编辑报表配置'));
  44 +
  45 + async function handleSubmit() {
  46 + setDrawerProps({ confirmLoading: true });
  47 + try {
  48 + const { createMessage } = useMessage();
  49 + const values = await validate();
  50 + if (!values) return;
  51 + let saveMessage = '添加成功';
  52 + let updateMessage = '修改成功';
  53 + await createOrEditCameraManage(values);
  54 + closeDrawer();
  55 + emit('success');
  56 + createMessage.success(unref(isUpdate) ? updateMessage : saveMessage);
  57 + } finally {
  58 + setTimeout(() => {
  59 + setDrawerProps({ confirmLoading: false });
  60 + }, 300);
  61 + }
  62 + }
  63 +</script>
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import type { FormSchema as QFormSchema } from '/@/components/Form/index';
  3 +import moment from 'moment';
  4 +import { getOrganizationList } from '/@/api/system/system';
  5 +import { copyTransFun } from '/@/utils/fnUtils';
  6 +import { screenLinkPageByDeptIdGetDevice } from '/@/api/ruleengine/ruleengineApi';
  7 +import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
  8 +
  9 +export enum TypeEnum {
  10 + IS_TIMING = 'TIMING',
  11 + IS_WEEK = 'week',
  12 + IS_MONTH = 'month',
  13 +}
  14 +export const isTiming = (type: string) => {
  15 + return type === TypeEnum.IS_TIMING;
  16 +};
  17 +export const isWeek = (type: string) => {
  18 + return type === TypeEnum.IS_WEEK;
  19 +};
  20 +
  21 +export const isMonth = (type: string) => {
  22 + return type === TypeEnum.IS_MONTH;
  23 +};
  24 +
  25 +
  26 +export enum AggregateDataEnum {
  27 + MIN = 'MIN',
  28 + MAX = 'MAX',
  29 + AVG = 'AVG',
  30 + SUM = 'SUM',
  31 + COUNT = 'COUNT',
  32 + NONE = 'NONE',
  33 +}
  34 +
  35 +// 表格配置
  36 +export const columns: BasicColumn[] = [
  37 + {
  38 + title: '配置名称',
  39 + dataIndex: '1',
  40 + width: 80,
  41 + },
  42 + {
  43 + title: '所属组织',
  44 + dataIndex: '2',
  45 + width: 120,
  46 + },
  47 + {
  48 + title: '数据类型',
  49 + dataIndex: '3',
  50 + width: 120,
  51 + },
  52 + {
  53 + title: '配置状态',
  54 + dataIndex: '4',
  55 + width: 120,
  56 + },
  57 + {
  58 + title: '执行方式',
  59 + dataIndex: '5',
  60 + width: 160,
  61 + },
  62 + {
  63 + title: '执行设备',
  64 + dataIndex: '6',
  65 + width: 160,
  66 + slots: { customRender: 'doDeviceSlot' },
  67 + },
  68 + {
  69 + title: '创建人',
  70 + dataIndex: '7',
  71 + width: 180,
  72 + },
  73 + {
  74 + title: '创建日期',
  75 + dataIndex: '8',
  76 + width: 180,
  77 + },
  78 +];
  79 +export const viewDeviceColumn: BasicColumn[] = [
  80 + {
  81 + title: '设备',
  82 + dataIndex: 'tdDevice',
  83 + width: 80,
  84 + },
  85 + {
  86 + title: '属性',
  87 + dataIndex: 'attr',
  88 + width: 120,
  89 + },
  90 +];
  91 +
  92 +// 查询配置
  93 +export const searchFormSchema: FormSchema[] = [
  94 + {
  95 + field: '1',
  96 + label: '配置名称',
  97 + component: 'Input',
  98 + colProps: { span: 6 },
  99 + componentProps: {
  100 + maxLength: 36,
  101 + placeholder: '请输入配置名称',
  102 + },
  103 + },
  104 + {
  105 + field: '2',
  106 + label: '配置状态',
  107 + component: 'Select',
  108 + colProps: { span: 6 },
  109 + componentProps: {
  110 + options: [
  111 + {
  112 + label: '启用',
  113 + value: 1,
  114 + },
  115 + {
  116 + label: '禁用',
  117 + value: 0,
  118 + },
  119 + ],
  120 + placeholder: '请选择配置状态',
  121 + },
  122 + },
  123 + {
  124 + field: '3',
  125 + label: '创建时间',
  126 + component: 'RangePicker',
  127 + componentProps: {
  128 + showTime: {
  129 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  130 + },
  131 + },
  132 + colProps: { span: 6 },
  133 + },
  134 +];
  135 +
  136 +// 新增编辑配置
  137 +export const formSchema: QFormSchema[] = [
  138 + {
  139 + field: '1',
  140 + label: '报表名称',
  141 + colProps: { span: 24 },
  142 + helpMessage: ['报表配置', '只对拥有数值型属性', '的设备才能配置'],
  143 + required: true,
  144 + component: 'Input',
  145 + componentProps: {
  146 + maxLength: 255,
  147 + placeholder: '请输入报表名称',
  148 + },
  149 + },
  150 + {
  151 + field: 'organizationId',
  152 + label: '所属组织',
  153 + colProps: { span: 24 },
  154 + component: 'ApiTreeSelect',
  155 + required: true,
  156 + componentProps: ({ formActionType }) => {
  157 + const { updateSchema } = formActionType;
  158 + return {
  159 + async onChange(e) {
  160 + if (e) {
  161 + //获取该组织下的设备
  162 + const { items } = await screenLinkPageByDeptIdGetDevice({
  163 + organizationId: e,
  164 + });
  165 + const options = items.map((item) => {
  166 + return {
  167 + label: item.name,
  168 + value: item.tbDeviceId,
  169 + };
  170 + });
  171 + updateSchema({
  172 + field: 'entityId',
  173 + componentProps: {
  174 + options,
  175 + async onChange(e1) {
  176 + if (e1) {
  177 + const res = await getAttribute(e, e1.join(','));
  178 + const attr = res.map((item) => {
  179 + return {
  180 + label: item,
  181 + value: item,
  182 + };
  183 + });
  184 + updateSchema({
  185 + field: 'attr',
  186 + componentProps: {
  187 + options: attr,
  188 + },
  189 + });
  190 + }
  191 + },
  192 + },
  193 + });
  194 + }
  195 + },
  196 + maxLength: 250,
  197 + placeholder: '请选择所属组织',
  198 + api: async () => {
  199 + const data = await getOrganizationList();
  200 + copyTransFun(data as any as any[]);
  201 + return data;
  202 + },
  203 + };
  204 + },
  205 + },
  206 + {
  207 + field: '3',
  208 + label: '描述',
  209 + colProps: { span: 24 },
  210 + component: 'InputTextArea',
  211 + componentProps: {
  212 + maxLength: 255,
  213 + placeholder: '请输入描述',
  214 + },
  215 + },
  216 + {
  217 + field: 'actionS',
  218 + component: 'RadioGroup',
  219 + label: '执行方式',
  220 + colProps: {
  221 + span: 24,
  222 + },
  223 + defaultValue: '1',
  224 + componentProps: {
  225 + placeholder: '请选择执行方式',
  226 + options: [
  227 + {
  228 + label: '立即执行',
  229 + value: '1',
  230 + },
  231 + {
  232 + label: '定时执行',
  233 + value: 'TIMING',
  234 + },
  235 + ],
  236 + },
  237 + },
  238 + {
  239 + field: 'timeWeek',
  240 + component: 'Select',
  241 + label: '周期',
  242 + required: true,
  243 + colProps: { span: 24 },
  244 + defaultValue: '1',
  245 + componentProps: {
  246 + placeholder: '请选择周期',
  247 + options: [
  248 + { label: '每日', value: 'day' },
  249 + { label: '每周', value: 'week' },
  250 + { label: '每月', value: 'month' },
  251 + ],
  252 + },
  253 + ifShow: ({ values }) => isTiming(values.actionS),
  254 + },
  255 + {
  256 + field: '5',
  257 + component: 'Select',
  258 + label: '每周',
  259 + required: true,
  260 + colProps: { span: 24 },
  261 + defaultValue: '1',
  262 + componentProps: {
  263 + placeholder: '请选择周期',
  264 + options: [
  265 + { label: '星期一', value: '1' },
  266 + { label: '星期二', value: '2' },
  267 + { label: '星期三', value: '3' },
  268 + { label: '星期四', value: '3' },
  269 + { label: '星期五', value: '3' },
  270 + ],
  271 + },
  272 + ifShow: ({ values }) => isWeek(values.timeWeek),
  273 + },
  274 + {
  275 + field: '5',
  276 + component: 'Select',
  277 + label: '每月',
  278 + required: true,
  279 + colProps: { span: 24 },
  280 + defaultValue: '1',
  281 + componentProps: {
  282 + placeholder: '请选择周期',
  283 + options: [
  284 + { label: '1日', value: '1' },
  285 + { label: '2日', value: '7' },
  286 + { label: '3日', value: '30' },
  287 + ],
  288 + },
  289 + ifShow: ({ values }) => isMonth(values.timeWeek),
  290 + },
  291 + {
  292 + field: '6',
  293 + component: 'Select',
  294 + label: '时间',
  295 + required: true,
  296 + colProps: { span: 24 },
  297 + defaultValue: '1',
  298 + componentProps: {
  299 + placeholder: '请选择时间',
  300 + options: [
  301 + { label: '00点', value: '1' },
  302 + { label: '01点', value: '2' },
  303 + { label: '02点', value: '3' },
  304 + ],
  305 + },
  306 + ifShow: ({ values }) => isTiming(values.actionS),
  307 + },
  308 + {
  309 + field: 'entityId',
  310 + label: '设备',
  311 + required: true,
  312 + component: 'Select',
  313 + componentProps: {
  314 + placeholder: '请选择设备',
  315 + mode: 'multiple',
  316 + },
  317 + colProps: { span: 24 },
  318 + },
  319 + {
  320 + field: '7',
  321 + component: 'RadioGroup',
  322 + label: '属性性质',
  323 + required: true,
  324 + colProps: {
  325 + span: 24,
  326 + },
  327 + defaultValue: '1',
  328 + componentProps: {
  329 + placeholder: '请选择属性性质',
  330 + options: [
  331 + {
  332 + label: '共有',
  333 + value: '1',
  334 + },
  335 + {
  336 + label: '全部',
  337 + value: '2',
  338 + },
  339 + ],
  340 + },
  341 + },
  342 + {
  343 + field: 'attr',
  344 + label: '设备属性',
  345 + required: true,
  346 + component: 'Select',
  347 + componentProps: {
  348 + placeholder: '请选择设备属性',
  349 + },
  350 + colProps: { span: 24 },
  351 + },
  352 + {
  353 + field: '9',
  354 + label: '数据类型',
  355 + required: true,
  356 + component: 'Select',
  357 + componentProps: {
  358 + placeholder: '请选择数据类型',
  359 + options: [
  360 + { label: '历史数据', value: '1' },
  361 + { label: '同比', value: '2' },
  362 + { label: '环比', value: '2' },
  363 + ],
  364 + },
  365 + colProps: { span: 24 },
  366 + },
  367 + {
  368 + field: '10',
  369 + label: '聚合条件',
  370 + required: true,
  371 + component: 'Select',
  372 + componentProps: {
  373 + placeholder: '请选择聚合条件',
  374 + options: [
  375 + {
  376 + label: '平均值',
  377 + value: AggregateDataEnum.AVG,
  378 + },
  379 + {
  380 + label: '总和',
  381 + value: AggregateDataEnum.SUM,
  382 + },
  383 + {
  384 + label: '最大值',
  385 + value: AggregateDataEnum.MAX,
  386 + },
  387 + {
  388 + label: '最小值',
  389 + value: AggregateDataEnum.MIN,
  390 + },
  391 + ],
  392 + },
  393 + colProps: { span: 24 },
  394 + },
  395 + {
  396 + field: '9',
  397 + label: '查询周期',
  398 + required: true,
  399 + component: 'Select',
  400 + componentProps: {
  401 + placeholder: '请选择查询周期',
  402 + options: [
  403 + { label: '1分钟', value: '60000' },
  404 + { label: '2分钟', value: '120000' },
  405 + { label: '3分钟', value: '180000' },
  406 + ],
  407 + },
  408 + colProps: { span: 24 },
  409 + },
  410 + {
  411 + field: '10',
  412 + label: '间隔时间',
  413 + required: true,
  414 + component: 'Select',
  415 + componentProps: {
  416 + placeholder: '请选择间隔时间',
  417 + options: [
  418 + { label: '1秒', value: '1000' },
  419 + { label: '5秒', value: '5000' },
  420 + { label: '10秒', value: '10000' },
  421 + { label: '15秒', value: '150000' },
  422 + ],
  423 + },
  424 + colProps: { span: 24 },
  425 + },
  426 +];
  1 +<template>
  2 + <div>
  3 + <BasicTable :clickToRowSelect="false" @register="registerTable" :searchInfo="searchInfo">
  4 + <template #toolbar>
  5 + <Authority value="api:yt:report:post">
  6 + <a-button type="primary" @click="handleCreateOrEdit(null)"> 新增报表 </a-button>
  7 + </Authority>
  8 + <Authority value="api:yt:report:get">
  9 + <a-button type="primary" @click="go('/report/export')"> 下载报表 </a-button>
  10 + </Authority>
  11 + <Authority value="api:yt:report:delete">
  12 + <Popconfirm
  13 + title="您确定要批量删除数据"
  14 + ok-text="确定"
  15 + cancel-text="取消"
  16 + @confirm="handleDeleteOrBatchDelete(null)"
  17 + >
  18 + <a-button type="primary" color="error" :disabled="hasBatchDelete"> 批量删除 </a-button>
  19 + </Popconfirm>
  20 + </Authority>
  21 + </template>
  22 + <template #doDeviceSlot="{ record }">
  23 + <a-button type="text" @click="handleDeviceView(record)">
  24 + <span style="color: #377dff">查看设备</span>
  25 + </a-button>
  26 + </template>
  27 + <template #action="{ record }">
  28 + <TableAction
  29 + :actions="[
  30 + {
  31 + label: '编辑',
  32 + icon: 'clarity:note-edit-line',
  33 + auth: 'api:yt:report:update',
  34 + onClick: handleCreateOrEdit.bind(null, record),
  35 + },
  36 + {
  37 + label: '删除',
  38 + icon: 'ant-design:delete-outlined',
  39 + auth: 'api:yt:report:delete',
  40 + color: 'error',
  41 + popConfirm: {
  42 + title: '是否确认删除',
  43 + confirm: handleDeleteOrBatchDelete.bind(null, record),
  44 + },
  45 + },
  46 + ]"
  47 + />
  48 + </template>
  49 + </BasicTable>
  50 + <ReportConfigDrawer @register="registerDrawer" @success="handleSuccess" />
  51 + <DevicePreviewModal @register="registerModal" />
  52 + </div>
  53 +</template>
  54 +
  55 +<script lang="ts" setup>
  56 + import { reactive, nextTick } from 'vue';
  57 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  58 + import { useDrawer } from '/@/components/Drawer';
  59 + import ReportConfigDrawer from './ReportConfigDrawer.vue';
  60 + import DevicePreviewModal from './DevicePreviewModal.vue';
  61 + import { cameraPage, deleteCameraManage } from '/@/api/camera/cameraManager';
  62 + import { searchFormSchema, columns } from './config.data';
  63 + import { Authority } from '/@/components/Authority';
  64 + import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
  65 + import { Popconfirm } from 'ant-design-vue';
  66 + import { useModal } from '/@/components/Modal';
  67 + import { useGo } from '/@/hooks/web/usePage';
  68 +
  69 + const searchInfo = reactive<Recordable>({});
  70 +
  71 + const [registerTable, { reload, setProps }] = useTable({
  72 + title: '报表列表',
  73 + api: cameraPage,
  74 + columns,
  75 + showIndexColumn: false,
  76 + clickToRowSelect: false,
  77 + formConfig: {
  78 + labelWidth: 120,
  79 + schemas: searchFormSchema,
  80 + },
  81 + useSearchForm: true,
  82 + showTableSetting: true,
  83 + bordered: true,
  84 + rowKey: 'id',
  85 + actionColumn: {
  86 + width: 200,
  87 + title: '操作',
  88 + dataIndex: 'action',
  89 + slots: { customRender: 'action' },
  90 + fixed: 'right',
  91 + },
  92 + });
  93 +
  94 + // 弹框
  95 + const [registerDrawer, { openDrawer }] = useDrawer();
  96 +
  97 + // 刷新
  98 + const handleSuccess = () => {
  99 + reload();
  100 + };
  101 +
  102 + const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
  103 + deleteCameraManage,
  104 + handleSuccess,
  105 + setProps
  106 + );
  107 +
  108 + nextTick(() => {
  109 + setProps(selectionOptions);
  110 + });
  111 +
  112 + // 新增或编辑
  113 + const handleCreateOrEdit = (record: Recordable | null) => {
  114 + if (record) {
  115 + openDrawer(true, {
  116 + isUpdate: true,
  117 + record,
  118 + });
  119 + } else {
  120 + openDrawer(true, {
  121 + isUpdate: false,
  122 + });
  123 + }
  124 + };
  125 +
  126 + //查看设备
  127 + const [registerModal, { openModal }] = useModal();
  128 + const handleDeviceView = (record) => {
  129 + openModal(true, {
  130 + isUpdate: true,
  131 + record,
  132 + });
  133 + };
  134 + const go = useGo();
  135 +</script>
  1 +<template>
  2 + <div>
  3 + <BasicModal
  4 + v-bind="$attrs"
  5 + width="55rem"
  6 + :height="heightNum"
  7 + @register="register"
  8 + title="报表趋势图"
  9 + @cancel="handleCancel"
  10 + :showOkBtn="false"
  11 + >
  12 + <div ref="chartRef" :style="{ height, width }"></div>
  13 + </BasicModal>
  14 + </div>
  15 +</template>
  16 +<script setup lang="ts">
  17 + import { ref, PropType, Ref, nextTick } from 'vue';
  18 + import { BasicModal, useModalInner } from '/@/components/Modal';
  19 + import { useECharts } from '/@/hooks/web/useECharts';
  20 +
  21 + defineProps({
  22 + width: {
  23 + type: String as PropType<string>,
  24 + default: '100%',
  25 + },
  26 + height: {
  27 + type: String as PropType<string>,
  28 + default: 'calc(100vh - 378px)',
  29 + },
  30 + });
  31 + defineEmits(['register']);
  32 + const heightNum = ref(800);
  33 + const chartRef = ref<HTMLDivElement | null>(null);
  34 + const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
  35 + const [register] = useModalInner((data) => {
  36 + console.log(data);
  37 + nextTick(() => {
  38 + setOptions({
  39 + title: {
  40 + text: '报表趋势图',
  41 + },
  42 + tooltip: {
  43 + trigger: 'axis',
  44 + },
  45 + legend: {
  46 + data: ['CO', 'CO2'],
  47 + },
  48 + toolbox: {},
  49 + grid: {
  50 + left: '3%',
  51 + right: '4%',
  52 + bottom: '3%',
  53 + containLabel: true,
  54 + },
  55 + xAxis: {
  56 + type: 'category',
  57 + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  58 + boundaryGap: false,
  59 + },
  60 + yAxis: {
  61 + type: 'value',
  62 + boundaryGap: false,
  63 + },
  64 + series: [
  65 + {
  66 + name: 'CO',
  67 + type: 'line',
  68 + stack: 'Total',
  69 + data: [150, 230, 224, 218, 135, 147, 260],
  70 + },
  71 + {
  72 + name: 'CO2',
  73 + type: 'line',
  74 + stack: 'Total',
  75 + data: [15, 23, 22, 28, 15, 17, 20],
  76 + },
  77 + ],
  78 + });
  79 + });
  80 + });
  81 +
  82 + const handleCancel = () => {};
  83 +</script>
  84 +<style>
  85 + .video-sty {
  86 + width: 100%;
  87 + display: flex;
  88 + align-items: center;
  89 + justify-content: center;
  90 + }
  91 +</style>
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import moment from 'moment';
  3 +
  4 +// 表格配置
  5 +export const columns: BasicColumn[] = [
  6 + {
  7 + title: '配置名称',
  8 + dataIndex: '1',
  9 + width: 80,
  10 + },
  11 + {
  12 + title: '所属组织',
  13 + dataIndex: '2',
  14 + width: 120,
  15 + },
  16 + {
  17 + title: '数据类型',
  18 + dataIndex: '3',
  19 + width: 120,
  20 + },
  21 + {
  22 + title: '执行状态',
  23 + dataIndex: '4',
  24 + width: 120,
  25 + },
  26 + {
  27 + title: '执行日期',
  28 + dataIndex: '8',
  29 + width: 180,
  30 + },
  31 +];
  32 +
  33 +// 查询配置
  34 +export const searchFormSchema: FormSchema[] = [
  35 + {
  36 + field: '1',
  37 + label: '配置名称',
  38 + component: 'Input',
  39 + colProps: { span: 6 },
  40 + componentProps: {
  41 + maxLength: 36,
  42 + placeholder: '请输入配置名称',
  43 + },
  44 + },
  45 + {
  46 + field: '2',
  47 + label: '执行状态',
  48 + component: 'Select',
  49 + colProps: { span: 6 },
  50 + componentProps: {
  51 + options: [
  52 + {
  53 + label: '进行中',
  54 + value: 1,
  55 + },
  56 + {
  57 + label: '成功',
  58 + value: 0,
  59 + },
  60 + {
  61 + label: '失败',
  62 + value: 0,
  63 + },
  64 + ],
  65 + placeholder: '请选择执行状态',
  66 + },
  67 + },
  68 + {
  69 + field: '3',
  70 + label: '执行时间',
  71 + component: 'RangePicker',
  72 + componentProps: {
  73 + showTime: {
  74 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  75 + },
  76 + },
  77 + colProps: { span: 6 },
  78 + },
  79 +];
  1 +<template>
  2 + <div>
  3 + <BasicTable :clickToRowSelect="false" @register="registerTable" :searchInfo="searchInfo">
  4 + <template #toolbar>
  5 + <Authority value="api:yt:reportExport:export">
  6 + <a-button type="primary" @click="exportCharts"> 导出报表 </a-button>
  7 + </Authority>
  8 + <Authority value="api:yt:reportExport:delete">
  9 + <Popconfirm
  10 + title="您确定要批量删除数据"
  11 + ok-text="确定"
  12 + cancel-text="取消"
  13 + @confirm="handleDeleteOrBatchDelete(null)"
  14 + >
  15 + <a-button type="primary" color="error" :disabled="hasBatchDelete"> 批量删除 </a-button>
  16 + </Popconfirm>
  17 + </Authority>
  18 + </template>
  19 + <template #action="{ record }">
  20 + <TableAction
  21 + :actions="[
  22 + {
  23 + label: '查看',
  24 + icon: 'clarity:note-edit-line',
  25 + auth: 'api:yt:reportExport:get',
  26 + onClick: handleView.bind(null, record),
  27 + },
  28 + {
  29 + label: '导出报表',
  30 + icon: 'clarity:note-edit-line',
  31 + auth: 'api:yt:reportExport:export',
  32 + },
  33 + {
  34 + label: '删除',
  35 + icon: 'ant-design:delete-outlined',
  36 + auth: 'api:yt:reportExport:delete',
  37 + color: 'error',
  38 + popConfirm: {
  39 + title: '是否确认删除',
  40 + confirm: handleDeleteOrBatchDelete.bind(null, record),
  41 + },
  42 + },
  43 + ]"
  44 + />
  45 + </template>
  46 + </BasicTable>
  47 + <ReportPreviewModal @register="registerModal" />
  48 + </div>
  49 +</template>
  50 +
  51 +<script lang="ts" setup>
  52 + import { reactive, nextTick } from 'vue';
  53 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  54 + import { cameraPage, deleteCameraManage } from '/@/api/camera/cameraManager';
  55 + import { searchFormSchema, columns } from './config.data';
  56 + import { Authority } from '/@/components/Authority';
  57 + import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
  58 + import { Popconfirm } from 'ant-design-vue';
  59 + import { useModal } from '/@/components/Modal';
  60 + import ReportPreviewModal from './ReportPreviewModal.vue';
  61 +
  62 + const searchInfo = reactive<Recordable>({});
  63 +
  64 + const [registerTable, { reload, setProps }] = useTable({
  65 + title: '报表导出列表',
  66 + api: cameraPage,
  67 + columns,
  68 + showIndexColumn: false,
  69 + clickToRowSelect: false,
  70 + formConfig: {
  71 + labelWidth: 120,
  72 + schemas: searchFormSchema,
  73 + },
  74 + useSearchForm: true,
  75 + showTableSetting: true,
  76 + bordered: true,
  77 + rowKey: 'id',
  78 + actionColumn: {
  79 + width: 200,
  80 + title: '操作',
  81 + dataIndex: 'action',
  82 + slots: { customRender: 'action' },
  83 + fixed: 'right',
  84 + },
  85 + });
  86 +
  87 + const handleSuccess = () => {
  88 + reload();
  89 + };
  90 +
  91 + const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions } = useBatchDelete(
  92 + deleteCameraManage,
  93 + handleSuccess,
  94 + setProps
  95 + );
  96 +
  97 + nextTick(() => {
  98 + setProps(selectionOptions);
  99 + });
  100 +
  101 + const [registerModal, { openModal }] = useModal();
  102 + const handleView = (_, record) => {
  103 + openModal(true, {
  104 + isUpdate: true,
  105 + record,
  106 + });
  107 + };
  108 + const exportCharts = () => {};
  109 +</script>
@@ -292,11 +292,15 @@ @@ -292,11 +292,15 @@
292 return createMessage.error('请填写属性'); 292 return createMessage.error('请填写属性');
293 } 293 }
294 } 294 }
295 - await postAddConvertApi(isEdit.value ? noEditObj : allPostForm);  
296 - createMessage.success('数据流转编辑成功');  
297 - emit('success');  
298 - defineClearFunc();  
299 - closeModal(); 295 + const res = await postAddConvertApi(isEdit.value ? noEditObj : allPostForm);
  296 + if (res) {
  297 + closeModal();
  298 + createMessage.success('数据流转编辑成功');
  299 + setTimeout(() => {
  300 + emit('success');
  301 + }, 500);
  302 + defineClearFunc();
  303 + }
300 } 304 }
301 } catch { 305 } catch {
302 } finally { 306 } finally {