WeixinChannelForm.vue 11 KB

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