JsonParamsInput.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <!-- JSON参数输入组件 - 通用版本 -->
  2. <template>
  3. <!-- 参数配置 -->
  4. <div class="w-full space-y-12px">
  5. <!-- JSON 输入框 -->
  6. <div class="relative">
  7. <el-input
  8. v-model="paramsJson"
  9. type="textarea"
  10. :rows="4"
  11. :placeholder="placeholder"
  12. @input="handleParamsChange"
  13. :class="{ 'is-error': jsonError }"
  14. />
  15. <!-- 查看详细示例弹出层 -->
  16. <div class="absolute top-8px right-8px">
  17. <el-popover
  18. placement="left-start"
  19. :width="450"
  20. trigger="click"
  21. :show-arrow="true"
  22. :offset="8"
  23. popper-class="json-params-detail-popover"
  24. >
  25. <template #reference>
  26. <el-button
  27. type="info"
  28. :icon="InfoFilled"
  29. circle
  30. size="small"
  31. :title="JSON_PARAMS_INPUT_CONSTANTS.VIEW_EXAMPLE_TITLE"
  32. />
  33. </template>
  34. <!-- 弹出层内容 -->
  35. <div class="json-params-detail-content">
  36. <div class="flex items-center gap-8px mb-16px">
  37. <Icon :icon="titleIcon" class="text-[var(--el-color-primary)] text-18px" />
  38. <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
  39. {{ title }}
  40. </span>
  41. </div>
  42. <div class="space-y-16px">
  43. <!-- 参数列表 -->
  44. <div v-if="paramsList.length > 0">
  45. <div class="flex items-center gap-8px mb-8px">
  46. <Icon :icon="paramsIcon" class="text-[var(--el-color-primary)] text-14px" />
  47. <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
  48. {{ paramsLabel }}
  49. </span>
  50. </div>
  51. <div class="ml-22px space-y-8px">
  52. <div
  53. v-for="param in paramsList"
  54. :key="param.identifier"
  55. class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
  56. >
  57. <div class="flex-1">
  58. <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
  59. {{ param.name }}
  60. <el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
  61. {{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
  62. </el-tag>
  63. </div>
  64. <div class="text-11px text-[var(--el-text-color-secondary)]">
  65. {{ param.identifier }}
  66. </div>
  67. </div>
  68. <div class="flex items-center gap-8px">
  69. <el-tag :type="getParamTypeTag(param.dataType)" size="small">
  70. {{ getParamTypeName(param.dataType) }}
  71. </el-tag>
  72. <span class="text-11px text-[var(--el-text-color-secondary)]">
  73. {{ getExampleValue(param) }}
  74. </span>
  75. </div>
  76. </div>
  77. </div>
  78. <div class="mt-12px ml-22px">
  79. <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
  80. {{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
  81. </div>
  82. <pre
  83. 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)]"
  84. >
  85. <code>{{ generateExampleJson() }}</code>
  86. </pre>
  87. </div>
  88. </div>
  89. <!-- 无参数提示 -->
  90. <div v-else>
  91. <div class="text-center py-16px">
  92. <p class="text-14px text-[var(--el-text-color-secondary)]">{{ emptyMessage }}</p>
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. </el-popover>
  98. </div>
  99. </div>
  100. <!-- 验证状态和错误提示 -->
  101. <div class="flex items-center justify-between">
  102. <div class="flex items-center gap-8px">
  103. <Icon
  104. :icon="
  105. jsonError
  106. ? JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.ERROR
  107. : JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.SUCCESS
  108. "
  109. :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
  110. class="text-14px"
  111. />
  112. <span
  113. :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
  114. class="text-12px"
  115. >
  116. {{ jsonError || JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_CORRECT }}
  117. </span>
  118. </div>
  119. <!-- 快速填充按钮 -->
  120. <div v-if="paramsList.length > 0" class="flex items-center gap-8px">
  121. <span class="text-12px text-[var(--el-text-color-secondary)]">{{
  122. JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL
  123. }}</span>
  124. <el-button size="small" type="primary" plain @click="fillExampleJson">
  125. {{ JSON_PARAMS_INPUT_CONSTANTS.EXAMPLE_DATA_BUTTON }}
  126. </el-button>
  127. <el-button size="small" type="danger" plain @click="clearParams">{{
  128. JSON_PARAMS_INPUT_CONSTANTS.CLEAR_BUTTON
  129. }}</el-button>
  130. </div>
  131. </div>
  132. </div>
  133. </template>
  134. <script setup lang="ts">
  135. import { useVModel } from '@vueuse/core'
  136. import { InfoFilled } from '@element-plus/icons-vue'
  137. import {
  138. IoTDataSpecsDataTypeEnum,
  139. JSON_PARAMS_INPUT_CONSTANTS,
  140. JSON_PARAMS_INPUT_ICONS,
  141. JSON_PARAMS_EXAMPLE_VALUES,
  142. JsonParamsInputTypeEnum,
  143. type JsonParamsInputType
  144. } from '@/views/iot/utils/constants'
  145. /** JSON参数输入组件 - 通用版本 */
  146. defineOptions({ name: 'JsonParamsInput' })
  147. interface JsonParamsConfig {
  148. // 服务配置
  149. service?: {
  150. name: string
  151. inputParams?: any[]
  152. }
  153. // 事件配置
  154. event?: {
  155. name: string
  156. outputParams?: any[]
  157. }
  158. // 属性配置
  159. properties?: any[]
  160. // 自定义配置
  161. custom?: {
  162. name: string
  163. params: any[]
  164. }
  165. }
  166. interface Props {
  167. modelValue?: string
  168. config?: JsonParamsConfig
  169. type?: JsonParamsInputType
  170. placeholder?: string
  171. }
  172. interface Emits {
  173. (e: 'update:modelValue', value: string): void
  174. }
  175. const props = withDefaults(defineProps<Props>(), {
  176. type: JsonParamsInputTypeEnum.SERVICE,
  177. placeholder: JSON_PARAMS_INPUT_CONSTANTS.PLACEHOLDER
  178. })
  179. const emit = defineEmits<Emits>()
  180. const localValue = useVModel(props, 'modelValue', emit, {
  181. defaultValue: ''
  182. })
  183. const paramsJson = ref('') // JSON参数字符串
  184. const jsonError = ref('') // JSON验证错误信息
  185. // 计算属性:参数列表
  186. const paramsList = computed(() => {
  187. switch (props.type) {
  188. case JsonParamsInputTypeEnum.SERVICE:
  189. return props.config?.service?.inputParams || []
  190. case JsonParamsInputTypeEnum.EVENT:
  191. return props.config?.event?.outputParams || []
  192. case JsonParamsInputTypeEnum.PROPERTY:
  193. return props.config?.properties || []
  194. case JsonParamsInputTypeEnum.CUSTOM:
  195. return props.config?.custom?.params || []
  196. default:
  197. return []
  198. }
  199. })
  200. // 计算属性:标题
  201. const title = computed(() => {
  202. switch (props.type) {
  203. case JsonParamsInputTypeEnum.SERVICE:
  204. return JSON_PARAMS_INPUT_CONSTANTS.TITLES.SERVICE(props.config?.service?.name)
  205. case JsonParamsInputTypeEnum.EVENT:
  206. return JSON_PARAMS_INPUT_CONSTANTS.TITLES.EVENT(props.config?.event?.name)
  207. case JsonParamsInputTypeEnum.PROPERTY:
  208. return JSON_PARAMS_INPUT_CONSTANTS.TITLES.PROPERTY
  209. case JsonParamsInputTypeEnum.CUSTOM:
  210. return JSON_PARAMS_INPUT_CONSTANTS.TITLES.CUSTOM(props.config?.custom?.name)
  211. default:
  212. return JSON_PARAMS_INPUT_CONSTANTS.TITLES.DEFAULT
  213. }
  214. })
  215. // 计算属性:标题图标
  216. const titleIcon = computed(() => {
  217. switch (props.type) {
  218. case JsonParamsInputTypeEnum.SERVICE:
  219. return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.SERVICE
  220. case JsonParamsInputTypeEnum.EVENT:
  221. return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.EVENT
  222. case JsonParamsInputTypeEnum.PROPERTY:
  223. return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.PROPERTY
  224. case JsonParamsInputTypeEnum.CUSTOM:
  225. return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.CUSTOM
  226. default:
  227. return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.DEFAULT
  228. }
  229. })
  230. // 计算属性:参数图标
  231. const paramsIcon = computed(() => {
  232. switch (props.type) {
  233. case JsonParamsInputTypeEnum.SERVICE:
  234. return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.SERVICE
  235. case JsonParamsInputTypeEnum.EVENT:
  236. return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.EVENT
  237. case JsonParamsInputTypeEnum.PROPERTY:
  238. return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.PROPERTY
  239. case JsonParamsInputTypeEnum.CUSTOM:
  240. return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.CUSTOM
  241. default:
  242. return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.DEFAULT
  243. }
  244. })
  245. // 计算属性:参数标签
  246. const paramsLabel = computed(() => {
  247. switch (props.type) {
  248. case JsonParamsInputTypeEnum.SERVICE:
  249. return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.SERVICE
  250. case JsonParamsInputTypeEnum.EVENT:
  251. return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.EVENT
  252. case JsonParamsInputTypeEnum.PROPERTY:
  253. return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.PROPERTY
  254. case JsonParamsInputTypeEnum.CUSTOM:
  255. return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.CUSTOM
  256. default:
  257. return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.DEFAULT
  258. }
  259. })
  260. // 计算属性:空状态消息
  261. const emptyMessage = computed(() => {
  262. switch (props.type) {
  263. case JsonParamsInputTypeEnum.SERVICE:
  264. return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.SERVICE
  265. case JsonParamsInputTypeEnum.EVENT:
  266. return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.EVENT
  267. case JsonParamsInputTypeEnum.PROPERTY:
  268. return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.PROPERTY
  269. case JsonParamsInputTypeEnum.CUSTOM:
  270. return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.CUSTOM
  271. default:
  272. return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.DEFAULT
  273. }
  274. })
  275. // 计算属性:无配置消息
  276. const noConfigMessage = computed(() => {
  277. switch (props.type) {
  278. case JsonParamsInputTypeEnum.SERVICE:
  279. return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.SERVICE
  280. case JsonParamsInputTypeEnum.EVENT:
  281. return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.EVENT
  282. case JsonParamsInputTypeEnum.PROPERTY:
  283. return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.PROPERTY
  284. case JsonParamsInputTypeEnum.CUSTOM:
  285. return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.CUSTOM
  286. default:
  287. return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.DEFAULT
  288. }
  289. })
  290. /**
  291. * 处理参数变化事件
  292. */
  293. const handleParamsChange = () => {
  294. try {
  295. jsonError.value = '' // 清除之前的错误
  296. if (paramsJson.value.trim()) {
  297. const parsed = JSON.parse(paramsJson.value)
  298. localValue.value = paramsJson.value
  299. // 额外的参数验证
  300. if (typeof parsed !== 'object' || parsed === null) {
  301. jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT
  302. return
  303. }
  304. // 验证必填参数
  305. for (const param of paramsList.value) {
  306. if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
  307. jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(param.name)
  308. return
  309. }
  310. }
  311. } else {
  312. localValue.value = ''
  313. }
  314. // 验证通过
  315. jsonError.value = ''
  316. } catch (error) {
  317. jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
  318. error instanceof Error ? error.message : JSON_PARAMS_INPUT_CONSTANTS.UNKNOWN_ERROR
  319. )
  320. }
  321. }
  322. /**
  323. * 快速填充示例数据
  324. */
  325. const fillExampleJson = () => {
  326. paramsJson.value = generateExampleJson()
  327. handleParamsChange()
  328. }
  329. /**
  330. * 清空参数
  331. */
  332. const clearParams = () => {
  333. paramsJson.value = ''
  334. localValue.value = ''
  335. jsonError.value = ''
  336. }
  337. /**
  338. * 获取参数类型名称
  339. * @param dataType 数据类型
  340. * @returns 类型名称
  341. */
  342. const getParamTypeName = (dataType: string) => {
  343. // 使用 constants.ts 中已有的 getDataTypeName 函数逻辑
  344. const typeMap = {
  345. [IoTDataSpecsDataTypeEnum.INT]: '整数',
  346. [IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
  347. [IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
  348. [IoTDataSpecsDataTypeEnum.TEXT]: '字符串',
  349. [IoTDataSpecsDataTypeEnum.BOOL]: '布尔值',
  350. [IoTDataSpecsDataTypeEnum.ENUM]: '枚举',
  351. [IoTDataSpecsDataTypeEnum.DATE]: '日期',
  352. [IoTDataSpecsDataTypeEnum.STRUCT]: '结构体',
  353. [IoTDataSpecsDataTypeEnum.ARRAY]: '数组'
  354. }
  355. return typeMap[dataType] || dataType
  356. }
  357. /**
  358. * 获取参数类型标签样式
  359. * @param dataType 数据类型
  360. * @returns 标签样式
  361. */
  362. const getParamTypeTag = (dataType: string) => {
  363. const tagMap = {
  364. [IoTDataSpecsDataTypeEnum.INT]: 'primary',
  365. [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
  366. [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
  367. [IoTDataSpecsDataTypeEnum.TEXT]: 'info',
  368. [IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
  369. [IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
  370. [IoTDataSpecsDataTypeEnum.DATE]: 'primary',
  371. [IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
  372. [IoTDataSpecsDataTypeEnum.ARRAY]: 'warning'
  373. }
  374. return tagMap[dataType] || 'info'
  375. }
  376. /**
  377. * 获取示例值
  378. * @param param 参数对象
  379. * @returns 示例值
  380. */
  381. const getExampleValue = (param: any) => {
  382. const exampleConfig =
  383. JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
  384. return exampleConfig.display
  385. }
  386. /**
  387. * 生成示例JSON
  388. * @returns JSON字符串
  389. */
  390. const generateExampleJson = () => {
  391. if (paramsList.value.length === 0) {
  392. return '{}'
  393. }
  394. const example = {}
  395. paramsList.value.forEach((param) => {
  396. const exampleConfig =
  397. JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
  398. example[param.identifier] = exampleConfig.value
  399. })
  400. return JSON.stringify(example, null, 2)
  401. }
  402. /**
  403. * 处理数据回显
  404. * @param value 值字符串
  405. */
  406. const handleDataDisplay = (value: string) => {
  407. if (!value || !value.trim()) {
  408. paramsJson.value = ''
  409. jsonError.value = ''
  410. return
  411. }
  412. try {
  413. // 尝试解析JSON,如果成功则格式化
  414. const parsed = JSON.parse(value)
  415. paramsJson.value = JSON.stringify(parsed, null, 2)
  416. jsonError.value = ''
  417. } catch {
  418. // 如果不是有效的JSON,直接使用原字符串
  419. paramsJson.value = value
  420. jsonError.value = ''
  421. }
  422. }
  423. // 监听外部值变化(编辑模式数据回显)
  424. watch(
  425. () => localValue.value,
  426. async (newValue, oldValue) => {
  427. // 避免循环更新
  428. if (newValue === oldValue) return
  429. // 使用 nextTick 确保在下一个 tick 中处理数据
  430. await nextTick()
  431. handleDataDisplay(newValue || '')
  432. },
  433. { immediate: true }
  434. )
  435. // 组件挂载后也尝试处理一次数据回显
  436. onMounted(async () => {
  437. await nextTick()
  438. if (localValue.value) {
  439. handleDataDisplay(localValue.value)
  440. }
  441. })
  442. // 监听配置变化
  443. watch(
  444. () => props.config,
  445. (newConfig, oldConfig) => {
  446. // 只有在配置真正变化时才清空数据
  447. if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
  448. // 如果没有外部传入的值,才清空数据
  449. if (!localValue.value) {
  450. paramsJson.value = ''
  451. jsonError.value = ''
  452. }
  453. }
  454. }
  455. )
  456. </script>
  457. <style scoped>
  458. /* 弹出层内容样式 */
  459. .json-params-detail-content {
  460. padding: 4px 0;
  461. }
  462. /* 弹出层自定义样式 */
  463. :global(.json-params-detail-popover) {
  464. max-width: 500px !important;
  465. }
  466. :global(.json-params-detail-popover .el-popover__content) {
  467. padding: 16px !important;
  468. }
  469. /* JSON 代码块样式 */
  470. .json-params-detail-content pre {
  471. max-height: 200px;
  472. overflow-y: auto;
  473. }
  474. </style>