Showing
9 changed files
with
157 additions
and
69 deletions
1 | 1 | <script lang="ts"> |
2 | 2 | export default { |
3 | + components: { Spin }, | |
3 | 4 | inheritAttrs: false, |
4 | 5 | }; |
5 | 6 | </script> |
6 | 7 | <script lang="ts" setup> |
8 | + import { Spin } from 'ant-design-vue'; | |
7 | 9 | import { RadioRecord } from '../../detail/config/util'; |
8 | 10 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; |
9 | 11 | import { useSendCommand } from './useSendCommand'; |
12 | + import { ref } from 'vue'; | |
10 | 13 | |
11 | 14 | interface VisualComponentProps<Layout = Recordable, Value = ControlComponentValue> { |
12 | 15 | value?: Value; |
... | ... | @@ -23,33 +26,45 @@ |
23 | 26 | const emit = defineEmits(['update:value', 'change']); |
24 | 27 | |
25 | 28 | const { sendCommand } = useSendCommand(); |
26 | - const handleChange = (event: Event) => { | |
29 | + | |
30 | + const loading = ref(false); | |
31 | + const handleChange = async (event: Event) => { | |
27 | 32 | const _value = (event.target as HTMLInputElement).checked; |
33 | + if (props.value) { | |
34 | + loading.value = true; | |
35 | + const flag = await sendCommand(props.value, _value); | |
36 | + loading.value = false; | |
37 | + if (!flag) { | |
38 | + (event.target as HTMLInputElement).checked = !_value; | |
39 | + return; | |
40 | + } | |
41 | + } | |
28 | 42 | emit('update:value', _value); |
29 | 43 | emit('change', _value); |
30 | - sendCommand(props.value!, _value); | |
31 | 44 | }; |
32 | 45 | </script> |
33 | 46 | |
34 | 47 | <template> |
35 | 48 | <div class="flex flex-col justify-center"> |
36 | - <label class="sliding-switch"> | |
37 | - <input | |
38 | - :value="!!Number(props.value?.value)" | |
39 | - type="checkbox" | |
40 | - :checked="!!Number(props.value?.value)" | |
41 | - @change="handleChange" | |
42 | - /> | |
43 | - <span class="slider"></span> | |
44 | - <span class="on">ON</span> | |
45 | - <span class="off">OFF</span> | |
46 | - </label> | |
47 | - <div | |
48 | - class="text-center mt-2 text-gray-700" | |
49 | - :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
50 | - > | |
51 | - {{ props.value?.attributeRename || props.value?.attribute }}</div | |
52 | - > | |
49 | + <Spin :spinning="loading"> | |
50 | + <label class="sliding-switch"> | |
51 | + <input | |
52 | + :value="!!Number(props.value?.value)" | |
53 | + type="checkbox" | |
54 | + :checked="!!Number(props.value?.value)" | |
55 | + @change="handleChange" | |
56 | + /> | |
57 | + <span class="slider"></span> | |
58 | + <span class="on">ON</span> | |
59 | + <span class="off">OFF</span> | |
60 | + </label> | |
61 | + <div | |
62 | + class="text-center mt-2 text-gray-700" | |
63 | + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
64 | + > | |
65 | + {{ props.value?.attributeRename || props.value?.attribute }}</div | |
66 | + > | |
67 | + </Spin> | |
53 | 68 | </div> |
54 | 69 | </template> |
55 | 70 | ... | ... |
... | ... | @@ -31,8 +31,14 @@ |
31 | 31 | const checked = ref(!!Number(props.value.value)); |
32 | 32 | |
33 | 33 | const { sendCommand } = useSendCommand(); |
34 | - const handleChange = (value: boolean) => { | |
35 | - sendCommand(props.value, value); | |
34 | + const loading = ref(false); | |
35 | + const handleChange = async (value: boolean) => { | |
36 | + loading.value = true; | |
37 | + const flag = await sendCommand(props.value, value); | |
38 | + loading.value = false; | |
39 | + if (!flag) { | |
40 | + checked.value = !value; | |
41 | + } | |
36 | 42 | }; |
37 | 43 | |
38 | 44 | watchEffect(() => { |
... | ... | @@ -59,6 +65,6 @@ |
59 | 65 | {{ props.value.attributeRename || props.value.attribute }} |
60 | 66 | </span> |
61 | 67 | </div> |
62 | - <Switch v-model:checked="checked" @change="handleChange" /> | |
68 | + <Switch v-model:checked="checked" :loading="loading" @change="handleChange" /> | |
63 | 69 | </div> |
64 | 70 | </template> | ... | ... |
1 | 1 | <script lang="ts"> |
2 | 2 | export default { |
3 | + components: { Spin }, | |
3 | 4 | inheritAttrs: false, |
4 | 5 | }; |
5 | 6 | </script> |
... | ... | @@ -8,6 +9,8 @@ |
8 | 9 | import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util'; |
9 | 10 | import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config'; |
10 | 11 | import { useSendCommand } from './useSendCommand'; |
12 | + import { ref } from 'vue'; | |
13 | + import { Spin } from 'ant-design-vue'; | |
11 | 14 | |
12 | 15 | const props = defineProps<{ |
13 | 16 | value?: ControlComponentValue; |
... | ... | @@ -18,11 +21,20 @@ |
18 | 21 | const emit = defineEmits(['update:value', 'change']); |
19 | 22 | |
20 | 23 | const { sendCommand } = useSendCommand(); |
21 | - const handleChange = (event: Event) => { | |
24 | + const loading = ref(false); | |
25 | + const handleChange = async (event: Event) => { | |
22 | 26 | const _value = (event.target as HTMLInputElement).checked; |
27 | + if (props.value) { | |
28 | + loading.value = true; | |
29 | + const flag = await sendCommand(props.value, _value); | |
30 | + loading.value = false; | |
31 | + if (!flag) { | |
32 | + (event.target as HTMLInputElement).checked = !_value; | |
33 | + return; | |
34 | + } | |
35 | + } | |
23 | 36 | emit('update:value', _value); |
24 | 37 | emit('change', _value); |
25 | - sendCommand(props.value!, _value); | |
26 | 38 | }; |
27 | 39 | |
28 | 40 | const getRadio = computed(() => { |
... | ... | @@ -32,35 +44,37 @@ |
32 | 44 | |
33 | 45 | <template> |
34 | 46 | <div class="flex flex-col"> |
35 | - <div | |
36 | - class="toggle-switch" | |
37 | - :style="{ | |
38 | - width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }), | |
39 | - height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }), | |
40 | - }" | |
41 | - > | |
42 | - <label class="switch"> | |
43 | - <input | |
44 | - :value="!!Number(props.value?.value)" | |
45 | - type="checkbox" | |
46 | - :checked="!!Number(props.value?.value)" | |
47 | - @change="handleChange" | |
48 | - /> | |
49 | - <div class="button"> | |
50 | - <div class="light"></div> | |
51 | - <div class="dots"></div> | |
52 | - <div class="characters"></div> | |
53 | - <div class="shine"></div> | |
54 | - <div class="shadow"></div> | |
55 | - </div> | |
56 | - </label> | |
57 | - </div> | |
58 | - <div | |
59 | - class="text-center mt-2 text-gray-700" | |
60 | - :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
61 | - > | |
62 | - {{ props.value?.attributeRename || props.value?.attribute }}</div | |
63 | - > | |
47 | + <Spin :spinning="loading"> | |
48 | + <div | |
49 | + class="toggle-switch" | |
50 | + :style="{ | |
51 | + width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }), | |
52 | + height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }), | |
53 | + }" | |
54 | + > | |
55 | + <label class="switch"> | |
56 | + <input | |
57 | + :value="!!Number(props.value?.value)" | |
58 | + type="checkbox" | |
59 | + :checked="!!Number(props.value?.value)" | |
60 | + @change="handleChange" | |
61 | + /> | |
62 | + <div class="button"> | |
63 | + <div class="light"></div> | |
64 | + <div class="dots"></div> | |
65 | + <div class="characters"></div> | |
66 | + <div class="shine"></div> | |
67 | + <div class="shadow"></div> | |
68 | + </div> | |
69 | + </label> | |
70 | + </div> | |
71 | + <div | |
72 | + class="text-center mt-2 text-gray-700" | |
73 | + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }" | |
74 | + > | |
75 | + {{ props.value?.attributeRename || props.value?.attribute }}</div | |
76 | + > | |
77 | + </Spin> | |
64 | 78 | </div> |
65 | 79 | </template> |
66 | 80 | ... | ... |
... | ... | @@ -5,33 +5,44 @@ import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; |
5 | 5 | import { getModelServices } from '/@/api/device/modelOfMatter'; |
6 | 6 | import { useMessage } from '/@/hooks/web/useMessage'; |
7 | 7 | import { isString } from '/@/utils/is'; |
8 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | |
8 | 9 | |
9 | 10 | const { createMessage } = useMessage(); |
10 | 11 | export function useSendCommand() { |
12 | + const error = () => { | |
13 | + createMessage.error('下发指令失败'); | |
14 | + return false; | |
15 | + }; | |
11 | 16 | const sendCommand = async (record: ControlComponentValue, value: any) => { |
12 | - if (!record) return; | |
17 | + if (!record) return error(); | |
13 | 18 | const { deviceProfileId, attribute, deviceType } = record; |
14 | 19 | let { deviceId } = record; |
15 | - if (!deviceId) return; | |
20 | + if (!deviceId) return error(); | |
16 | 21 | try { |
17 | 22 | const list = await getDeviceProfile(); |
18 | 23 | const deviceProfile = list.find((item) => item.id === deviceProfileId); |
19 | - if (!deviceProfile) return; | |
24 | + if (!deviceProfile) return error(); | |
25 | + | |
20 | 26 | let params: string | Recordable = { |
21 | 27 | [attribute!]: Number(value), |
22 | 28 | }; |
23 | - if (deviceProfile.transportType === 'TCP') { | |
24 | - const serviceList = await getModelServices({ deviceProfileId: deviceProfileId! }); | |
29 | + | |
30 | + // 如果是TCP设备从物模型中获取下发命令(TCP网关子设备无物模型服务与事件) | |
31 | + if (deviceProfile!.transportType === TransportTypeEnum.TCP) { | |
32 | + const serviceList = (await getModelServices({ deviceProfileId: deviceProfileId! })) || []; | |
25 | 33 | const record = serviceList.find((item) => item.identifier === attribute); |
26 | 34 | const sendCommand = record?.functionJson.inputData?.at(0)?.serviceCommand || ''; |
27 | 35 | params = isString(sendCommand) ? sendCommand : JSON.stringify(sendCommand); |
28 | 36 | } |
37 | + | |
29 | 38 | if (deviceType === DeviceTypeEnum.SENSOR) { |
30 | 39 | deviceId = await getDeviceRelation({ |
31 | 40 | deviceId, |
32 | 41 | isSlave: deviceType === DeviceTypeEnum.SENSOR, |
33 | 42 | }); |
34 | 43 | } |
44 | + | |
45 | + // 控制按钮下发命令为0 或 1 | |
35 | 46 | await sendCommandOneway({ |
36 | 47 | deviceId, |
37 | 48 | value: { |
... | ... | @@ -44,7 +55,11 @@ export function useSendCommand() { |
44 | 55 | }, |
45 | 56 | }); |
46 | 57 | createMessage.success('命令下发成功'); |
47 | - } catch (error) {} | |
58 | + } catch (msg) { | |
59 | + return error(); | |
60 | + } finally { | |
61 | + return true; | |
62 | + } | |
48 | 63 | }; |
49 | 64 | return { |
50 | 65 | sendCommand, | ... | ... |
... | ... | @@ -57,6 +57,13 @@ |
57 | 57 | } |
58 | 58 | }; |
59 | 59 | |
60 | + const resetFormFields = async () => { | |
61 | + const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]); | |
62 | + for (const id of hasExistEl) { | |
63 | + await dataSourceEl[id]?.resetFields(); | |
64 | + } | |
65 | + }; | |
66 | + | |
60 | 67 | const validate = async () => { |
61 | 68 | await basicMethod.validate(); |
62 | 69 | await validateDataSourceField(); |
... | ... | @@ -258,6 +265,16 @@ |
258 | 265 | return isControlComponent(props.frontId as FrontComponent); |
259 | 266 | }); |
260 | 267 | |
268 | + watch( | |
269 | + () => props.frontId, | |
270 | + async (target, oldTarget) => { | |
271 | + if (isControlComponent(oldTarget!)) return; | |
272 | + if (isControlComponent(target!)) { | |
273 | + await resetFormFields(); | |
274 | + } | |
275 | + } | |
276 | + ); | |
277 | + | |
261 | 278 | onMounted(() => handleSort()); |
262 | 279 | |
263 | 280 | defineExpose({ |
... | ... | @@ -293,7 +310,7 @@ |
293 | 310 | |
294 | 311 | <section ref="formListEl"> |
295 | 312 | <div v-for="item in dataSource" :data-id="item.id" :key="item.id" class="flex bg-light-50"> |
296 | - <div class="w-24 text-right flex justify-end"> 选择设备 </div> | |
313 | + <div class="w-24 text-right flex justify-end" style="flex: 0 0 96px"> 选择设备 </div> | |
297 | 314 | <div class="pl-2 flex-auto"> |
298 | 315 | <component |
299 | 316 | :frontId="$props.frontId" | ... | ... |
... | ... | @@ -9,6 +9,8 @@ import { getModelServices } from '/@/api/device/modelOfMatter'; |
9 | 9 | import { findDictItemByCode } from '/@/api/system/dict'; |
10 | 10 | import { DeviceTypeEnum } from '/@/api/device/model/deviceModel'; |
11 | 11 | import { DataTypeEnum } from '/@/components/Form/src/externalCompns/components/StructForm/config'; |
12 | +import { TransportTypeEnum } from '/@/views/device/profiles/components/TransportDescript/const'; | |
13 | +import { nextTick } from 'vue'; | |
12 | 14 | |
13 | 15 | export enum BasicConfigField { |
14 | 16 | NAME = 'name', |
... | ... | @@ -66,7 +68,7 @@ export const isMapComponent = (frontId: FrontComponent) => { |
66 | 68 | }; |
67 | 69 | |
68 | 70 | const isTcpProfile = (transportType: string) => { |
69 | - return transportType === 'TCP'; | |
71 | + return transportType === TransportTypeEnum.TCP; | |
70 | 72 | }; |
71 | 73 | |
72 | 74 | export const basicSchema: FormSchema[] = [ |
... | ... | @@ -117,6 +119,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
117 | 119 | component: 'ApiSelect', |
118 | 120 | label: '设备类型', |
119 | 121 | colProps: { span: 8 }, |
122 | + rules: [{ message: '请选择设备类型', required: true }], | |
120 | 123 | // defaultValue: DeviceTypeEnum.SENSOR, |
121 | 124 | componentProps: ({ formActionType }) => { |
122 | 125 | const { setFieldsValue } = formActionType; |
... | ... | @@ -180,7 +183,7 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
180 | 183 | colProps: { span: 8 }, |
181 | 184 | rules: [{ required: true, message: '组织为必填项' }], |
182 | 185 | componentProps({ formActionType }) { |
183 | - const { setFieldsValue } = formActionType; | |
186 | + const { setFieldsValue, getFieldsValue } = formActionType; | |
184 | 187 | return { |
185 | 188 | placeholder: '请选择组织', |
186 | 189 | api: async () => { |
... | ... | @@ -192,6 +195,9 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
192 | 195 | setFieldsValue({ |
193 | 196 | [DataSourceField.DEVICE_ID]: null, |
194 | 197 | }); |
198 | + nextTick(() => { | |
199 | + console.log('org change', getFieldsValue()); | |
200 | + }); | |
195 | 201 | }, |
196 | 202 | getPopupContainer: () => document.body, |
197 | 203 | }; |
... | ... | @@ -250,11 +256,24 @@ export const dataSourceSchema = (isEdit: boolean, frontId?: FrontComponent): For |
250 | 256 | component: 'ApiSelect', |
251 | 257 | label: '属性', |
252 | 258 | colProps: { span: 8 }, |
253 | - rules: [{ required: true, message: '属性为必填项' }], | |
259 | + dynamicRules: ({ model }) => { | |
260 | + const transportType = model[DataSourceField.TRANSPORT_TYPE]; | |
261 | + return [ | |
262 | + { | |
263 | + required: true, | |
264 | + message: `${ | |
265 | + isControlComponent(frontId as FrontComponent) && isTcpProfile(transportType) | |
266 | + ? '服务' | |
267 | + : '属性' | |
268 | + }为必填项`, | |
269 | + }, | |
270 | + ]; | |
271 | + }, | |
254 | 272 | componentProps({ formModel }) { |
255 | 273 | const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID]; |
256 | 274 | const transportType = formModel[DataSourceField.TRANSPORT_TYPE]; |
257 | - if (isEdit && ![deviceProfileId, transportType].every(Boolean)) return {}; | |
275 | + if (isEdit && ![deviceProfileId, transportType].every(Boolean)) | |
276 | + return { placeholder: '请选择属性', getPopupContainer: () => document.body }; | |
258 | 277 | return { |
259 | 278 | api: async () => { |
260 | 279 | try { | ... | ... |
... | ... | @@ -126,7 +126,7 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { |
126 | 126 | const { subscriptionId, data = {} } = res; |
127 | 127 | if (isNullAndUnDef(subscriptionId)) return; |
128 | 128 | const mappingRecord = cmdIdMapping.get(subscriptionId); |
129 | - if (!mappingRecord) return; | |
129 | + if (!mappingRecord || !data) return; | |
130 | 130 | mappingRecord.forEach((item) => { |
131 | 131 | const { attribute, recordIndex, dataSourceIndex } = item; |
132 | 132 | const [[timespan, value]] = data[attribute]; | ... | ... |
... | ... | @@ -249,17 +249,19 @@ |
249 | 249 | </div> |
250 | 250 | </div> |
251 | 251 | <div class="flex justify-between mt-4 text-sm" style="color: #999"> |
252 | - <div> | |
252 | + <div class="flex min-w-20 mr-3"> | |
253 | 253 | <span> |
254 | 254 | {{ item.viewType === ViewType.PRIVATE_VIEW ? '私有看板' : '公共看板' }} |
255 | 255 | </span> |
256 | 256 | <span v-if="item.viewType === ViewType.PUBLIC_VIEW"> |
257 | 257 | <Tooltip title="点击复制分享链接"> |
258 | - <ShareAltOutlined class="ml-2" @click.stop="handleCopyShareUrl(item)" /> | |
258 | + <ShareAltOutlined class="ml-1" @click.stop="handleCopyShareUrl(item)" /> | |
259 | 259 | </Tooltip> |
260 | 260 | </span> |
261 | 261 | </div> |
262 | - <div>{{ item.createTime }}</div> | |
262 | + <Tooltip placement="topLeft" :title="item.createTime"> | |
263 | + <div class="truncate">{{ item.createTime }}</div> | |
264 | + </Tooltip> | |
263 | 265 | </div> |
264 | 266 | </section> |
265 | 267 | </Card> | ... | ... |