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> | ... | ... |