balance.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  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. current.value = index + 1
  122. select.value = item
  123. getUnpaidOrderList()
  124. }
  125. const inputChange = () => {
  126. if (!inputValue.value) return
  127. current.value = list.value.length
  128. const item = list.value[list.value.length-1]
  129. item.payPrice = FenYuanTransform(inputValue.value, 'toCent')
  130. item.id = 'custom' + inputValue.value
  131. select.value = item
  132. getUnpaidOrderList()
  133. }
  134. const timeout = ref(null)
  135. watch(
  136. () => inputValue.value,
  137. (val) => {
  138. let num = val && val !=='0' ? (val.match(/\d+/g)?.join('') || null) : null
  139. if (num > 100000000) {
  140. num = '100000000'
  141. Snackbar.warning('最多不可超过一亿元')
  142. }
  143. inputValue.value = num
  144. clearTimeout(timeout.value)
  145. timeout.value = setTimeout(() => inputChange(), 500) // 防抖
  146. }
  147. )
  148. // 2.发起充值
  149. const loading = ref(true)
  150. const payOrder = ref({})
  151. const getUnpaidOrderList = async () => {
  152. if (select.value.payPrice === undefined) return payQrCodeTxt.value = ''
  153. const params = {
  154. payPrice: (select.value.payPrice-0),
  155. }
  156. if (typeof select.value.id === 'string' && !select.value.id.includes('custom')) params.packageId = select.value.id
  157. const data = await rechargeOrderCreate(params)
  158. payOrder.value = data || {}
  159. paySubmit()
  160. }
  161. const payTypeChange = (val) => {
  162. payType.value = val
  163. paySubmit()
  164. }
  165. const timer = ref(null)
  166. onUnmounted(() => {
  167. if (timer.value) clearInterval(timer.value); timer.value = null
  168. })
  169. // 更新账户余额
  170. const updateAccountInfo = async (init = false) => {
  171. await store.getEnterpriseUserAccountInfo()
  172. if (init) return
  173. loading.value = false
  174. }
  175. const payStatus = async () => {
  176. try {
  177. const data = await getOrderPayStatus({ id: payOrder.value.payOrderId })
  178. if ((data?.status - 0) === 10) {
  179. // 支付成功
  180. if (timer.value) clearInterval(timer.value); timer.value = null
  181. setTimeout(() => {
  182. // 更新点数(充值、发布职位)
  183. updateAccountInfo()
  184. // 清除定时器
  185. clearTimer()
  186. // 支付成功
  187. Snackbar.success('支付成功')
  188. }, 2000);
  189. }
  190. } catch (error) {
  191. console.log(error)
  192. }
  193. }
  194. const paySubmit = async () => {
  195. if (!payType.value) return
  196. try {
  197. if (payOrder.value) {
  198. if (!payOrder.value?.payOrderId) return
  199. // 提交支付订单
  200. const params = {
  201. channelCode: payType.value, // 支付渠道
  202. id: payOrder.value.payOrderId // 支付单编号
  203. }
  204. const res = await payOrderSubmit(params)
  205. payQrCodeTxt.value = res?.displayContent || '' // 生成二维码内容
  206. calcStyle(current.value-1)
  207. initIntervalFun()
  208. if (timer.value) clearInterval(timer.value); timer.value = null
  209. timer.value = setInterval(() => { payStatus() }, 1000) // 轮巡查询用户是否支付
  210. }
  211. } catch (error) {
  212. console.log(error)
  213. }
  214. }
  215. // 1.支付方式
  216. const payType = ref('')
  217. const payTypeList = ref([])
  218. const codeList = ref([])
  219. const getCodeList = async () => {
  220. try {
  221. const list = await getEnableCodeList({ appId: 11 })
  222. codeList.value = list || []
  223. } catch (error) {
  224. console.log(error)
  225. } finally {
  226. if (definePayTypeList?.length && codeList.value?.length) {
  227. codeList.value.forEach(code => {
  228. const item = definePayTypeList.find(p => p.code === code)
  229. if (item) {
  230. if (!payType.value) {
  231. // 默认值赋值(暂时只支持扫码)
  232. const bool = qrCodePay.includes(code)
  233. if (bool) payType.value = code
  234. }
  235. payTypeList.value.push(item)
  236. }
  237. })
  238. }
  239. }
  240. }
  241. nextTick(async () => {
  242. await getData()
  243. await getCodeList()
  244. })
  245. const refreshQRCode =() => { // 刷新二维码
  246. getUnpaidOrderList()
  247. }
  248. const remainderTimer = ref(null)
  249. const countdownTime = 60000 * 3 // 倒计时三分钟
  250. let remainder = 0 // number
  251. // 初始化倒计时
  252. const initIntervalFun = () => {
  253. remainder = countdownTime // 初始倒计时时间
  254. if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null // 每一次点击都清除上一个轮询
  255. // 倒计时计算
  256. remainderCalc()
  257. remainderTimer.value = setInterval(() => { remainderCalc() }, 1000)
  258. if (timer.value) clearInterval(timer.value); timer.value = null
  259. timer.value = setInterval(() => { payStatus() }, 2000) // 轮巡查询用户是否支付
  260. }
  261. const formatDuration = (remainder) => {
  262. // 将毫秒转换为秒
  263. var seconds = Math.floor(remainder / 1000)
  264. // 计算分钟和剩余的秒数
  265. var minutes = Math.floor(seconds / 60)
  266. var remainingSeconds = seconds % 60
  267. // 格式化分钟和秒数,确保秒数为两位数(如果小于10,则前面补0)
  268. minutes = minutes.toString().padStart(2, '0')
  269. remainingSeconds = remainingSeconds.toString().padStart(2, '0')
  270. // 返回格式化的字符串
  271. return `${minutes}分${remainingSeconds}秒`
  272. }
  273. const clearTimer = () => {
  274. if (timer.value) clearInterval(timer.value); timer.value = null
  275. if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null
  276. remainderZhShow.value = ''
  277. }
  278. const remainderCalc = () => {
  279. remainder -= 1000
  280. remainderZhShow.value = formatDuration(remainder)
  281. if (remainder <= 0) clearTimer()
  282. }
  283. const handleToOrder = () => {
  284. router.push('/recruit/enterprise/tradingOrder?key=tab_recharge')
  285. }
  286. const mlArr = [119, 344, 567, 791, 1298, 1298, 1298, 1298]
  287. </script>
  288. <style scoped lang="scss">
  289. .packagesItem {
  290. border: 1px solid var(--color-f3);
  291. border-radius: 8px;
  292. background-color: var(--color-f2f4f742);
  293. }
  294. .dailyPrice {
  295. border-radius: 14px;
  296. background-color: #dde3e94f;
  297. padding: 2px 18px;
  298. color: var(--color-666);
  299. }
  300. .active {
  301. border: 2px solid #00897B;
  302. .priceBox {
  303. color: var(--v-primary-base);
  304. }
  305. .dailyPrice {
  306. color: var(--v-error-base);
  307. background-color: #fff4e7;
  308. }
  309. }
  310. .custom-input-num {
  311. border: none;
  312. outline: none;
  313. background-color: transparent;
  314. width: 120px;
  315. max-width: 120px;
  316. text-align: center;
  317. background-color: #d9d9d98c;
  318. border-radius: 20px;
  319. font-size: 20px;
  320. color: var(--v-primary-base);
  321. }
  322. .code {
  323. // max-width: 1328px;
  324. max-width: 100%;
  325. background-color: #f7f8fa;
  326. border-radius: 6px;
  327. margin: 0 auto;
  328. &-left {
  329. border: 1px solid #00897B;
  330. border-radius: 6px;
  331. padding: 5px;
  332. }
  333. &-right {
  334. .price {
  335. font-size: 30px;
  336. font-weight: 700;
  337. color: var(--v-error-base);
  338. }
  339. }
  340. }
  341. .vip {
  342. width: 50px;
  343. height: 50px;
  344. position: absolute;
  345. top: 0;
  346. right: 0;
  347. overflow: hidden;
  348. border-radius: 8px
  349. }
  350. .custom-point-show {
  351. width: 100%;
  352. text-align: center;
  353. font-weight: normal;
  354. color: gray;
  355. font-size: 13px;
  356. }
  357. :deep(.v-slide-group__content) {
  358. background: none !important;
  359. }
  360. </style>