فهرست منبع

feat(退款流程): 改进退款详情和地址选择交互

- 在退款详情弹窗中增加进度条状态显示和按钮禁用逻辑
- 重构退货地址选择为更直观的弹出面板形式
- 优化退款步骤计算逻辑,支持更多状态场景
ylong 2 هفته پیش
والد
کامیت
27fbdaeae6
2فایلهای تغییر یافته به همراه153 افزوده شده و 44 حذف شده
  1. 125 33
      src/views/mallOrder/refund/components/agree-dialog.vue
  2. 28 11
      src/views/mallOrder/refund/components/refund-detail-dialog.vue

+ 125 - 33
src/views/mallOrder/refund/components/agree-dialog.vue

@@ -30,30 +30,110 @@
                 >
             </div>
             <el-form-item prop="refundAddressId" label-width="0">
-                <el-select
-                    v-model="form.refundAddressId"
-                    placeholder="请选择退货地址"
-                    class="w-full"
-                    :loading="addressLoading"
+                <el-popover
+                    v-model:visible="addressPickerVisible"
+                    trigger="click"
+                    placement="bottom-start"
+                    :width="520"
+                    popper-class="refund-address-popper"
                 >
-                    <el-option
-                        v-for="item in addressList"
-                        :key="item.id"
-                        :label="`收货人:${item.consignee} ${item.phone} | 收货地址:${item.province}${item.city}${item.district} ${item.address}`"
-                        :value="item.id"
-                    >
-                        <div class="py-2 leading-tight">
-                            <div class="font-bold text-gray-800"
-                                >收货人:{{ item.consignee }}
-                                {{ item.phone }}</div
-                            >
-                            <div class="text-xs text-gray-500 mt-1"
-                                >收货地址:{{ item.province }}{{ item.city
-                                }}{{ item.district }} {{ item.address }}</div
+                    <template #reference>
+                        <div
+                            class="w-full rounded-md border px-3 py-2 flex items-center justify-between cursor-pointer transition"
+                            :class="
+                                addressPickerVisible
+                                    ? 'border-emerald-400 ring-2 ring-emerald-100'
+                                    : 'border-gray-200'
+                            "
+                        >
+                            <div class="flex-1 min-w-0">
+                                <template v-if="selectedAddress">
+                                    <div
+                                        class="text-sm text-gray-900 truncate"
+                                    >
+                                        {{ selectedAddress.name }}
+                                        {{ selectedAddress.mobile }}
+                                        <el-tag
+                                            v-if="
+                                                Number(
+                                                    selectedAddress.defaultReturnFlag
+                                                ) === 1
+                                            "
+                                            size="small"
+                                            type="success"
+                                            class="ml-2"
+                                            >默认</el-tag
+                                        >
+                                    </div>
+                                    <div
+                                        class="text-xs text-gray-500 truncate mt-0.5"
+                                    >
+                                        {{ selectedAddress.address }}
+                                    </div>
+                                </template>
+                                <template v-else>
+                                    <div class="text-sm text-gray-400">
+                                        请选择退货地址
+                                    </div>
+                                </template>
+                            </div>
+                            <el-icon class="ml-2 text-gray-400"
+                                ><ArrowDown
+                            /></el-icon>
+                        </div>
+                    </template>
+
+                    <div class="max-h-72 overflow-auto p-2">
+                        <div
+                            v-if="addressLoading"
+                            class="py-6 flex items-center justify-center text-gray-500 text-sm"
+                        >
+                            <el-icon class="is-loading mr-2"
+                                ><Loading
+                            /></el-icon>
+                            加载中
+                        </div>
+                        <div
+                            v-else-if="!addressList.length"
+                            class="py-6 text-center text-gray-400 text-sm"
+                        >
+                            暂无可用退货地址
+                        </div>
+                        <div v-else class="space-y-2">
+                            <div
+                                v-for="item in addressList"
+                                :key="item.id"
+                                class="rounded-md border px-3 py-2 cursor-pointer transition"
+                                :class="
+                                    String(form.refundAddressId) ===
+                                    String(item.id)
+                                        ? 'border-emerald-400 bg-emerald-50'
+                                        : 'border-gray-100 hover:border-gray-200 hover:bg-gray-50'
+                                "
+                                @click="handleSelectAddress(item)"
                             >
