Przeglądaj źródła

订单支付页面编写

“陈博论” 9 miesięcy temu
rodzic
commit
681195b448

+ 7 - 2
web/src/views/backend/package/package/popupForm.vue

@@ -33,6 +33,11 @@
 							<div v-loading="specLoading" class="spec-list">
 								<el-table :data="specListData" border stripe style="width: 100%">
 									<el-table-column prop="spec_name" align="center" label="规格名称" min-width="120" />
+									<el-table-column prop="image" align="center" label="图片" min-width="100">
+										<template #default="scope">
+											<el-image :src="scope.row.image" style="width: 50px; height: 50px;" />
+										</template>
+									</el-table-column>
 									<el-table-column prop="price" align="center" label="价格" min-width="100">
 										<template #default="scope">
 											<span>¥{{ scope.row.price }}</span>
@@ -41,8 +46,8 @@
 									<el-table-column prop="stock" align="center" label="库存" min-width="80" />
 									<el-table-column prop="type" align="center" label="套餐类型" min-width="100">
 										<template #default="scope">
-											<el-tag :type="scope.row.type === '1' ? 'primary' : 'success'">
-												{{ scope.row.type === '1' ? '个人套餐' : '企业套餐' }}
+											<el-tag :type="scope.row.type == '1' ? 'primary' : 'success'">
+												{{ scope.row.type == '1' ? '个人套餐' : '企业套餐' }}
 											</el-tag>
 										</template>
 									</el-table-column>

+ 11 - 2
web/src/views/backend/package/package/specDialog.vue

@@ -31,6 +31,8 @@
 							show-word-limit
 						/>
 					</el-form-item>
+					<FormItem label="规格图片" prop="image" type="image" v-model="formData.image"
+                        :input-attr="{ returnFullUrl: true }" />
 					
 					<el-row :gutter="20">
 						<el-col :span="12">
@@ -71,7 +73,6 @@
 									v-model="formData.type" 
 									placeholder="请选择套餐类型" 
 									style="width: 100%"
-									clearable
 								>
 									<el-option
 										v-for="opt in packageTypeOptions"
@@ -89,7 +90,6 @@
 									placeholder="请选择卡类型"
 									style="width: 100%"
 									@change="handleCardTypeChange"
-									clearable
 								>
 									<el-option
 										v-for="opt in cardTypeOptions"
@@ -166,6 +166,7 @@
 import { ref, reactive } from 'vue'
 import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
 import { useI18n } from 'vue-i18n'
+import FormItem from '/@/components/formItem/index.vue'
 import { editSpec } from '/@/api/backend/package/package'
 
 const { t } = useI18n()
