HttpSetter.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. <!--
  2. * @Author: liuJie
  3. * @Date: 2026-01-25 22:08:04
  4. * @LastEditors: liuJie
  5. * @LastEditTime: 2026-01-25 22:24:34
  6. * @Describe: http设置器
  7. -->
  8. <script lang="ts" setup>
  9. import { watch, ref } from 'vue'
  10. import { Input, IconButton } from '@repo/ui'
  11. import { clone, isEqual } from 'lodash-es'
  12. const props = withDefaults(
  13. defineProps<{
  14. data: any
  15. }>(),
  16. {
  17. data: {}
  18. }
  19. )
  20. const emit = defineEmits<{
  21. update: [data: unknown]
  22. }>()
  23. const DEFAULT_DATA: Record<string, any> = {
  24. method: 'GET',
  25. url: '',
  26. headers: [],
  27. params: [],
  28. bodyType: 'json',
  29. body: '',
  30. verifySSL: true,
  31. timeoutConfig: {
  32. connect: 8,
  33. read: 6,
  34. write: 1
  35. },
  36. output: {
  37. body: '',
  38. status_code: 200,
  39. headers: [],
  40. files: []
  41. },
  42. errorConfig: {
  43. retry: true,
  44. max_retry: 3,
  45. retry_delay: 100
  46. },
  47. exception: 'none',
  48. exceptionDefaultValue: {
  49. body: '',
  50. status_code: 0,
  51. headers: '{}'
  52. }
  53. }
  54. const formData = ref<Record<string, any>>(clone(DEFAULT_DATA))
  55. const formDataItem = { key: '', value: '', type: 'text' }
  56. const defaultItem = { key: '', value: '' }
  57. const headers = ref([{ key: '', value: '' }])
  58. const params = ref([{ key: '', value: '' }])
  59. const body = ref<string | Record<string, any>[]>('')
  60. const normalizeBodyValue = (body: any, bodyType: string) => {
  61. if (body == null) return ''
  62. if (typeof body === 'string' || Array.isArray(body)) {
  63. return body
  64. }
  65. if (typeof body === 'object' && 'type' in body && 'data' in body) {
  66. const sourceType = body.type || bodyType
  67. const sourceData = Array.isArray(body.data) ? body.data : []
  68. if (['form-data', 'x-www-form-urlencoded'].includes(sourceType)) {
  69. return sourceData.map((item: any) => ({
  70. key: item?.key ?? '',
  71. value: item?.value ?? '',
  72. type: item?.type ?? 'text'
  73. }))
  74. }
  75. if (sourceType === 'json' || sourceType === 'raw' || sourceType === 'binary') {
  76. const value = sourceData?.[0]?.value
  77. if (typeof value === 'string') {
  78. return value
  79. }
  80. return value != null ? String(value) : ''
  81. }
  82. return ''
  83. }
  84. try {
  85. return JSON.stringify(body, null, 2)
  86. } catch {
  87. return ''
  88. }
  89. }
  90. const normalizeIncomingData = (raw: any) => {
  91. const merged = {
  92. ...clone(DEFAULT_DATA),
  93. ...(raw || {})
  94. }
  95. const bodyType = merged.bodyType || raw?.body?.type || 'json'
  96. const bodyValue = normalizeBodyValue(raw?.body ?? merged.body, bodyType)
  97. return {
  98. ...merged,
  99. method: (raw?.method || merged.method || 'GET').toUpperCase(),
  100. bodyType,
  101. body: bodyValue,
  102. headers: raw?.headers || raw?.heads || merged.headers || [],
  103. verifySSL: raw?.verifySSL ?? raw?.ssl_verify ?? merged.verifySSL,
  104. timeoutConfig: {
  105. connect: raw?.timeoutConfig?.connect ?? raw?.timeout_config?.max_connect_timeout ?? 8,
  106. read: raw?.timeoutConfig?.read ?? raw?.timeout_config?.max_read_timeout ?? 6,
  107. write: raw?.timeoutConfig?.write ?? raw?.timeout_config?.max_write_timeout ?? 1
  108. },
  109. errorConfig: {
  110. retry: raw?.errorConfig?.retry ?? raw?.retry_config?.retry_enabled ?? true,
  111. max_retry: raw?.errorConfig?.max_retry ?? raw?.retry_config?.max_retries ?? 3,
  112. retry_delay: raw?.errorConfig?.retry_delay ?? raw?.retry_config?.retry_interval ?? 100
  113. }
  114. }
  115. }
  116. watch(
  117. () => props.data,
  118. (newVal) => {
  119. const normalizedData = normalizeIncomingData(newVal)
  120. if (!isEqual(normalizedData, formData.value)) {
  121. formData.value = normalizedData
  122. headers.value = formData.value.headers?.length
  123. ? [...formData.value.headers, { key: '', value: '' }]
  124. : [{ key: '', value: '' }]
  125. params.value = formData.value.params?.length
  126. ? [...formData.value.params, { key: '', value: '' }]
  127. : [{ key: '', value: '' }]
  128. if (['form-data', 'x-www-form-urlencoded'].includes(formData.value.bodyType)) {
  129. const bodyArray = Array.isArray(formData.value.body) ? formData.value.body : []
  130. const item = formData.value.bodyType === 'form-data' ? formDataItem : defaultItem
  131. body.value = bodyArray.length ? [...bodyArray, { ...item }] : [{ ...item }]
  132. } else {
  133. body.value = typeof formData.value.body === 'string' ? formData.value.body : ''
  134. }
  135. }
  136. },
  137. {
  138. deep: true,
  139. immediate: true
  140. }
  141. )
  142. const methodOptions = [
  143. { label: 'GET', value: 'GET' },
  144. { label: 'POST', value: 'POST' },
  145. { label: 'PUT', value: 'PUT' },
  146. { label: 'DELETE', value: 'DELETE' },
  147. { label: 'PATCH', value: 'PATCH' }
  148. ]
  149. const bodyTypeOptions = [
  150. { label: 'none', value: 'none' },
  151. { label: 'form-data', value: 'form-data' },
  152. { label: 'x-www-form-urlencoded', value: 'x-www-form-urlencoded' },
  153. { label: 'raw', value: 'raw' },
  154. { label: 'json', value: 'json' },
  155. { label: 'binary', value: 'binary' }
  156. ]
  157. const exceptionOptions = [
  158. { label: '无', value: 'none' },
  159. { label: '默认值', value: 'default_value' },
  160. { label: '异常分支', value: 'exception_branch' }
  161. ]
  162. watch(
  163. () => formData.value,
  164. (value) => {
  165. emit('update', value)
  166. },
  167. { deep: true }
  168. )
  169. watch(
  170. () => headers.value,
  171. (val) => {
  172. formData.value.headers = val.filter((item) => item.key && item.value)
  173. }
  174. )
  175. watch(
  176. () => params.value,
  177. (val) => {
  178. formData.value.params = val.filter((item) => item.key && item.value)
  179. }
  180. )
  181. watch(
  182. () => body.value,
  183. (val) => {
  184. if (typeof val === 'object') {
  185. val = val.filter((item) => item.key)
  186. }
  187. formData.value.body = val
  188. }
  189. )
  190. const handleAddHeader = (index: number) => {
  191. if (index === headers.value.length - 1) {
  192. headers.value.push({ key: '', value: '' })
  193. }
  194. }
  195. const handleAddParam = (index: number) => {
  196. if (index === params.value.length - 1) {
  197. params.value.push({ key: '', value: '' })
  198. }
  199. }
  200. const handleDeleteHeader = (index: number) => {
  201. headers.value.splice(index, 1)
  202. if (headers.value.length === 0) {
  203. headers.value.push({ key: '', value: '' })
  204. }
  205. }
  206. const handleDeleteParam = (index: number) => {
  207. params.value.splice(index, 1)
  208. if (params.value.length === 0) {
  209. params.value.push({ key: '', value: '' })
  210. }
  211. }
  212. const handleChangeBodyType = (type: string) => {
  213. if (['form-data', 'x-www-form-urlencoded'].includes(type)) {
  214. const item = type === 'form-data' ? formDataItem : defaultItem
  215. body.value = [{ ...item }]
  216. formData.value.body = [{ ...item }]
  217. } else {
  218. body.value = ''
  219. formData.value.body = ''
  220. }
  221. }
  222. const handleAddBody = (index: number) => {
  223. if (index !== body.value.length - 1) return
  224. const item = formData.value.bodyType === 'form-data' ? formDataItem : defaultItem
  225. if (typeof body.value === 'string') return
  226. body.value = [...body.value, { ...item }]
  227. }
  228. const handleDeleteBody = (index: number) => {
  229. if (typeof body.value === 'string') return
  230. body.value.splice(index, 1)
  231. if (body.value.length === 0) {
  232. const item = formData.value.bodyType === 'form-data' ? formDataItem : defaultItem
  233. body.value = [{ ...item }]
  234. }
  235. }
  236. </script>
  237. <template>
  238. <el-scrollbar class="w-full box-border p-12px">
  239. <el-form label-width="50px">
  240. <el-form-item label="API" label-position="top">
  241. <div class="w-full flex gap-8px">
  242. <el-select
  243. style="width: 100px"
  244. :options="methodOptions"
  245. v-model="formData.method"
  246. placeholder="请选择"
  247. >
  248. </el-select>
  249. <el-input class="flex-1" v-model="formData.url" placeholder="URL..."></el-input>
  250. </div>
  251. </el-form-item>
  252. <el-form-item label="HEADERS" label-position="top">
  253. <el-table :data="headers" border>
  254. <el-table-column align="center" prop="key" label="键">
  255. <template #default="{ row }">
  256. <Input v-model="row.key" variant="borderless" placeholder="请输入" />
  257. </template>
  258. </el-table-column>
  259. <el-table-column align="center" prop="value" label="值">
  260. <template #default="{ row, $index }">
  261. <div class="relative">
  262. <Input
  263. v-model="row.value"
  264. variant="borderless"
  265. placeholder="请输入"
  266. @focus="handleAddHeader($index)"
  267. />
  268. <IconButton
  269. class="absolute right-0 top-5px"
  270. icon="ep:delete"
  271. link
  272. @click="handleDeleteHeader($index)"
  273. />
  274. </div>
  275. </template>
  276. </el-table-column>
  277. </el-table>
  278. </el-form-item>
  279. <el-form-item label="PARAMS" label-position="top">
  280. <el-table :data="params" border>
  281. <el-table-column align="center" prop="key" label="键">
  282. <template #default="{ row }">
  283. <Input v-model="row.key" variant="borderless" placeholder="请输入" />
  284. </template>
  285. </el-table-column>
  286. <el-table-column align="center" prop="value" label="值">
  287. <template #default="{ row, $index }">
  288. <div class="relative">
  289. <Input
  290. v-model="row.value"
  291. variant="borderless"
  292. placeholder="请输入"
  293. @focus="handleAddParam($index)"
  294. />
  295. <IconButton
  296. class="absolute right-0 top-5px"
  297. icon="ep:delete"
  298. link
  299. @click="handleDeleteParam($index)"
  300. />
  301. </div>
  302. </template>
  303. </el-table-column>
  304. </el-table>
  305. </el-form-item>
  306. <el-form-item label="BODY" label-position="top">
  307. <el-radio-group v-model="formData.bodyType" @change="handleChangeBodyType">
  308. <el-radio v-for="type in bodyTypeOptions" :key="type.value" :value="type.value">{{
  309. type.label
  310. }}</el-radio>
  311. </el-radio-group>
  312. </el-form-item>
  313. <div
  314. v-if="formData.bodyType === 'form-data' || formData.bodyType === 'x-www-form-urlencoded'"
  315. class="mb-12px"
  316. >
  317. <el-table :data="body" border>
  318. <el-table-column align="center" prop="key" label="键">
  319. <template #default="{ row }">
  320. <Input v-model="row.key" variant="borderless" placeholder="请输入" />
  321. </template>
  322. </el-table-column>
  323. <el-table-column
  324. v-if="formData.bodyType === 'form-data'"
  325. align="center"
  326. prop="type"
  327. label="类型"
  328. >
  329. <template #default="{ row }">
  330. <el-select
  331. v-model="row.type"
  332. placeholder="请选择"
  333. :options="[
  334. { label: 'text', value: 'text' },
  335. { label: 'file', value: 'file' }
  336. ]"
  337. />
  338. </template>
  339. </el-table-column>
  340. <el-table-column align="center" prop="value" label="值">
  341. <template #default="{ row, $index }">
  342. <div class="relative">
  343. <Input
  344. v-model="row.value"
  345. variant="borderless"
  346. placeholder="请输入"
  347. @focus="handleAddBody($index)"
  348. />
  349. <IconButton
  350. class="absolute right-0 top-5px"
  351. icon="ep:delete"
  352. link
  353. @click="handleDeleteBody($index)"
  354. />
  355. </div>
  356. </template>
  357. </el-table-column>
  358. </el-table>
  359. </div>
  360. <div v-if="['json', 'raw', 'binary'].includes(formData.bodyType)" class="mb-12px">
  361. <el-input
  362. v-model="formData.body"
  363. :type="formData.bodyType != 'binary' ? 'textarea' : ''"
  364. :placeholder="formData.bodyType != 'binary' ? '请输入' : '请输入变量'"
  365. :autosize="{ minRows: 5, maxRows: 10 }"
  366. />
  367. </div>
  368. <el-form-item label="验证SSL证书" label-width="120px" label-position="left">
  369. <div class="w-full text-right">
  370. <el-switch v-model="formData.verifySSL"></el-switch>
  371. </div>
  372. </el-form-item>
  373. <el-collapse>
  374. <el-collapse-item title="超时设置" name="1">
  375. <el-form-item label="连接超时" label-width="120px" label-position="top">
  376. <el-input-number
  377. v-model="formData.timeoutConfig.connect"
  378. :min="1"
  379. :max="10"
  380. controls-position="right"
  381. style="width: 100%"
  382. suffix="s"
  383. placeholder="请输入连接超时"
  384. ></el-input-number>
  385. </el-form-item>
  386. <el-form-item label="读取超时" label-width="120px" label-position="top">
  387. <el-input-number
  388. v-model="formData.timeoutConfig.read"
  389. :min="1"
  390. :max="10"
  391. controls-position="right"
  392. style="width: 100%"
  393. suffix="s"
  394. placeholder="请输入连接超时"
  395. ></el-input-number>
  396. </el-form-item>
  397. <el-form-item label="写入超时" label-width="120px" label-position="top">
  398. <el-input-number
  399. v-model="formData.timeoutConfig.write"
  400. :min="1"
  401. :max="10"
  402. controls-position="right"
  403. style="width: 100%"
  404. suffix="s"
  405. placeholder="请输入连接超时"
  406. ></el-input-number>
  407. </el-form-item>
  408. </el-collapse-item>
  409. <el-collapse-item title="输出变量" name="2">
  410. <ul>
  411. <li>
  412. <div>
  413. <span class="text-#333">body</span>
  414. <span class="text-#999 ml-8px">string</span>
  415. </div>
  416. <div class="text-#666">响应内容</div>
  417. </li>
  418. <li>
  419. <div>
  420. <span class="text-#333">status_code</span>
  421. <span class="text-#999 ml-8px">number</span>
  422. </div>
  423. <div class="text-#666">响应状态码</div>
  424. </li>
  425. <li>
  426. <div>
  427. <span class="text-#333">headers</span>
  428. <span class="text-#999 ml-8px">object</span>
  429. </div>
  430. <div class="text-#666">响应头列表JSON</div>
  431. </li>
  432. <li>
  433. <div>
  434. <span class="text-#333">files</span>
  435. <span class="text-#999 ml-8px">Array[File]</span>
  436. </div>
  437. <div class="text-#666">文件列表</div>
  438. </li>
  439. </ul>
  440. </el-collapse-item>
  441. </el-collapse>
  442. <el-form-item label="失败时重试" label-width="120px" label-position="left">
  443. <div class="w-full text-right">
  444. <el-switch v-model="formData.errorConfig.retry"></el-switch>
  445. </div>
  446. </el-form-item>
  447. <div v-if="formData.errorConfig.retry" class="flex items-center mb-12px">
  448. <div class="w-150px text-12px text-gray-500">最大重试次数</div>
  449. <div class="flex-1 flex items-center gap-8px">
  450. <el-slider
  451. v-model="formData.errorConfig.max_retry"
  452. :max="10"
  453. :min="1"
  454. :step="1"
  455. style="flex: 1"
  456. ></el-slider>
  457. <el-input-number
  458. v-model="formData.errorConfig.max_retry"
  459. :min="1"
  460. :max="10"
  461. controls-position="right"
  462. style="flex: 1"
  463. ></el-input-number>
  464. </div>
  465. </div>
  466. <div v-if="formData.errorConfig.retry" class="flex items-center mb-12px">
  467. <div class="w-150px text-12px text-gray-500">重试次数间隔时间(ms)</div>
  468. <div class="flex-1 flex items-center gap-8px">
  469. <el-slider
  470. v-model="formData.errorConfig.retry_delay"
  471. :max="5000"
  472. :min="100"
  473. :step="1"
  474. style="flex: 1"
  475. ></el-slider>
  476. <el-input-number
  477. v-model="formData.errorConfig.retry_delay"
  478. :max="5000"
  479. :min="100"
  480. controls-position="right"
  481. style="flex: 1"
  482. ></el-input-number>
  483. </div>
  484. </div>
  485. <el-form-item label="异常处理" label-width="90px" label-position="left">
  486. <div class="w-full text-right">
  487. <el-select
  488. v-model="formData.exception"
  489. :options="exceptionOptions"
  490. style="width: 120px"
  491. />
  492. </div>
  493. </el-form-item>
  494. <div v-if="formData.exception === 'default_value'">
  495. <div class="text-12px text-gray-500">当发生异常时,指定默认数据输出</div>
  496. <el-form-item label="body" label-position="top">
  497. <el-input v-model="formData.exceptionDefaultValue.body" type="textarea" rows="3" />
  498. </el-form-item>
  499. <el-form-item label="status_code" label-position="top">
  500. <el-input-number
  501. controls-position="right"
  502. v-model="formData.exceptionDefaultValue.status_code"
  503. />
  504. </el-form-item>
  505. <el-form-item label="headers" label-position="top">
  506. <el-input v-model="formData.exceptionDefaultValue.headers" type="textarea" rows="5" />
  507. </el-form-item>
  508. </div>
  509. <div v-if="formData.exception === 'exception_branch'">
  510. <div class="text-12px text-gray-500">请在画布定义异常处理逻辑!</div>
  511. </div>
  512. </el-form>
  513. </el-scrollbar>
  514. </template>