+                                <div class="flex items-center">
+                                    <div class="font-medium text-gray-900">
+                                        {{ item.name }}
+                                    </div>
+                                    <div class="ml-2 text-sm text-gray-500">
+                                        {{ item.mobile }}
+                                    </div>
+                                    <el-tag
+                                        v-if="Number(item.defaultReturnFlag) === 1"
+                                        size="small"
+                                        type="success"
+                                        class="ml-2"
+                                        >默认</el-tag
+                                    >
+                                </div>
+                                <div class="text-xs text-gray-500 mt-1">
+                                    {{ item.address }}
+                                </div>
+                            </div>
                         </div>
-                    </el-option>
-                </el-select>
+                    </div>
+                </el-popover>
             </el-form-item>
 
             <el-form-item label="退货说明" prop="description">
@@ -88,9 +168,9 @@
 </template>
 
 <script setup>
-    import { ref, reactive } from 'vue';
+    import { ref, reactive, computed } from 'vue';
     import { EleMessage } from 'ele-admin-plus/es';
-    import { WarningFilled } from '@element-plus/icons-vue';
+    import { ArrowDown, Loading, WarningFilled } from '@element-plus/icons-vue';
     import { useRouter } from 'vue-router';
     import request from '@/utils/request';
 
@@ -98,9 +178,17 @@
     const visible = ref(false);
     const loading = ref(false);
     const addressLoading = ref(false);
+    const addressPickerVisible = ref(false);
     const formRef = ref(null);
     const currentRefundOrderId = ref('');
     const addressList = ref([]);
