package-copy.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. <template>
  2. <div class="d-flex list mb-3">
  3. <div v-for="(val, index) in list" :key="index" :id="'positioning'+index" class="list-item text-center cursor-pointer" :class="{'active': index === current}" @click="handleClick(index, val)">
  4. <template v-if="val.id === 'custom'">
  5. <div class="d-flex flex-column algin-center justify-center" style="height: 100%">
  6. <div>需要发布更多职位</div>
  7. <div>请联系门墩儿购买企业套餐</div>
  8. <div style="width: 100%; position: relative;">
  9. <div style="width: 80px; height: 80px; margin: auto; opacity: 0.5;">
  10. <v-img src="https://minio.menduner.com/dev/menduner/contact.png" width="80" height="80"></v-img>
  11. </div>
  12. <!-- <div style="opacity: 0.5;"><v-icon size="60">mdi-qrcode</v-icon></div> -->
  13. <div class="absolute-center" style="color: #444; margin: 3px 0 0 2px;"><v-icon size="30">mdi-magnify-plus-outline</v-icon></div>
  14. </div>
  15. </div>
  16. </template>
  17. <template v-else>
  18. <h4 class="mt-5">{{ val.name }}</h4>
  19. <div class="color-primary">
  20. <span>¥</span>
  21. <span style="font-size: 35px;">{{ val.price / 100 }}</span>
  22. <span> 元</span>
  23. </div>
  24. <div class="text-decoration-line-through color-666">原价:{{ val.originalPrice / 100 }}元</div>
  25. <div class="font-size-14 color-999 mt-3 periodValidity py-2">有效期:{{ val.day }}天</div>
  26. </template>
  27. </div>
  28. </div>
  29. <div v-if="!Object.keys(select).length && !showCustom" class="color-warning text-center mt-15 font-size-20">请选择要购买的套餐</div>
  30. <div v-if="payType && payQrCodeTxt" id="codeBox" class="code pa-5 resume-box">
  31. <!-- <div class="resume-header">
  32. <div class="resume-title">扫码支付</div>
  33. </div> -->
  34. <div class="d-flex" :style="{'margin-left': offsetCalc + 'px'}">
  35. <div id="codeItem" class="d-flex flex-column align-center my-2">
  36. <div class="d-flex align-center">
  37. <span class="color-666 font-weight-bold">支付方式:</span>
  38. <v-chip-group v-model="payType" selected-class="text-primary" column mandatory @update:modelValue="payTypeChange">
  39. <v-chip filter v-for="k in payTypeList" :key="k.code" :value="k.code" class="mr-3" label>
  40. {{ k.name }}
  41. <svg-icon v-if="k.icon" class="ml-1" :name="k.icon" :size="k.size"></svg-icon>
  42. </v-chip>
  43. </v-chip-group>
  44. </div>
  45. <div class="code-right">
  46. <div class="price">
  47. <span class="font-size-13">¥</span>
  48. {{ FenYuanTransform(select?.price || 0) }}元
  49. </div>
  50. </div>
  51. <div class="code-left">
  52. <QrCode :text="payQrCodeTxt" :disabled="!remainderTimer" :width="170" @refresh="refreshQRCode" />
  53. </div>
  54. <div class="mt-52" style="color: var(--v-error-base);">
  55. 扫码支付时请勿离开
  56. <span v-if="remainderZhShow">{{ remainderZhShow }}</span>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. <div v-if="showCustom" id="codeBox" class="code pa-5 resume-box">
  62. <div style="width: 100%;">
  63. <div class="text-center">请扫码添加下方企业微信联系我们:</div>
  64. <div class="my-3" style="width: 180px; height: 180px; margin: auto;">
  65. <v-img src="https://minio.menduner.com/dev/menduner/contact.png" width="180" height="180"></v-img>
  66. </div>
  67. <div class="text-center mt-2">潘青海先生(Peter Pan)</div>
  68. </div>
  69. </div>
  70. </template>
  71. <script setup>
  72. defineOptions({ name: 'membershipPackageDynamicPackage' })
  73. import { ref, onUnmounted, nextTick } from 'vue'
  74. import { getEnterprisePackageList } from '@/api/enterprise'
  75. import { FenYuanTransform } from '@/utils/position'
  76. import { getEnableCodeList, payOrderSubmit, getOrderPayStatus, getUnpaidOrder } from '@/api/common'
  77. import { definePayTypeList, qrCodePay } from '@/utils/payType'
  78. import { useUserStore } from '@/store/user'; const store = useUserStore()
  79. import Snackbar from '@/plugins/snackbar'
  80. import { createTradeOrder } from '@/api/position'
  81. import { useRoute } from 'vue-router'; const route = useRoute()
  82. import { useRouter } from 'vue-router'; const router = useRouter()
  83. import Confirm from '@/plugins/confirm'
  84. import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
  85. const current = ref()
  86. const select = ref({})
  87. // 套餐列表
  88. const list = ref([])
  89. const getPackageList = async () => {
  90. const data = await getEnterprisePackageList()
  91. list.value = data
  92. list.value.push({id:'custom'})
  93. }
  94. const offsetCalc = ref(0)
  95. let codeItemOffsetWidth = null
  96. const codeBoxPx = 40 // codeBox左右内边距
  97. const calcStyle = (index) => {
  98. nextTick(() => {
  99. const codeBox = document.getElementById('codeBox')
  100. const codeBoxOffsetWidth = codeBox.offsetWidth - codeBoxPx
  101. if (!codeItemOffsetWidth) {
  102. const codeItem = document.getElementById('codeItem')
  103. codeItemOffsetWidth = codeItem.offsetWidth
  104. }
  105. //
  106. const menu = document.getElementById(`positioning${index}`)
  107. offsetCalc.value = 0
  108. if (menu && codeBoxOffsetWidth && codeItemOffsetWidth) {
  109. const menuHalfWidth= menu.offsetWidth/2
  110. const codeItemHalfWidth = codeItemOffsetWidth/2
  111. const calcNum = (menuHalfWidth+menuHalfWidth*2*index - codeItemHalfWidth) + (index ? 6 : 0)
  112. //
  113. if (calcNum+codeItemOffsetWidth > codeBoxOffsetWidth) { // 超出右侧,取最大
  114. offsetCalc.value = codeBoxOffsetWidth-codeItemOffsetWidth
  115. } else if (calcNum < 0) { // 超出左侧,取最小
  116. offsetCalc.value = 0
  117. } else {
  118. offsetCalc.value = calcNum-20 > 0 ? calcNum-20 : 0
  119. }
  120. }
  121. })
  122. }
  123. const showCustom = ref(false)
  124. const handleClick = (index, val) => {
  125. if (val.id == 'custom') {
  126. payQrCodeTxt.value = ''
  127. showCustom.value = true
  128. return
  129. }
  130. showCustom.value = false
  131. payQrCodeTxt.value = ''
  132. current.value = index
  133. select.value = val
  134. getUnpaidOrderList()
  135. }
  136. const payQrCodeTxt = ref('')
  137. // 2.发起充值
  138. const loading = ref(true)
  139. const payOrder = ref({})
  140. let maxCount = 0
  141. const getUnpaidOrderList = async () => {
  142. const data = await getUnpaidOrder({ spuId: select.value.id, type: 4 })
  143. if (!data) {
  144. await createTradeOrder({ price: (select.value.price-0), spuId: select.value.id, spuName: select.value.name, type: 4 })
  145. if (maxCount > 3) return // 避免死循环
  146. maxCount++
  147. setTimeout(() => {
  148. getUnpaidOrderList()
  149. }, 1000)
  150. }
  151. payOrder.value = data?.payOrder || null
  152. paySubmit()
  153. }
  154. const payTypeChange = (val) => {
  155. payType.value = val
  156. getUnpaidOrderList()
  157. }
  158. const timer = ref(null)
  159. onUnmounted(() => {
  160. if (timer.value) clearInterval(timer.value); timer.value = null
  161. })
  162. // 更新职位可发布数量
  163. const updateAccountInfo = async (init = false) => {
  164. await store.getEnterpriseInfo(true)
  165. if (init) return
  166. loading.value = false
  167. }
  168. const fromName = ref(route.query?.fromName || '')
  169. const callBackUrl = () => {
  170. // if (!fromName.value) return
  171. const urls = {
  172. position: '/recruit/enterprise/position',
  173. positionPay: '/recruit/enterprise/position?tab=0',
  174. }
  175. const texts = {
  176. position: '职位管理页面',
  177. positionPay: '职位管理待发布页面'
  178. }
  179. const url = fromName.value ? urls[fromName.value] : -1
  180. const text = fromName.value ? texts[fromName.value] : '购买前页面'
  181. //
  182. Confirm(t('common.confirmTitle'), `支付成功!是否返回${text}?`).then(() => {
  183. router.push(url)
  184. })
  185. }
  186. // callBackUrl() 测试
  187. const payStatus = async () => {
  188. try {
  189. const data = await getOrderPayStatus({ id: payOrder.value.id })
  190. if ((data?.status - 0) === 10) {
  191. // 支付成功
  192. if (timer.value) clearInterval(timer.value); timer.value = null
  193. setTimeout(() => {
  194. // 更新点数(充值、发布职位)
  195. updateAccountInfo()
  196. // 清除定时器
  197. clearTimer()
  198. // 支付成功
  199. if (fromName.value) callBackUrl()
  200. else Snackbar.success('支付成功')
  201. }, 2000);
  202. }
  203. } catch (error) {
  204. console.log(error)
  205. }
  206. }
  207. const paySubmit = async () => {
  208. if (!payType.value) return
  209. try {
  210. // 提交支付订单
  211. const params = {
  212. channelCode: payType.value, // 支付渠道
  213. id: payOrder.value.id
  214. }
  215. const res = await payOrderSubmit(params)
  216. payQrCodeTxt.value = res?.displayContent || '' // 生成二维码内容
  217. calcStyle(current.value)
  218. initIntervalFun()
  219. if (timer.value) clearInterval(timer.value); timer.value = null
  220. timer.value = setInterval(() => { payStatus() }, 1000) // 轮巡查询用户是否支付
  221. } catch (error) {
  222. console.log(error)
  223. }
  224. }
  225. // 1.支付方式
  226. const payType = ref('')
  227. const payTypeList = ref([])
  228. const codeList = ref([])
  229. const getCodeList = async () => {
  230. try {
  231. const list = await getEnableCodeList({ appId: 11 })
  232. codeList.value = list || []
  233. } catch (error) {
  234. console.log(error)
  235. } finally {
  236. if (definePayTypeList?.length && codeList.value?.length) {
  237. codeList.value.forEach(code => {
  238. const item = definePayTypeList.find(p => p.code === code)
  239. if (item) {
  240. if (!payType.value) {
  241. // 默认值赋值(暂时只支持扫码)
  242. const bool = qrCodePay.includes(code)
  243. if (bool) payType.value = code
  244. }
  245. payTypeList.value.push(item)
  246. }
  247. })
  248. }
  249. }
  250. }
  251. nextTick(async () => {
  252. await getPackageList()
  253. await getCodeList()
  254. })
  255. const refreshQRCode =() => { // 刷新二维码
  256. getUnpaidOrderList()
  257. }
  258. const remainderTimer = ref(null)
  259. const countdownTime = 60000 * 3 // 倒计时三分钟
  260. let remainder = 0 // number
  261. // 初始化倒计时
  262. const initIntervalFun = () => {
  263. remainder = countdownTime // 初始倒计时时间
  264. if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null // 每一次点击都清除上一个轮询
  265. // 倒计时计算
  266. remainderCalc()
  267. remainderTimer.value = setInterval(() => { remainderCalc() }, 1000)
  268. if (timer.value) clearInterval(timer.value); timer.value = null
  269. timer.value = setInterval(() => { payStatus() }, 2000) // 轮巡查询用户是否支付
  270. }
  271. const formatDuration = (remainder) => {
  272. // 将毫秒转换为秒
  273. var seconds = Math.floor(remainder / 1000)
  274. // 计算分钟和剩余的秒数
  275. var minutes = Math.floor(seconds / 60)
  276. var remainingSeconds = seconds % 60
  277. // 格式化分钟和秒数,确保秒数为两位数(如果小于10,则前面补0)
  278. minutes = minutes.toString().padStart(2, '0')
  279. remainingSeconds = remainingSeconds.toString().padStart(2, '0')
  280. // 返回格式化的字符串
  281. return `${minutes}分${remainingSeconds}秒`
  282. }
  283. const remainderZhShow = ref('')
  284. const clearTimer = () => {
  285. if (timer.value) clearInterval(timer.value); timer.value = null
  286. if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null
  287. remainderZhShow.value = ''
  288. }
  289. const remainderCalc = () => {
  290. remainder -= 1000
  291. remainderZhShow.value = formatDuration(remainder)
  292. if (remainder <= 0) clearTimer()
  293. }
  294. </script>
  295. <style scoped lang="scss">
  296. .list {
  297. &-item {
  298. width: 25%;
  299. height: 172px;
  300. background-color: #fcfcfd;
  301. border: 1px solid #f3f3f3;
  302. border-radius: 8px;
  303. margin-right: 12px;
  304. &:last-child {
  305. margin-right: 0;
  306. }
  307. .periodValidity {
  308. background-color: #f2f4f7;
  309. border-radius: 0 0 8px 8px;
  310. }
  311. }
  312. .active {
  313. border: 1px solid #00B760;
  314. }
  315. }
  316. .code {
  317. background-color: #f7f8fa;
  318. border-radius: 6px;
  319. margin: 0 auto;
  320. &-left {
  321. border: 1px solid #00B760;
  322. border-radius: 6px;
  323. padding: 5px;
  324. }
  325. &-right {
  326. .price {
  327. font-size: 30px;
  328. font-weight: 700;
  329. color: var(--v-error-base);
  330. }
  331. }
  332. }
  333. </style>