| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698 | <template>  <view>    <view class="vipBox">      <view class="avatar">        <img :src="getUserAvatar(baseInfo?.avatar, baseInfo?.sex)" alt="" class="img-box" :class="{'img-box-atc': userInfo?.vipExpireDate}">        <image v-if="new Date().getTime() < userInfo?.vipExpireDate" src="/static/svg/vip.svg" class="vipIcon"></image>      </view>      <view class="nameBox">        <view class="name font-weight-bold font-size-16 MiSans-Semibold">{{ baseInfo?.name || userInfo?.phone }}</view>        <view class="vipInfo font-size-14 MiSans-Normal" v-if="new Date().getTime() < userInfo?.vipExpireDate">          {{ pName }}          <view>将于{{ remaining }}后过期</view>        </view>      </view>    </view>    <view>      <swiper class="swiper-box" :current="current" indicator-dots indicator-active-color="#f1b17a">        <swiper-item v-for="(item, index) in memberListLength" :key="index" class="swiper-items">          <view class="swiper-item" v-for="val in item" :key="val.id">            <view              class="card"              :class="{ recommend: val.recommend, vipFlag: val.my, active: val.id === chooseId}"              @tap="handleChoose(val)"            >              <text class="MiSans-Normal">{{ val.name }}</text>              <view>                <uni-icons color="#f30" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>                <text class="MiSans-Normal">{{ val.price }}</text>              </view>            </view>          </view>        </swiper-item>	      </swiper>      <view v-if="typeof chooseId === 'number'" class="itemBox">        <span class="MiSans-Normal">套餐权益 ( {{ list.name }} )</span>        <uni-section          v-for="item in list.list"          :key="item.id"          class="item MiSans-Normal"          :class="{ active: item.active }"          titleColor="#774e20"          subTitleColor="#774e20"          :title="item.text"        >          <template v-slot:right>            <uni-icons color="#774e20" :type="item.active ? 'checkmarkempty' : 'closeempty'" size="20"/>          </template>        </uni-section>      </view>    </view>    <view class="pay" v-if="!list.my">      <view class="pay-box">        <view class="price MiSans-Bold">          <uni-icons color="#e68735" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>          {{ amount }}        </view>        <view class="btn MiSans-Medium" @tap="handleOpen">          立刻升级        </view>        </view>    </view>    <uni-popup ref="popup" :is-mask-click="false" borderRadius="10px 10px 0 0" background-color="#eee">      <view class="popup-content">        <view class="popup-content-close">          <view class="icon" @tap="handleClose">            <uni-icons              type="closeempty"              color="#999"              size="24"            />          </view>        </view>        <view class="popup-content-main">          <view class="popup-content-main-count">            <view class="title">{{ list.name }} 充值</view>            <view class="pay">              <uni-icons color="#000" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>              <view>{{ amount }}</view>            </view>          </view>          <view class="popup-content-main-type">            <view v-if="showPayMethods" class="card">              <radio-group @change="radioChange">                <label class="card-label" v-for="item in payTypeList" :key="item.value">                  <view class="name">                    <uni-icons :color="item.color" class="mr-1" :type="item.icon" size="24" custom-prefix="iconfont"></uni-icons>                    {{item.name}}                  </view>                  <view>                    <radio :value="item.value" :disabled="item.disabled" :checked="item.value === channelValue" />                  </view>                </label>              </radio-group>            </view>          </view>        </view>        <view class="popup-content-btn">          <button class="popup-content-btn-s" @tap="handlePay">            确认支付             <uni-icons color="#FFF" type="icon-renminbi1688" size="16" custom-prefix="iconfont"></uni-icons>            {{ amount }}          </button>        </view>      </view>    </uni-popup>  </view></template><script setup>import { onHide, onShow } from '@dcloudio/uni-app';import { ref, computed } from 'vue'import { getUserAvatar } from '@/utils/avatar'import { userStore } from '@/store/user'import { getMembershipPackageList } from '@/api/vip'import { orderCreated, getOrder, getSocialUser, socialUserBind, payOrderSubmit, getOrderPayStatus, getEnableCodeList } from '@/api/common'const useUserStore = userStore()const baseInfo = computed(() => useUserStore?.baseInfo)// const userInfo = computed(() => useUserStore?.userInfo)const userInfo = ref(useUserStore?.userInfo || {})const memberList = ref([])const recommend = ref(null)const chooseId = ref(null)const orderInfo = ref(null)const popup = ref()const amount = computed(() => {  return parseFloat(+list.value.price).toFixed(2)})const memberListLength = computed(() => {  const result = [];    for (let i = 0; i < memberList.value.length; i += 2) {        const pair = memberList.value.slice(i, i + 2)      result.push(pair)  }  return result})const pName = computed(() => {  return memberList.value.find(item => +item.id === +userInfo.value?.vipFlag)?.name})const remaining = computed(() => {  if (!userInfo.value?.vipExpireDate) return null  const diffInMs =  userInfo.value?.vipExpireDate - new Date().getTime()  const day = diffInMs / (1000 * 60 * 60 * 24)  return day < 1 ? '今天' : Math.floor(day) + '天'})const list = computed(() => {  const item = memberList.value.find(item => item.id === chooseId.value)  return item ?? {}})const current = ref(0)const channelValue = ref('')const payType = [  {    name: '微信支付',    value: 'wx_lite',    icon: 'icon-weixinzhifu',    color: '#1AAD19'  },  {    name: '钱包支付',    value: 'wallet',    disabled: true,    icon: 'icon-qianbao1',    // color: '#fd9702'    color: '#00B760'  }]const radioChange = (e) => {  channelValue.value = e?.detail?.value || ''}const chooseItem = ref(null)const handleChoose = (val) => {  chooseId.value = val.id  chooseItem.value = val}const handleOpen = () => {  popup.value.open('bottom')}const handleClose = () => {  popup.value.close()}// 设置 openid 到本地存储,目前只有 pay 支付时会使用const setOpenid = (openid) => {  uni.setStorageSync('openid', openid)}const bind = () => {  return new Promise(async (resolve, reject) => {    // 1. 获得微信 code    const codeResult = await uni.login()    if (codeResult.errMsg !== 'login:ok') {      return resolve(false)    }    // 2. 绑定账号 // // 社交快捷登录    const obj = {      type: socialType,      code: codeResult.code,      state: 'default',    }    const bindResult = await socialUserBind(obj);    if (bindResult.code === 0) {      setOpenid(bindResult.data)      return resolve(true)    } else {      return resolve(false)    }  })}const bindWeiXin = () => {  uni.showModal({    title: '微信支付',    content: '请先绑定微信再使用微信支付',    success: function (res) {      if (res.confirm) {        // 微信小程序绑定        bind()      }    },  });}const socialType = 34; // 社交类型 - 微信小程序// 预支付const prepay = async (channel, orderData) => {   return new Promise(async (resolve, reject) => {    let data = {      id: orderData.payOrder.id,      channelCode: channel,      channelExtras: {},    };    // 特殊逻辑:微信公众号、小程序支付时,必须传入 openid    if (['wx_pub', 'wx_lite'].includes(channel)) {      const userRes = await getSocialUser(socialType)      const openid = userRes?.data?.openid ? userRes.data.openid : null      // 如果获取不到 openid,微信无法发起支付,此时需要引导      if (!openid) {        bindWeiXin()        return      }      // console.log('openid:', openid)      data.channelExtras.openid = openid    }    // 发起预支付 API 调用    payOrderSubmit(data).then((res) => {      // 成功时      res.code === 0 && resolve(res)      // 失败时      if (res.code !== 0 && res.msg.indexOf('无效的openid') >= 0) {        // 特殊逻辑:微信公众号、小程序支付时,必须传入 openid 不正确的情况        if (          res.msg.indexOf('无效的openid') >= 0 || // 获取的 openid 不正确时,或者随便输入了个 openid          res.msg.indexOf('下单账号与支付账号不一致') >= 0        ) {          bindWeiXin()        }      }    })  })}let interTimer = nulllet payLoading = falseconst checkPayStatus = async (id) => {  if (!id) return  try {    if (payLoading || !interTimer) return    payLoading = true    const res = await getOrderPayStatus({ id })    if (res?.data?.status === 10) {      if (interTimer) clearInterval(interTimer)      // emit('paySuccess')      setTimeout(async () => {        const _userInfo = await useUserStore.getUserInfo()        userInfo.value = _userInfo        getMemberList() // 刷新套餐列表      }, 1500)    }  } catch (error) {    console.log(error)  } finally {    payLoading = false  }}// 计时器const initIntervalFun = () => {  if (interTimer) clearInterval(interTimer)  // 查询是否已经支付  const id = orderInfo.value?.payOrder?.id || orderInfo.value?.order?.payOrderId  if (id) {    interTimer = setInterval(() => {      checkPayStatus(id)    }, 1000)  }}const weChatMiniProgramPay = async (orderData) => {  orderInfo.value = orderData  let res = await prepay(channelValue.value, orderData); // 预支付  if (res?.code !== 0) {    return;  }  // 调用微信小程序支付  const payConfig = res?.data?.displayContent ? JSON.parse(res.data.displayContent) : null  if (!payConfig) return uni.showToast({ title: '购买失败', icon: 'none'})  uni.requestPayment({    provider: 'wxpay',    timeStamp: payConfig.timeStamp,    nonceStr: payConfig.nonceStr,    package: payConfig.packageValue,    signType: 'RSA',    paySign: payConfig.paySign,    success: (res) => {      initIntervalFun()      popup.value.close()      uni.showToast({ title: '支付成功', icon: 'none'})    },    fail: (err) => {      if (err.errMsg === 'requestPayment:fail cancel') {        uni.showToast({ title: '支付已取消', icon: 'none'})      } else {        // this.payResult('fail');        uni.showToast({ title: '支付失败', icon: 'none'})      }    },  });}// 支付const handlePay = async () => {  if (!channelValue.value) {    uni.showToast({ title: '请选择支付方式', icon: 'none'})    return  }  const val = chooseItem.value  try {    const res = await getOrder({      spuId: val.id, // 商品编号      type: val.type    })    if (res.data) {      // 获取支付码      weChatMiniProgramPay(res.data)      return    }    await orderCreated({      spuId: val.id, // 商品编号      spuName: val.name, // 商品名称      price: val.price*100, // 价格      type: val.type // 订单类型 type:订单类型 0平台订单|1发布职位|2发布众聘职位|3会员套餐|4企业会员套餐|5招聘会门票    })    const _res = await getOrder({      spuId: val.id, // 商品编号      type: val.type    })    // 获取支付码    weChatMiniProgramPay(_res.data)  } catch (error) {    console.log(error)  } finally {    val.loading = false  }}const getMemberList = async () => {  try {    const { data } = await getMembershipPackageList()    if (!data || data.length === 0) {      return    }    // memberList.value = data    let vipFlagIndex = null    const list = data.map((item, index) => {      if (new Date().getTime() < userInfo.value?.vipExpireDate && +item.id === +userInfo.value?.vipFlag) {        vipFlagIndex = index // 低于当前套餐的(套餐)不展示      }      if (item.recommend) {        recommend.value = index // 推荐套餐      }      return {        ...item,        price: item.price/100,        my: vipFlagIndex === index,        list: JSON.parse(item.text),        type: 3, // 订单类型 type:订单类型 0平台订单|1发布职位|2发布众聘职位|3会员套餐|4企业会员套餐|5招聘会门票        loading: false      }    })    memberList.value = vipFlagIndex ? list.slice(vipFlagIndex) : list // 低于当前套餐的(套餐)不展示    // 轮播current位置    // if ((!userInfo.value?.vipFlag || userInfo.value?.vipExpireDate - new Date().getTime() > 0 ) && typeof recommend.value === 'number') {    //   current.value = parseInt(recommend.value/2)    // }    handleChoose(memberList.value[0]) // 已购买或者未购买都选中第一个  } catch (error) {    uni.showToast({ title: '查询数据失败,请重试', icon: 'none' })  }}getMemberList()onShow(() => {  if (orderInfo && orderInfo.value?.id) initIntervalFun()})onHide(() => {  if (interTimer) clearInterval(interTimer)})// 余额和其他还没有接暂时只支持微信支付let payTypeList = []const showPayMethods = ref(false)// 获取支付方式const getPayMethodsList = async () => {  showPayMethods.value = false  payTypeList = []  try {    const res = await getEnableCodeList({appId: 14})    if (!res?.data?.length) {      return    }    payTypeList.push(...payType.filter(e => res.data.includes(e.value)))    const result = payType.find(item => !item.disabled && item.value)    if (result) channelValue.value = result.value    showPayMethods.value = true  } catch (error) {    console.log(error)  }}getPayMethodsList()</script><style lang="scss" scoped>.vipBox {	// color: #a18a0f;  padding: 80rpx 50rpx;  display: flex;  background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);  .avatar{    position: relative;    width: 100rpx;    height: 100rpx;    margin: 0;    .img-box {      width: 100%;      height: 100%;      border: 2rpx solid #ccc;      border-radius: 50%;      border: 1px solid gold;    }    .img-box-atc {      border: 1px solid gold;    }    .vipIcon {      position: absolute;      width: 50%;      height: 50%;      bottom: 0;      right: 0;      transform: translate(0, 30%);    }  }  .nameBox {    display: flex;    flex-direction: column;    justify-content: space-around;    margin-left: 30rpx;    .name {      color: #724d2b;    }    .vipInfo {      color: #572a00;    }  }}.swiper-box {  height: 230rpx;  .swiper-items {    display: grid;    grid-template-columns: 1fr 1fr;  }  .swiper-item {    display: flex;    flex-direction: column;    justify-content: center;    align-items: center;    height: 200rpx;    padding: 20rpx 10rpx;    box-sizing: border-box;    .card {      color: #774e20;      background-color: rgb(255, 251, 248);      border: 1px solid #f1b17a;      width: 100%;      height: 100%;      border-radius: 10rpx;      padding: 0 20rpx;      box-sizing: border-box;      display: flex;      justify-content: space-between;      align-items: center;      position: relative;      overflow: hidden;      &.recommend {        &::after {          content: '推荐';          position: absolute;          right: 0;          top: 0;          padding: 6rpx 10rpx;          font-size: 28rpx;          background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);        }      }      &.vipFlag {        &::before {          content: '我的套餐';          position: absolute;          left: 0;          top: 0;          padding: 6rpx 10rpx;          font-size: 28rpx;          background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);        }      }      &.active {        box-shadow: 0 0 18rpx 0 rgb(216 160 82);      }    }  }}.itemBox {  padding: 20rpx 40rpx;  .item {    // padding: 10rpx 0;    margin-top: 20rpx;    // color: rgba(119,78,32,.5);    // &.active {      color:#774e20;    // }  }}.pay {  position: sticky;  bottom: 0;  padding: 0 40rpx 50rpx 40rpx;  box-sizing: border-box;  &-box {    width: 100%;    background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);    border-radius: 180rpx 0 180rpx 0;    box-shadow: 3rpx 6rpx 10rpx 0rpx rgb(216 160 82);    display: flex;    justify-content: space-between;    align-items: center;    height: 100rpx;    .price {      padding: 0 40rpx;      font-size: 40rpx;      font-weight: 600;      color: #e68735;    }    .btn {      height: 100%;      display: flex;      align-items: center;      padding: 0 40rpx;      border: 2rpx solid #00B760;      background: #00B760;      color: #FFF;      border-radius: 180rpx 0 180rpx 0;      position: relative;      // &::after {      //   content: '';      //   position: absolute;      //   width: 50rpx;      //   height: 50rpx;      //   background: radial-gradient(top right, transparent 50%, #00B760 50%);      //   left: 0;      //   top: 0;      //   margin-left: -25rpx;      //   border-radius: 180rpx;      // }    }  }}.popup-content {  max-height: 500px;  display: flex;  flex-direction: column;  &-close {    display: flex;    padding: 10px;    justify-content: flex-end;    .icon {      width: 30px;      height: 30px;      background: #ccc;      border-radius: 30px;      display: flex;      align-items: center;      justify-content: center;    }  }  &-main {    flex: 1;    height: 0;    overflow-y: auto;    &-count {      margin-bottom: 20px;      text-align: center;      .title {        font-size: 28rpx;        color: #666      }      .pay {        font-size: 52rpx;        color: #000;        font-weight: 600;        display: flex;        align-items: center;        justify-content: center;        padding: 10px 0;      }    }    &-type {      width: 100%;      padding: 0 20px;      box-sizing: border-box;      .card {        border-radius: 10px;        margin: 0 auto;        background: #FFF;        padding: 10px;        &-label {          padding: 15px 0;          box-sizing: border-box;          display: flex;          justify-content: space-between;          border-bottom: 1px solid #eee;          &:last-of-type {            border-bottom: none;          }          .name {            display: flex;            align-items: center;            color: #333;          }        }      }    }  }  &-btn {    height: 70px;    width: 100%;    margin-top: 10px;    display: flex;    align-items: center;    justify-content: center;    &-s {      height: 40px;      width: 75%;      line-height: 40px;      color: #FFF;      // color: #724d2b;      background: #00B760;      // background: linear-gradient(121deg,#fde2c2 29.02%,#c19164 104.03%);      border-radius: 90px;    }  }}.mr-1 {  margin-right: 10px;}</style>
 |