Browse Source

feat(mallOrder/refund): 实现拒绝和同意退货的对话框功能

- 添加拒绝退款对话框组件,包含原因、描述和凭证上传
- 添加同意退货对话框组件,包含退货地址选择和说明
- 在退款详情中集成新对话框,替换原有的开发中提示
ylong 4 days ago
parent
commit
f7cb90beef

+ 175 - 0
src/views/mallOrder/refund/components/agree-dialog.vue

@@ -0,0 +1,175 @@
+<template>
+    <ele-modal :width="600" v-model="visible" title="同意退货" :body-style="{ padding: '20px 40px' }">
+        <div class="mb-4 p-3 bg-orange-50 text-orange-500 rounded text-sm flex items-start">
+            <el-icon class="mr-2 mt-0.5"><WarningFilled /></el-icon>
+            <span>如因您提供的退货地址错误,导致买家无法退货或退回商品后无法送达,将由您承担产生的后果。</span>
+        </div>
+
+        <el-form :model="form" :rules="rules" ref="formRef" label-width="80px" label-position="top">
+            <div class="flex justify-between items-center mb-2">
+                <span class="text-red-500">* <span class="text-gray-700">退货地址</span></span>
+                <el-button link type="primary" @click="handleManageAddress">管理退货地址</el-button>
+            </div>
+            <el-form-item prop="refundAddressId" label-width="0">
+                <el-select 
+                    v-model="form.refundAddressId" 
+                    placeholder="请选择退货地址" 
+                    class="w-full"
+                    :loading="addressLoading"
+                >
+                    <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>
+                        </div>
+                    </el-option>
+                </el-select>
+            </el-form-item>
+            
+            <el-form-item label="退货说明" prop="description">
+                <el-input 
+                    v-model="form.description" 
+                    type="textarea" 
+                    :rows="4" 
+                    placeholder="请填入退货说明"
+                    maxlength="200"
+                    show-word-limit
+                    class="bg-gray-50"
+                />
+            </el-form-item>
+        </el-form>
+        
+        <template #footer>
+            <div class="flex justify-center gap-4">
+                <el-button @click="visible = false" class="bg-blue-50 text-blue-500 border-none">取消并返回</el-button>
+                <el-button type="primary" :loading="loading" @click="handleSubmit">同意退货</el-button>
+            </div>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import { WarningFilled } from '@element-plus/icons-vue';
+import { useRouter } from 'vue-router';
+import request from '@/utils/request';
+
+const router = useRouter();
+const visible = ref(false);
+const loading = ref(false);
+const addressLoading = ref(false);
+const formRef = ref(null);
+const currentRefundOrderId = ref('');
+const addressList = ref([]);
+
+const form = reactive({
+    refundAddressId: '',
+    description: ''
+});
+
+const rules = {
+    refundAddressId: [{ required: true, message: '请选择退货地址', trigger: 'change' }]
+};
+
+const emit = defineEmits(['success']);
+
+const fetchAddressList = () => {
+    addressLoading.value = true;
+    // Assuming there's an endpoint to get the shop's return addresses.
+    // Replace with the correct endpoint if different.
+    request.get('/shop/shopAddress/list')
+        .then(res => {
+            if (res.data.code === 200) {
+                addressList.value = res.data.data || [];
+                // Auto-select the default address if available
+                if (addressList.value.length > 0 && !form.refundAddressId) {
+                    const defaultAddr = addressList.value.find(a => a.isDefault === 1);
+                    form.refundAddressId = defaultAddr ? defaultAddr.id : addressList.value[0].id;
+                }
+            } else {
+                EleMessage.error(res.data.msg || '获取地址列表失败');
+            }
+        })
+        .catch(e => {
+            console.error(e);
+            EleMessage.error('获取地址列表失败');
+        })
+        .finally(() => {
+            addressLoading.value = false;
+        });
+};
+
+const open = (refundOrderId) => {
+    currentRefundOrderId.value = refundOrderId;
+    form.refundAddressId = '';
+    form.description = '';
+    if (formRef.value) {
+        formRef.value.clearValidate();
+    }
+    fetchAddressList();
+    visible.value = true;
+};
+
+const handleManageAddress = () => {
+    // Navigate to address management page if exists
+    EleMessage.info('前往地址管理页面');
+    // router.push('/setting/address'); 
+};
+
+const handleSubmit = async () => {
+    if (!formRef.value) return;
+    
+    await formRef.value.validate((valid) => {
+        if (valid) {
+            submitData();
+        }
+    });
+};
+
+const submitData = () => {
+    const params = {
+        refundOrderId: currentRefundOrderId.value,
+        refundAddressId: form.refundAddressId,
+        description: form.description
+    };
+
+    loading.value = true;
+    request.post('/shop/shopOrder/refund/auditAgree', params)
+        .then(res => {
+            if (res.data.code === 200) {
+                EleMessage.success('操作成功');
+                visible.value = false;
+                emit('success');
+            } else {
+                EleMessage.error(res.data.msg || '操作失败');
+            }
+        })
+        .catch(e => {
+            console.error(e);
+            EleMessage.error('网络错误');
+        })
+        .finally(() => {
+            loading.value = false;
+        });
+};
+
+defineExpose({
+    open
+});
+</script>
+
+<style scoped>
+/* 隐藏原生select的边框使其更贴合设计图,或者使用自定义样式 */
+:deep(.el-select .el-input__wrapper) {
+    padding: 8px 15px;
+}
+:deep(.el-textarea__inner) {
+    background-color: #f9fafb;
+}
+</style>

