Browse Source

feat: 商城订单退款入口与状态展示

用户端与管理端支持发起退款,列表与详情展示已退款状态及次数流水类型。

Co-authored-by: Cursor <cursoragent@cursor.com>
Pchen. 6 days ago
parent
commit
22fb5c3676

+ 21 - 1
src/api/order.js

@@ -3,10 +3,12 @@ import request from '../utils/request'
 const api = {
   Create: '/Order/CreateOrder',
   Cancel: '/Order/CancelOrder',
+  Refund: '/Order/RefundOrder',
   Detail: '/Order/Detail',
   GetMyOrder: '/Order/GetMyOrders',
   AdminOrderList: '/Admin/Order/List',
-  AdminOrderDetail: '/Admin/Order/Detail'
+  AdminOrderDetail: '/Admin/Order/Detail',
+  AdminRefund: '/Admin/Order/RefundOrder'
 }
 
 export function createOrder(parameter) {
@@ -33,6 +35,15 @@ export function cancelOrder(parameter) {
   })
 }
 
+export function refundOrder(parameter) {
+  return request({
+    url: api.Refund,
+    method: 'post',
+    data: parameter,
+    timeout: 60000
+  })
+}
+
 export function getMyOrder(parameter) {
   return request({
     url: api.GetMyOrder,
@@ -56,3 +67,12 @@ export function adminOrderDetail(parameter) {
     params: parameter
   })
 }
+
+export function adminRefundOrder(parameter) {
+  return request({
+    url: api.AdminRefund,
+    method: 'post',
+    data: parameter,
+    timeout: 60000
+  })
+}

+ 1 - 0
src/components/store/OrderStateTag.vue

@@ -34,5 +34,6 @@ const state = computed(() => props.state)
   &--1 { opacity: 0.9; }
   &--2 { opacity: 0.85; }
   &--3 { opacity: 0.6; }
+  &--5 { opacity: 0.75; }
 }
 </style>

+ 63 - 4
src/pages/admin/goods/orderDetail.vue

@@ -49,6 +49,9 @@
             <a-descriptions-item v-if="data.ic_count != null" label="IC次数">
               {{ data.ic_count }}
             </a-descriptions-item>
+            <a-descriptions-item v-if="data.user_lepao_count != null" label="用户当前次数">
+              {{ data.user_lepao_count }}
+            </a-descriptions-item>
           </a-descriptions>
         </a-card>
 
@@ -65,6 +68,21 @@
           <template #title>商品详情</template>
           <div class="goods-content" v-html="goodsContent" />
         </a-card>
+
+        <div v-if="data.orderId" class="admin-actions">
+          <a-button
+            v-if="data.state === 2 && data.canRefund"
+            type="primary"
+            status="warning"
+            :loading="refundLoading"
+            @click="handleRefundOrder"
+          >
+            发起退款
+          </a-button>
+          <a-tooltip v-else-if="data.state === 2 && data.refundDisabledReason" :content="data.refundDisabledReason">
+            <a-button status="warning" disabled>发起退款</a-button>
+          </a-tooltip>
+        </div>
         </div>
       </template>
     </a-skeleton>
@@ -74,8 +92,8 @@
 <script setup>
 import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRoute } from 'vue-router'
