WeixinChannelForm.vue 9.7 KB

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