DeviceControlConfig.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. <!-- 设备控制配置组件 -->
  2. <!-- TODO @puhui999:貌似没生效~~~ -->
  3. <template>
  4. <div class="flex flex-col gap-16px">
  5. <!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
  6. <el-row :gutter="16">
  7. <el-col :span="12">
  8. <el-form-item label="产品" required>
  9. <ProductSelector v-model="action.productId" @change="handleProductChange" />
  10. </el-form-item>
  11. </el-col>
  12. <el-col :span="12">
  13. <el-form-item label="设备" required>
  14. <DeviceSelector
  15. v-model="action.deviceId"
  16. :product-id="action.productId"
  17. @change="handleDeviceChange"
  18. />
  19. </el-form-item>
  20. </el-col>
  21. </el-row>
  22. <!-- 服务选择 - 服务调用类型时显示 -->
  23. <div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
  24. <el-form-item label="服务" required>
  25. <el-select
  26. v-model="action.identifier"
  27. placeholder="请选择服务"
  28. filterable
  29. clearable
  30. class="w-full"
  31. :loading="loadingServices"
  32. @change="handleServiceChange"
  33. >
  34. <el-option
  35. v-for="service in serviceList"
  36. :key="service.identifier"
  37. :label="service.name"
  38. :value="service.identifier"
  39. >
  40. <div class="flex items-center justify-between">
  41. <span>{{ service.name }}</span>
  42. <el-tag :type="service.callType === 'sync' ? 'primary' : 'success'" size="small">
  43. {{ service.callType === 'sync' ? '同步' : '异步' }}
  44. </el-tag>
  45. </div>
  46. </el-option>
  47. </el-select>
  48. </el-form-item>
  49. <!-- 服务参数配置 -->
  50. <div v-if="action.identifier" class="space-y-16px">
  51. <el-form-item label="服务参数" required>
  52. <div class="w-full space-y-8px">
  53. <!-- JSON 输入框 -->
  54. <div class="relative">
  55. <el-input
  56. v-model="paramsJson"
  57. type="textarea"
  58. :rows="6"
  59. placeholder="请输入JSON格式的服务参数"
  60. @input="handleParamsChange"
  61. :class="{ 'is-error': jsonError }"
  62. />
  63. <!-- 查看详细示例按钮 -->
  64. <div class="absolute top-8px right-8px">
  65. <el-button
  66. ref="exampleTriggerRef"
  67. type="info"
  68. :icon="InfoFilled"
  69. circle
  70. size="small"
  71. @click="toggleExampleDetail"
  72. title="查看详细示例"
  73. />
  74. </div>
  75. </div>
  76. <!-- 验证状态和错误提示 -->
  77. <div class="flex items-center justify-between">
  78. <div class="flex items-center gap-8px">
  79. <Icon
  80. :icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
  81. :class="
  82. jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
  83. "
  84. class="text-14px"
  85. />
  86. <span
  87. :class="
  88. jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
  89. "
  90. class="text-12px"
  91. >
  92. {{ jsonError || 'JSON格式正确' }}
  93. </span>
  94. </div>
  95. <!-- 快速填充按钮 -->
  96. <div
  97. v-if="selectedService?.inputParams?.length > 0"
  98. class="flex items-center gap-8px"
  99. >
  100. <span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
  101. <el-button size="small" type="primary" plain @click="fillServiceExampleJson">
  102. 示例数据
  103. </el-button>
  104. <el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
  105. </div>
  106. </div>
  107. </div>
  108. </el-form-item>
  109. </div>
  110. </div>
  111. <!-- 控制参数配置 - 属性设置类型时显示 -->
  112. <div v-if="action.productId && isPropertySetAction" class="space-y-16px">
  113. <!-- 参数配置 -->
  114. <el-form-item label="参数" required>
  115. <div class="w-full space-y-8px">
  116. <!-- JSON 输入框 -->
  117. <div class="relative">
  118. <el-input
  119. v-model="paramsJson"
  120. type="textarea"
  121. :rows="6"
  122. placeholder="请输入JSON格式的控制参数"
  123. @input="handleParamsChange"
  124. :class="{ 'is-error': jsonError }"
  125. />
  126. <!-- 查看详细示例按钮 -->
  127. <div class="absolute top-8px right-8px">
  128. <el-button
  129. ref="exampleTriggerRef"
  130. type="info"
  131. :icon="InfoFilled"
  132. circle
  133. size="small"
  134. @click="toggleExampleDetail"
  135. title="查看详细示例"
  136. />
  137. </div>
  138. </div>
  139. <!-- 验证状态和错误提示 -->
  140. <div class="flex items-center justify-between">
  141. <div class="flex items-center gap-8px">
  142. <Icon
  143. :icon="jsonError ? 'ep:warning' : 'ep:circle-check'"
  144. :class="
  145. jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
  146. "
  147. class="text-14px"
  148. />
  149. <span
  150. :class="
  151. jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'
  152. "
  153. class="text-12px"
  154. >
  155. {{ jsonError || 'JSON格式正确' }}
  156. </span>
  157. </div>
  158. <!-- 快速填充按钮 -->
  159. <div v-if="thingModelProperties.length > 0" class="flex items-center gap-8px">
  160. <span class="text-12px text-[var(--el-text-color-secondary)]">快速填充:</span>
  161. <el-button size="small" type="primary" plain @click="fillExampleJson">
  162. 示例数据
  163. </el-button>
  164. <el-button size="small" type="default" plain @click="clearParams"> 清空 </el-button>
  165. </div>
  166. </div>
  167. </div>
  168. </el-form-item>
  169. <!-- 详细示例弹出层 -->
  170. <Teleport to="body">
  171. <div
  172. v-if="showExampleDetail"
  173. ref="exampleDetailRef"
  174. class="example-detail-popover"
  175. :style="examplePopoverStyle"
  176. >
  177. <div
  178. class="p-16px bg-white rounded-8px shadow-lg border border-[var(--el-border-color)] min-w-400px max-w-500px"
  179. >
  180. <div class="flex items-center gap-8px mb-16px">
  181. <Icon icon="ep:document" class="text-[var(--el-color-info)] text-18px" />
  182. <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
  183. 参数配置详细示例
  184. </span>
  185. </div>
  186. <div class="space-y-16px">
  187. <!-- 服务参数示例 - 服务调用时显示 -->
  188. <div v-if="isServiceInvokeAction && selectedService?.inputParams?.length > 0">
  189. <div class="flex items-center gap-8px mb-8px">
  190. <Icon icon="ep:service" class="text-[var(--el-color-success)] text-14px" />
  191. <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
  192. 当前服务输入参数
  193. </span>
  194. </div>
  195. <div class="ml-22px space-y-8px">
  196. <div
  197. v-for="param in selectedService.inputParams.slice(0, 4)"
  198. :key="param.identifier"
  199. class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
  200. >
  201. <div class="flex-1">
  202. <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
  203. {{ param.name }}
  204. </div>
  205. <div class="text-11px text-[var(--el-text-color-secondary)]">
  206. {{ param.identifier }}
  207. </div>
  208. </div>
  209. <div class="flex items-center gap-8px">
  210. <el-tag :type="getPropertyTypeTag(param.dataType)" size="small">
  211. {{ getPropertyTypeName(param.dataType) }}
  212. </el-tag>
  213. <span class="text-11px text-[var(--el-text-color-secondary)]">
  214. {{ getExampleValueForParam(param) }}
  215. </span>
  216. </div>
  217. </div>
  218. </div>
  219. <div class="mt-12px ml-22px">
  220. <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
  221. 完整JSON格式:
  222. </div>
  223. <pre
  224. class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-success)]"
  225. ><code>{{ generateServiceExampleJson() }}</code></pre>
  226. </div>
  227. </div>
  228. <!-- 物模型属性示例 - 属性设置时显示 -->
  229. <div v-if="isPropertySetAction && thingModelProperties.length > 0">
  230. <div class="flex items-center gap-8px mb-8px">
  231. <Icon icon="ep:edit" class="text-[var(--el-color-primary)] text-14px" />
  232. <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
  233. 当前物模型属性
  234. </span>
  235. </div>
  236. <div class="ml-22px space-y-8px">
  237. <div
  238. v-for="property in thingModelProperties.slice(0, 4)"
  239. :key="property.identifier"
  240. class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
  241. >
  242. <div class="flex-1">
  243. <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
  244. {{ property.name }}
  245. </div>
  246. <div class="text-11px text-[var(--el-text-color-secondary)]">
  247. {{ property.identifier }}
  248. </div>
  249. </div>
  250. <div class="flex items-center gap-8px">
  251. <el-tag :type="getPropertyTypeTag(property.dataType)" size="small">
  252. {{ getPropertyTypeName(property.dataType) }}
  253. </el-tag>
  254. <span class="text-11px text-[var(--el-text-color-secondary)]">
  255. {{ getExampleValue(property) }}
  256. </span>
  257. </div>
  258. </div>
  259. </div>
  260. <div class="mt-12px ml-22px">
  261. <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
  262. 完整JSON格式:
  263. </div>
  264. <pre
  265. class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
  266. ><code>{{ generateExampleJson() }}</code></pre>
  267. </div>
  268. </div>
  269. <!-- 通用示例 -->
  270. <div>
  271. <div class="flex items-center gap-8px mb-8px">
  272. <Icon icon="ep:service" class="text-[var(--el-color-success)] text-14px" />
  273. <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
  274. 通用格式示例
  275. </span>
  276. </div>
  277. <div class="ml-22px space-y-8px">
  278. <div class="text-12px text-[var(--el-text-color-secondary)]">
  279. 服务调用格式:
  280. </div>
  281. <pre
  282. class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-success)]"
  283. ><code>{
  284. "method": "restart",
  285. "params": {
  286. "delay": 5,
  287. "force": false
  288. }
  289. }</code></pre>
  290. </div>
  291. </div>
  292. </div>
  293. <!-- 关闭按钮 -->
  294. <div class="flex justify-end mt-16px">
  295. <el-button size="small" @click="hideExampleDetail">关闭</el-button>
  296. </div>
  297. </div>
  298. </div>
  299. </Teleport>
  300. </div>
  301. </div>
  302. </template>
  303. <script setup lang="ts">
  304. import { useVModel } from '@vueuse/core'
  305. import { InfoFilled } from '@element-plus/icons-vue'
  306. import ProductSelector from '../selectors/ProductSelector.vue'
  307. import DeviceSelector from '../selectors/DeviceSelector.vue'
  308. import { ActionFormData, ThingModelService } from '@/api/iot/rule/scene/scene.types'
  309. import { IotRuleSceneActionTypeEnum } from '@/views/iot/utils/constants'
  310. /** 设备控制配置组件 */
  311. defineOptions({ name: 'DeviceControlConfig' })
  312. const props = defineProps<{
  313. modelValue: ActionFormData
  314. }>()
  315. const emit = defineEmits<{
  316. (e: 'update:modelValue', value: ActionFormData): void
  317. }>()
  318. const action = useVModel(props, 'modelValue', emit)
  319. const paramsJson = ref('') // 参数JSON字符串
  320. const jsonError = ref('') // JSON格式错误信息
  321. const thingModelProperties = ref<any[]>([]) // 物模型属性列表
  322. const loadingThingModel = ref(false) // 物模型加载状态
  323. const propertyValues = ref<Record<string, any>>({}) // 属性值映射
  324. const selectedService = ref<ThingModelService | null>(null) // 选中的服务对象
  325. const serviceList = ref<ThingModelService[]>([]) // 服务列表
  326. const loadingServices = ref(false) // 服务加载状态
  327. const showExampleDetail = ref(false) // 示例详情弹出层显示状态
  328. const exampleTriggerRef = ref() // 示例触发按钮引用
  329. const exampleDetailRef = ref() // 示例详情弹出层引用
  330. const examplePopoverStyle = ref({}) // 示例弹出层样式
  331. const isPropertySetAction = computed(() => {
  332. // 是否为属性设置类型
  333. return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
  334. })
  335. const isServiceInvokeAction = computed(() => {
  336. // 是否为服务调用类型
  337. return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
  338. })
  339. /**
  340. * 处理产品变化事件
  341. * @param productId 产品ID
  342. */
  343. const handleProductChange = (productId?: number) => {
  344. // 当产品变化时,清空设备选择和参数配置
  345. if (action.value.productId !== productId) {
  346. action.value.deviceId = undefined
  347. action.value.identifier = undefined // 清空服务标识符
  348. action.value.params = {}
  349. paramsJson.value = ''
  350. jsonError.value = ''
  351. propertyValues.value = {}
  352. selectedService.value = null // 清空选中的服务
  353. serviceList.value = [] // 清空服务列表
  354. }
  355. // 加载新产品的物模型属性或服务列表
  356. if (productId) {
  357. if (isPropertySetAction.value) {
  358. loadThingModelProperties(productId)
  359. } else if (isServiceInvokeAction.value) {
  360. loadServiceList(productId)
  361. }
  362. }
  363. }
  364. /**
  365. * 处理设备变化事件
  366. * @param deviceId 设备ID
  367. */
  368. const handleDeviceChange = (deviceId?: number) => {
  369. // 当设备变化时,清空参数配置
  370. if (action.value.deviceId !== deviceId) {
  371. action.value.params = {}
  372. paramsJson.value = ''
  373. jsonError.value = ''
  374. }
  375. }
  376. /**
  377. * 处理服务变化事件
  378. * @param serviceIdentifier 服务标识符
  379. */
  380. const handleServiceChange = (serviceIdentifier?: string) => {
  381. // 根据服务标识符找到对应的服务对象
  382. const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
  383. selectedService.value = service
  384. // 当服务变化时,清空参数配置并根据服务输入参数生成默认参数结构
  385. action.value.params = {}
  386. paramsJson.value = ''
  387. jsonError.value = ''
  388. // 如果选择了服务且有输入参数,生成默认参数结构
  389. if (service && service.inputParams && service.inputParams.length > 0) {
  390. const defaultParams = {}
  391. service.inputParams.forEach((param) => {
  392. defaultParams[param.identifier] = getDefaultValueForParam(param)
  393. })
  394. action.value.params = defaultParams
  395. paramsJson.value = JSON.stringify(defaultParams, null, 2)
  396. }
  397. }
  398. /**
  399. * 快速填充示例数据
  400. */
  401. const fillExampleJson = () => {
  402. const exampleData = generateExampleJson()
  403. paramsJson.value = exampleData
  404. handleParamsChange()
  405. }
  406. /**
  407. * 快速填充服务示例数据
  408. */
  409. const fillServiceExampleJson = () => {
  410. if (selectedService.value && selectedService.value.inputParams) {
  411. const exampleData = generateServiceExampleJson()
  412. paramsJson.value = exampleData
  413. handleParamsChange()
  414. }
  415. }
  416. /**
  417. * 清空参数
  418. */
  419. const clearParams = () => {
  420. paramsJson.value = ''
  421. action.value.params = {}
  422. propertyValues.value = {}
  423. jsonError.value = ''
  424. }
  425. // 更新属性值(保留但不在模板中使用)
  426. // const updatePropertyValue = (identifier: string, value: any) => {
  427. // propertyValues.value[identifier] = value
  428. // // 同步更新到 action.params
  429. // action.value.params = { ...propertyValues.value }
  430. // // 同步更新 JSON 显示
  431. // paramsJson.value = JSON.stringify(action.value.params, null, 2)
  432. // jsonError.value = ''
  433. // }
  434. /**
  435. * 加载物模型属性
  436. * @param productId 产品ID
  437. */
  438. const loadThingModelProperties = async (productId: number) => {
  439. if (!productId) {
  440. thingModelProperties.value = []
  441. return
  442. }
  443. try {
  444. loadingThingModel.value = true
  445. // TODO: 这里需要调用实际的物模型API
  446. // const response = await ProductApi.getThingModel(productId)
  447. // 暂时使用模拟数据
  448. thingModelProperties.value = [
  449. {
  450. identifier: 'BatteryLevel',
  451. name: '电池电量',
  452. dataType: 'int',
  453. description: '设备电池电量百分比'
  454. },
  455. {
  456. identifier: 'WaterLeachState',
  457. name: '漏水状态',
  458. dataType: 'bool',
  459. description: '设备漏水检测状态'
  460. },
  461. {
  462. identifier: 'Temperature',
  463. name: '温度',
  464. dataType: 'float',
  465. description: '环境温度值'
  466. },
  467. {
  468. identifier: 'Humidity',
  469. name: '湿度',
  470. dataType: 'float',
  471. description: '环境湿度值'
  472. }
  473. ]
  474. // 初始化属性值
  475. thingModelProperties.value.forEach((property) => {
  476. if (!(property.identifier in propertyValues.value)) {
  477. propertyValues.value[property.identifier] = ''
  478. }
  479. })
  480. } catch (error) {
  481. console.error('加载物模型失败:', error)
  482. thingModelProperties.value = []
  483. } finally {
  484. loadingThingModel.value = false
  485. }
  486. }
  487. /**
  488. * 加载服务列表
  489. * @param productId 产品ID
  490. */
  491. const loadServiceList = async (productId: number) => {
  492. if (!productId) {
  493. serviceList.value = []
  494. return
  495. }
  496. loadingServices.value = true
  497. try {
  498. const { ThingModelApi } = await import('@/api/iot/thingmodel')
  499. const tslData = await ThingModelApi.getThingModelTSLByProductId(productId)
  500. serviceList.value = tslData?.services || []
  501. } catch (error) {
  502. console.error('加载服务列表失败:', error)
  503. serviceList.value = []
  504. } finally {
  505. loadingServices.value = false
  506. }
  507. }
  508. /**
  509. * 从TSL加载服务信息(用于编辑模式回显)
  510. * @param productId 产品ID
  511. * @param serviceIdentifier 服务标识符
  512. */
  513. const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
  514. // 先加载服务列表
  515. await loadServiceList(productId)
  516. // 然后设置选中的服务
  517. const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
  518. if (service) {
  519. selectedService.value = service
  520. }
  521. }
  522. /**
  523. * 处理参数变化事件
  524. */
  525. const handleParamsChange = () => {
  526. try {
  527. jsonError.value = '' // 清除之前的错误
  528. if (paramsJson.value.trim()) {
  529. const parsed = JSON.parse(paramsJson.value)
  530. action.value.params = parsed
  531. // 同步更新到属性值
  532. propertyValues.value = { ...parsed }
  533. // 额外的参数验证
  534. if (typeof parsed !== 'object' || parsed === null) {
  535. jsonError.value = '参数必须是一个有效的JSON对象'
  536. return
  537. }
  538. } else {
  539. action.value.params = {}
  540. propertyValues.value = {}
  541. }
  542. } catch (error) {
  543. jsonError.value = `JSON格式错误: ${error instanceof Error ? error.message : '未知错误'}`
  544. console.error('JSON格式错误:', error)
  545. }
  546. }
  547. /**
  548. * 获取属性类型名称
  549. * @param dataType 数据类型
  550. * @returns 类型名称
  551. */
  552. const getPropertyTypeName = (dataType: string) => {
  553. const typeMap = {
  554. int: '整数',
  555. float: '浮点数',
  556. double: '双精度',
  557. text: '字符串',
  558. bool: '布尔值',
  559. enum: '枚举',
  560. date: '日期',
  561. struct: '结构体',
  562. array: '数组'
  563. }
  564. return typeMap[dataType] || dataType
  565. }
  566. /**
  567. * 根据参数类型获取默认值
  568. * @param param 参数对象
  569. * @returns 默认值
  570. */
  571. const getDefaultValueForParam = (param: any) => {
  572. switch (param.dataType) {
  573. case 'int':
  574. return 0
  575. case 'float':
  576. case 'double':
  577. return 0.0
  578. case 'bool':
  579. return false
  580. case 'text':
  581. return ''
  582. case 'enum':
  583. // 如果有枚举值,使用第一个
  584. if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
  585. return param.dataSpecs.dataSpecsList[0].value
  586. }
  587. return ''
  588. default:
  589. return ''
  590. }
  591. }
  592. /**
  593. * 获取属性类型标签样式
  594. * @param dataType 数据类型
  595. * @returns 标签类型
  596. */
  597. const getPropertyTypeTag = (dataType: string) => {
  598. const tagMap = {
  599. int: 'primary',
  600. float: 'success',
  601. double: 'success',
  602. text: 'info',
  603. bool: 'warning',
  604. enum: 'danger',
  605. date: 'primary',
  606. struct: 'info',
  607. array: 'warning'
  608. }
  609. return tagMap[dataType] || 'info'
  610. }
  611. /**
  612. * 获取属性示例值
  613. * @param property 属性对象
  614. * @returns 示例值
  615. */
  616. const getExampleValue = (property: any) => {
  617. switch (property.dataType) {
  618. case 'int':
  619. return property.identifier === 'BatteryLevel' ? '85' : '25'
  620. case 'float':
  621. case 'double':
  622. return property.identifier === 'Temperature' ? '25.5' : '60.0'
  623. case 'bool':
  624. return 'false'
  625. case 'text':
  626. return '"auto"'
  627. case 'enum':
  628. return '"option1"'
  629. default:
  630. return '""'
  631. }
  632. }
  633. /**
  634. * 获取参数示例值
  635. * @param param 参数对象
  636. * @returns 示例值
  637. */
  638. const getExampleValueForParam = (param: any) => {
  639. switch (param.dataType) {
  640. case 'int':
  641. return '0'
  642. case 'float':
  643. case 'double':
  644. return '0.0'
  645. case 'bool':
  646. return 'false'
  647. case 'text':
  648. return '"text"'
  649. case 'enum':
  650. if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
  651. return `"${param.dataSpecs.dataSpecsList[0].name}"`
  652. }
  653. return '"option1"'
  654. default:
  655. return '""'
  656. }
  657. }
  658. /**
  659. * 生成示例JSON
  660. * @returns JSON字符串
  661. */
  662. const generateExampleJson = () => {
  663. if (thingModelProperties.value.length === 0) {
  664. return JSON.stringify(
  665. {
  666. BatteryLevel: '',
  667. WaterLeachState: ''
  668. },
  669. null,
  670. 2
  671. )
  672. }
  673. const example = {}
  674. thingModelProperties.value.forEach((property) => {
  675. switch (property.dataType) {
  676. case 'int':
  677. example[property.identifier] = property.identifier === 'BatteryLevel' ? 85 : 25
  678. break
  679. case 'float':
  680. case 'double':
  681. example[property.identifier] = property.identifier === 'Temperature' ? 25.5 : 60.0
  682. break
  683. case 'bool':
  684. example[property.identifier] = false
  685. break
  686. case 'text':
  687. example[property.identifier] = 'auto'
  688. break
  689. default:
  690. example[property.identifier] = ''
  691. }
  692. })
  693. return JSON.stringify(example, null, 2)
  694. }
  695. /**
  696. * 生成服务示例JSON
  697. * @returns JSON字符串
  698. */
  699. const generateServiceExampleJson = () => {
  700. if (!selectedService.value || !selectedService.value.inputParams) {
  701. return JSON.stringify({}, null, 2)
  702. }
  703. const example = {}
  704. selectedService.value.inputParams.forEach((param) => {
  705. example[param.identifier] = getDefaultValueForParam(param)
  706. })
  707. return JSON.stringify(example, null, 2)
  708. }
  709. /**
  710. * 切换示例详情弹出层显示状态
  711. */
  712. const toggleExampleDetail = () => {
  713. if (showExampleDetail.value) {
  714. hideExampleDetail()
  715. } else {
  716. showExampleDetailPopover()
  717. }
  718. }
  719. /**
  720. * 显示示例详情弹出层
  721. */
  722. const showExampleDetailPopover = () => {
  723. if (!exampleTriggerRef.value) return
  724. showExampleDetail.value = true
  725. nextTick(() => {
  726. updateExamplePopoverPosition()
  727. })
  728. }
  729. /**
  730. * 隐藏示例详情弹出层
  731. */
  732. const hideExampleDetail = () => {
  733. showExampleDetail.value = false
  734. }
  735. /**
  736. * 更新示例弹出层位置
  737. */
  738. const updateExamplePopoverPosition = () => {
  739. if (!exampleTriggerRef.value || !exampleDetailRef.value) return
  740. const triggerEl = exampleTriggerRef.value.$el
  741. const triggerRect = triggerEl.getBoundingClientRect()
  742. // 计算弹出层位置
  743. const left = triggerRect.left + triggerRect.width + 8
  744. const top = triggerRect.top
  745. // 检查是否超出视窗右边界
  746. const popoverWidth = 500 // 最大宽度
  747. const viewportWidth = window.innerWidth
  748. let finalLeft = left
  749. if (left + popoverWidth > viewportWidth - 16) {
  750. // 如果超出右边界,显示在左侧
  751. finalLeft = triggerRect.left - popoverWidth - 8
  752. }
  753. // 检查是否超出视窗下边界
  754. let finalTop = top
  755. const popoverHeight = exampleDetailRef.value.offsetHeight || 300
  756. const viewportHeight = window.innerHeight
  757. if (top + popoverHeight > viewportHeight - 16) {
  758. finalTop = Math.max(16, viewportHeight - popoverHeight - 16)
  759. }
  760. examplePopoverStyle.value = {
  761. position: 'fixed',
  762. left: `${finalLeft}px`,
  763. top: `${finalTop}px`,
  764. zIndex: 9999
  765. }
  766. }
  767. /**
  768. * 点击外部关闭弹出层
  769. * @param event 鼠标事件
  770. */
  771. const handleClickOutside = (event: MouseEvent) => {
  772. if (
  773. showExampleDetail.value &&
  774. exampleDetailRef.value &&
  775. exampleTriggerRef.value &&
  776. !exampleDetailRef.value.contains(event.target as Node) &&
  777. !exampleTriggerRef.value.$el.contains(event.target as Node)
  778. ) {
  779. hideExampleDetail()
  780. }
  781. }
  782. /**
  783. * 监听窗口大小变化,重新计算弹出层位置
  784. */
  785. const handleResize = () => {
  786. if (showExampleDetail.value) {
  787. updateExamplePopoverPosition()
  788. }
  789. }
  790. /**
  791. * 组件初始化
  792. */
  793. onMounted(() => {
  794. if (action.value.params && Object.keys(action.value.params).length > 0) {
  795. try {
  796. paramsJson.value = JSON.stringify(action.value.params, null, 2)
  797. propertyValues.value = { ...action.value.params }
  798. jsonError.value = '' // 清除错误状态
  799. } catch (error) {
  800. console.error('初始化参数格式化失败:', error)
  801. jsonError.value = '初始参数格式错误'
  802. }
  803. }
  804. // 如果已经选择了产品且是属性设置类型,加载物模型
  805. if (action.value.productId && isPropertySetAction.value) {
  806. loadThingModelProperties(action.value.productId)
  807. }
  808. // 如果是服务调用类型且已有标识符,初始化服务选择
  809. if (action.value.productId && isServiceInvokeAction.value && action.value.identifier) {
  810. // 加载物模型TSL以获取服务信息
  811. loadServiceFromTSL(action.value.productId, action.value.identifier)
  812. }
  813. // 添加事件监听器
  814. document.addEventListener('click', handleClickOutside)
  815. window.addEventListener('resize', handleResize)
  816. })
  817. /**
  818. * 组件卸载时清理事件监听器
  819. */
  820. onUnmounted(() => {
  821. document.removeEventListener('click', handleClickOutside)
  822. window.removeEventListener('resize', handleResize)
  823. })
  824. // 监听参数变化
  825. watch(
  826. () => action.value.params,
  827. (newParams) => {
  828. if (newParams && typeof newParams === 'object' && Object.keys(newParams).length > 0) {
  829. try {
  830. const newJsonString = JSON.stringify(newParams, null, 2)
  831. // 只有当JSON字符串真正改变时才更新,避免循环更新
  832. if (newJsonString !== paramsJson.value) {
  833. paramsJson.value = newJsonString
  834. jsonError.value = ''
  835. }
  836. } catch (error) {
  837. console.error('参数格式化失败:', error)
  838. jsonError.value = '参数格式化失败'
  839. }
  840. } else {
  841. // 参数为空时清空JSON显示
  842. if (paramsJson.value !== '') {
  843. paramsJson.value = ''
  844. jsonError.value = ''
  845. }
  846. }
  847. },
  848. { deep: true }
  849. )
  850. // 监听action.value变化,处理编辑模式的数据回显
  851. watch(
  852. () => action.value,
  853. async (newAction) => {
  854. if (newAction) {
  855. // 处理服务调用的数据回显
  856. if (isServiceInvokeAction.value && newAction.productId) {
  857. if (newAction.identifier) {
  858. // 编辑模式:加载服务信息并设置选中的服务
  859. await loadServiceFromTSL(newAction.productId, newAction.identifier)
  860. } else {
  861. // 新建模式:只加载服务列表
  862. await loadServiceList(newAction.productId)
  863. }
  864. } else if (isServiceInvokeAction.value) {
  865. // 清空服务选择
  866. selectedService.value = null
  867. serviceList.value = []
  868. }
  869. // 处理参数回显
  870. if (newAction.params && Object.keys(newAction.params).length > 0) {
  871. try {
  872. const newJsonString = JSON.stringify(newAction.params, null, 2)
  873. if (paramsJson.value !== newJsonString) {
  874. paramsJson.value = newJsonString
  875. propertyValues.value = { ...newAction.params }
  876. jsonError.value = ''
  877. }
  878. } catch (error) {
  879. console.error('参数格式化失败:', error)
  880. jsonError.value = '参数格式化失败'
  881. }
  882. } else {
  883. if (paramsJson.value !== '') {
  884. paramsJson.value = ''
  885. propertyValues.value = {}
  886. jsonError.value = ''
  887. }
  888. }
  889. }
  890. },
  891. { deep: true, immediate: true }
  892. )
  893. </script>
  894. <style scoped>
  895. /* 参考 PropertySelector 的弹出层样式 */
  896. @keyframes fadeInScale {
  897. from {
  898. opacity: 0;
  899. transform: scale(0.9) translateY(-4px);
  900. }
  901. to {
  902. opacity: 1;
  903. transform: scale(1) translateY(0);
  904. }
  905. }
  906. .example-detail-popover {
  907. animation: fadeInScale 0.2s ease-out;
  908. transform-origin: top left;
  909. }
  910. /* 弹出层箭头效果 */
  911. .example-detail-popover::before {
  912. position: absolute;
  913. top: 20px;
  914. left: -8px;
  915. width: 0;
  916. height: 0;
  917. border-top: 8px solid transparent;
  918. border-right: 8px solid var(--el-border-color);
  919. border-bottom: 8px solid transparent;
  920. content: '';
  921. }
  922. .example-detail-popover::after {
  923. position: absolute;
  924. top: 20px;
  925. left: -7px;
  926. width: 0;
  927. height: 0;
  928. border-top: 8px solid transparent;
  929. border-right: 8px solid white;
  930. border-bottom: 8px solid transparent;
  931. content: '';
  932. }
  933. :deep(.example-content code) {
  934. font-family: 'Courier New', monospace;
  935. color: var(--el-color-primary);
  936. }
  937. </style>