ApiTestForm.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. <template>
  2. <el-card shadow="never">
  3. <template #header>
  4. <div class="card-header">
  5. <span>① 输入 API 信息</span>
  6. </div>
  7. </template>
  8. <!-- ref="formRef":拿到 el-form 的实例,后面才能调用 validate() 做整体校验 -->
  9. <!-- :model 绑定表单数据对象;:rules 绑定校验规则;label-position="top" 让标签显示在输入框上方 -->
  10. <!-- @submit.prevent:阻止浏览器原生表单提交(否则在单行输入框按回车会刷新页面) -->
  11. <el-form ref="formRef" :model="form" :rules="rules" label-position="top" @submit.prevent>
  12. <el-form-item label="OpenAPI 文档来源">
  13. <el-radio-group v-model="mode" :disabled="loading">
  14. <el-radio-button value="text">粘贴文档</el-radio-button>
  15. <el-radio-button value="url">URL 抓取</el-radio-button>
  16. </el-radio-group>
  17. </el-form-item>
  18. <el-form-item v-if="mode === 'text'" label="OpenAPI 文档(粘贴 yaml/json)" prop="openapiText">
  19. <el-input
  20. v-model="form.openapiText"
  21. type="textarea"
  22. :rows="10"
  23. placeholder="在此粘贴 OpenAPI 文档内容..."
  24. />
  25. </el-form-item>
  26. <el-form-item v-else label="OpenAPI 文档 URL" prop="openapiUrl">
  27. <el-input
  28. v-model="form.openapiUrl"
  29. type="url"
  30. placeholder="例如:https://httpbin.org/spec.json"
  31. clearable
  32. @keyup.enter="handleSubmit"
  33. />
  34. <div class="field-tip">后端将直接抓取 URL 内容,无需先下载文档</div>
  35. </el-form-item>
  36. <el-form-item label="目标 API 基础地址(base_url)" prop="baseUrl">
  37. <el-input
  38. v-model="form.baseUrl"
  39. placeholder="例如:https://jsonplaceholder.typicode.com"
  40. clearable
  41. @keyup.enter="handleSubmit"
  42. />
  43. </el-form-item>
  44. <el-form-item label="请求头(选填,JSON 格式)" prop="headers">
  45. <el-input
  46. v-model="form.headers"
  47. type="textarea"
  48. :rows="3"
  49. placeholder='例如:{"Authorization": "Bearer 你的token"}'
  50. />
  51. <div class="field-tip">受保护接口需要认证头,按 JSON 对象填写(留空则不发送)</div>
  52. </el-form-item>
  53. <div class="btn-row">
  54. <!-- :loading="loading":父组件传进来的状态,请求期间按钮转圈并禁用 -->
  55. <el-button type="primary" size="large" :loading="loading" @click="handleSubmit">
  56. 🚀 开始测试
  57. </el-button>
  58. <el-button size="large" :disabled="loading" @click="fillExample">📝 填入示例</el-button>
  59. <el-button size="large" :disabled="loading" @click="clearForm">🗑️ 清空</el-button>
  60. </div>
  61. </el-form>
  62. </el-card>
  63. </template>
  64. <script setup>
  65. import { computed, reactive, ref } from 'vue'
  66. import { EXAMPLE_DOC, EXAMPLE_BASE_URL } from '@/constants/exampleDoc'
  67. // ===== 父组件传进来的 props(只读)=====
  68. const props = defineProps({
  69. loading: {
  70. type: Boolean,
  71. default: false,
  72. },
  73. })
  74. // ===== 声明本组件能向外触发的事件 =====
  75. // 组件里只能 emit 在 defineEmits 里声明过的事件,这层声明让代码自文档化
  76. const emit = defineEmits(['submit'])
  77. // formRef 绑定到模板里的 el-form;模板中用 ref="formRef",这里用同名 ref 变量接收
  78. const formRef = ref(null)
  79. const mode = ref('text')
  80. // reactive 创建响应式对象:输入框用 v-model 双向绑定到它
  81. // (一个对象里有多个值、会被整体操作时,用 reactive 比多个 ref 更顺)
  82. const form = reactive({
  83. openapiText: '',
  84. openapiUrl: '',
  85. baseUrl: '',
  86. headers: '',
  87. })
  88. // 请求头可选:留空跳过;非空必须是合法 JSON 对象
  89. function validateHeaders(rule, value, callback) {
  90. if (!value || !value.trim()) return callback()
  91. try {
  92. const parsed = JSON.parse(value)
  93. if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
  94. return callback(new Error('请求头必须是 JSON 对象,例如 {"Authorization": "Bearer xxx"}'))
  95. }
  96. return callback()
  97. } catch {
  98. return callback(new Error('请求头格式错误:不是合法的 JSON 对象'))
  99. }
  100. }
  101. // 校验规则:required 必填。trigger 表示何时触发校验
  102. const rules = computed(() => ({
  103. openapiText: mode.value === 'text'
  104. ? [{ required: true, message: '请粘贴 OpenAPI 文档内容', trigger: 'blur' }]
  105. : [],
  106. openapiUrl: mode.value === 'url'
  107. ? [{ required: true, message: '请输入 OpenAPI 文档 URL', trigger: 'blur' }]
  108. : [],
  109. baseUrl: [{ required: true, message: '请填写目标 API 基础地址', trigger: 'blur' }],
  110. headers: [{ validator: validateHeaders, trigger: 'blur' }],
  111. }))
  112. // 填入示例:把示例文档和地址写进表单,并清掉红色校验提示
  113. function fillExample() {
  114. mode.value = 'text'
  115. form.openapiText = EXAMPLE_DOC
  116. form.openapiUrl = ''
  117. form.baseUrl = EXAMPLE_BASE_URL
  118. form.headers = ''
  119. formRef.value?.clearValidate() // ?. 空值保护:ref 还没绑定时不会报错
  120. }
  121. function clearForm() {
  122. form.openapiText = ''
  123. form.openapiUrl = ''
  124. form.baseUrl = ''
  125. form.headers = ''
  126. formRef.value?.clearValidate()
  127. }
  128. async function handleSubmit() {
  129. // validate() 校验整张表单;不通过会 reject(并自动标红),直接 return
  130. try {
  131. await formRef.value.validate()
  132. } catch {
  133. return
  134. }
  135. // 通过:把数据整理成后端要的字段名(下划线风格),emit 给父组件
  136. const payload = {
  137. base_url: form.baseUrl.trim(),
  138. }
  139. if (mode.value === 'url') {
  140. payload.openapi_url = form.openapiUrl.trim()
  141. } else {
  142. payload.openapi_text = form.openapiText.trim()
  143. }
  144. // 请求头是选填的 JSON 对象,非空才解析并带上
  145. if (form.headers.trim()) {
  146. payload.headers = JSON.parse(form.headers)
  147. }
  148. emit('submit', payload)
  149. }
  150. </script>
  151. <style scoped>
  152. .card-header {
  153. display: flex;
  154. align-items: center;
  155. font-weight: 600;
  156. font-size: 16px;
  157. }
  158. .btn-row {
  159. margin-top: 6px;
  160. }
  161. .field-tip {
  162. margin-top: 6px;
  163. color: #909399;
  164. font-size: 12px;
  165. line-height: 1.5;
  166. }
  167. </style>