@@ -187,6 +188,7 @@ const emits = defineEmits(['success'])
 
 const formData = reactive({
 	spec_name: '',
+	image:'' as string,
 	price: undefined as number | undefined,
 	stock: undefined as number | undefined,
 	type: '',
@@ -201,6 +203,9 @@ const rules: FormRules = {
 	spec_name: [
 		{ required: true, message: '请输入规格名称', trigger: 'change' }
 	],
+	image: [
+		{ required: true, message: '请上传规格图片', trigger: 'change' }
+	],
 	price: [
 		{ required: true, message: '请输入价格', trigger: 'change' }
 	],
@@ -282,6 +287,8 @@ const editDialog = (pkgId: number, specData: any) => {
 
 	// 回填数据,确保数字类型正确转换
 	formData.spec_name = specData.spec_name || specData.specName || ''
+	// 规格图片
+	formData.image = specData.image || ''
 	// 价格:转换为数字,如果为空或无效则设为 undefined
 	formData.price = toNumber(specData.price)
 	// 库存:转换为数字,如果为空或无效则设为 undefined
@@ -301,6 +308,7 @@ const editDialog = (pkgId: number, specData: any) => {
 const resetForm = () => {
 	formRef.value?.clearValidate()
 	formData.spec_name = ''
+	formData.image = ''
 	formData.price = undefined
 	formData.stock = undefined
 	formData.type = ''
@@ -334,6 +342,7 @@ const onSubmit = async () => {
 			id: currentSpecId.value || '',
 			package_id: packageId.value,
 			spec_name: formData.spec_name,
+			image: formData.image,
 			// 确保数字类型正确
 			price: formData.price != null ? Number(formData.price) : undefined,
 			stock: formData.stock != null ? Number(formData.stock) : undefined,

+ 31 - 11
web/src/views/frontend/Order.vue

@@ -9,7 +9,7 @@
           <!-- 左侧:大图区域 -->
           <div class="goods-cover">
             <div class="cover-image">
-              <img v-if="goodsInfo.product?.cover" :src="goodsInfo.product.cover" alt="商品封面">
+              <img v-if="goodsInfo.image" :src="goodsInfo.image" alt="商品封面">
               <div v-else class="no-image">暂无图片</div>
             </div>
           </div>
@@ -143,7 +143,7 @@
 
 <script setup>
 import { computed, onMounted, onBeforeUnmount, ref } from 'vue'
-import { useRoute } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { ChatDotRound ,Coin} from '@element-plus/icons-vue'
 import Header from './components/header.vue'
@@ -155,6 +155,7 @@ import { createOrder, createQrCode, checkOrderStatus } from '/@/api/frontend/ord
 defineOptions({ name: 'ClientOrder' })
 
 const route = useRoute()
+const router = useRouter()
 
 const goodsId = ref(null)
 const goodsInfo = ref({})
@@ -276,21 +277,40 @@ const createOrderAndQrCode = async () => {
       return
     }
     
-    if (!qrCodeRes.data.code_url) {
+    // 根据接口返回数据结构,使用 qr_code_url 或 qr_code_content
+    const qrCodeValue = qrCodeRes.data.qr_code_url || qrCodeRes.data.qr_code_content
+    if (!qrCodeValue) {
       ElMessage.error(qrCodeRes.data.msg || '二维码生成失败,请重试')
       qrCodeLoading.value = false
       return
     }
 
-    qrCodeUrl.value = qrCodeRes.data.code_url
+    // 更新订单信息,使用接口返回的数据
+    if (qrCodeRes.data.order_no) {
+      currentOrderInfo.value.order_no = qrCodeRes.data.order_no
+    }
+    if (qrCodeRes.data.pay_amount) {
+      currentOrderInfo.value.total_amount = qrCodeRes.data.pay_amount
+      currentOrderInfo.value.amount = qrCodeRes.data.pay_amount
+      currentOrderInfo.value.total_price = qrCodeRes.data.pay_amount
+    }
+    if (qrCodeRes.data.payment_id) {
+      currentOrderInfo.value.payment_id = qrCodeRes.data.payment_id
+    }
+    if (qrCodeRes.data.expire_time) {
+      currentOrderInfo.value.expire_time = qrCodeRes.data.expire_time
+    }
+
+    qrCodeUrl.value = qrCodeValue
     qrCodeLoading.value = false
     payStatus.value = ''
     
     // 3. 显示支付弹窗
     payDialogVisible.value = true
 
-    // 4. 开始轮询支付状态
-    startPayStatusPolling(orderId, orderRes.data.order_no)
+    // 4. 开始轮询支付状态(使用接口返回的 order_no,如果没有则使用原来的)
+    const orderNo = qrCodeRes.data.order_no || orderRes.data.order_no
+    startPayStatusPolling(orderId, orderNo)
   } catch (error) {
     console.error('下单失败:', error)
     ElMessage.error(error?.message || '下单失败,请重试')
@@ -328,8 +348,7 @@ const startPayStatusPolling = (orderId, orderNo) => {
       const res = await checkOrderStatus(checkData)
       
       // 支付状态判断:1-已支付,0-待支付,2-支付失败,3-已取消
-      // 兼容不同的数据结构(可能返回 pay_status 或 status 字段)
-      const status = res.data?.pay_status ?? res.data?.status ?? res.data?.order?.pay_status
+      const status = res.data?.order.pay_status
       
       if (status == '1') {
         // 支付成功
@@ -349,7 +368,7 @@ const startPayStatusPolling = (orderId, orderNo) => {
         setTimeout(() => {
           handlePaySuccess()
         }, 3000)
-      } else if (status === 2 || status === 3 || status === 'failed' || status === 'cancelled' || status === '2' || status === '3') {
+      } else if (status === '2' || status == '3') {
         // 支付失败或已取消
         payStatus.value = 'failed'
         stopPayStatusPolling()
@@ -362,6 +381,7 @@ const startPayStatusPolling = (orderId, orderNo) => {
       // 查询失败不中断轮询,继续尝试(除非超过最大次数)
       if (payCheckCount.value > MAX_PAY_CHECK_COUNT) {
         stopPayStatusPolling()
+        router.push({ name: 'success', params: { id: currentOrderInfo.value.id } })
       }
     }
   }, 3000)
@@ -393,7 +413,7 @@ const handlePaySuccess = () => {
   ElMessage.success('支付成功!')
   
   // 可以在这里跳转到订单详情页或订单列表页
-  // router.push({ name: 'OrderDetail', params: { id: currentOrderInfo.value.id } })
+  router.push({ name: 'success', params: { id: currentOrderInfo.value.id } })
   
   // 重置状态
   qrCodeUrl.value = ''
@@ -477,7 +497,7 @@ onBeforeUnmount(() => {
     border-radius: 14px;
     overflow: hidden;
     background: #f2f6fc;
-    min-height: 300px;
+    min-height: 200px;
     display: flex;
     align-items: center;
     justify-content: center;

+ 80 - 1
web/src/views/frontend/components/PayQrCodeDialog.vue

@@ -64,6 +64,7 @@
             </el-icon>
             <span v-if="payMethod === 'wechat'">请使用微信扫码支付</span>
             <span v-else>请使用支付宝扫码支付</span>
+            <span v-if="countdown > 0" class="countdown-text">(剩余 {{ formatTime(countdown) }})</span>
           </div>
         </template>
       </el-alert>
@@ -96,7 +97,7 @@
 </template>
 
 <script setup>
-import { computed, ref, watch } from 'vue'
+import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
 import { ElMessage } from 'element-plus'
 import { Loading, CircleCheck, CircleClose, ChatDotRound, Coin } from '@element-plus/icons-vue'
 import QrcodeVue from 'qrcode.vue'
@@ -156,6 +157,78 @@ const formatAmount = (amount) => {
   }
   return num.toFixed(2)
 }
+
+// 倒计时相关
+const countdown = ref(0)
+const countdownTimer = ref(null)
+
+// 格式化时间显示(MM:SS)
+const formatTime = (seconds) => {
+  const mins = Math.floor(seconds / 60)
+  const secs = seconds % 60
+  return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
+}
+
+// 开始倒计时
+const startCountdown = () => {
+  // 清除之前的定时器
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+  }
+  
+  // 从订单信息中获取过期时间(秒)
+  const expireTime = props.orderInfo?.expire_time
+  if (expireTime && expireTime > 0) {
+    countdown.value = expireTime
+    
+    countdownTimer.value = setInterval(() => {
+      if (countdown.value > 0) {
+        countdown.value--
+      } else {
+        // 倒计时结束
+        clearInterval(countdownTimer.value)
+        countdownTimer.value = null
+        ElMessage.warning('支付二维码已过期,请重新下单')
+      }
+    }, 1000)
+  }
+}
+
+// 停止倒计时
+const stopCountdown = () => {
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+    countdownTimer.value = null
+  }
+  countdown.value = 0
+}
+
+// 监听弹窗显示状态和订单信息变化
+watch(() => props.visible, (newVal) => {
+  if (newVal && props.orderInfo?.expire_time) {
+    startCountdown()
+  } else {
+    stopCountdown()
+  }
+})
+
+watch(() => props.orderInfo?.expire_time, (newVal) => {
+  if (props.visible && newVal) {
+    startCountdown()
+  }
+})
+
+// 监听支付状态,支付成功时停止倒计时
+watch(() => props.payStatus, (newVal) => {
+  if (newVal === 'success') {
+    stopCountdown()
+  }
+})
+
+// 组件卸载时清除定时器
+onBeforeUnmount(() => {
+  stopCountdown()
+})
 </script>
 
 <style scoped lang="scss">
@@ -306,6 +379,12 @@ const formatAmount = (amount) => {
     span {
       font-size: 14px;
     }
+    
+    .countdown-text {
+      color: #f56c6c;
+      font-weight: 500;
+      margin-left: 4px;
+    }
   }
 }