+    const selectedAddress = computed(() => {
+        const id = String(form.refundAddressId || '');
+        if (!id) return null;
+        return (
+            addressList.value.find((a) => String(a.id) === id) || null
+        );
+    });
 
     const form = reactive({
         refundAddressId: '',
@@ -117,17 +205,16 @@
 
     const fetchAddressList = () => {
         addressLoading.value = true;
-        // Assuming there's an endpoint to get the shop's return addresses.
-        // Replace with the correct endpoint if different.
+        // 使用图2的接口获取退款地址列表
         request
-            .get('/shop/shopAddress/list')
+            .get('/shop/shopAddress/getRefundAddressList')
             .then((res) => {
                 if (res.data.code === 200) {
                     addressList.value = res.data.data || [];
-                    // Auto-select the default address if available
+                    // 自动选择默认退货地址(依据接口文档中的 defaultReturnFlag === 1)
                     if (addressList.value.length > 0 && !form.refundAddressId) {
                         const defaultAddr = addressList.value.find(
-                            (a) => a.isDefault === 1
+                            (a) => Number(a.defaultReturnFlag) === 1
                         );
                         form.refundAddressId = defaultAddr
                             ? defaultAddr.id
@@ -146,10 +233,17 @@
             });
     };
 
+    const handleSelectAddress = (item) => {
+        form.refundAddressId = item.id;
+        addressPickerVisible.value = false;
+        formRef.value?.validateField?.('refundAddressId');
+    };
+
     const open = (refundOrderId) => {
         currentRefundOrderId.value = refundOrderId;
         form.refundAddressId = '';
         form.description = '';
+        addressPickerVisible.value = false;
         if (formRef.value) {
             formRef.value.clearValidate();
         }
@@ -159,8 +253,7 @@
 
     const handleManageAddress = () => {
         // Navigate to address management page if exists
-        EleMessage.info('前往地址管理页面');
-        // router.push('/setting/address');
+        router.push('/mallLogistics/address');
     };
 
     const handleSubmit = async () => {
@@ -207,9 +300,8 @@
 </script>
 
 <style scoped>
-    /* 隐藏原生select的边框使其更贴合设计图,或者使用自定义样式 */
-    :deep(.el-select .el-input__wrapper) {
-        padding: 8px 15px;
+    :deep(.refund-address-popper) {
+        padding: 0;
     }
     :deep(.el-textarea__inner) {
         background-color: #f9fafb;

+ 28 - 11
src/views/mallOrder/refund/components/refund-detail-dialog.vue

@@ -47,7 +47,7 @@
                         </div>
                     </div>
                     <div style="min-width: 600px" class="pt-2">
-                        <el-steps :active="currentStep" align-center finish-status="success">
+                        <el-steps :active="currentStep" align-center finish-status="success" :process-status="processStatus">
                             <template v-if="isRefundOnly">
                                 <el-step :title="`买家申请${getRefundTypeText(form.refundType)}`" />
                                 <el-step title="卖家处理退款申请" />
@@ -240,14 +240,14 @@
 
         <template #footer>
             <div class="flex justify-center gap-4">
-                <el-button :disabled="!canOperate">备注</el-button>
+                <el-button :disabled="!canOperate && !['4', '5'].includes(String(form.status))">备注</el-button>
                 <el-button @click="handleRefuse" :disabled="!canOperate">{{
                     isRefundOnly ? '拒绝退款申请' : '拒绝退货申请'
                     }}</el-button>
                 <el-button @click="handleAgree" :disabled="!canOperate">{{
                     isRefundOnly ? '同意退款' : '同意退货'
                     }}</el-button>
-                <el-button type="primary" @click="handleNegotiate" :disabled="!canOperate">与买家协商</el-button>
+                <el-button type="primary" @click="handleNegotiate" :disabled="!canOperate && !['4', '5'].includes(String(form.status))">与买家协商</el-button>
             </div>
         </template>
         <negotiation-apply-dialog v-model="negotiationDialogVisible" :refund-order-id="form.refundOrderId"
@@ -290,24 +290,41 @@ const refundCount = computed(() => {
     );
 });
 
+// 进度条状态
+const processStatus = computed(() => {
+    const status = Number(form.value.status);
+    if ([5, 6].includes(status)) {
+        return 'error';
+    }
+    return 'process';
+});
+
 // 当前步骤
 const currentStep = computed(() => {
     const status = Number(form.value.status);
     if (isRefundOnly.value) {
-        if (status === 1) return 1;
-        if (status === 2 || status === 3 || status === 4 || status === 7) return 2;
-        if (status === 5 || status === 6 || status === 8) return 3;
+        // 1:申请, 2,3:协商中, 5:驳回, 6:超时关闭 -> 停在第2步(卖家处理)
+        if ([1, 2, 3, 5, 6].includes(status)) return 1;
+        // 7:买家已发货 -> 停在第3步(对于仅退款可能不常见)
+        if (status === 7) return 2;
+        // 4:审核通过, 8:已完成 -> 全绿(第3步完成)
+        if ([4, 8].includes(status)) return 3;
         return 1;
     }
 
-    if (status === 1) return 1;
-    if (status === 2 || status === 3) return 2;
-    if (status === 4 || status === 7) return 3;
-    if (status === 5 || status === 6 || status === 8) return 4;
+    // 退货退款
+    // 1:申请, 2,3:协商中, 5:驳回, 6:超时关闭 -> 停在第2步(卖家处理)
+    if ([1, 2, 3, 5, 6].includes(status)) return 1;
+    // 4:审核通过(商家同意退货,等待买家退货) -> 停在第3步(买家退货)
+    if (status === 4) return 2;
+    // 7:买家已发货 -> 停在第4步(退款完毕,等待商家处理打款,实际业务这往往是个过渡态)
+    if (status === 7) return 3;
+    // 8:已完成 -> 全绿(第4步完成)
+    if (status === 8) return 4;
     return 1;
 });
 
-// 是否可操作(仅在待商家处理状态下可用)
+// 是否可操作主流程(拒绝、同意
 const canOperate = computed(() => {
     return ['1', '3'].includes(String(form.value.status));
 });