Commit 620c62bbd99d33af726272e89194d335bf56db66
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 | 17 | DEVICE_CONFIG_EXPORT = '/deviceProfile/export', |
18 | 18 | DEVICE_CONFIG_IMPORT = '/deviceProfile/import', |
19 | 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 | 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 | +}; | ... | ... |
... | ... | @@ -9,5 +9,7 @@ export { useForm } from './src/hooks/useForm'; |
9 | 9 | export { default as ApiSelect } from './src/components/ApiSelect.vue'; |
10 | 10 | export { default as RadioButtonGroup } from './src/components/RadioButtonGroup.vue'; |
11 | 11 | export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue'; |
12 | +export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue'; | |
13 | + | |
12 | 14 | |
13 | 15 | export { BasicForm }; | ... | ... |
... | ... | @@ -27,6 +27,7 @@ import { BasicUpload } from '/@/components/Upload'; |
27 | 27 | import { StrengthMeter } from '/@/components/StrengthMeter'; |
28 | 28 | import { IconPicker } from '/@/components/Icon'; |
29 | 29 | import { CountdownInput } from '/@/components/CountDown'; |
30 | +import ApiRadioGroup from './components/ApiRadioGroup.vue'; | |
30 | 31 | //自定义组件 |
31 | 32 | import JAddInput from './jeecg/components/JAddInput.vue'; |
32 | 33 | |
... | ... | @@ -39,6 +40,7 @@ componentMap.set('InputSearch', Input.Search); |
39 | 40 | componentMap.set('InputTextArea', Input.TextArea); |
40 | 41 | componentMap.set('InputNumber', InputNumber); |
41 | 42 | componentMap.set('AutoComplete', AutoComplete); |
43 | +componentMap.set('ApiRadioGroup', ApiRadioGroup); | |
42 | 44 | |
43 | 45 | componentMap.set('Select', Select); |
44 | 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> | ... | ... |
... | ... | @@ -77,7 +77,7 @@ |
77 | 77 | } |
78 | 78 | |
79 | 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 | 82 | if (diff < ONE_SECONDS) { |
83 | 83 | resStr = t('component.time.just'); | ... | ... |
... | ... | @@ -477,14 +477,14 @@ |
477 | 477 | cursor: pointer; |
478 | 478 | position: absolute; |
479 | 479 | top: 0.85rem; |
480 | - left: 1.1rem; | |
480 | + left: 1.1vw; | |
481 | 481 | } |
482 | 482 | .fold-right { |
483 | 483 | z-index: 1; |
484 | 484 | cursor: pointer; |
485 | 485 | position: absolute; |
486 | 486 | top: 0.85rem; |
487 | - left: 21.6rem; | |
487 | + left: 18.2vw; | |
488 | 488 | } |
489 | 489 | @prefix-cls: ~'@{namespace}-basic-tree'; |
490 | 490 | ... | ... |
... | ... | @@ -9,9 +9,7 @@ |
9 | 9 | import { useMessage } from '/@/hooks/web/useMessage'; |
10 | 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 | 14 | const createFlag = ref(false); |
17 | 15 | ... | ... |
... | ... | @@ -15,6 +15,7 @@ |
15 | 15 | import { ref } from 'vue'; |
16 | 16 | import { useMessage } from '/@/hooks/web/useMessage'; |
17 | 17 | import { Popconfirm } from 'ant-design-vue'; |
18 | + import { Authority } from '/@/components/Authority'; | |
18 | 19 | |
19 | 20 | const enabledBatchDelete = ref(true); |
20 | 21 | const [register, { reload, getSelectRowKeys }] = useTable({ | ... | ... |
... | ... | @@ -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 | 56 | label: '选择厂家', |
57 | + required: true, | |
57 | 58 | colProps: { |
58 | 59 | span: 8, |
59 | 60 | }, |
61 | + defaultValue: 'DIY_', | |
60 | 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 | 70 | ifShow: ({ values }) => isGateWay(values.deviceType), |
73 | 71 | }, | ... | ... |
... | ... | @@ -22,6 +22,15 @@ |
22 | 22 | <TabPane key="4" tab="子设备" v-if="deviceDetail?.deviceType === 'GATEWAY'"> |
23 | 23 | <ChildDevice :fromId="deviceDetail?.tbDeviceId" /> |
24 | 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 | 34 | </Tabs> |
26 | 35 | </BasicDrawer> |
27 | 36 | </template> |
... | ... | @@ -34,6 +43,7 @@ |
34 | 43 | import RealTimeData from '../tabs/RealTimeData.vue'; |
35 | 44 | import Alarm from '../tabs/Alarm.vue'; |
36 | 45 | import ChildDevice from '../tabs/ChildDevice.vue'; |
46 | + import TBoxDetail from '../tabs/TBoxDetail.vue'; | |
37 | 47 | import CommandIssuance from '../tabs/CommandIssuance.vue'; |
38 | 48 | import { getDeviceDetail } from '/@/api/device/deviceManager'; |
39 | 49 | export default defineComponent({ |
... | ... | @@ -47,6 +57,7 @@ |
47 | 57 | Alarm, |
48 | 58 | ChildDevice, |
49 | 59 | CommandIssuance, |
60 | + TBoxDetail, | |
50 | 61 | }, |
51 | 62 | emits: ['reload', 'register'], |
52 | 63 | setup() { | ... | ... |
... | ... | @@ -53,12 +53,6 @@ |
53 | 53 | <a-button type="primary" class="mr-4" @click="copyTbDeviceId">复制设备ID</a-button> |
54 | 54 | <a-button type="primary" class="mr-4" @click="copyDeviceToken">复制访问令牌</a-button> |
55 | 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 | 56 | <ManageDeviceTokenModal @register="registerModal" /> |
63 | 57 | </div> |
64 | 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 | 214 | if (isUpdate.value == 1) { |
215 | 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 | 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 | 231 | if (f.mappings.length == 0) { |
233 | - isMappings = true; | |
232 | + isMappingsKey.value = true; | |
233 | + isMappingValue.value = true; | |
234 | 234 | } else { |
235 | 235 | f.mappings.forEach((f1) => { |
236 | 236 | const findNoneKey = Object.keys(f1).includes(''); |
237 | 237 | if (findNoneKey) { |
238 | - isMappings = findNoneKey; | |
239 | - } else { | |
240 | - isMappings = false; | |
238 | + isMappingsKey.value = true; | |
241 | 239 | } |
242 | 240 | }); |
243 | 241 | f.mappings.forEach((f2) => { |
244 | 242 | const findNoneVal = Object.values(f2).includes(''); |
245 | 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 | 260 | deviceConfigAddOrEdit(isEdit.value ? noEditObj : postDeviceConfogData) |
257 | 261 | .then((res) => { |
258 | 262 | if (!res) return; | ... | ... |
... | ... | @@ -32,7 +32,7 @@ export const CoapSchemas: FormSchema[] = [ |
32 | 32 | { |
33 | 33 | field: 'coapDeviceType', |
34 | 34 | component: 'Select', |
35 | - label: 'CoAP 设备类型', | |
35 | + label: 'CoAP设备类型', | |
36 | 36 | defaultValue: 'DEFAULT', |
37 | 37 | componentProps: { |
38 | 38 | options: [ |
... | ... | @@ -45,7 +45,7 @@ export const CoapSchemas: FormSchema[] = [ |
45 | 45 | { |
46 | 46 | field: 'transportPayloadType', |
47 | 47 | component: 'Select', |
48 | - label: 'CoAP 设备消息 Payload', | |
48 | + label: 'CoAP设备消息Payload', | |
49 | 49 | defaultValue: 'JSON', |
50 | 50 | componentProps: { |
51 | 51 | options: [ |
... | ... | @@ -59,7 +59,7 @@ export const CoapSchemas: FormSchema[] = [ |
59 | 59 | { |
60 | 60 | field: 'powerMode', |
61 | 61 | component: 'Select', |
62 | - label: 'Power Saving Mode', | |
62 | + label: '节能模式', | |
63 | 63 | defaultValue: 'DRX', |
64 | 64 | componentProps: { |
65 | 65 | options: [ |
... | ... | @@ -76,11 +76,11 @@ export const CoapSchemas: FormSchema[] = [ |
76 | 76 | { |
77 | 77 | field: 'psmActivityTimer', |
78 | 78 | component: 'InputNumber', |
79 | - label: 'PSM Activity Timer', | |
79 | + label: 'PSM活动计时器', | |
80 | 80 | required: true, |
81 | - defaultValue: '10', | |
81 | + defaultValue: 10, | |
82 | 82 | componentProps: { |
83 | - placeholder: '请输入PSM Activity Timer', | |
83 | + placeholder: '请输入PSM活动计时器', | |
84 | 84 | }, |
85 | 85 | colProps: { span: 11 }, |
86 | 86 | ifShow: ({ values }) => isPsm(values.powerMode), |
... | ... | @@ -104,11 +104,11 @@ export const CoapSchemas: FormSchema[] = [ |
104 | 104 | { |
105 | 105 | field: 'edrxCycle', |
106 | 106 | component: 'InputNumber', |
107 | - label: 'eDRX cycle', | |
107 | + label: 'eDRX循环', | |
108 | 108 | required: true, |
109 | - defaultValue: '81', | |
109 | + defaultValue: 81, | |
110 | 110 | componentProps: { |
111 | - placeholder: '请输入PSM Activity Timer', | |
111 | + placeholder: '请输入eDRX循环', | |
112 | 112 | }, |
113 | 113 | colProps: { span: 11 }, |
114 | 114 | ifShow: ({ values }) => isDrx(values.powerMode), |
... | ... | @@ -132,11 +132,11 @@ export const CoapSchemas: FormSchema[] = [ |
132 | 132 | { |
133 | 133 | field: 'pagingTransmissionWindow', |
134 | 134 | component: 'InputNumber', |
135 | - label: 'Paging Transmission Window', | |
135 | + label: '寻呼传输窗口', | |
136 | 136 | required: true, |
137 | - defaultValue: '10', | |
137 | + defaultValue: 10, | |
138 | 138 | componentProps: { |
139 | - placeholder: '请输入Paging Transmission Window', | |
139 | + placeholder: '请输入寻呼传输窗口', | |
140 | 140 | }, |
141 | 141 | colProps: { span: 11 }, |
142 | 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 | 236 | colProps: { span: 22 }, |
237 | 237 | component: 'InputTextArea', |
238 | 238 | componentProps: { |
239 | - disabled: true, | |
240 | 239 | autoSize: { |
241 | 240 | maxRows: 50, |
242 | 241 | }, |
... | ... | @@ -250,45 +249,31 @@ export const deviceSchemas: FormSchema[] = [ |
250 | 249 | "keyName": {}, |
251 | 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 | 255 | "bootstrapServerIs": false, |
274 | 256 | "host": "0.0.0.0", |
275 | 257 | "port": 5685, |
276 | - "securityHost": "0.0.0.0", | |
277 | - "securityPort": 5686, | |
278 | - "serverId": 123, | |
279 | 258 | "clientHoldOffTime": 1, |
280 | 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 | 269 | "clientLwM2mSettings": { |
285 | 270 | "clientOnlyObserveAfterConnect": 1, |
286 | 271 | "fwUpdateStrategy": 1, |
287 | - "swUpdateStrategy": 2, | |
288 | - "swUpdateResource": "coap://localhost:5685", | |
272 | + "swUpdateStrategy": 1, | |
289 | 273 | "powerMode": "DRX", |
290 | 274 | "compositeOperationsSupport": false |
291 | 275 | }, |
276 | + "bootstrapServerUpdateEnable": false, | |
292 | 277 | "type": "LWM2M" |
293 | 278 | } |
294 | 279 | `, | ... | ... |
... | ... | @@ -11,29 +11,44 @@ |
11 | 11 | </TabPane> |
12 | 12 | <TabPane forceRender key="2" tab="Bootstrap"> |
13 | 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 | 20 | v-for="(item, index) in dynamicBOOTSTRAP.bootstrap" |
17 | 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 | 33 | </Button> |
27 | 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 | 45 | <div style="margin-top: 2vh"> |
32 | - <Button size="middle" type="dashed" @click="handleAdd"> | |
46 | + <Button size="middle" type="text" @click="handleAdd"> | |
33 | 47 | <template #icon> |
34 | 48 | <PlusCircleOutlined /> |
35 | 49 | </template> |
36 | - Add LwM2M Server | |
50 | + <span v-if="selectCheckStatus">Add server config</span> | |
51 | + <span v-else>Add LwM2M Server</span> | |
37 | 52 | </Button> |
38 | 53 | </div> |
39 | 54 | </div> |
... | ... | @@ -61,6 +76,7 @@ |
61 | 76 | <BasicForm :showResetButton="false" :showSubmitButton="false" @register="registerDevice" /> |
62 | 77 | </TabPane> |
63 | 78 | </Tabs> |
79 | + <ServerConfigModal @register="registerModal" @emitSelect="acceptEmitFunc" /> | |
64 | 80 | </div> |
65 | 81 | </template> |
66 | 82 | |
... | ... | @@ -70,8 +86,11 @@ |
70 | 86 | import { BasicForm, useForm } from '/@/components/Form'; |
71 | 87 | import { modelSchemas, settingsSchemas, deviceSchemas } from './index'; |
72 | 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 | 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 | 95 | export default defineComponent({ |
77 | 96 | name: 'index', |
... | ... | @@ -80,16 +99,21 @@ |
80 | 99 | TabPane: Tabs.TabPane, |
81 | 100 | BasicForm, |
82 | 101 | BootStrapForm, |
83 | - MinusCircleOutlined, | |
102 | + DeleteOutlined, | |
84 | 103 | Card, |
85 | 104 | PlusCircleOutlined, |
86 | 105 | Button, |
87 | 106 | Checkbox, |
107 | + CollapseContainer, | |
108 | + ServerConfigModal, | |
88 | 109 | }, |
89 | 110 | setup() { |
111 | + const collapseTitle = (item) => { | |
112 | + return `LwM2M Server Short server ID: ${item.shortServerId} Security config mode: ${item.securityMode}`; | |
113 | + }; | |
90 | 114 | const bootstrapServerUpdateEnable = ref(false); |
91 | 115 | const dynamicBOOTSTRAP: any = reactive({ |
92 | - bootstrap: [{}], | |
116 | + bootstrap: [{ securityMode: 'NO_SEC', shortServerId: 1234 }], | |
93 | 117 | }); |
94 | 118 | const dynamicBindRef: any = { |
95 | 119 | BootStrapFormItemRef: ref([]), |
... | ... | @@ -140,9 +164,20 @@ |
140 | 164 | span: 14, |
141 | 165 | }, |
142 | 166 | }); |
167 | + const [registerModal, { openModal }] = useModal(); | |
143 | 168 | |
144 | 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 | 182 | const handleRemove = (index) => { |
148 | 183 | dynamicBOOTSTRAP.bootstrap.splice(index, 1); |
... | ... | @@ -240,7 +275,32 @@ |
240 | 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 | 304 | return { |
245 | 305 | currentKey, |
246 | 306 | currentSize, |
... | ... | @@ -256,6 +316,11 @@ |
256 | 316 | handleAdd, |
257 | 317 | handleRemove, |
258 | 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 | 20 | { |
21 | 21 | field: 'deviceTelemetryTopic', |
22 | 22 | component: 'Input', |
23 | - label: '遥测数据 topic 筛选器', | |
23 | + label: '遥测数据主题筛选器', | |
24 | 24 | required: true, |
25 | 25 | defaultValue: 'v1/devices/me/telemetry', |
26 | 26 | componentProps: { |
... | ... | @@ -32,7 +32,7 @@ export const MqttSchemas: FormSchema[] = [ |
32 | 32 | field: 'deviceAttributesTopic', |
33 | 33 | component: 'Input', |
34 | 34 | required: true, |
35 | - label: 'Attributes topic filter', | |
35 | + label: '属性主题过滤器', | |
36 | 36 | defaultValue: 'v1/devices/me/attributes', |
37 | 37 | componentProps: { |
38 | 38 | placeholder: '请输入Attributes topic 筛选器', |
... | ... | @@ -64,7 +64,14 @@ export const MqttSchemas: FormSchema[] = [ |
64 | 64 | colProps: { span: 23 }, |
65 | 65 | defaultValue: false, |
66 | 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 | 75 | ifShow: ({ values }) => isProtobuf(values.transportPayloadType), |
69 | 76 | }, |
70 | 77 | { |
... | ... | @@ -73,7 +80,16 @@ export const MqttSchemas: FormSchema[] = [ |
73 | 80 | colProps: { span: 23 }, |
74 | 81 | defaultValue: false, |
75 | 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 | 93 | ifShow: ({ values }) => |
78 | 94 | isProtobuf(values.transportPayloadType) && |
79 | 95 | !!values.useJsonPayloadFormatForDefaultDownlinkTopics, | ... | ... |
... | ... | @@ -19,6 +19,28 @@ |
19 | 19 | </p> |
20 | 20 | </div> |
21 | 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 | 44 | </BasicForm> |
23 | 45 | </div> |
24 | 46 | </div> | ... | ... |
... | ... | @@ -2,7 +2,7 @@ |
2 | 2 | <div |
3 | 3 | v-for="(param, index) in dynamicInput.params" |
4 | 4 | :key="index" |
5 | - style="display: flex; margin-top: 0.2vh" | |
5 | + style="display: flex; margin-top: 0.25vh" | |
6 | 6 | > |
7 | 7 | <Select |
8 | 8 | v-model:value="param.dataType" |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | @change="emitChange" |
18 | 18 | /> |
19 | 19 | <a-input |
20 | - placeholder="请输入OID" | |
20 | + placeholder="请输入OID(不能重复)" | |
21 | 21 | v-model:value="param.oid" |
22 | 22 | style="width: 38%; margin: 0 0 5px 8px" |
23 | 23 | @change="emitChange" |
... | ... | @@ -30,7 +30,7 @@ |
30 | 30 | /> |
31 | 31 | </div> |
32 | 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 | 34 | <PlusCircleOutlined /> |
35 | 35 | Add mapping |
36 | 36 | </a-button> | ... | ... |
... | ... | @@ -16,10 +16,12 @@ |
16 | 16 | > |
17 | 17 | <InputNumber |
18 | 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 | 25 | <MappingsForm |
24 | 26 | :value="item.mappings" |
25 | 27 | @change="handleMappingsChange" |
... | ... | @@ -33,7 +35,7 @@ |
33 | 35 | style="text-align: center; line-height: 20vh" |
34 | 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 | 39 | <template #icon> |
38 | 40 | <MinusCircleOutlined /> |
39 | 41 | </template> |
... | ... | @@ -47,6 +49,7 @@ |
47 | 49 | import { Button, InputNumber } from 'ant-design-vue'; |
48 | 50 | import { MinusCircleOutlined } from '@ant-design/icons-vue'; |
49 | 51 | import MappingsForm from './MappingsForm.vue'; |
52 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
50 | 53 | |
51 | 54 | const props = defineProps({ |
52 | 55 | item: { |
... | ... | @@ -59,7 +62,8 @@ |
59 | 62 | }); |
60 | 63 | const emit = defineEmits(['removeItem']); |
61 | 64 | const dynamicHeight = ref(25); |
62 | - | |
65 | + const queryingFrequencyMs = ref(5000); | |
66 | + const { createMessage } = useMessage(); | |
63 | 67 | const handleMappingsChange = (e) => { |
64 | 68 | // eslint-disable-next-line vue/no-mutating-props |
65 | 69 | props.item.mappings = e; |
... | ... | @@ -70,9 +74,18 @@ |
70 | 74 | //设置回显的高度 |
71 | 75 | const setFieldsValueFunc = () => { |
72 | 76 | dynamicHeight.value = props.item.mappings.length * 3 + props.item.mappings.length + 15; |
77 | + queryingFrequencyMs.value = props.item.queryingFrequencyMs; | |
73 | 78 | }; |
74 | 79 | //获取表单的值 |
75 | 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 | 89 | let obj: any = {}; |
77 | 90 | obj = { |
78 | 91 | ...{ spec: props.item.spec }, |
... | ... | @@ -80,7 +93,7 @@ |
80 | 93 | ...{ |
81 | 94 | queryingFrequencyMs: |
82 | 95 | props.item.spec == 'TELEMETRY_QUERYING' || props.item.spec == 'CLIENT_ATTRIBUTES_QUERYING' |
83 | - ? props.item.queryingFrequencyMs | |
96 | + ? queryingFrequencyMs.value | |
84 | 97 | : null, |
85 | 98 | }, |
86 | 99 | }; | ... | ... |
... | ... | @@ -27,22 +27,14 @@ |
27 | 27 | :options="selectOptions" |
28 | 28 | @change="handleChange(item.spec)" |
29 | 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 | 31 | </SnmpForm> |
40 | 32 | </template> |
41 | 33 | <div |
42 | 34 | v-if="dynamicSNMP.communicationConfigs.length < 4" |
43 | 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 | 38 | <template #icon> |
47 | 39 | <PlusCircleOutlined /> |
48 | 40 | </template> |
... | ... | @@ -60,7 +52,7 @@ |
60 | 52 | import { BasicForm, useForm } from '/@/components/Form'; |
61 | 53 | import { snmpSchemas } from './config'; |
62 | 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 | 56 | import SnmpForm from './cpns/SnmpForm.vue'; |
65 | 57 | |
66 | 58 | interface mappingsI { |
... | ... | @@ -77,26 +69,22 @@ |
77 | 69 | { |
78 | 70 | label: 'Telemetry', |
79 | 71 | value: 'TELEMETRY_QUERYING', |
80 | - disabled: false, | |
81 | 72 | }, |
82 | 73 | { |
83 | 74 | label: 'Client attributes', |
84 | 75 | value: 'CLIENT_ATTRIBUTES_QUERYING', |
85 | - disabled: false, | |
86 | 76 | }, |
87 | 77 | { |
88 | 78 | label: 'Shared attributes', |
89 | 79 | value: 'SHARED_ATTRIBUTES_SETTING', |
90 | - disabled: false, | |
91 | 80 | }, |
92 | 81 | { |
93 | 82 | label: 'RPC request', |
94 | 83 | value: 'TO_DEVICE_RPC_REQUEST', |
95 | - disabled: false, | |
96 | 84 | }, |
97 | 85 | ]); |
98 | 86 | const selectValue = ref(''); |
99 | - //解决新增选择框互斥问题和编辑回显互斥问题 | |
87 | + //Select互斥 | |
100 | 88 | const handleChange = (value: string) => { |
101 | 89 | selectValue.value = value; |
102 | 90 | selectOptions.value.forEach((ele: any) => { |
... | ... | @@ -154,12 +142,15 @@ |
154 | 142 | }); |
155 | 143 | }; |
156 | 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 | 150 | dynamicSNMP.communicationConfigs.splice(index, 1); |
160 | 151 | }; |
152 | + | |
161 | 153 | //回显表单值 |
162 | - //TODO 采用这种方式动态绑定ref 都会造成回显数据顺序是随机的但不影响回显数据的正确性 | |
163 | 154 | const setStepFieldsValueFunc = (v) => { |
164 | 155 | setFieldsValue({ |
165 | 156 | timeoutMs: v.timeoutMs, |
... | ... | @@ -168,14 +159,13 @@ |
168 | 159 | dynamicSNMP.communicationConfigs = v.communicationConfigs; |
169 | 160 | dynamicSNMP.communicationConfigs.forEach((snmp, index: number) => { |
170 | 161 | nextTick(() => { |
171 | - //编辑回显互斥赋值 | |
172 | 162 | handleChange(snmp.spec); |
173 | 163 | unref(dynamicBindRef.SnmpFormItemRef)[index]?.setFieldsValueFunc(); |
174 | 164 | }); |
175 | 165 | }); |
176 | 166 | }; |
177 | 167 | |
178 | - //获取最终的值 | |
168 | + //获取表单值 | |
179 | 169 | const getSnmpForm = async () => { |
180 | 170 | let value = await validate(); |
181 | 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> | ... | ... |
src/views/report/config/config.data.ts
0 → 100644
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 | +]; | ... | ... |
src/views/report/config/index.vue
0 → 100644
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> | ... | ... |
src/views/report/export/config.data.ts
0 → 100644
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 | +]; | ... | ... |
src/views/report/export/index.vue
0 → 100644
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 | 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 | 305 | } catch { |
302 | 306 | } finally { | ... | ... |