فهرست منبع

Merge branch 'dev' of https://git.citupro.com/zhengnaiwen_citu/menduner into dev

Xiao_123 4 ماه پیش
والد
کامیت
cb92a77b90

+ 8 - 0
src/router/modules/recruit.js

@@ -51,6 +51,14 @@ const recruit = [
           title: '商品列表'
         }
       },
+      {
+        path: '/mall/confirm_order',
+        component: () => import('@/views/mall/components/confirm_order'),
+        name: 'confirmOrder',
+        meta: {
+          title: '购买支付'
+        }
+      },
       {
         path: '/mall/goodsDetail/:id',
         component: () => import('@/views/mall/components/details.vue'),

+ 45 - 40
src/views/mall/cart/index.vue

@@ -63,13 +63,13 @@
     </div>
   </v-card>
   <!-- 结算 -->
-  <CtDialog :visible="showSettlement" titleClass="text-h6" :widthType="3" title="订单信息" @submit="handleSubmit" @close="handleClose">
+  <!-- <CtDialog :visible="showSettlement" titleClass="text-h6" :widthType="3" title="订单信息" @submit="handleSubmit" @close="handleClose">
     <confirm ref="confirmRef" :data="skuInfo" @orderCreated="orderCreated"></confirm>
-  </CtDialog>
+  </CtDialog> -->
   <!-- 支付 -->
-  <CtDialog :visible="showPay" titleClass="text-h6" :widthType="3" title="收银台" :footer="false" @close="payCancel">
+  <!-- <CtDialog :visible="showPay" titleClass="text-h6" :widthType="3" title="收银台" :footer="false" @close="payCancel">
     <pay ref="payRef" :id="payOrderId" @paySuccess="paySuccess"></pay>
-  </CtDialog>
+  </CtDialog> -->
 </template>
 
 <script setup>
@@ -82,8 +82,8 @@ import { fen2yuan } from '@/hooks/web/useGoods'
 import GoodsItem from '../components/GoodsItem'
 import { updateCartSelected, updateCartCount, deleteCartGoods } from '@/api/mall/cart'
 import Snackbar from '@/plugins/snackbar'
-import confirm from '@/views/mall/components/details/order/confirm.vue'
-import pay from '@/views/mall/components/details/order/pay.vue'
+// import confirm from '@/views/mall/components/details/order/confirm.vue'
+// import pay from '@/views/mall/components/details/order/pay.vue'
 import Confirm from '@/plugins/confirm'
 import { useI18n } from '@/hooks/web/useI18n'; const { t } = useI18n()
 const props = defineProps({
@@ -182,9 +182,9 @@ const handleChangeCount = async (count, id) => {
 }
 
 // 结算
-const showSettlement = ref(false)
+// const showSettlement = ref(false)
 const selectedList = ref([])
-const skuInfo = ref(null) // 购买商品规格信息
+// const skuInfo = ref(null) // 购买商品规格信息
 const handleSettlement = () => {
   let items = []
   let goods_list = [];
@@ -208,42 +208,47 @@ const handleSettlement = () => {
     return
   }
   
-  skuInfo.value = JSON.stringify({
-    items
+  // skuInfo.value = JSON.stringify({
+  //   items
+  // })
+  // showSettlement.value = true
+
+  localStorage.setItem('confirm_order_data', JSON.stringify({ items }))
+  nextTick(() => {
+    router.push('/mall/confirm_order')
   })
-  showSettlement.value = true
-}
-const confirmRef = ref()
-const handleSubmit = () => {
-  if (confirmRef.value) confirmRef.value.onConfirm()
-}
-const handleClose = () => {
-  showSettlement.value = false
 }
+// const confirmRef = ref()
+// const handleSubmit = () => {
+//   if (confirmRef.value) confirmRef.value.onConfirm()
+// }
+// const handleClose = () => {
+//   showSettlement.value = false
+// }
 
 // 创建订单完成
-const payRef = ref()
-const showPay = ref(false)
-const payOrderId = ref('')
-const orderCreated = (id) => {
-  showSettlement.value = false
-  payOrderId.value = id
-  showPay.value = true
-  // 更新购物车列表
-  getCartList()
-}
-const payCancel = () => {
-  Snackbar.warning('您已取消支付!')
-  showPay.value = false
-  // window.open('/mall/user/order?tab=0')
-  setTimeout(() => { router.push({ path: '/mall/user/order', query: { tab: 0 } }) }, 500);
-}
-const paySuccess = (e) => {
-  // Snackbar.success('支付成功!')
-  // showPay.value = false
-  // setTimeout(() => { router.push({ path: '/mall/user/order', query: { tab: 10 } }) }, 500);
-  router.push({ path: '/mall/payOver', query: { price: e.price }})
-}
+// const payRef = ref()
+// const showPay = ref(false)
+// const payOrderId = ref('')
+// const orderCreated = (id) => {
+//   showSettlement.value = false
+//   payOrderId.value = id
+//   showPay.value = true
+//   // 更新购物车列表
+//   getCartList()
+// }
+// const payCancel = () => {
+//   Snackbar.warning('您已取消支付!')
+//   showPay.value = false
+//   // window.open('/mall/user/order?tab=0')
+//   setTimeout(() => { router.push({ path: '/mall/user/order', query: { tab: 0 } }) }, 500);
+// }
+// const paySuccess = (e) => {
+//   // Snackbar.success('支付成功!')
+//   // showPay.value = false
+//   // setTimeout(() => { router.push({ path: '/mall/user/order', query: { tab: 10 } }) }, 500);
+//   router.push({ path: '/mall/payOver', query: { price: e.price }})
+// }
 </script>
 
 <style scoped lang="scss">

+ 109 - 0
src/views/mall/components/confirm_order/addressSelection.vue

@@ -0,0 +1,109 @@
+<!-- 下单界面,收货地址 or 自提门店的选择组件 -->
+<template>
+  <div>
+    <v-card class="allAddress" elevation="1" >
+      <div class="d-flex justify-space-between align-center">
+        <div
+          class="address py-4 px-5"
+          v-if="state.deliveryType === 1"
+        >
+          <div class="addressCon" v-if="state.addressInfo.name">
+            <div class="name d-flex"
+              >{{ state.addressInfo.name }}
+              <div class="phone ml-3">{{ state.addressInfo.mobile }}</div>
+            </div>
+            <div class="d-flex mt-1">
+              <div class="default font-color mr-3" v-if="state.addressInfo.defaultStatus">[默认]</div>
+              <div class="line2">
+                {{ state.addressInfo.areaName }} {{ state.addressInfo.detailAddress }}
+              </div>
+            </div>
+          </div>
+          <div class="addressCon" v-else>
+            <div class="setaddress">设置收货地址</div>
+          </div>
+        </div>
+        <div class="mx-1">
+          <v-btn color="primary" variant="text" @click="onSelectAddress">{{ state.addressInfo?.name ? '切换' : '去添加'}}</v-btn>
+        </div>
+      </div>
+    </v-card>
+    <CtDialog :visible="selectAddress" titleClass="text-h6" :footer="true" :widthType="1" title="选择收货地址" @submit="handleSubmit" @close="selectAddress = false">
+      <div style="min-height: 60vh;">
+        <addressPage ref="addressPageRef" showSelect></addressPage>
+      </div>
+    </CtDialog>
+  </div>
+</template>
+
+<script setup>
+  import { computed, ref } from 'vue';
+  import { isEmpty1 } from '@/utils/is'
+  import addressPage from '@/views/mall/user/address'
+  import Snackbar from '@/plugins/snackbar'
+  const props = defineProps({
+    modelValue: {
+      type: Object,
+      default() {},
+    },
+  });
+  const emits = defineEmits(['update:modelValue']);
+
+  // computed 解决父子组件双向数据同步
+  const state = computed({
+    get() {
+      return new Proxy(props.modelValue, {
+        set(obj, name, val) {
+          emits('update:modelValue', {
+            ...obj,
+            [name]: val,
+          });
+          return true;
+        },
+      });
+    },
+    set(val) {
+      emits('update:modelValue', val);
+    },
+  });
+
+  const selectAddress = ref(false)
+  // 选择地址
+  function onSelectAddress() {
+    selectAddress.value = true
+  }
+  
+  const addressPageRef = ref()
+  const handleSubmit = () => {
+    const selected = addressPageRef.value?.getSelected() || []
+    if (!selected) return Snackbar.warning('请选择收货地址')
+    changeConsignee(selected)
+    selectAddress.value = false
+  }
+
+  // 更改收货人地址&计算订单信息
+  async function changeConsignee(addressInfo = {}) {
+    if (!isEmpty1(addressInfo)) {
+      if (state.value.deliveryType === 1) {
+        state.value.addressInfo = addressInfo;
+      }
+      if (state.value.deliveryType === 2) {
+        state.value.pickUpInfo = addressInfo;
+      }
+    }
+  }
+
+</script>
+
+<style scoped lang="scss">
+  .allAddress .font-color {
+    color: #e93323 !important;
+  }
+
+  .address .addressCon .name {
+    font-size: 16px;
+    color: #282828;
+    font-weight: bold;
+    // margin-bottom: 10px;
+  }
+</style>

+ 243 - 0
src/views/mall/components/confirm_order/confirm.vue

@@ -0,0 +1,243 @@
+<template>
+  <div>
+    <!-- 头部地址选择【配送地址】【自提地址】 -->
+    <AddressSelection v-model="addressState" class="addressBox" />
+    
+    <!-- 购买的商品信息 -->
+    <v-card class="goodsListBox mb-3 pa-3">
+      <s-goods-item
+        v-for="(item, index) in state.orderInfo.items"
+        :key="item.skuId"
+        :img="item.picUrl"
+        :title="item.spuName"
+        :skuText="item.properties.map((property) => property.valueName).join(' ')"
+        :price="item.price"
+        :num="item.count"
+        :style="{'marginTop': index ? '8px' : '0px'}"
+      />
+    </v-card>
+
+    <!-- 价格信息 -->
+    <div>
+      <div>
+        <div class="order-item d-flex">
+          <div class="item-title mr-3 ">商品金额:</div>
+          <div>¥{{ fen2yuan(state.orderInfo.price.totalPrice) }}</div>
+        </div>
+        <!-- 快递配置时,信息的展示 -->
+        <div
+          class="order-item d-flex"
+          v-if="addressState.deliveryType === 1"
+        >
+          <div class="item-title mr-3">运{{ spaces() }}费:</div>
+          <div>
+            <span class="text-red" v-if="state.orderInfo.price.deliveryPrice > 0">
+              +¥{{ fen2yuan(state.orderInfo.price.deliveryPrice) }}
+            </span>
+            <div class="item-value" v-else>免运费</div>
+          </div>
+        </div>
+        <!-- 门店自提时,需要填写姓名和手机号 -->
+      </div>
+      
+      <div class="mt-5">
+        <v-text-field
+          v-model="state.orderPayload.remark"
+          label="订单备注" 
+          placeholder="建议留言前先与商家沟通"
+          variant="outlined" 
+          density="compact"
+          color="primary"
+        ></v-text-field>
+      </div>
+
+      <div class="total-box-footer d-flex flex-column align-end">
+        <div class="d-flex">
+          <div class="mr-3">
+            <span class="total-num">共</span>
+            <span class="mx-1" style="color: var(--v-primary-base);">{{ state.orderInfo.items.reduce((acc, item) => acc + item.count, 0) }}</span>
+            <span class="total-num">件</span>
+          </div>
+          <div>合计:</div>
+          <div class="total-num text-red"> ¥{{ fen2yuan(state.orderInfo.price.payPrice) }}</div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  import { reactive, ref, watch } from 'vue'
+  import AddressSelection from './addressSelection.vue'
+  import sGoodsItem from '@/views/mall/components/s-goods-item'
+  import { fen2yuan } from '@/hooks/web/useGoods'
+  import { spaces } from '@/utils/index.js'
+  import { getTradeConfig, createOrder, settlementOrder } from '@/api/mall/trade'
+  import Snackbar from '@/plugins/snackbar'
+  const emit = defineEmits(['orderCreated'])
+  const props = defineProps({
+    data: {
+      type: String,
+      default: ''
+    }
+  })
+
+  const state = reactive({
+    orderPayload: {},
+    orderInfo: {
+      items: [], // 商品项列表
+      price: {}, // 价格信息
+    },
+    showCoupon: false, // 是否展示优惠劵
+    couponInfo: [], // 优惠劵列表
+    showDiscount: false, // 是否展示营销活动
+    // ========== 积分 ==========
+    pointStatus: false, //是否使用积分
+  });
+  
+  // 检测支付环境
+  // const payState = reactive({
+  //   orderType: 'goods', // 订单类型; goods - 商品订单, recharge - 充值订单
+  //   orderInfo: {}, // 支付单信息
+  //   payStatus: 0, // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
+  //   payMethods: [], // 可选的支付方式
+  //   payment: '', // 选中的支付方式
+  // });
+
+  const addressState = ref({
+    addressInfo: {}, // 选择的收货地址
+    deliveryType: undefined, // 收货方式:1-快递配送,2-门店自提
+    isPickUp: true, // 门店自提是否开启
+    pickUpInfo: {}, // 选择的自提门店信息
+    receiverName: '', // 收件人名称
+    receiverMobile: '', // 收件人手机
+  });
+  
+  watch(
+    () => props.data, 
+    (newVal) => {
+      if (newVal) {
+        state.orderPayload = JSON.parse(newVal);
+        tradeConfig()
+      }
+    },
+    { immediate: true },
+    // { deep: true }
+  )
+  async function tradeConfig () {
+    // 获取交易配置
+    const data = await getTradeConfig();
+    addressState.value.isPickUp = data.deliveryPickUpEnabled;
+
+    // 价格计算
+    // 情况一:先自动选择“快递物流”
+    addressState.value.deliveryType = 1;
+    let orderCode = await getOrderInfo();
+    if (orderCode === 0) {
+      return;
+    }
+    // 情况二:失败,再自动选择“门店自提”
+    if (addressState.value.isPickUp) {
+      addressState.value.deliveryType = 2;
+      let orderCode = await getOrderInfo();
+      if (orderCode === 0) {
+        return;
+      }
+    }
+    // 情况三:都失败,则不选择
+    addressState.value.deliveryType = undefined;
+    await getOrderInfo()
+  }
+
+  // 提交订单
+  function onConfirm() {
+    if (addressState.value.deliveryType === 1 && !addressState.value.addressInfo.id) {
+      Snackbar.warning('请选择收货地址')
+      return;
+    }
+    submitOrder()
+  }
+  
+  // 创建订单&跳转
+  async function submitOrder() {
+    const data = await createOrder({
+      items: state.orderPayload.items,
+      couponId: state.orderPayload.couponId,
+      remark: state.orderPayload.remark,
+      deliveryType: addressState.value.deliveryType,
+      addressId: addressState.value.addressInfo.id, // 收件地址编号
+      pickUpStoreId: addressState.value.pickUpInfo.id, //自提门店编号
+      receiverName: addressState.value.receiverName, // 选择门店自提时,该字段为联系人名
+      receiverMobile: addressState.value.receiverMobile, // 选择门店自提时,该字段为联系人手机
+      pointStatus: state.pointStatus,
+      combinationActivityId: state.orderPayload.combinationActivityId,
+      combinationHeadId: state.orderPayload.combinationHeadId,
+      seckillActivityId: state.orderPayload.seckillActivityId,
+      pointActivityId: state.orderPayload.pointActivityId,
+    });
+    if (!data.payOrderId && data.payOrderId > 0) return
+    emit('orderCreated', data.payOrderId, data.id) // 更新购物车列表,如果来自购物车
+    // payImmediately(data.payOrderId)
+  }
+
+  // 检查库存 & 计算订单价格
+  async function getOrderInfo() {
+    // 计算价格
+    const data = await settlementOrder({
+      items: state.orderPayload.items,
+      couponId: state.orderPayload.couponId,
+      deliveryType: addressState.value.deliveryType,
+      addressId: addressState.value.addressInfo.id, // 收件地址编号
+      pickUpStoreId: addressState.value.pickUpInfo.id, //自提门店编号
+      receiverName: addressState.value.receiverName, // 选择门店自提时,该字段为联系人名
+      receiverMobile: addressState.value.receiverMobile, // 选择门店自提时,该字段为联系人手机
+      pointStatus: state.pointStatus,
+      combinationActivityId: state.orderPayload.combinationActivityId,
+      combinationHeadId: state.orderPayload.combinationHeadId,
+      seckillActivityId: state.orderPayload.seckillActivityId,
+      pointActivityId: state.orderPayload.pointActivityId,
+    });
+    state.orderInfo = data;
+    state.couponInfo = data.coupons || [];
+    // 设置收货地址
+    if (state.orderInfo.address) {
+      addressState.value.addressInfo = state.orderInfo.address;
+    }
+    return 0;
+  }
+
+  // 使用 watch 监听地址和配送方式的变化
+  watch(addressState, async (newAddress, oldAddress) => {
+    // 如果收货地址或配送方式有变化,则重新计算价格
+    if (
+      newAddress.addressInfo.id !== oldAddress.addressInfo.id ||
+      newAddress.deliveryType !== oldAddress.deliveryType
+    ) {
+      await getOrderInfo();
+    }
+  });
+
+  defineExpose({
+    onConfirm
+  })
+</script>
+
+<style lang="scss" scoped>
+  .addressBox {
+    margin-bottom: 20px;
+  }
+  .goodsListBox {
+    // background: linear-gradient(to bottom, #e93323 0%, #e93323 100%);
+    // background: linear-gradient(to bottom, var(--v-primary-base) 0%, var(--v-primary-base) 100%);
+    border-radius: 4px;
+  }
+  .order-item {
+    color: #7a7a7a;
+  }
+
+  .total-box-footer {
+    .total-num {
+      color: #7a7a7a;
+    }
+  }
+</style>

+ 97 - 0
src/views/mall/components/confirm_order/index.vue

@@ -0,0 +1,97 @@
+<!--  -->
+<template>
+  <div class="default-width py-3">
+    <v-card class=" pa-5">
+      <v-btn class="mb-1" variant="text" size="x-large" prepend-icon="mdi-chevron-triple-left" color="primary" @click.stop="goBack">返回</v-btn>
+      <div v-if="skuInfo">
+        <confirm ref="confirmRef" :data="skuInfo" @orderCreated="orderCreated"></confirm>
+        <div class="text-center">
+          <v-btn color="primary" width="200" @click="onBuy">{{ confirmWord }}</v-btn>
+        </div>
+      </div>
+      <div v-else>
+        <Empty :elevation="false" message="结算已失效" />
+        <div class="text-center my-5">
+          <v-btn color="primary" width="200" to="/mall">返回商城首页</v-btn>
+        </div>
+      </div>
+
+      <!-- 支付 -->
+      <CtDialog :visible="showPay" titleClass="text-h6" :widthType="3" title="收银台" :footer="false" @close="payCancel">
+        <pay ref="payRef" :id="payOrderId" @paySuccess="paySuccess"></pay>
+      </CtDialog>
+    </v-card>
+  </div>
+</template>
+
+<script setup>
+defineOptions({name: 'confirm_order-index'})
+import Confirm from '@/plugins/confirm'
+import Snackbar from '@/plugins/snackbar'
+import { useI18n } from '@/hooks/web/useI18n'
+import { useRouter } from 'vue-router'; const router = useRouter()
+import { computed, ref } from 'vue'
+import confirm from './confirm.vue'
+import pay from './pay.vue'
+const { t } = useI18n()
+
+const skuInfo = ref(localStorage.getItem('confirm_order_data')) // 购买商品规格信息
+
+const confirmRef = ref()
+// onMounted(() => {
+//   confirmRef.value.onConfirm()
+// })
+const onBuy = () => {
+  if (payOrderId.value) {
+    showPay.value = true
+  } else {
+    confirmRef.value.onConfirm()
+  }
+}
+
+
+// 创建订单完成
+const payRef = ref()
+const showPay = ref(false)
+const payOrderId = ref('')
+const orderId = ref('')
+const orderCreated = (id, order) => {
+  payOrderId.value = id
+  orderId.value = order
+  showPay.value = true
+}
+const confirmWord = computed(() => {
+  return orderId.value ? '立即购买' : '立即支付'
+})
+
+let closeConfirm = false // 关闭路由拦截
+
+const paySuccess = (e) => {
+  closeConfirm = true
+  router.replace({ path: '/mall/payOver', query: { price: e.price, spuId: id, orderId: orderId.value } })
+}
+const payCancel = () => {
+  Snackbar.warning('您已取消支付!')
+  showPay.value = false
+  setTimeout(() => { router.replace({ path: '/mall/user/order', query: { tab: 0 } }) }, 500);
+}
+
+const goBack = () => {
+  if (!orderId.value) return router.go(-1)
+  router.replace({ path: '/mall/user/order', query: { tab: 0 } })
+}
+
+router.beforeEach((to, from, next) => {
+  if (!orderId.value || closeConfirm) {
+    next()
+    return
+  }
+  Confirm(t('common.confirmTitle'), '您尚未支付成功,是否确定离开?').then(async () => {
+    closeConfirm = true
+    router.replace({ path: '/mall/user/order', query: { tab: 0 } })
+  })
+})
+
+</script>
+<style lang="scss" scoped>
+</style>

+ 331 - 0
src/views/mall/components/confirm_order/pay.vue

@@ -0,0 +1,331 @@
+<!-- 支付方式 -->
+<template>
+  <v-card elevation="0" :loading="loading" :disabled="loading">
+    <!-- 加载样式 -->
+    <template v-slot:loader="{ isActive }">
+      <v-progress-linear
+        :active="isActive"
+        color="var(--v-primary-base)"
+        height="1"
+        indeterminate
+      ></v-progress-linear>
+    </template>
+    <div style="color: var(--v-error-base); font-weight: bold; text-align: center;">
+      <span class="font-size-13 mr-2">¥</span>
+      <span class="font-size-40"> {{ orderInfo?.price ? orderInfo?.price / 100 : 0 }}</span>
+    </div>
+    <template v-if="payMethods?.length">
+      <v-chip-group v-model="payment" selected-class="text-primary" column mandatory @update:modelValue="payTypeChange">
+        <v-chip filter v-for="k in payMethods" :key="k.code" :value="k.code" class="mr-3" label>
+          {{ k.name }}
+          <svg-icon v-if="k.icon" class="ml-1" :name="k.icon" :size="k.size"></svg-icon>
+        </v-chip>
+      </v-chip-group>
+      <div v-if="tip" style="text-align: center;" class="mt-2">{{ tip }}</div>
+      <div v-if="isQrCodePay && remainder<=0" style="text-align: center;" class="my-10">
+        二维码失效,请重试!
+        <!-- 二维码刷新 -->
+        <span @click="submitOrderFun(true)">
+          <v-icon size="20" style="color: var(--v-primary-base)">mdi-refresh</v-icon>
+          <span class="text-decoration-underline cursor-pointer mr-2" style="color: var(--v-primary-base)">刷新</span>
+        </span>
+      </div>
+      <div>
+        <!-- 钱包支付 -->
+        <div v-if="isWalletPay" class="py-10" style="text-align: center;">
+          <div>
+            <span>剩余现金:</span>
+            <span style="color: var(--v-primary-base);">{{ balance ? (balance / 100.0).toFixed(2) : 0 }}</span>
+          </div>
+          <!-- 余额刷新 -->
+          <div class="mt-3" @click="updateAccountInfo">
+            <v-icon size="20" style="color: var(--v-primary-base)">mdi-refresh</v-icon>
+            <span class="text-decoration-underline cursor-pointer mr-2" style="color: var(--v-primary-base)">刷新</span>
+          </div>
+          <div class="my-3" v-if="notEnoughMoney">
+            <span class="color-error">
+              余额不足,请微信扫码付款
+              <span class="text-decoration-underline cursor-pointer" @click="open">(充值)</span>
+            </span>
+          </div>
+        </div>
+        <!-- 模拟支付 -->
+        <div v-if="payment === 'mock'" class="py-10"></div>
+        <!-- 二维码支付 -->
+        <div v-if="isQrCodePay && remainder>0" style="text-align: center;">
+          <QrCode :text="payQrCodeTxt" :width="170" style="margin: 0 auto;" />
+          <div
+            v-if="payQrCodeTxt"
+            class="mb-5"
+            style="color: var(--v-error-base);"
+          >
+            扫码支付时请勿离开
+            <span v-if="remainderZhShow">{{ remainderZhShow }}</span>
+          </div>
+        </div>
+        <!-- 钱包支付确认按钮 -->
+        <div v-if="(isWalletPay && !notEnoughMoney) || payment === 'mock'" class="mt-2" style="text-align: center;">
+          <v-btn
+            class="buttons" color="primary"
+            :loading="payLoading"
+            @click="submitBtn"
+          >
+            确认
+          </v-btn>
+        </div>
+      </div>
+    </template>
+  </v-card>
+</template>
+
+<script setup>
+defineOptions({ name: 'mall-pay'})
+import { computed, onBeforeUnmount, ref } from 'vue'
+import QrCode from '@/components/QrCode'
+import { definePayTypeList, qrCodePay, walletPay } from '@/utils/payType'
+import { getEnableCodeList, getOrderPayStatus } from '@/api/common'
+import { getOrder, submitOrder } from '@/api/mall/trade'
+const emit = defineEmits(['payTypeChange', 'paySuccess', 'stopInterval', 'getOrderFail'])
+const props = defineProps({
+  code: {
+    type: String,
+    default: 'mall' // mall:商城付款
+  },
+  appId: {
+    type: Number,
+    default: 12 // 12:商城付款
+  },
+  id: {
+    type: String,
+    default: '',
+  }
+})
+const loading = ref(true)
+const tip = ref('')
+// const orderType = ref('goods') // 订单类型; goods - 商品订单, recharge - 充值订单
+const orderInfo = ref({})
+const payStatus = ref(0) // 0=检测支付环境, -2=未查询到支付单信息, -1=支付已过期, 1=待支付,2=订单已支付
+const payMethods = ref([]) // 可选的支付方式
+const payment = ref('') // 选中的支付方式
+
+import { useUserStore } from '@/store/user'; const userStore = useUserStore()
+const userAccount = ref(JSON.parse(localStorage.getItem('userAccount')) || {}) // 账户信息
+userStore.$subscribe((mutation, state) => {
+  if (Object.keys(state.userAccount).length) userAccount.value = state.userAccount
+})
+
+const updateAccountInfo = async (isSnackbar = true) => {
+  await userStore.getUserAccountBalance()
+  userAccount.value = JSON.parse(localStorage.getItem('userAccount')) || {}
+  if (isSnackbar) Snackbar.success('刷新成功!')
+}
+
+// 对比余额是否不足 订单金额:orderInfo?.price-0
+const balance = computed(() => {
+  return userAccount.value?.balance ? userAccount.value.balance-0 : 0
+})
+const notEnoughMoney = computed(() => {
+  return orderInfo.value?.price-0 > balance.value
+})
+
+const Destroyed = ref(false)
+onBeforeUnmount(() => {
+  Destroyed.value = true // 避免执行paySubmit中关闭支付弹窗,造成setInterval一直存在
+  clear()
+})
+
+// 提交支付订单 (提交后倒计时显示及支付状态轮巡)
+const timer = ref(null) // 支付状态轮询
+const submitOrderFun = async (showLoading = false) => {
+  if (!payment.value) return
+  try {
+    if (orderInfo.value) {
+      // 提交支付订单
+      // channelExtras: { openid: null} // 特殊逻辑:微信公众号、小程序支付时,必须传入 openid
+      const params = {
+        id: orderInfo.value.id, // 支付单编号
+        channelCode: payment.value, // 支付渠道
+        // returnUrl: , // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
+      }
+      if (showLoading) loading.value = true
+      const res = await submitOrder(params)
+      // 二维码内容赋值
+      payQrCodeTxt.value = res?.displayContent || ''
+      remainder.value = 1 
+      //
+      initIntervalFun() // 倒计时显示及支付状态轮巡
+    }
+  } catch (error) {
+    console.log(error)
+  } finally {
+    if (showLoading) loading.value = false
+  }
+}
+
+// 状态转换:payOrder.status => payStatus
+const checkPayStatus = () => {
+  if (orderInfo.value.status === 10 || orderInfo.value.status === 20) {
+    // 支付成功
+    payStatus.value = 2;
+    Snackbar.warning('订单已支付')
+    // emit('payed')
+    router.go(0)
+    return
+  }
+  if (orderInfo.value.status === 30) {
+    // 支付关闭
+    payStatus.value = -1;
+    return;
+  }
+  payStatus.value = 1; // 待支付
+}
+
+// 获得支付订单信息
+const setOrder = async () => {
+  if (!props.id-0) return emit('getOrderFail')
+  try {
+    const data = await getOrder(props.id, true) // 获取待支付的订单 (order:业务订单; orderInfo:支付订单)
+    orderInfo.value = data || null
+    // 设置支付状态
+    checkPayStatus()
+    // await updateAccountInfo()
+    // 获得支付方式
+    await setPayMethods()
+    // 获得支付方式
+    // await setPayMethods();
+    if (isQrCodePay.value) submitOrderFun() // 二维码支付时自动创建订单获取二维码内容展示
+  } catch (error) {
+    console.log('error:', error)
+  } finally {
+    loading.value = false
+  }
+}
+setOrder()
+
+// 清空setInterval
+function clear() {
+  remainder.value = 1
+  payQrCodeTxt.value = '' // 二维码内容
+  if (timer.value) clearInterval(timer.value); timer.value = null
+  if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null
+}
+
+// 1.获得支付方式
+const isWalletPay = ref(false)
+const isQrCodePay = ref(false)
+const payTypeChange = (value) => {
+  payment.value = value
+  tip.value = payMethods.value.find(e => e.code === payment.value)?.tip || ''
+  isQrCodePay.value = qrCodePay.includes(payment.value)
+  isWalletPay.value = walletPay.includes(payment.value)
+  if (isQrCodePay.value) submitOrderFun() // 二维码支付时自动创建订单获取二维码内容展示
+  else { clear() }
+}
+// 1.支付方式
+const setPayMethods = async () => {
+  let list = []
+  payMethods.value = []
+  try {
+    // list = await getEnableCodeList2(props.code)
+    list = await getEnableCodeList({ appId: props.appId })
+  } catch (error) {
+    console.log(error)
+  } finally {
+    if (definePayTypeList?.length && list?.length) {
+      list.forEach(code => {
+        const item = definePayTypeList.find(p => p.code === code)
+        if (item) {
+          if (!payment.value) {
+            tip.value = item.tip || ''
+            payTypeChange(code) // 默认值赋值
+          }
+          payMethods.value.push(item)
+        }
+      })
+    }
+  }
+}
+
+
+const payLoading = ref(false)
+const payQrCodeTxt = ref('')
+
+// 钱包支付(余额支付)、模拟支付
+const submitBtn = () => {
+  payLoading.value = true
+  submitOrderFun()
+}
+
+import Snackbar from '@/plugins/snackbar'
+import { useRoute } from 'vue-router'; const route = useRoute()
+import { useRouter } from 'vue-router'; const router = useRouter()
+const getPayStatus = async () => {
+  if (Destroyed.value) return clear() // 用户关闭支付
+  try {
+    const data = await getOrderPayStatus({ id: orderInfo.value.id || orderInfo.value.payOrderId })
+    if ((data?.status - 0) === 10) {
+      // 支付成功
+      clear()
+      setTimeout(() => {
+        emit('paySuccess', { price: orderInfo.value.price })
+        if (isWalletPay.value) updateAccountInfo() // 更新余额
+        // Snackbar.success('支付成功')
+        if (route.fullPath === props.returnUrl) router.go(0) // 刷新页面
+        else if (props.returnUrl) router.push(props.returnUrl) // 返回指定页面
+      }, 2000);
+    }
+  } catch (error) {
+    console.log(error)
+  }
+}
+
+// 倒计时
+const countdownTime = 60000 * 3 // 倒计时三分钟
+const remainder = ref(1) // number 初始化不能为假,否则不能显示二维码
+const remainderZhShow = ref('') // 倒计时展示
+
+const remainderTimer = ref(null)
+// 初始化倒计时
+const initIntervalFun = async () => {
+  remainder.value = countdownTime // 初始倒计时时间
+  if (remainderTimer.value) clearInterval(remainderTimer.value); remainderTimer.value = null // 每一次点击都清除上一个轮询
+  if (timer.value) clearInterval(timer.value); timer.value = null
+  //
+  await getPayStatus() // 立即查询一次支付状态
+  remainderCalc(); remainderTimer.value = setInterval(() => { remainderCalc() }, 1000) // 倒计时计算
+  timer.value = setInterval(() => { getPayStatus() }, 2000) // 轮巡支付状态
+}
+
+const remainderCalc = () => {
+  if (Destroyed.value) return clear() // 用户关闭支付
+  remainder.value -= 1000
+  remainderZhShow.value = formatDuration(remainder.value)
+  if (remainder.value <= 0) { // 倒计时结束
+    tip.value = ''
+    if (timer.value) clearInterval(timer.value); timer.value = null
+    emit('stopInterval') // 倒计时结束,关闭倒计时弹窗
+  }
+}
+
+const formatDuration = (remainder) => {
+  // 将毫秒转换为秒
+  var seconds = Math.floor(remainder / 1000)
+  // 计算分钟和剩余的秒数
+  var minutes = Math.floor(seconds / 60)
+  var remainingSeconds = seconds % 60
+  // 格式化分钟和秒数,确保秒数为两位数(如果小于10,则前面补0)
+  minutes = minutes.toString().padStart(2, '0')
+  remainingSeconds = remainingSeconds.toString().padStart(2, '0')
+  // 返回格式化的字符串
+  return `${minutes}分${remainingSeconds}秒`
+}
+
+const open = () => {
+  window.open('/personalRecharge')
+}
+
+</script>
+
+<style lang="scss" scoped>
+.font-size-40 { font-size: 40px; }
+</style>

+ 47 - 42
src/views/mall/components/details.vue

@@ -79,33 +79,33 @@
   <loginPage v-if="showLogin" @loginSuccess="loginSuccess" @close="loginClose"></loginPage>
 
   <!-- 结算 -->
-  <CtDialog :visible="showSettlement" titleClass="text-h6" :widthType="3" title="订单提交" submitText="提交支付" @submit="handleSubmit" @close="handleClose">
+  <!-- <CtDialog :visible="showSettlement" titleClass="text-h6" :widthType="3" title="订单提交" submitText="提交支付" @submit="handleSubmit" @close="handleClose">
     <confirm ref="confirmRef" :data="skuInfo" @orderCreated="orderCreated"></confirm>
-  </CtDialog>
+  </CtDialog> -->
   
   <!-- 支付 -->
-  <CtDialog :visible="showPay" titleClass="text-h6" :widthType="3" title="收银台" :footer="false" @close="payCancel">
+  <!-- <CtDialog :visible="showPay" titleClass="text-h6" :widthType="3" title="收银台" :footer="false" @close="payCancel">
     <pay ref="payRef" :id="payOrderId" @paySuccess="paySuccess"></pay>
-  </CtDialog>
+  </CtDialog> -->
 </div>
 </template>
 
 <script setup>
 defineOptions({name: 'goods-details'})
-import Navbar from '@/views/mall/components/navbar.vue'
+// import Navbar from '@/views/mall/components/navbar.vue'
 import { getProductDetail } from '@/api/mall/product'
 import { addCart } from '@/api/mall/cart'
 import selectSku from './details/s-select-sku.vue'
 import describe from './details/describe.vue'
 // import commentCard from './details/detail-comment-card.vue'
 // import prizeDrawContent from './details/prizeDrawContent.vue'
-import confirm from './details/order/confirm.vue'
-import { ref, reactive } from 'vue'
+// import confirm from './details/order/confirm.vue'
+import { ref, reactive, nextTick } from 'vue'
 import { useRouter } from 'vue-router'
 import Snackbar from '@/plugins/snackbar'
 import { getToken } from '@/utils/auth'
 import loginPage from '@/views/common/loginDialog.vue'
-import pay from '@/views/mall/components/details/order/pay.vue'
+// import pay from '@/views/mall/components/details/order/pay.vue'
 import { getPrizeByGoodsId } from '@/api/mall/prize'
 
 const router = useRouter()
@@ -169,42 +169,47 @@ function onSkuChange(e) {
   selectedSkuMarketPrice.value = calcPrice(state.selectedSku?.marketPrice || state.goodsInfo.marketPrice)
 }
 
-const showSettlement = ref(false)
-const skuInfo = ref(null) // 购买商品规格信息
+// const showSettlement = ref(false)
+// const skuInfo = ref(null) // 购买商品规格信息
 const onBuy = async (e) => {
   if (!getToken()) return handleLogin()
   if (!e?.id) return Snackbar.warning('请选择商品规格!')
-
-  skuInfo.value = JSON.stringify({
-    items: [{ skuId: e.id, count: e.goods_num, categoryId: state.goodsInfo.categoryId }]
+  const data = JSON.stringify({
+    items: [
+      {
+        skuId: e.id,
+        count: e.goods_num,
+        categoryId: state.goodsInfo.categoryId,
+      },
+    ],
   })
-  showSettlement.value = true
 
-  // router.push(`/mall/order/settlement/${e.id}?count=${e.goods_num}`)
+  localStorage.setItem('confirm_order_data', data)
+  nextTick(() => {
+    router.push('/mall/confirm_order')
+  })
 }
 
 // 创建订单完成
-const payRef = ref()
-const showPay = ref(false)
-const payOrderId = ref('')
-const orderId = ref('')
-const orderCreated = (id, order) => {
-  showSettlement.value = false
-  payOrderId.value = id
-  orderId.value = order
-  showPay.value = true
-}
+// const payRef = ref()
+// const showPay = ref(false)
+// const payOrderId = ref('')
+// const orderId = ref('')
+// const orderCreated = (id, order) => {
+//   showSettlement.value = false
+//   payOrderId.value = id
+//   orderId.value = order
+//   showPay.value = true
+// }
 // 
-const payCancel = () => {
-  Snackbar.warning('您已取消支付!')
-  showPay.value = false
-  setTimeout(() => { router.push({ path: '/mall/user/order', query: { tab: 0 } }) }, 500);
-}
-const paySuccess = (e) => {
-  // Snackbar.success('支付成功,请前往我的订单查看!')
-  // showPay.value = false
-  router.push({ path: '/mall/payOver', query: { price: e.price, spuId: id, orderId: orderId.value } })
-}
+// const payCancel = () => {
+//   Snackbar.warning('您已取消支付!')
+//   showPay.value = false
+//   setTimeout(() => { router.push({ path: '/mall/user/order', query: { tab: 0 } }) }, 500);
+// }
+// const paySuccess = (e) => {
+//   router.push({ path: '/mall/payOver', query: { price: e.price, spuId: id, orderId: orderId.value } })
+// }
 
 // 添加购物车
 const onAddCart = async (e) => {
@@ -218,13 +223,13 @@ const onAddCart = async (e) => {
   // 刷新购物车列表
 }
 
-const confirmRef = ref()
-const handleSubmit = () => {
-  if (confirmRef.value) confirmRef.value.onConfirm()
-}
-const handleClose = () => {
-  showSettlement.value = false
-}
+// const confirmRef = ref()
+// const handleSubmit = () => {
+//   if (confirmRef.value) confirmRef.value.onConfirm()
+// }
+// const handleClose = () => {
+//   showSettlement.value = false
+// }
 
 const showLogin = ref(false)
 const handleLogin = () => {

+ 4 - 3
src/views/mall/components/s-goods-item/index.vue

@@ -5,7 +5,7 @@
         <v-img :src="img" :aspect-ratio="1" style="border-radius: 8px;"></v-img>
       </div>
       <div
-        class="d-flex flex-column justify-space-between"
+        class="d-flex justify-space-between"
         style="flex: 1; font-size: 13px;"
         :style="[{ width: titleWidth ? titleWidth + 'px' : '' }]"
       >
@@ -18,9 +18,10 @@
           >
             ¥{{ fen2yuan(price) }}
           </div>
-          <div v-if="point && Number(price) > 0">+</div>
-          <div v-if="num" class="buyNum ml-2"> x {{ num }}</div>
+          <!-- <div v-if="point && Number(price) > 0">+</div> -->
         </div>
+        <div v-if="num" class="buyNum ml-2"> x {{ num }}</div>
+        <div v-if="price && Number(price) > 0" class="">¥{{ fen2yuan(num*price) }}</div>
       </div>
     </div>
   </div>