WeixinChannelForm.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. <template>
  2. <div>
  3. <Dialog v-model="dialogVisible" :title="dialogTitle" @close="close" width="800px">
  4. <el-form
  5. ref="formRef"
  6. :model="formData"
  7. :rules="formRules"
  8. label-width="120px"
  9. v-loading="formLoading"
  10. >
  11. <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
  12. <el-input
  13. v-model="formData.feeRate"
  14. placeholder="请输入渠道费率"
  15. clearable
  16. :style="{ width: '100%' }"
  17. >
  18. <template #append>%</template>
  19. </el-input>
  20. </el-form-item>
  21. <el-form-item label-width="180px" label="微信 APPID" prop="config.appId">
  22. <el-input
  23. v-model="formData.config.appId"
  24. placeholder="请输入微信 APPID"
  25. clearable
  26. :style="{ width: '100%' }"
  27. />
  28. </el-form-item>
  29. <el-form-item label-width="180px" label="商户号" prop="config.mchId">
  30. <el-input v-model="formData.config.mchId" :style="{ width: '100%' }" />
  31. </el-form-item>
  32. <el-form-item label-width="180px" label="渠道状态" prop="status">
  33. <el-radio-group v-model="formData.status">
  34. <el-radio
  35. v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
  36. :key="parseInt(dict.value)"
  37. :label="parseInt(dict.value)"
  38. >
  39. {{ dict.label }}
  40. </el-radio>
  41. </el-radio-group>
  42. </el-form-item>
  43. <el-form-item label-width="180px" label="API 版本" prop="config.apiVersion">
  44. <el-radio-group v-model="formData.config.apiVersion">
  45. <el-radio label="v2">v2</el-radio>
  46. <el-radio label="v3">v3</el-radio>
  47. </el-radio-group>
  48. </el-form-item>
  49. <div v-if="formData.config.apiVersion === 'v2'">
  50. <el-form-item label-width="180px" label="商户密钥" prop="config.mchKey">
  51. <el-input
  52. v-model="formData.config.mchKey"
  53. placeholder="请输入商户密钥"
  54. clearable
  55. />
  56. </el-form-item>
  57. <el-form-item
  58. label-width="180px"
  59. label="apiclient_cert.p12 证书"
  60. prop="config.keyContent"
  61. >
  62. <el-input
  63. v-model="formData.config.keyContent"
  64. type="textarea"
  65. placeholder="请上传 apiclient_cert.p12 证书"
  66. readonly
  67. :autosize="{ minRows: 8, maxRows: 8 }"
  68. :style="{ width: '100%' }"
  69. />
  70. </el-form-item>
  71. <el-form-item label-width="180px" label="">
  72. <el-upload
  73. :limit="1"
  74. accept=".p12"
  75. action=""
  76. :before-upload="p12FileBeforeUpload"
  77. :http-request="keyContentUpload"
  78. >
  79. <el-button type="primary">
  80. <Icon icon="ep:upload" class="mr-5px" />
  81. 点击上传
  82. </el-button>
  83. </el-upload>
  84. </el-form-item>
  85. </div>
  86. <div v-if="formData.config.apiVersion === 'v3'">
  87. <el-form-item label-width="180px" label="API V3 密钥" prop="config.apiV3Key">
  88. <el-input
  89. v-model="formData.config.apiV3Key"
  90. placeholder="请输入 API V3 密钥"
  91. clearable
  92. />
  93. </el-form-item>
  94. <el-form-item
  95. label-width="180px"
  96. label="apiclient_key.pem 证书"
  97. prop="config.privateKeyContent"
  98. >
  99. <el-input
  100. v-model="formData.config.privateKeyContent"
  101. type="textarea"
  102. placeholder="请上传 apiclient_key.pem 证书"
  103. readonly
  104. :autosize="{ minRows: 8, maxRows: 8 }"
  105. :style="{ width: '100%' }"
  106. />
  107. </el-form-item>
  108. <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
  109. <el-upload
  110. ref="privateKeyContentFile"
  111. :limit="1"
  112. accept=".pem"
  113. action=""
  114. :before-upload="pemFileBeforeUpload"
  115. :http-request="privateKeyContentUpload"
  116. >
  117. <el-button type="primary">
  118. <Icon icon="ep:upload" class="mr-5px" />
  119. 点击上传
  120. </el-button>
  121. </el-upload>
  122. </el-form-item>
  123. <el-form-item label-width="180px" label="证书序列号" prop="config.certSerialNo">
  124. <el-input
  125. v-model="formData.config.certSerialNo"
  126. placeholder="请输入证书序列号"
  127. clearable
  128. />
  129. </el-form-item>
  130. </div>
  131. <el-form-item label-width="180px" label="备注" prop="remark">
  132. <el-input v-model="formData.remark" :style="{ width: '100%' }" />
  133. </el-form-item>
  134. </el-form>
  135. <template #footer>
  136. <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
  137. <el-button @click="dialogVisible = false">取 消</el-button>
  138. </template>
  139. </Dialog>
  140. </div>
  141. </template>
  142. <script lang="ts" setup>
  143. import { CommonStatusEnum } from '@/utils/constants'
  144. import { DICT_TYPE, getDictOptions } from '@/utils/dict'
  145. import * as ChannelApi from '@/api/pay/channel'
  146. defineOptions({ name: 'WeixinChannelForm' })
  147. const { t } = useI18n() // 国际化
  148. const message = useMessage() // 消息弹窗
  149. const dialogVisible = ref(false) // 弹窗的是否展示
  150. const dialogTitle = ref('') // 弹窗的标题
  151. const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  152. const formData = ref<any>({
  153. appId: '',
  154. code: '',
  155. status: undefined,
  156. feeRate: undefined,
  157. remark: '',
  158. config: {
  159. appId: '',
  160. mchId: '',
  161. apiVersion: '',
  162. mchKey: '',
  163. keyContent: '',
  164. privateKeyContent: '',
  165. certSerialNo: '',
  166. apiV3Key: ''
  167. }
  168. })
  169. const formRules = {
  170. feeRate: [{ required: true, message: '请输入渠道费率', trigger: 'blur' }],
  171. status: [{ required: true, message: '渠道状态不能为空', trigger: 'blur' }],
  172. 'config.mchId': [{ required: true, message: '请传入商户号', trigger: 'blur' }],
  173. 'config.appId': [{ required: true, message: '请输入公众号APPID', trigger: 'blur' }],
  174. 'config.apiVersion': [{ required: true, message: 'API版本不能为空', trigger: 'blur' }],
  175. 'config.mchKey': [{ required: true, message: '请输入商户密钥', trigger: 'blur' }],
  176. 'config.keyContent': [
  177. { required: true, message: '请上传 apiclient_cert.p12 证书', trigger: 'blur' }
  178. ],
  179. 'config.privateKeyContent': [
  180. { required: true, message: '请上传 apiclient_key.pem 证书', trigger: 'blur' }
  181. ],
  182. 'config.certSerialNo': [
  183. { required: true, message: '请输入证书序列号', trigger: 'blur' }
  184. ],
  185. 'config.apiV3Key': [{ required: true, message: '请上传 api V3 密钥值', trigger: 'blur' }]
  186. }
  187. const formRef = ref() // 表单 Ref
  188. /** 打开弹窗 */
  189. const open = async (appId, code) => {
  190. dialogVisible.value = true
  191. formLoading.value = true
  192. resetForm(appId, code)
  193. // 加载数据
  194. try {
  195. const data = await ChannelApi.getChannel(appId, code)
  196. if (data && data.id) {
  197. formData.value = data
  198. formData.value.config = JSON.parse(data.config)
  199. }
  200. dialogTitle.value = !formData.value.id ? '创建支付渠道' : '编辑支付渠道'
  201. } finally {
  202. formLoading.value = false
  203. }
  204. }
  205. defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  206. /** 提交表单 */
  207. const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  208. const submitForm = async () => {
  209. // 校验表单
  210. if (!formRef) return
  211. const valid = await formRef.value.validate()
  212. if (!valid) return
  213. // 提交请求
  214. formLoading.value = true
  215. try {
  216. const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
  217. data.config = JSON.stringify(formData.value.config)
  218. if (!data.id) {
  219. await ChannelApi.createChannel(data)
  220. message.success(t('common.createSuccess'))
  221. } else {
  222. await ChannelApi.updateChannel(data)
  223. message.success(t('common.updateSuccess'))
  224. }
  225. dialogVisible.value = false
  226. // 发送操作成功的事件
  227. emit('success')
  228. } finally {
  229. formLoading.value = false
  230. }
  231. }
  232. /** 重置表单 */
  233. const resetForm = (appId, code) => {
  234. formData.value = {
  235. appId: appId,
  236. code: code,
  237. status: CommonStatusEnum.ENABLE,
  238. feeRate: undefined,
  239. remark: '',
  240. config: {
  241. appId: '',
  242. mchId: '',
  243. apiVersion: '',
  244. mchKey: '',
  245. keyContent: '',
  246. privateKeyContent: '',
  247. certSerialNo: '',
  248. apiV3Key: ''
  249. }
  250. }
  251. formRef.value?.resetFields()
  252. }
  253. /**
  254. * apiclient_cert.p12、apiclient_key.pem 上传前的校验
  255. */
  256. const fileBeforeUpload = (file, fileAccept) => {
  257. let format = '.' + file.name.split('.')[1]
  258. if (format !== fileAccept) {
  259. debugger
  260. message.error('请上传指定格式"' + fileAccept + '"文件')
  261. return false
  262. }
  263. let isRightSize = file.size / 1024 / 1024 < 2
  264. if (!isRightSize) {
  265. message.error('文件大小超过 2MB')
  266. }
  267. return isRightSize
  268. }
  269. const p12FileBeforeUpload = (file) => {
  270. fileBeforeUpload(file, '.p12')
  271. }
  272. const pemFileBeforeUpload = (file) => {
  273. fileBeforeUpload(file, '.pem')
  274. }
  275. /**
  276. * 读取 apiclient_key.pem 到 privateKeyContent 字段
  277. */
  278. const privateKeyContentUpload = async (event) => {
  279. const readFile = new FileReader()
  280. readFile.onload = (e: any) => {
  281. formData.value.config.privateKeyContent = e.target.result
  282. }
  283. readFile.readAsText(event.file)
  284. }
  285. /**
  286. * 读取 apiclient_cert.p12 到 keyContent 字段
  287. */
  288. const keyContentUpload = async (event) => {
  289. const readFile = new FileReader()
  290. readFile.onload = (e: any) => {
  291. formData.value.config.keyContent = e.target.result.split(',')[1]
  292. }
  293. readFile.readAsDataURL(event.file) // 读成 base64
  294. }
  295. </script>