balance.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. <template>
  2. <div>
  3. <div class="text-end color-primary my-3 text-decoration-underline cursor-pointer" @click="handleToOrder">充值记录<v-icon>mdi-chevron-double-right</v-icon></div>
  4. <div class="d-flex align-center justify-center mt-5">
  5. <div
  6. v-for="(item, index) in list"
  7. :key="index"
  8. class="packagesItem cursor-pointer mx-3"
  9. :class="{'active': current === (index+1)}"
  10. :id="'positioning'+index"
  11. :style="{'width': width+'%'}"
  12. @click="handleClick(index, item)"
  13. >
  14. <div class="d-flex flex-column align-center pb-5" style="position: relative;">
  15. <div class="my-5 font-size-16 font-weight-bold" style="z-index: 2;">{{ item.name }}</div>
  16. <div class="priceBox mt-3" style="position: relative;">
  17. <span v-if="item.custom">
  18. <div v-if="inputValue" class="custom-point-show" style="position: absolute; top: -24px;">{{ inputValue }}M豆</div>
  19. <input v-model="inputValue" @blur="inputChange" type="text" class="custom-input-num mr-1" :placeholder="item.placeholder">元
  20. <div class="color-warning font-size-12 text-center mt-1">只能输入整数</div>
  21. </span>
  22. <span class="color-primary" v-else>
  23. <span style="font-size: 25px;">{{ FenYuanTransform(item.payPrice) }}</span>
  24. </span>
  25. </div>
  26. <div class="vip">
  27. <svg-icon name="diamond" size="50"></svg-icon>
  28. </div>
  29. </div>
  30. </div>
  31. </div>
  32. <div v-if="!Object.keys(select).length" class="color-warning text-center mt-15 font-size-20">请选择要充值的金额</div>
  33. <div v-if="payType && payQrCodeTxt" id="codeBox" class="code pa-5 resume-box my-5">
  34. <!-- <div class="resume-header">
  35. <div class="resume-title">扫码支付</div>
  36. </div> -->
  37. <div class="d-flex" :style="{'margin-left': offsetCalc + 'px'}">
  38. <div id="codeItem" class="d-flex flex-column align-center my-2">
  39. <div class="d-flex align-center">
  40. <span class="color-666 font-weight-bold">支付方式:</span>
  41. <v-chip-group v-model="payType" selected-class="text-primary" column mandatory @update:modelValue="payTypeChange">
  42. <v-chip filter v-for="k in payTypeList" :key="k.code" :value="k.code" class="mr-3" label>
  43. {{ k.name }}
  44. <svg-icon v-if="k.icon" class="ml-1" :name="k.icon" :size="k.size"></svg-icon>
  45. </v-chip>
  46. </v-chip-group>
  47. </div>
  48. <div class="code-right">
  49. <div class="price">
  50. <span class="font-size-13">¥</span>
  51. {{ FenYuanTransform(select?.payPrice || 0) }}元
  52. </div>
  53. </div>
  54. <div class="code-left">
  55. <QrCode :text="payQrCodeTxt" :disabled="!remainderTimer" :width="170" @refresh="refreshQRCode" />
  56. </div>
  57. <div class="mt-5" style="color: var(--v-error-base);">
  58. 扫码支付时请勿离开
  59. <span v-if="remainderZhShow">{{ remainderZhShow }}</span>
  60. </div>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. </template>
  66. <script setup>
  67. defineOptions({ name: 'membershipPackageBalance' })
  68. import { ref, onUnmounted, nextTick, watch } from 'vue'
  69. import { FenYuanTransform } from '@/utils/position'
  70. import { getEnableCodeList, payOrderSubmit, getOrderPayStatus } from '@/api/common'
  71. import { definePayTypeList, qrCodePay } from '@/utils/payType'
  72. import { rechargeOrderCreate } from '@/api/recruit/enterprise/member/points'
  73. import { getEnterpriseRechargePackageList } from '@/api/recruit/enterprise/member/points'
  74. import { useUserStore } from '@/store/user'; const store = useUserStore()
  75. import Snackbar from '@/plugins/snackbar'
  76. import { useRouter } from 'vue-router'
  77. const router = useRouter()
  78. const current = ref()
  79. const inputValue = ref('')
  80. const payQrCodeTxt = ref('')
  81. const remainderZhShow = ref('') // 倒计时展示
  82. const select = ref({})
  83. const width = ref(15)
  84. const list = ref([])
  85. const getData = async () => {
  86. const data = await getEnterpriseRechargePackageList()
  87. const end = { name: '自定义金额', id: 'custom', placeholder: '请输入', custom: true }
  88. list.value = data ? [...data, end] : [end]
  89. width.value = 100/list.value.length
  90. }
  91. const offsetCalc = ref(0)
  92. let codeItemOffsetWidth = null
  93. const codeBoxPx = 40 // codeBox左右内边距
  94. const calcStyle = (index) => {
  95. nextTick(() => {
  96. const codeBox = document.getElementById('codeBox')
  97. const codeBoxOffsetWidth = codeBox.offsetWidth - codeBoxPx
  98. if (!codeItemOffsetWidth) {
  99. const codeItem = document.getElementById('codeItem')
  100. codeItemOffsetWidth = codeItem.offsetWidth
  101. }
  102. //
  103. const menu = document.getElementById(`positioning${index}`)
  104. offsetCalc.value = 0
  105. if (menu && codeBoxOffsetWidth && codeItemOffsetWidth) {
  106. const menuHalfWidth= menu.offsetWidth/2 + 12
  107. const codeItemHalfWidth = codeItemOffsetWidth/2
  108. const calcNum = menuHalfWidth+menuHalfWidth*2*index - codeItemHalfWidth
  109. //
  110. if (calcNum+codeItemOffsetWidth > codeBoxOffsetWidth) { // 超出右侧,取最大
  111. offsetCalc.value = codeBoxOffsetWidth-codeItemOffsetWidth
  112. } else if (calcNum < 0) { // 超出左侧,取最小
  113. offsetCalc.value = 0
  114. } else {
  115. offsetCalc.value = calcNum-20 > 0 ? calcNum-20 : 0
  116. }
  117. }
  118. })
  119. }
  120. const handleClick = (index, item) => {
  121. payQrCodeTxt.value = ''
  122. current.value = index + 1
  123. select.value = item
  124. getUnpaidOrderList()
  125. }
  126. const inputChange = () => {
  127. if (!inputValue.value) return
  128. current.value = list.value.length
  129. const item = list.value[list.value.length-1]
  130. item.payPrice = FenYuanTransform(inputValue.value, 'toCent')
  131. item.id = 'custom' + inputValue.value
  132. select.value = item
  133. getUnpaidOrderList()
  134. }
  135. const timeout = ref(null)
  136. watch(
  137. () => inputValue.value,
  138. (val) => {
  139. let num = val && val !=='0' ? (val.match(/\d+/g)?.join('') || null) : null
  140. if (num > 100000000) {
  141. num = '100000000'
  142. Snackbar.warning('最多不可超过一亿元')
  143. }
  144. inputValue.value = num
  145. clearTimeout(timeout.value)
  146. timeout.value = setTimeout(() => inputChange(), 500) // 防抖
  147. }
  148. )
  149. // 2.发起充值
  150. const loading = ref(true)
  151. const payOrder = ref({})
  152. const getUnpaidOrderList = async () => {
  153. if (select.value.payPrice === undefined) return payQrCodeTxt.value = ''
  154. const params = {
  155. // payPrice: (select.value.payPrice-0),
  156. packageId: select.value.id,
  157. }
  158. if (typeof select.value.id === 'string' && !select.value.id.includes('custom')) params.packageId = select.value.id
  159. const data = await rechargeOrderCreate(params)
  160. payOrder.value = data || {}
  161. paySubmit()
  162. }
  163. const payTypeChange = (val) => {
  164. payType.value = val
  165. paySubmit()
  166. }
  167. const timer = ref(null)
  168. onUnmounted(() => {
  169. if (timer.value) clearInterval(timer.value); timer.value = null
  170. })
  171. // 更新账户余额
  172. const updateAccountInfo = async (init = false) => {
  173. await store.getEnterpriseUserAccountInfo()
  174. if (init) return
  175. loading.value = false
  176. }
  177. const payStatus = async () => {
  178. try {
  179. const data = await getOrderPayStatus({ id: payOrder.value.payOrderId })
  180. if ((data?.status - 0) === 10) {
  181. // 支付成功
  182. if (timer.value) clearInterval(timer.value); timer.value = null
  183. setTimeout(() => {
  184. // 更新点数(充值、发布职位)
  185. updateAccountInfo()
  186. // 清除定时器
  187. clearTimer()
  188. // 支付成功
  189. Snackbar.success('支付成功')
  190. }, 2000);
  191. }
  192. } catch (error) {
  193. console.log(error)
  194. }
  195. }
  196. const paySubmit = async () => {
  197. if (!payType.value) return
  198. try {
  199. if (payOrder.value) {
  200. if (!payOrder.value?.payOrderId) return
  201. // 提交支付订单
  202. const params = {
  203. channelCode: payType.value, // 支付渠道
  204. id: payOrder.value.payOrderId // 支付单编号
  205. }
  206. const res = await payOrderSubmit(params)
  207. payQrCodeTxt.value = res?.displayContent || '' // 生成二维码内容
  208. calcStyle(current.value-1)
  209. initIntervalFun()
  210. if (timer.value) clearInterval(timer.value); timer.value = null
  211. timer.value = setInterval(() => { payStatus() }, 1000) // 轮巡查询用户是否支付
  212. }
  213. } catch (error) {
  214. console.log(error)
  215. }
  216. }
  217. // 1.支付方式
  218. const payType = ref('')
  219. const payTypeList = ref([])
  220. const codeList = ref([])
  221. const getCodeList = async () => {
  222. try {
  223. const list = await getEnableCodeList({ appId: 11 })
  224. codeList.value = list || []
  225. } catch (error) {
  226. console.log(error)
  227. } finally {
  228. if (definePayTypeList?.length && codeList.value?.length) {
  229. codeList.value.forEach(code => {
  230. const item = definePayTypeList.find(p => p.code === code)
  231. if (item) {
  232. if (!payType.value) {
  233. // 默认值赋值(暂时只支持扫码)
  234. const bool = qrCodePay.includes(code)
  235. if (bool) payType.value = code
  236. }
  237. payTypeList.value.push(item)
  238. }
  239. })
  240. }
  241. }
  242. }
  243. nextTick(async () => {
  244. await getData()
  245. await getCodeList()
  246. })
  247. const refreshQRCode =() => { // 刷新二维码
  248. getUnpaidOrderList()
  249. }
  250. const remainderTimer = ref(null)
  251. const countdownTime = 60000 * 3 // 倒计时三分钟
  252. let remainder = 0 // number
  253. // 初始化倒计时
  254. const initIntervalFun = () => {
  255. remainder = countdownTime // 初始倒计时时间
  256. if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null // 每一次点击都清除上一个轮询
  257. // 倒计时计算
  258. remainderCalc()
  259. remainderTimer.value = setInterval(() => { remainderCalc() }, 1000)
  260. if (timer.value) clearInterval(timer.value); timer.value = null
  261. timer.value = setInterval(() => { payStatus() }, 2000) // 轮巡查询用户是否支付
  262. }
  263. const formatDuration = (remainder) => {
  264. // 将毫秒转换为秒
  265. var seconds = Math.floor(remainder / 1000)
  266. // 计算分钟和剩余的秒数
  267. var minutes = Math.floor(seconds / 60)
  268. var remainingSeconds = seconds % 60
  269. // 格式化分钟和秒数,确保秒数为两位数(如果小于10,则前面补0)
  270. minutes = minutes.toString().padStart(2, '0')
  271. remainingSeconds = remainingSeconds.toString().padStart(2, '0')
  272. // 返回格式化的字符串
  273. return `${minutes}分${remainingSeconds}秒`
  274. }
  275. const clearTimer = () => {
  276. if (timer.value) clearInterval(timer.value); timer.value = null
  277. if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null
  278. remainderZhShow.value = ''
  279. }
  280. const remainderCalc = () => {
  281. remainder -= 1000
  282. remainderZhShow.value = formatDuration(remainder)
  283. if (remainder <= 0) clearTimer()
  284. }
  285. const handleToOrder = () => {
  286. router.push('/recruit/enterprise/tradingOrder?key=tab_recharge')
  287. }
  288. const mlArr = [119, 344, 567, 791, 1298, 1298, 1298, 1298]
  289. </script>
  290. <style scoped lang="scss">
  291. .packagesItem {
  292. border: 1px solid var(--color-f3);
  293. border-radius: 8px;
  294. background-color: var(--color-f2f4f742);
  295. }
  296. .dailyPrice {
  297. border-radius: 14px;
  298. background-color: #dde3e94f;
  299. padding: 2px 18px;
  300. color: var(--color-666);
  301. }
  302. .active {
  303. border: 2px solid #00897B;
  304. .priceBox {
  305. color: var(--v-primary-base);
  306. }
  307. .dailyPrice {
  308. color: var(--v-error-base);
  309. background-color: #fff4e7;
  310. }
  311. }
  312. .custom-input-num {
  313. border: none;
  314. outline: none;
  315. background-color: transparent;
  316. width: 120px;
  317. max-width: 120px;
  318. text-align: center;
  319. background-color: #d9d9d98c;
  320. border-radius: 20px;
  321. font-size: 20px;
  322. color: var(--v-primary-base);
  323. }
  324. .code {
  325. // max-width: 1328px;
  326. max-width: 100%;
  327. background-color: #f7f8fa;
  328. border-radius: 6px;
  329. margin: 0 auto;
  330. &-left {
  331. border: 1px solid #00897B;
  332. border-radius: 6px;
  333. padding: 5px;
  334. }
  335. &-right {
  336. .price {
  337. font-size: 30px;
  338. font-weight: 700;
  339. color: var(--v-error-base);
  340. }
  341. }
  342. }
  343. .vip {
  344. width: 50px;
  345. height: 50px;
  346. position: absolute;
  347. top: 0;
  348. right: 0;
  349. overflow: hidden;
  350. border-radius: 8px
  351. }
  352. .custom-point-show {
  353. width: 100%;
  354. text-align: center;
  355. font-weight: normal;
  356. color: gray;
  357. font-size: 13px;
  358. }
  359. :deep(.v-slide-group__content) {
  360. background: none !important;
  361. }
  362. </style>