gaoluyang
2026-06-24 c0cb161bb52ce0fbdce5c66ec391d107c75e2452
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
<script lang="ts" setup>
import type { IotDeviceApi } from '#/api/iot/device/device';
import type { IotProductApi } from '#/api/iot/product/product';
 
import { computed, nextTick, onMounted, ref, watch } from 'vue';
 
import { useVbenModal } from '..\..\..\..\..\packages\effects\common-ui\src';
 
import { Button, Collapse, message, Space } from 'ant-design-vue';
 
import { useVbenForm } from '#/adapter/form';
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
import { getSimpleProductList } from '#/api/iot/product/product';
import { MapDialog } from '#/components/map';
import { $t } from '#/locales';
 
import { useAdvancedFormSchema, useBasicFormSchema } from '../data';
 
const emit = defineEmits(['success']);
const formData = ref<IotDeviceApi.Device>();
const products = ref<IotProductApi.Product[]>([]);
const activeKey = ref<string[]>([]);
const mapDialogRef = ref<InstanceType<typeof MapDialog>>();
 
const getTitle = computed(() => {
  return formData.value?.id
    ? $t('ui.actionTitle.edit', ['设备'])
    : $t('ui.actionTitle.create', ['设备']);
});
 
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-2',
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: useBasicFormSchema(),
  showDefaultActions: false,
  handleValuesChange: async (values, changedFields) => {
    // 当产品 ProductId 变化时,自动设置设备类型
    if (changedFields.includes('productId')) {
      const productId = values.productId;
      if (!productId) {
        await formApi.setFieldValue('deviceType', undefined);
        return;
      }
      // 从产品列表中查找产品
      const product = products.value.find((item) => item.id === productId);
      if (product?.deviceType !== undefined) {
        await formApi.setFieldValue('deviceType', product.deviceType);
      }
    }
  },
});
 
const [AdvancedForm, advancedFormApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-2',
    labelWidth: 100,
  },
  layout: 'horizontal',
  schema: useAdvancedFormSchema(),
  showDefaultActions: false,
});
 
/** 获取高级表单的值(如果表单未挂载,则从 formData 中获取) */
async function getAdvancedFormValues() {
  if (advancedFormApi.isMounted) {
    return await advancedFormApi.getValues();
  }
  // 表单未挂载(折叠状态),从 formData 中获取
  return {
    nickname: formData.value?.nickname,
    picUrl: formData.value?.picUrl,
    groupIds: formData.value?.groupIds,
    serialNumber: formData.value?.serialNumber,
    longitude: formData.value?.longitude,
    latitude: formData.value?.latitude,
  };
}
 
/** 打开地图选择弹窗 */
async function openMapDialog() {
  // 如果高级表单未挂载,先展开 Collapse
  if (!advancedFormApi.isMounted) {
    activeKey.value = ['advanced'];
    await nextTick();
    await nextTick();
  }
  const values = await advancedFormApi.getValues();
  mapDialogRef.value?.open(
    values.longitude ? Number(values.longitude) : undefined,
    values.latitude ? Number(values.latitude) : undefined,
  );
}
 
/** 处理地图选择确认 */
async function handleMapConfirm(data: {
  address: string;
  latitude: string;
  longitude: string;
}) {
  if (advancedFormApi.isMounted) {
    await advancedFormApi.setFieldValue('longitude', Number(data.longitude));
    await advancedFormApi.setFieldValue('latitude', Number(data.latitude));
  }
}
 
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    // 高级表单:先单独校验,再做经纬度成对填写的跨字段检查
    if (advancedFormApi.isMounted) {
      const { valid: advancedValid } = await advancedFormApi.validate();
      if (!advancedValid) {
        return;
      }
      const advValues = await advancedFormApi.getValues();
      const hasLongitude =
        advValues.longitude !== undefined &&
        advValues.longitude !== null &&
        advValues.longitude !== '';
      const hasLatitude =
        advValues.latitude !== undefined &&
        advValues.latitude !== null &&
        advValues.latitude !== '';
      if (hasLongitude !== hasLatitude) {
        message.warning(
          hasLongitude ? '请同时填写设备纬度' : '请同时填写设备经度',
        );
        return;
      }
    }
    modalApi.lock();
    // 合并两个表单的值(字段不冲突,可直接合并)
    const basicValues = await formApi.getValues();
    const advancedValues = await getAdvancedFormValues();
    const data = {
      ...basicValues,
      ...advancedValues,
    } as IotDeviceApi.Device;
    try {
      await (formData.value?.id ? updateDevice(data) : createDevice(data));
      // 关闭并提示
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      activeKey.value = [];
      return;
    }
    // 加载数据
    const data = modalApi.getData<IotDeviceApi.Device>();
    if (!data || !data.id) {
      return;
    }
    // 编辑模式:加载数据
    modalApi.lock();
    try {
      formData.value = await getDevice(data.id);
      await formApi.setValues(formData.value);
    } finally {
      modalApi.unlock();
    }
  },
});
 
/** 监听 Collapse 展开,自动设置高级表单的值 */
watch(
  activeKey,
  async (newKeys) => {
    // 当用户手动展开 Collapse 且存在表单数据时,设置高级表单的值
    if (newKeys.includes('advanced') && formData.value) {
      // 等待表单挂载
      await nextTick();
      await nextTick();
      if (advancedFormApi.isMounted) {
        await advancedFormApi.setValues(formData.value);
      }
    }
  },
  { immediate: false },
);
 
/** 初始化产品列表 */
onMounted(async () => {
  products.value = await getSimpleProductList();
});
</script>
 
<template>
  <Modal :title="getTitle" class="w-2/5">
    <div class="mx-4">
      <Form />
      <Collapse v-model:active-key="activeKey" class="mt-4">
        <Collapse.Panel key="advanced" header="更多设置">
          <AdvancedForm />
          <Space class="mt-2">
            <Button type="primary" @click="openMapDialog">坐标拾取</Button>
          </Space>
        </Collapse.Panel>
      </Collapse>
    </div>
  </Modal>
  <!-- 地图选择弹窗 -->
  <MapDialog ref="mapDialogRef" @confirm="handleMapConfirm" />
</template>