|
@@ -0,0 +1,824 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="apply-after-container">
|
|
|
|
|
+ <div class="page-header">
|
|
|
|
|
+ <div class="page-inner header-inner">
|
|
|
|
|
+ <div class="header-left">
|
|
|
|
|
+ <div class="header-icon">
|
|
|
|
|
+ <el-icon><Clock /></el-icon>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="header-title">申请售后</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="header-center">
|
|
|
|
|
+ <div class="header-item">
|
|
|
|
|
+ <span class="label">订单编号</span>
|
|
|
|
|
+ <span class="value">{{ orderInfo.orderNo }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="header-item">
|
|
|
|
|
+ <span class="label">下单时间</span>
|
|
|
|
|
+ <span class="value">{{ orderInfo.orderTime }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="header-right">
|
|
|
|
|
+ <el-button @click="handleViewEvaluation">查看评价</el-button>
|
|
|
|
|
+ <el-button @click="handleBuyAgain">再次购买</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="content">
|
|
|
|
|
+ <div class="page-inner">
|
|
|
|
|
+ <div class="panel">
|
|
|
|
|
+ <div class="panel-title">选择服务类型:</div>
|
|
|
|
|
+ <div class="service-type">
|
|
|
|
|
+ <el-radio-group v-model="form.serviceType" size="large">
|
|
|
|
|
+ <el-radio-button label="1">退换货</el-radio-button>
|
|
|
|
|
+ <el-radio-button label="3">维修</el-radio-button>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+ <div class="service-tip">如商品出现质量问题,退货价格按照销售价格退货!</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section-title">提交数量</div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="summary-bar" v-if="selectedProducts.length > 0">
|
|
|
|
|
+ <div class="summary-left">
|
|
|
|
|
+ <span>商品总数:{{ summary.totalKinds }}个</span>
|
|
|
|
|
+ <span>本单提交商品总数量:{{ summary.totalQty }}个</span>
|
|
|
|
|
+ <span>退货总金额:¥ {{ summary.totalAmount }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-icon class="summary-close" @click="handleClearProducts"><Close /></el-icon>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="product-table">
|
|
|
|
|
+ <div class="table-header">
|
|
|
|
|
+ <div class="col col-product">商品信息</div>
|
|
|
|
|
+ <div class="col col-price">单价</div>
|
|
|
|
|
+ <div class="col col-sold">销售数量</div>
|
|
|
|
|
+ <div class="col col-available">未售后数量</div>
|
|
|
|
|
+ <div class="col col-qty">退货数量</div>
|
|
|
|
|
+ <div class="col col-amount">退货金额</div>
|
|
|
|
|
+ <div class="col col-op">操作</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="selectedProducts.length === 0" class="table-empty">
|
|
|
|
|
+ <el-empty description="暂无商品" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-for="item in selectedProducts" :key="item.productId" class="table-row">
|
|
|
|
|
+ <div class="col col-product">
|
|
|
|
|
+ <div class="product-info">
|
|
|
|
|
+ <div class="product-image">
|
|
|
|
|
+ <el-image :src="item.productImage" fit="cover">
|
|
|
|
|
+ <template #error>
|
|
|
|
|
+ <div class="image-placeholder"></div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-image>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="product-meta">
|
|
|
|
|
+ <div class="name">{{ item.name }}</div>
|
|
|
|
|
+ <div class="spec">{{ item.spec }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col col-price">¥{{ formatMoney(item.unitPrice) }}</div>
|
|
|
|
|
+ <div class="col col-sold">{{ item.soldQty }}</div>
|
|
|
|
|
+ <div class="col col-available">{{ item.availableQty }}</div>
|
|
|
|
|
+ <div class="col col-qty">
|
|
|
|
|
+ <el-input-number
|
|
|
|
|
+ v-model="item.returnQuantity"
|
|
|
|
|
+ :min="0"
|
|
|
|
|
+ :max="item.availableQty"
|
|
|
|
|
+ controls-position="right"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ @change="handleQtyChange"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="col col-amount">¥{{ formatMoney(item.returnQuantity * item.unitPrice) }}</div>
|
|
|
|
|
+ <div class="col col-op">
|
|
|
|
|
+ <el-button type="danger" link @click="handleRemoveProduct(item.productId)">删除</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="panel">
|
|
|
|
|
+ <div class="panel-title">售后服务单信息:</div>
|
|
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" class="after-form">
|
|
|
|
|
+ <el-form-item label="退货原因" prop="returnReasonId">
|
|
|
|
|
+ <el-select v-model="form.returnReasonId" placeholder="请选择" style="width: 200px" @change="handleReturnReasonChange">
|
|
|
|
|
+ <el-option v-for="r in reasonOptions" :key="r.id" :label="r.returnReasonName" :value="r.id" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="问题描述" prop="problemDescription">
|
|
|
|
|
+ <el-input v-model="form.problemDescription" type="textarea" :rows="5" placeholder="请输入问题描述" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="凭证图片">
|
|
|
|
|
+ <el-upload v-model:file-list="form.voucherPhotoArray" list-type="picture-card" action="#" :auto-upload="false" :limit="5">
|
|
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="panel">
|
|
|
|
|
+ <div class="panel-title">选择取件方式:</div>
|
|
|
|
|
+ <el-radio-group v-model="form.returnMethod" class="pickup-type">
|
|
|
|
|
+ <el-radio label="1">上门取件</el-radio>
|
|
|
|
|
+ <el-radio label="2">快递配送</el-radio>
|
|
|
|
|
+ </el-radio-group>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="pickup-info" v-if="form.returnMethod === '1'">
|
|
|
|
|
+ <div class="pickup-row">
|
|
|
|
|
+ <span class="k">顾客姓名:</span><span class="v">{{ form.chargebackName }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="pickup-row">
|
|
|
|
|
+ <span class="k">手机号:</span><span class="v">{{ form.chargebackPhone }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="pickup-row">
|
|
|
|
|
+ <span class="k">取件时间:</span>
|
|
|
|
|
+ <el-date-picker v-model="form.chargebackPickupTime" type="datetime" placeholder="请选择" style="width: 240px" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="pickup-row">
|
|
|
|
|
+ <span class="k">取件地址:</span><span class="v">{{ form.chargebackAddress }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="pickup-row hint">提交服务单后,售后专员可能与您电话沟通,请保持手机畅通</div>
|
|
|
|
|
+ <div class="pickup-row">
|
|
|
|
|
+ <el-button plain @click="handleChangeAddress">切换地址</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="pickup-info" v-else>
|
|
|
|
|
+ <div class="pickup-row hint">请在提交后按提示将商品寄回,售后专员将与您确认寄送信息。</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="footer">
|
|
|
|
|
+ <el-button type="danger" class="submit-btn" @click="handleSubmit">提交</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-dialog v-model="addressDialogVisible" title="切换地址" width="900px" class="address-dialog">
|
|
|
|
|
+ <div v-loading="addressLoading" class="address-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="addr in addressList"
|
|
|
|
|
+ :key="addr.id"
|
|
|
|
|
+ :class="['address-item', { active: selectedAddressId === String(addr.id) }]"
|
|
|
|
|
+ @click="handleSelectAddress(addr)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="address-left">
|
|
|
|
|
+ <div class="line">收货人: {{ addr.consignee || addr.receiverName || addr.name || '-' }}</div>
|
|
|
|
|
+ <div class="line">联系方式: {{ addr.phone || addr.mobile || '-' }}</div>
|
|
|
|
|
+ <div class="line">收货地址: {{ formatAddress(addr) }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="address-right">
|
|
|
|
|
+ <span v-if="isDefaultAddress(addr)" class="default-tag">默认地址</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="dialog-footer">
|
|
|
|
|
+ <el-button type="primary" @click="handleConfirmAddress">确认</el-button>
|
|
|
|
|
+ <el-button @click="addressDialogVisible = false">取消</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed, reactive, ref } from 'vue';
|
|
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
|
|
+import { Clock, Close, Plus } from '@element-plus/icons-vue';
|
|
|
|
|
+
|
|
|
|
|
+import { useRouter, useRoute } from 'vue-router';
|
|
|
|
|
+import { getOrderInfo, getOrderProductsWithAvailableQty, getReturnReason } from '@/api/pc/enterprise/order';
|
|
|
|
|
+import { getEnterpriseInfo, getAddressList } from '@/api/pc/enterprise';
|
|
|
|
|
+import { getOrderReturnInfo, addOrderReturn } from '@/api/pc/enterprise/orderReturn';
|
|
|
|
|
+import type { OrderReturn } from '@/api/pc/enterprise/orderReturnTypes';
|
|
|
|
|
+import { el } from 'element-plus/es/locale/index.mjs';
|
|
|
|
|
+
|
|
|
|
|
+const router = useRouter();
|
|
|
|
|
+const route = useRoute();
|
|
|
|
|
+const loading = ref(false);
|
|
|
|
|
+const orderId = ref<any>(0);
|
|
|
|
|
+
|
|
|
|
|
+const addressDialogVisible = ref(false);
|
|
|
|
|
+const addressLoading = ref(false);
|
|
|
|
|
+const addressList = ref<any[]>([]);
|
|
|
|
|
+const selectedAddressId = ref<string>('');
|
|
|
|
|
+const selectedAddress = ref<any>(null);
|
|
|
|
|
+
|
|
|
|
|
+const orderInfo = reactive({
|
|
|
|
|
+ orderId: 0,
|
|
|
|
|
+ orderNo: '',
|
|
|
|
|
+ orderTime: '',
|
|
|
|
|
+ freight: '0.00',
|
|
|
|
|
+ totalAmount: '0.00',
|
|
|
|
|
+ address: '',
|
|
|
|
|
+ receiverName: '',
|
|
|
|
|
+ receiverPhone: '',
|
|
|
|
|
+ deliveryTime: '',
|
|
|
|
|
+ purchaseReason: '',
|
|
|
|
|
+ costType: '',
|
|
|
|
|
+ remark: ''
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+type ApplyProduct = {
|
|
|
|
|
+ productId: number;
|
|
|
|
|
+ productNo: string;
|
|
|
|
|
+ productUnit: string;
|
|
|
|
|
+ productImage: string;
|
|
|
|
|
+ productName: string;
|
|
|
|
|
+ orderProductId: number;
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ spec: string;
|
|
|
|
|
+ unitPrice: number;
|
|
|
|
|
+ soldQty: number;
|
|
|
|
|
+ totalAmount: number;
|
|
|
|
|
+ reasonDetail: string;
|
|
|
|
|
+ availableQty: number;
|
|
|
|
|
+ returnQuantity: number;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const selectedProducts = ref<ApplyProduct[]>([]);
|
|
|
|
|
+
|
|
|
|
|
+const formRef = ref();
|
|
|
|
|
+
|
|
|
|
|
+const form = reactive({
|
|
|
|
|
+ orderId: 0,
|
|
|
|
|
+ orderNo: '',
|
|
|
|
|
+ customerNo: '',
|
|
|
|
|
+ orderAmount: 0,
|
|
|
|
|
+ returnAmount: '',
|
|
|
|
|
+ serviceType: '1',
|
|
|
|
|
+ returnReasonId: '',
|
|
|
|
|
+ returnReason: '',
|
|
|
|
|
+ problemDescription: '',
|
|
|
|
|
+ returnProductNum: 0,
|
|
|
|
|
+ afterSaleAmount: '',
|
|
|
|
|
+ voucherPhotoArray: [],
|
|
|
|
|
+ voucherPhoto: '',
|
|
|
|
|
+ returnMethod: '1',
|
|
|
|
|
+ chargebackPickupTime: '' as any,
|
|
|
|
|
+ chargebackName: '',
|
|
|
|
|
+ chargebackPhone: '',
|
|
|
|
|
+ chargebackAddress: '',
|
|
|
|
|
+ orderReturnItemList: []
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const rules = {
|
|
|
|
|
+ returnReasonId: [{ required: true, message: '请选择退货原因', trigger: 'change' }],
|
|
|
|
|
+ problemDescription: [{ required: true, message: '请输入问题描述', trigger: 'blur' }]
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const reasonOptions = ref([]);
|
|
|
|
|
+
|
|
|
|
|
+const summary = computed(() => {
|
|
|
|
|
+ const list = selectedProducts.value;
|
|
|
|
|
+ const totalKinds = list.length;
|
|
|
|
|
+ const totalQty = list.reduce((sum, p) => sum + (Number(p.returnQuantity) || 0), 0);
|
|
|
|
|
+ const totalAmountNum = list.reduce((sum, p) => sum + (Number(p.returnQuantity) || 0) * (Number(p.unitPrice) || 0), 0);
|
|
|
|
|
+ return {
|
|
|
|
|
+ totalKinds,
|
|
|
|
|
+ totalQty,
|
|
|
|
|
+ totalAmount: formatMoney(totalAmountNum)
|
|
|
|
|
+ };
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const formatMoney = (val: number | string) => {
|
|
|
|
|
+ const num = Number(val) || 0;
|
|
|
|
|
+ return num.toFixed(2);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleQtyChange = () => {
|
|
|
|
|
+ // computed 会自动更新
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleRemoveProduct = (id: number) => {
|
|
|
|
|
+ selectedProducts.value = selectedProducts.value.filter((p) => p.productId != id);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const isDefaultAddress = (addr: any) => {
|
|
|
|
|
+ const v = addr?.isDefault ?? addr?.defaultFlag;
|
|
|
|
|
+ return v === 0 || v === '0' || v === 1 || v === '1' || v === true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const formatAddress = (addr: any) => {
|
|
|
|
|
+ const parts = [addr?.provincialCityCountry].filter(Boolean);
|
|
|
|
|
+ const detail = addr?.detailAddress || addr?.address || addr?.fullAddress || '';
|
|
|
|
|
+ const prefix = parts.join('');
|
|
|
|
|
+ return `${prefix}${detail ? (prefix ? ' ' : '') + detail : ''}`.trim() || '-';
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const loadAddressList = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ addressLoading.value = true;
|
|
|
|
|
+ const res = await getAddressList();
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ const rows = res.rows || res.data || [];
|
|
|
|
|
+ addressList.value = Array.isArray(rows) ? rows : [];
|
|
|
|
|
+
|
|
|
|
|
+ const defaultAddr = addressList.value.find((a) => isDefaultAddress(a));
|
|
|
|
|
+ const initAddr = selectedAddress.value || defaultAddr || addressList.value[0];
|
|
|
|
|
+ if (initAddr) {
|
|
|
|
|
+ selectedAddress.value = initAddr;
|
|
|
|
|
+ selectedAddressId.value = String(initAddr.id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载地址列表失败:', error);
|
|
|
|
|
+ ElMessage.error('加载地址列表失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ addressLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleSelectAddress = (addr: any) => {
|
|
|
|
|
+ selectedAddress.value = addr;
|
|
|
|
|
+ selectedAddressId.value = String(addr.id);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleConfirmAddress = () => {
|
|
|
|
|
+ if (!selectedAddress.value) {
|
|
|
|
|
+ ElMessage.warning('请选择地址');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const addr = selectedAddress.value;
|
|
|
|
|
+ form.chargebackName = addr.consignee || addr.receiverName || addr.name || form.chargebackName;
|
|
|
|
|
+ form.chargebackPhone = addr.phone || addr.mobile || form.chargebackPhone;
|
|
|
|
|
+ form.chargebackAddress = formatAddress(addr);
|
|
|
|
|
+ addressDialogVisible.value = false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 加载退货原因列表
|
|
|
|
|
+const loadReturnReason = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getReturnReason();
|
|
|
|
|
+ if (res.code === 200) {
|
|
|
|
|
+ reasonOptions.value = res.rows || [];
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载退货原因列表失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 加载企业信息
|
|
|
|
|
+const loadEnterpriseInfo = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getEnterpriseInfo();
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ const data = res.data;
|
|
|
|
|
+ const salesInfo = data.customerSalesInfoVo || {};
|
|
|
|
|
+ const businessInfo = data.customerBusinessInfoVo || {};
|
|
|
|
|
+
|
|
|
|
|
+ // 辅助函数:处理空值,空字符串也转为 '-'
|
|
|
|
|
+ const formatValue = (value: any) => (value && value !== '' ? value : '-');
|
|
|
|
|
+
|
|
|
|
|
+ form.chargebackName = formatValue(data.customerName);
|
|
|
|
|
+
|
|
|
|
|
+ form.chargebackPhone = formatValue(data.landline);
|
|
|
|
|
+ form.chargebackAddress = formatValue(data.provincialCityCounty) + ' ' + formatValue(data.address);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载企业信息失败:', error);
|
|
|
|
|
+ ElMessage.error('加载企业信息失败');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleClearProducts = () => {
|
|
|
|
|
+ selectedProducts.value = [];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleChangeAddress = () => {
|
|
|
|
|
+ addressDialogVisible.value = true;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleViewEvaluation = () => {
|
|
|
|
|
+ ElMessage.info('查看评价');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleBuyAgain = () => {
|
|
|
|
|
+ ElMessage.info('再次购买');
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 加载订单详情
|
|
|
|
|
+const loadOrderDetail = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ const res = await getOrderInfo(orderId.value);
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ const order = res.data;
|
|
|
|
|
+ // 映射订单信息
|
|
|
|
|
+ orderInfo.orderId = order.id;
|
|
|
|
|
+ orderInfo.orderNo = order.orderNo;
|
|
|
|
|
+ form.orderId = order.id;
|
|
|
|
|
+ form.orderNo = order.orderNo;
|
|
|
|
|
+ form.orderAmount = order.payableAmount || '0.00';
|
|
|
|
|
+ orderInfo.orderTime = order.orderTime;
|
|
|
|
|
+ orderInfo.freight = order.shippingFee || '0.00';
|
|
|
|
|
+ orderInfo.totalAmount = order.payableAmount || '0.00';
|
|
|
|
|
+ orderInfo.deliveryTime = order.expectedDeliveryTime || '';
|
|
|
|
|
+ orderInfo.purchaseReason = order.purchaseReason || '';
|
|
|
|
|
+ orderInfo.remark = order.remark || '';
|
|
|
|
|
+ form.customerNo = order.customerCode;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取商品信息
|
|
|
|
|
+ const productsRes = await getOrderProductsWithAvailableQty([orderId.value]);
|
|
|
|
|
+ if (productsRes.code === 200 && productsRes.rows) {
|
|
|
|
|
+ selectedProducts.value = productsRes.rows.map((p: any) => {
|
|
|
|
|
+ const soldQty = Number(p.orderQuantity) || 0;
|
|
|
|
|
+ const afterSaleQty = Number(p.afterSaleQuantity) || 0;
|
|
|
|
|
+ // const availableQty = Math.max(soldQty - afterSaleQty, 0);
|
|
|
|
|
+ const availableQty = Math.max(p.availableQty, 0);
|
|
|
|
|
+ const unitPrice = Number(p.orderPrice) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ productId: p.productId,
|
|
|
|
|
+ productNo: p.productNo,
|
|
|
|
|
+ productUnit: p.productUnit || '',
|
|
|
|
|
+ productImage: p.productImage || '',
|
|
|
|
|
+ name: p.productName || '',
|
|
|
|
|
+ spec: `${p.productUnit || ''} ${p.productNo || ''}`.trim(),
|
|
|
|
|
+ unitPrice,
|
|
|
|
|
+ soldQty,
|
|
|
|
|
+ availableQty,
|
|
|
|
|
+ returnQuantity: availableQty > 0 ? 1 : 0,
|
|
|
|
|
+ orderProductId: p.id,
|
|
|
|
|
+ productName: p.productName,
|
|
|
|
|
+ reasonDetail: form.returnReason
|
|
|
|
|
+ } as ApplyProduct;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载订单详情失败:', error);
|
|
|
|
|
+ ElMessage.error('加载订单详情失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handleReturnReasonChange = (value: string) => {
|
|
|
|
|
+ const reason = reasonOptions.value.find((r) => r.id === value);
|
|
|
|
|
+ if (reason) {
|
|
|
|
|
+ form.returnReason = reason.returnReasonName;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ const paramId = route.query.orderId;
|
|
|
|
|
+
|
|
|
|
|
+ // 直接使用字符串,不转换为数字,避免精度丢失
|
|
|
|
|
+ orderId.value = paramId as string;
|
|
|
|
|
+ if (orderId.value) {
|
|
|
|
|
+ loadOrderDetail();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('订单ID无效,无法加载订单详情');
|
|
|
|
|
+ }
|
|
|
|
|
+ loadReturnReason();
|
|
|
|
|
+ loadEnterpriseInfo();
|
|
|
|
|
+ loadAddressList();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const handleSubmit = async () => {
|
|
|
|
|
+ if (selectedProducts.value.length === 0) {
|
|
|
|
|
+ ElMessage.warning('请至少选择一个商品');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const hasQty = selectedProducts.value.some((p) => Number(p.returnQuantity) > 0);
|
|
|
|
|
+ if (!hasQty) {
|
|
|
|
|
+ ElMessage.warning('请填写退货数量');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const valid = await formRef.value?.validate();
|
|
|
|
|
+ if (!valid) return;
|
|
|
|
|
+
|
|
|
|
|
+ form.returnProductNum = summary.value.totalQty;
|
|
|
|
|
+ form.afterSaleAmount = summary.value.totalAmount;
|
|
|
|
|
+ form.returnAmount = summary.value.totalAmount;
|
|
|
|
|
+ form.voucherPhoto = form.voucherPhotoArray.join(',');
|
|
|
|
|
+ form.orderReturnItemList = selectedProducts.value;
|
|
|
|
|
+ const res = await addOrderReturn(form as any);
|
|
|
|
|
+ if (res.code == 200) {
|
|
|
|
|
+ ElMessage.success('申请成功');
|
|
|
|
|
+ router.push('/order/afterSale');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error('申请失败');
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.apply-after-container {
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ min-height: 100%;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-inner {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: 1200px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-header {
|
|
|
|
|
+ height: 72px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
|
|
+
|
|
|
|
|
+ .header-inner {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .header-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ .header-icon {
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border: 2px solid #e60012;
|
|
|
|
|
+ color: #e60012;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .header-title {
|
|
|
|
|
+ color: #e60012;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .header-center {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 50px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+
|
|
|
|
|
+ .header-item {
|
|
|
|
|
+ .label {
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-right: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .value {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .header-right {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content {
|
|
|
|
|
+ padding: 16px 20px 30px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 1px solid #eee;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ padding: 16px 18px;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.service-type {
|
|
|
|
|
+ .service-tip {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.summary-bar {
|
|
|
|
|
+ background: #eef8ee;
|
|
|
|
|
+ border: 1px solid #d8f0d8;
|
|
|
|
|
+ color: #18a058;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ padding: 10px 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .summary-left {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 18px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .summary-close {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #18a058;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.product-table {
|
|
|
|
|
+ border: 1px solid #eee;
|
|
|
|
|
+
|
|
|
|
|
+ .table-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .table-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-bottom: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .col {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-right: 1px solid #f0f0f0;
|
|
|
|
|
+ min-height: 88px;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-right: none;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .col-product {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
|
|
+ }
|
|
|
|
|
+ .col-price,
|
|
|
|
|
+ .col-sold,
|
|
|
|
|
+ .col-available,
|
|
|
|
|
+ .col-qty,
|
|
|
|
|
+ .col-amount {
|
|
|
|
|
+ width: 110px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .col-op {
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .table-empty {
|
|
|
|
|
+ padding: 20px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .product-info {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+ .product-image {
|
|
|
|
|
+ width: 60px;
|
|
|
|
|
+ height: 60px;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ .el-image {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+ .image-placeholder {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ .product-meta {
|
|
|
|
|
+ .name {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ line-height: 1.4;
|
|
|
|
|
+ }
|
|
|
|
|
+ .spec {
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.after-form {
|
|
|
|
|
+ :deep(.el-form-item__label) {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pickup-type {
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.pickup-info {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+
|
|
|
|
|
+ .pickup-row {
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ .k {
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &.hint {
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 10px 0 20px;
|
|
|
|
|
+ .submit-btn {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.address-dialog {
|
|
|
|
|
+ .address-list {
|
|
|
|
|
+ max-height: 520px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .address-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ padding: 14px 16px;
|
|
|
|
|
+ border: 1px solid #e5e5e5;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-color: #d9d9d9;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ border-color: #e60012;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .address-left {
|
|
|
|
|
+ .line {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ line-height: 1.8;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .address-right {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ min-width: 80px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .default-tag {
|
|
|
|
|
+ color: #e60012;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .dialog-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|