-import { adminOrderDetail } from '@/api/order'
-import { Notification } from '@arco-design/web-vue'
+import { adminOrderDetail, adminRefundOrder } from '@/api/order'
+import { Notification, Message, Modal } from '@arco-design/web-vue'
 import OrderProgressSteps from '@/components/store/OrderProgressSteps.vue'
 import StoreDetailSkeleton from '@/components/skeleton/StoreDetailSkeleton.vue'
 import {
@@ -87,6 +105,7 @@ import {
 
 const route = useRoute()
 const loading = ref(true)
+const refundLoading = ref(false)
 const data = ref({})
 const goodsContent = ref('')
 const isMobile = ref(false)
@@ -105,14 +124,14 @@ const stepCurrent = computed(() => {
   const state = data.value?.state
   if (state === 0 || state === 3) return 1
   if (state === 1) return 2
-  if (state === 2 || state === 4) return 3
+  if (state === 2 || state === 4 || state === 5) return 3
   return 1
 })
 
 const stepStatus = computed(() => {
   const state = data.value?.state
   if (state === 3 || state === 4) return 'error'
-  if (state === 2) return 'finish'
+  if (state === 2 || state === 5) return 'finish'
   return 'process'
 })
 
@@ -138,6 +157,40 @@ const fetchDetail = async () => {
   }
 }
 
+const handleRefundOrder = () => {
+  const purchased = data.value?.lepao_count ?? 0
+  const remaining = data.value?.user_lepao_count ?? 0
+  Modal.confirm({
+    title: '管理员退款',
+    content: `将为用户退款 ¥${data.value?.price},并扣回 ${purchased} 次乐跑次数(用户当前剩余 ${remaining} 次)。确定继续吗?`,
+    okText: '确认退款',
+    cancelText: '取消',
+    okButtonProps: { status: 'warning' },
+    onOk: async () => {
+      try {
+        refundLoading.value = true
+        const res = await adminRefundOrder({ orderId: route.params.orderId })
+        if (!res || res.code !== 0) {
+          Notification.error({
+            title: '退款失败',
+            content: res?.msg ?? '请稍后再试'
+          })
+          return
+        }
+        Message.success(res.msg || '退款成功')
+        await fetchDetail()
+      } catch (error) {
+        Notification.error({
+          title: '退款失败',
+          content: error.message || '请稍后再试'
+        })
+      } finally {
+        refundLoading.value = false
+      }
+    }
+  })
+}
+
 onMounted(async () => {
   const syncMobile = () => {
     isMobile.value = window.innerWidth <= 768
@@ -196,6 +249,12 @@ onUnmounted(() => {
   }
 }
 
+.admin-actions {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
 @media (max-width: 768px) {
   .admin-order-detail {
     padding: 0 12px 16px;

+ 1 - 0
src/pages/admin/goods/orderList.vue

@@ -139,6 +139,7 @@ const stateOptions = [
   { label: '待支付', value: 0 },
   { label: '待处理', value: 1 },
   { label: '已完成', value: 2 },
+  { label: '已退款', value: 5 },
   { label: '已关闭', value: 3 },
   { label: '处理异常', value: 4 }
 ]

+ 1 - 0
src/pages/admin/lepaoCountLedger/index.vue

@@ -183,6 +183,7 @@ const typeOptions = [
   { label: '乐跑扣除', value: 'run_consume' },
   { label: '乐跑返还', value: 'run_refund' },
   { label: '购买增加', value: 'purchase' },
+  { label: '购买退款', value: 'purchase_refund' },
   { label: '赠送冻结', value: 'gift_send_lock' },
   { label: '赠送驳回返还', value: 'gift_send_refund' },
   { label: '接收赠送', value: 'gift_receive' },

+ 1 - 0
src/pages/lepao/countLedger/index.vue

@@ -139,6 +139,7 @@ const typeOptions = [
   { label: '乐跑扣除', value: 'run_consume' },
   { label: '乐跑返还', value: 'run_refund' },
   { label: '购买增加', value: 'purchase' },
+  { label: '购买退款', value: 'purchase_refund' },
   { label: '赠送冻结', value: 'gift_send_lock' },
   { label: '赠送驳回返还', value: 'gift_send_refund' },
   { label: '接收赠送', value: 'gift_receive' },

+ 60 - 1
src/pages/store/orders/orderDetail/index.vue

@@ -71,6 +71,12 @@
             <a-descriptions-item v-if="data?.pay_time" label="支付时间">
               {{ formatStoreTimeFull(data.pay_time) }}
             </a-descriptions-item>
+            <a-descriptions-item v-if="data?.state === 2 && data?.lepao_count > 0" label="购买次数">
+              {{ data.lepao_count }} 次
+            </a-descriptions-item>
+            <a-descriptions-item v-if="data?.state === 2 && data?.user_lepao_count != null" label="当前剩余次数">
+              {{ data.user_lepao_count }} 次
+            </a-descriptions-item>
           </a-descriptions>
         </a-card>
 
@@ -87,6 +93,17 @@
           >
             取消订单
           </a-button>
+          <a-button
+            v-if="data?.state === 2 && data?.canRefund"
+            status="warning"
+            :loading="refundLoading"
+            @click="handleRefundOrder"
+          >
+            申请退款
+          </a-button>
+          <a-tooltip v-if="data?.state === 2 && !data?.canRefund && data?.refundDisabledReason" :content="data.refundDisabledReason">
+            <a-button status="warning" disabled>申请退款</a-button>
+          </a-tooltip>
           <a-button @click="$router.push('/store/myOrder')">返回订单列表</a-button>
           <a-button type="outline" @click="$router.push('/store/goodsList')">继续购物</a-button>
         </div>
@@ -99,7 +116,7 @@
 
 <script setup>
 import { ref, computed, onUnmounted, onMounted } from 'vue'
-import { orderDeatil, cancelOrder } from '@/api/order'
+import { orderDeatil, cancelOrder, refundOrder } from '@/api/order'
 import { useRoute } from 'vue-router'
 import { Notification, Message, Modal } from '@arco-design/web-vue'
 import OrderStateTag from '@/components/store/OrderStateTag.vue'
@@ -117,6 +134,7 @@ const { id } = route.params
 
 const loading = ref(true)
 const cancelLoading = ref(false)
+const refundLoading = ref(false)
 const data = ref({})
 const payData = ref({})
 const content = ref('')
@@ -158,6 +176,13 @@ const updateStepState = () => {
       stepDescriptions.value[2] = '权益已发放至账户'
       stopPendingOrderTimers()
       break
+    case 5:
+      stepCurrent.value = 3
+      stepStatus.value = 'finish'
+      stepLabels.value = ['待支付', '待处理', '已退款']
+      stepDescriptions.value[2] = '订单已退款,款项将原路退回'
+      stopPendingOrderTimers()
+      break
     case 3:
       stepCurrent.value = 1
       stepStatus.value = 'error'
@@ -312,6 +337,40 @@ const handleCancelOrder = () => {
   })
 }
 
+const handleRefundOrder = () => {
+  const purchased = data.value?.lepao_count ?? 0
+  const remaining = data.value?.user_lepao_count ?? 0
+  Modal.confirm({
+    title: '申请退款',
+    content: `退款将扣回 ${purchased} 次乐跑次数(当前剩余 ${remaining} 次),款项原路退回。确定继续吗?`,
+    okText: '确认退款',
+    cancelText: '再想想',
+    okButtonProps: { status: 'warning' },
+    onOk: async () => {
+      try {
+        refundLoading.value = true
+        const res = await refundOrder({ orderId: id })
+        if (!res || res.code !== 0) {
+          Notification.error({
+            title: '退款失败',
+            content: res?.msg ?? '请稍后再试'
+          })
+          return
+        }
+        Message.success(res.msg || '退款成功')
+        await getOrderDetail()
+      } catch (error) {
+        Notification.error({
+          title: '退款失败',
+          content: error.message || '请稍后再试'
+        })
+      } finally {
+        refundLoading.value = false
+      }
+    }
+  })
+}
+
 function openPaymentWindow(payUrl, formData) {
   const form = document.createElement('form')
   form.method = 'POST'

+ 1 - 0
src/pages/store/orders/orderList/index.vue

@@ -103,6 +103,7 @@ const statusTabs = [
   { key: '0', title: '待支付' },
   { key: '1', title: '待处理' },
   { key: '2', title: '已完成' },
+  { key: '5', title: '已退款' },
   { key: '3', title: '已关闭' }
 ]
 

+ 2 - 1
src/utils/storeFormat.js

@@ -36,7 +36,8 @@ export const ORDER_STATE_MAP = {
   1: { label: '待处理', color: 'arcoblue' },
   2: { label: '已完成', color: 'green' },
   3: { label: '已关闭', color: 'gray' },
-  4: { label: '处理异常', color: 'red' }
+  4: { label: '处理异常', color: 'red' },
+  5: { label: '已退款', color: 'purple' }
 }
 
 export function getOrderStateMeta(state) {