+ 12 - 2
src/views/mallOrder/refund/components/refund-detail-dialog.vue

@@ -215,6 +215,8 @@
         </template>
         <negotiation-apply-dialog v-model="negotiationDialogVisible" :refund-order-id="form.refundOrderId"
             :max-refund-amount="Number(form.refundMoney) || 0" @success="handleNegotiationSuccess" />
+        <refuse-dialog ref="refuseDialogRef" @success="handleNegotiationSuccess" />
+        <agree-dialog ref="agreeDialogRef" @success="handleNegotiationSuccess" />
     </ele-modal>
 </template>
 
@@ -225,6 +227,8 @@
     import { useClipboard } from '@vueuse/core';
     import request from '@/utils/request';
     import NegotiationApplyDialog from './negotiation-apply-dialog.vue';
+    import RefuseDialog from './refuse-dialog.vue';
+    import AgreeDialog from './agree-dialog.vue';
 
     const visible = defineModel({ type: Boolean });
     const loading = ref(false);
@@ -232,6 +236,8 @@
     const historyList = ref([]);
     const { copy } = useClipboard();
     const negotiationDialogVisible = ref(false);
+    const refuseDialogRef = ref(null);
+    const agreeDialogRef = ref(null);
 
     const refundCount = computed(() => {
         if (form.value.totalNum) return form.value.totalNum;
@@ -374,11 +380,15 @@
     };
 
     const handleRefuse = () => {
-        EleMessage.warning('功能开发中');
+        if (refuseDialogRef.value) {
+            refuseDialogRef.value.open(form.value.refundOrderId);
+        }
     };
 
     const handleAgree = () => {
-        EleMessage.warning('功能开发中');
+        if (agreeDialogRef.value) {
+            agreeDialogRef.value.open(form.value.refundOrderId);
+        }
     };
 
     const handleLeaveMessage = () => {

+ 115 - 0
src/views/mallOrder/refund/components/refuse-dialog.vue

@@ -0,0 +1,115 @@
+<template>
+    <ele-modal :width="600" v-model="visible" title="拒绝退款" :body-style="{ padding: '20px 40px' }">
+        <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+            <el-form-item label="驳回原因" prop="reason">
+                <dict-data code="shop_refund_reason" v-model="form.reason" placeholder="请选择" class="w-full" />
+            </el-form-item>
+            
+            <el-form-item label="驳回描述" prop="description">
+                <el-input 
+                    v-model="form.description" 
+                    type="textarea" 
+                    :rows="4" 
+                    placeholder="请详细描述您向消费者提供的解决方案。"
+                    maxlength="200"
+                    show-word-limit
+                />
+            </el-form-item>
+            
+            <el-form-item label="协商凭证" prop="imgUrlsStr">
+                <ImageUpload v-model="form.imgUrlsStr" :limit="5" />
+                <div class="text-xs text-gray-400 mt-1">上传图片最多5张</div>
+            </el-form-item>
+        </el-form>
+        
+        <template #footer>
+            <div class="flex justify-center gap-4">
+                <el-button type="success" :loading="loading" @click="handleSubmit">提交</el-button>
+                <el-button @click="visible = false">取消</el-button>
+            </div>
+        </template>
+    </ele-modal>
+</template>
+
+<script setup>
+import { ref, reactive, watch } from 'vue';
+import { EleMessage } from 'ele-admin-plus/es';
+import request from '@/utils/request';
+import ImageUpload from '@/components/ImageUpload/index.vue';
+
+const visible = ref(false);
+const loading = ref(false);
+const formRef = ref(null);
+const currentRefundOrderId = ref('');
+
+const form = reactive({
+    reason: '',
+    description: '',
+    imgUrlsStr: ''
+});
+
+const rules = {
+    reason: [{ required: true, message: '请选择驳回原因', trigger: 'change' }],
+    description: [{ required: true, message: '请输入驳回描述', trigger: 'blur' }]
+};
+
+const emit = defineEmits(['success']);
+
+const open = (refundOrderId) => {
+    currentRefundOrderId.value = refundOrderId;
+    // 重置表单
+    form.reason = '';
+    form.description = '';
+    form.imgUrlsStr = '';
+    if (formRef.value) {
+        formRef.value.clearValidate();
+    }
+    visible.value = true;
+};
+
+const handleSubmit = async () => {
+    if (!formRef.value) return;
+    
+    await formRef.value.validate((valid) => {
+        if (valid) {
+            submitData();
+        }
+    });
+};
+
+const submitData = () => {
+    const params = {
+        refundOrderId: currentRefundOrderId.value,
+        reason: form.reason,
+        description: form.description,
+        imgUrls: form.imgUrlsStr ? form.imgUrlsStr.split(',') : []
+    };
+
+    loading.value = true;
+    request.post('/shop/shopOrder/refund/auditRefuse', params)
+        .then(res => {
+            if (res.data.code === 200) {
+                EleMessage.success('操作成功');
+                visible.value = false;
+                emit('success');
+            } else {
+                EleMessage.error(res.data.msg || '操作失败');
+            }
+        })
+        .catch(e => {
+            console.error(e);
+            EleMessage.error('网络错误');
+        })
+        .finally(() => {
+            loading.value = false;
+        });
+};
+
+defineExpose({
+    open
+});
+</script>
+
+<style scoped>
+/* 可根据需要调整样式 */
+</style>