WeixinChannelForm.vue 11 KB

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