spring
8 天以前 c65e1446d2d386c4e6baef2fcc606bdd6de90576
采购退货单
已添加4个文件
1767 ■■■■■ 文件已修改
src/views/sales/components/GenerateReturnDialog.vue 528 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/components/PurchaseReturnDialog.vue 453 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/components/PurchaseReturnViewDialog.vue 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/purchaseReturn.vue 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/components/GenerateReturnDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,528 @@
<template>
  <el-dialog
    :model-value="dialogGenerateVisible"
    @update:model-value="$emit('update:dialogGenerateVisible', $event)"
    title="一键生成退货单"
    width="1000px"
    :close-on-click-modal="false"
  >
    <div class="generate-container">
      <!-- é€‰æ‹©é‡‡è´­è®¢å• -->
      <el-card class="select-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>选择采购订单</span>
          </div>
        </template>
        <el-form :inline="true" :model="searchForm" class="search-form">
          <el-form-item label="供应商">
            <el-select
              v-model="searchForm.supplierId"
              placeholder="请选择供应商"
              clearable
              style="width: 200px"
              @change="handleSupplierChange"
            >
              <el-option
                :label="item.label"
                v-for="item in supplierList"
                :key="item.value"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="订单状态">
            <el-select
              v-model="searchForm.orderStatus"
              placeholder="请选择订单状态"
              clearable
              style="width: 150px"
            >
              <el-option
                :label="item.label"
                v-for="item in orderStatusList"
                :key="item.value"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="searchOrders">查询</el-button>
            <el-button @click="resetSearch">重置</el-button>
          </el-form-item>
        </el-form>
        <el-table
          :data="orderList"
          @selection-change="handleOrderSelectionChange"
          border
          style="width: 100%"
          max-height="300"
        >
          <el-table-column type="selection" width="55" />
          <el-table-column label="订单号" prop="orderNo" width="180" />
          <el-table-column label="供应商" prop="supplierName" width="150" />
          <el-table-column label="订单日期" prop="orderDate" width="120" />
          <el-table-column label="商品名称" prop="coalName" width="150" />
          <el-table-column label="订单数量" prop="orderQuantity" width="100">
            <template #default="scope">
              {{ scope.row.orderQuantity }} å¨
            </template>
          </el-table-column>
          <el-table-column label="已收货数量" prop="receivedQuantity" width="100">
            <template #default="scope">
              {{ scope.row.receivedQuantity }} å¨
            </template>
          </el-table-column>
          <el-table-column label="状态" prop="status" width="100">
            <template #default="scope">
              <el-tag :type="getOrderStatusType(scope.row.status)">
                {{ getOrderStatusText(scope.row.status) }}
              </el-tag>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
      <!-- é€€è´§ä¿¡æ¯é…ç½® -->
      <el-card class="config-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>退货信息配置</span>
          </div>
        </template>
        <el-form :model="returnConfig" label-width="120px" class="config-form">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="退货原因" prop="returnReason">
                <el-select
                  v-model="returnConfig.returnReason"
                  placeholder="请选择退货原因"
                  style="width: 100%"
                  filterable
                  allow-create
                >
                  <el-option
                    :label="item.label"
                    v-for="item in returnReasonList"
                    :key="item.value"
                    :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="操作员" prop="operatorId">
                <el-select
                  v-model="returnConfig.operatorId"
                  placeholder="请选择操作员"
                  style="width: 100%"
                  filterable
                >
                  <el-option
                    :label="item.label"
                    v-for="item in operatorList"
                    :key="item.value"
                    :value="item.value"
                  />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-form-item label="备注" prop="remark">
            <el-input
              v-model="returnConfig.remark"
              type="textarea"
              :rows="3"
              placeholder="请输入备注信息"
            />
          </el-form-item>
        </el-form>
      </el-card>
      <!-- é¢„览退货单 -->
      <el-card class="preview-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>预览退货单</span>
          </div>
        </template>
        <div v-if="selectedOrders.length === 0" class="no-selection">
          <el-empty description="请先选择要退货的采购订单" />
        </div>
        <div v-else class="preview-content">
          <el-table :data="previewReturnItems" border style="width: 100%">
            <el-table-column label="订单号" prop="orderNo" width="150" />
            <el-table-column label="商品名称" prop="coalName" width="150" />
            <el-table-column label="退货数量" width="120">
              <template #default="scope">
                <el-input
                  v-model.number="scope.row.returnQuantity"
                  placeholder="退货数量"
                  type="number"
                  @input="updateReturnQuantity(scope.$index)"
                >
                  <template v-slot:suffix>
                    <span>吨</span>
                  </template>
                </el-input>
              </template>
            </el-table-column>
            <el-table-column label="单价" prop="unitPrice" width="120">
              <template #default="scope">
                {{ scope.row.unitPrice }} å…ƒ/吨
              </template>
            </el-table-column>
            <el-table-column label="小计" width="120">
              <template #default="scope">
                {{ (scope.row.returnQuantity * scope.row.unitPrice).toFixed(2) }} å…ƒ
              </template>
            </el-table-column>
          </el-table>
          <div class="preview-summary">
            <span class="summary-item">
              æ€»æ•°é‡ï¼š<strong>{{ getTotalReturnQuantity() }} å¨</strong>
            </span>
            <span class="summary-item">
              æ€»é‡‘额:<strong>{{ getTotalReturnAmount() }} å…ƒ</strong>
            </span>
          </div>
        </div>
      </el-card>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button
          type="primary"
          @click="generateReturnOrder"
          :loading="generateLoading"
          :disabled="selectedOrders.length === 0"
        >
          ç”Ÿæˆé€€è´§å•
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, reactive, computed, watch } from "vue";
import { ElMessage } from "element-plus";
// Props
const props = defineProps({
  dialogGenerateVisible: {
    type: Boolean,
    default: false
  }
});
// Emits
const emit = defineEmits(['update:dialogGenerateVisible', 'success']);
// å“åº”式数据
const searchForm = reactive({
  supplierId: "",
  orderStatus: ""
});
const returnConfig = reactive({
  returnReason: "",
  operatorId: "",
  remark: ""
});
const orderList = ref([]);
const selectedOrders = ref([]);
const generateLoading = ref(false);
// ä¾›åº”商列表
const supplierList = ref([
  { value: "1", label: "供应商A" },
  { value: "2", label: "供应商B" },
  { value: "3", label: "供应商C" }
]);
// è®¢å•状态列表
const orderStatusList = ref([
  { value: "received", label: "已收货" },
  { value: "partial_received", label: "部分收货" },
  { value: "quality_issue", label: "质量问题" }
]);
// é€€è´§åŽŸå› åˆ—è¡¨
const returnReasonList = ref([
  { value: "质量不合格", label: "质量不合格" },
  { value: "交货滞后", label: "交货滞后" },
  { value: "规格不符", label: "规格不符" },
  { value: "数量不符", label: "数量不符" },
  { value: "其他原因", label: "其他原因" }
]);
// æ“ä½œå‘˜åˆ—表
const operatorList = ref([
  { value: "1", label: "陈志强" },
  { value: "2", label: "刘美玲" },
  { value: "3", label: "王建国" }
]);
// æ¨¡æ‹Ÿé‡‡è´­è®¢å•数据
const mockOrderData = [
  {
    id: "1",
    orderNo: "CG20241201001",
    supplierName: "供应商A",
    orderDate: "2024-12-01",
    coalName: "无烟煤",
    orderQuantity: 100,
    receivedQuantity: 80,
    status: "partial_received",
    unitPrice: 800
  },
  {
    id: "2",
    orderNo: "CG20241201002",
    supplierName: "供应商A",
    orderDate: "2024-12-01",
    coalName: "烟煤",
    orderQuantity: 50,
    receivedQuantity: 50,
    status: "quality_issue",
    unitPrice: 750
  },
  {
    id: "3",
    orderNo: "CG20241201003",
    supplierName: "供应商B",
    orderDate: "2024-12-01",
    coalName: "褐煤",
    orderQuantity: 80,
    receivedQuantity: 60,
    status: "partial_received",
    unitPrice: 600
  }
];
// èŽ·å–è®¢å•çŠ¶æ€ç±»åž‹
const getOrderStatusType = (status) => {
  const statusMap = {
    received: "success",
    partial_received: "warning",
    quality_issue: "danger"
  };
  return statusMap[status] || "";
};
// èŽ·å–è®¢å•çŠ¶æ€æ–‡æœ¬
const getOrderStatusText = (status) => {
  const statusMap = {
    received: "已收货",
    partial_received: "部分收货",
    quality_issue: "质量问题"
  };
  return statusMap[status] || status;
};
// ä¾›åº”商变化处理
const handleSupplierChange = () => {
  searchOrders();
};
// æŸ¥è¯¢è®¢å•
const searchOrders = () => {
  // æ¨¡æ‹ŸAPI调用
  orderList.value = mockOrderData.filter(order => {
    if (searchForm.supplierId && order.supplierName !== supplierList.value.find(s => s.value === searchForm.supplierId)?.label) {
      return false;
    }
    if (searchForm.orderStatus && order.status !== searchForm.orderStatus) {
      return false;
    }
    return true;
  });
};
// é‡ç½®æœç´¢
const resetSearch = () => {
  Object.assign(searchForm, {
    supplierId: "",
    orderStatus: ""
  });
  searchOrders();
};
// è®¢å•选择变化
const handleOrderSelectionChange = (selection) => {
  selectedOrders.value = selection;
};
// é¢„览退货商品
const previewReturnItems = computed(() => {
  return selectedOrders.value.map(order => ({
    ...order,
    returnQuantity: order.status === 'quality_issue' ? order.receivedQuantity : (order.orderQuantity - order.receivedQuantity)
  }));
});
// æ›´æ–°é€€è´§æ•°é‡
const updateReturnQuantity = (index) => {
  const item = previewReturnItems.value[index];
  if (item.returnQuantity > item.receivedQuantity) {
    item.returnQuantity = item.receivedQuantity;
    ElMessage.warning("退货数量不能超过已收货数量");
  }
};
// è®¡ç®—总退货数量
const getTotalReturnQuantity = () => {
  return previewReturnItems.value.reduce((total, item) => total + (item.returnQuantity || 0), 0);
};
// è®¡ç®—总退货金额
const getTotalReturnAmount = () => {
  return previewReturnItems.value.reduce((total, item) => {
    return total + ((item.returnQuantity || 0) * item.unitPrice);
  }, 0).toFixed(2);
};
// ç”Ÿæˆé€€è´§å•
const generateReturnOrder = async () => {
  if (!returnConfig.returnReason) {
    ElMessage.warning("请选择退货原因");
    return;
  }
  if (!returnConfig.operatorId) {
    ElMessage.warning("请选择操作员");
    return;
  }
  generateLoading.value = true;
  try {
    // æ¨¡æ‹Ÿç”Ÿæˆé€€è´§å•
    await new Promise(resolve => setTimeout(resolve, 1000));
    const returnOrder = {
      returnNo: `TH${Date.now()}`,
      supplierName: selectedOrders.value[0]?.supplierName,
      returnDate: new Date().toISOString().split('T')[0],
      operatorName: operatorList.value.find(op => op.value === returnConfig.operatorId)?.label,
      returnReason: returnConfig.returnReason,
      returnQuantity: getTotalReturnQuantity(),
      returnAmount: getTotalReturnAmount(),
      status: "draft",
      createTime: new Date().toLocaleString(),
      remark: returnConfig.remark,
      returnItems: previewReturnItems.value.map(item => ({
        coalId: item.id,
        coalName: item.coalName,
        specification: "标准规格",
        quantity: item.returnQuantity,
        unitPrice: item.unitPrice
      }))
    };
    ElMessage.success("退货单生成成功");
    emit('success', returnOrder);
    handleClose();
  } catch (error) {
    ElMessage.error("生成退货单失败");
  } finally {
    generateLoading.value = false;
  }
};
// å…³é—­å¯¹è¯æ¡†
const handleClose = () => {
  emit('update:dialogGenerateVisible', false);
  // é‡ç½®æ•°æ®
  Object.assign(searchForm, {
    supplierId: "",
    orderStatus: ""
  });
  Object.assign(returnConfig, {
    returnReason: "",
    operatorId: "",
    remark: ""
  });
  selectedOrders.value = [];
  orderList.value = mockOrderData;
};
// åˆå§‹åŒ–数据
watch(() => props.dialogGenerateVisible, (visible) => {
  if (visible) {
    orderList.value = mockOrderData;
  }
}, { immediate: true });
</script>
<style scoped>
.generate-container {
  padding: 0;
}
.select-card,
.config-card,
.preview-card {
  margin-bottom: 20px;
}
.select-card:last-child,
.config-card:last-child,
.preview-card:last-child {
  margin-bottom: 0;
}
.card-header {
  font-weight: bold;
  font-size: 16px;
}
.search-form {
  margin-bottom: 20px;
}
.config-form {
  padding: 20px 0;
}
.no-selection {
  text-align: center;
  padding: 40px 0;
}
.preview-content {
  padding: 20px 0;
}
.preview-summary {
  margin-top: 15px;
  text-align: right;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
}
.summary-item {
  margin-left: 20px;
  font-size: 14px;
}
.summary-item strong {
  color: #409eff;
  font-size: 16px;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/sales/components/PurchaseReturnDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,453 @@
<template>
  <el-dialog
    :model-value="dialogFormVisible"
    @update:model-value="$emit('update:dialogFormVisible', $event)"
    :title="title"
    width="1000px"
    :close-on-click-modal="false"
    @close="handleClose"
  >
         <el-form
       ref="formRef"
       :model="formData"
       :rules="rules"
       label-width="120px"
       class="purchase-return-form"
     >
      <el-row :gutter="20">
        <el-col :span="12">
                     <el-form-item label="供应商" prop="supplierId">
             <el-select
               v-model="formData.supplierId"
               placeholder="请选择供应商"
               style="width: 100%"
               filterable
             >
              <el-option
                :label="item.label"
                v-for="item in supplierList"
                :key="item.value"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
                     <el-form-item label="单据日期" prop="returnDate">
             <el-date-picker
               v-model="formData.returnDate"
               type="date"
               placeholder="请选择单据日期"
               format="YYYY-MM-DD"
               value-format="YYYY-MM-DD"
               style="width: 100%"
             />
           </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
                     <el-form-item label="操作员" prop="operatorId">
             <el-select
               v-model="formData.operatorId"
               placeholder="请选择操作员"
               style="width: 100%"
               filterable
             >
              <el-option
                :label="item.label"
                v-for="item in operatorList"
                :key="item.value"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
                     <el-form-item label="退货原因" prop="returnReason">
             <el-select
               v-model="formData.returnReason"
               placeholder="请选择退货原因"
               style="width: 100%"
               filterable
               allow-create
             >
              <el-option
                :label="item.label"
                v-for="item in returnReasonList"
                :key="item.value"
                :value="item.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="12">
                     <el-form-item label="退货数量" prop="returnQuantity">
             <el-input
               v-model.number="formData.returnQuantity"
               placeholder="请输入退货数量"
               style="width: 100%"
             >
              <template v-slot:suffix>
                <span>吨</span>
              </template>
            </el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
                     <el-form-item label="退货金额" prop="returnAmount">
             <el-input
               v-model.number="formData.returnAmount"
               placeholder="请输入退货金额"
               style="width: 100%"
             >
              <template v-slot:suffix>
                <span>元</span>
              </template>
            </el-input>
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item label="退货商品信息" prop="returnItems">
        <div class="return-items-container">
          <div class="return-items-header">
            <span>商品明细</span>
            <el-button type="primary" size="small" @click="addReturnItem">
              æ·»åР商品
            </el-button>
          </div>
                     <el-table :data="formData.returnItems" border style="width: 100%">
            <el-table-column label="商品名称" width="180">
              <template #default="scope">
                <el-select
                  v-model="scope.row.coalId"
                  placeholder="请选择商品"
                  style="width: 100%"
                  filterable
                >
                  <el-option
                    :label="item.label"
                    v-for="item in coalList"
                    :key="item.value"
                    :value="item.value"
                  />
                </el-select>
              </template>
            </el-table-column>
            <el-table-column label="规格型号" width="120">
              <template #default="scope">
                <el-input
                  v-model="scope.row.specification"
                  placeholder="规格型号"
                />
              </template>
            </el-table-column>
            <el-table-column label="数量" width="120">
              <template #default="scope">
                <el-input
                  v-model.number="scope.row.quantity"
                  placeholder="数量"
                  type="number"
                >
                  <template v-slot:suffix>
                    <span>吨</span>
                  </template>
                </el-input>
              </template>
            </el-table-column>
            <el-table-column label="单价" width="120">
              <template #default="scope">
                <el-input
                  v-model.number="scope.row.unitPrice"
                  placeholder="单价"
                  type="number"
                >
                  <template v-slot:suffix>
                    <span>元/吨</span>
                  </template>
                </el-input>
              </template>
            </el-table-column>
            <el-table-column label="小计" width="120">
              <template #default="scope">
                <span>{{ (scope.row.quantity * scope.row.unitPrice).toFixed(2) }} å…ƒ</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80">
              <template #default="scope">
                <el-button
                  type="danger"
                  size="small"
                  @click="removeReturnItem(scope.$index)"
                >
                  åˆ é™¤
                </el-button>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </el-form-item>
                 <el-form-item label="备注" prop="remark">
             <el-input
               v-model="formData.remark"
               type="textarea"
               :rows="3"
               placeholder="请输入备注信息"
             />
           </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="handleSubmit" :loading="submitLoading">
          æäº¤å®¡æ ¸
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
// Props
const props = defineProps({
  dialogFormVisible: {
    type: Boolean,
    default: false
  },
  form: {
    type: Object,
    default: () => ({})
  },
  title: {
    type: String,
    default: ""
  },
  isEdit: {
    type: Boolean,
    default: false
  }
});
// Emits
const emit = defineEmits(['update:dialogFormVisible', 'update:form', 'submit', 'success']);
// å“åº”式数据
const formRef = ref(null);
const submitLoading = ref(false);
// è¡¨å•数据
const formData = reactive({
  supplierId: "",
  returnDate: "",
  operatorId: "",
  returnReason: "",
  returnQuantity: "",
  returnAmount: "",
  returnItems: [],
  remark: ""
});
// åˆå§‹åŒ–表单数据
const initFormData = () => {
  Object.assign(formData, {
    supplierId: "",
    returnDate: "",
    operatorId: "",
    returnReason: "",
    returnQuantity: "",
    returnAmount: "",
    returnItems: [],
    remark: ""
  });
};
// ä¾›åº”商列表
const supplierList = ref([
  { value: "1", label: "供应商A" },
  { value: "2", label: "供应商B" },
  { value: "3", label: "供应商C" }
]);
// æ“ä½œå‘˜åˆ—表
const operatorList = ref([
  { value: "1", label: "陈志强" },
  { value: "2", label: "刘美玲" },
  { value: "3", label: "王建国" }
]);
// é€€è´§åŽŸå› åˆ—è¡¨
const returnReasonList = ref([
  { value: "质量不合格", label: "质量不合格" },
  { value: "交货滞后", label: "交货滞后" },
  { value: "规格不符", label: "规格不符" },
  { value: "数量不符", label: "数量不符" },
  { value: "其他原因", label: "其他原因" }
]);
// å•†å“åˆ—表
const coalList = ref([
  { value: "1", label: "无烟煤" },
  { value: "2", label: "烟煤" },
  { value: "3", label: "褐煤" },
  { value: "4", label: "焦煤" }
]);
// è¡¨å•验证规则
const rules = {
  supplierId: [
    { required: true, message: "请选择供应商", trigger: "change" }
  ],
  returnDate: [
    { required: true, message: "请选择单据日期", trigger: "change" }
  ],
  operatorId: [
    { required: true, message: "请选择操作员", trigger: "change" }
  ],
  returnReason: [
    { required: true, message: "请选择退货原因", trigger: "change" }
  ],
  returnQuantity: [
    { required: true, message: "请输入退货数量", trigger: "blur" },
    { type: "number", min: 0, message: "数量必须大于0", trigger: "blur" }
  ],
  returnItems: [
    {
      type: "array",
      required: true,
      message: "请至少添加一个退货商品",
      trigger: "change",
      validator: (rule, value, callback) => {
        if (!value || value.length === 0) {
          callback(new Error("请至少添加一个退货商品"));
        } else {
          callback();
        }
      }
    }
  ]
};
// ç›‘听表单数据变化
watch(() => props.form, (newVal) => {
  Object.assign(formData, newVal);
  if (!formData.returnItems || formData.returnItems.length === 0) {
    formData.returnItems = [];
  }
}, { deep: true, immediate: true });
// æ·»åŠ é€€è´§å•†å“
const addReturnItem = () => {
  formData.returnItems.push({
    coalId: "",
    specification: "",
    quantity: 0,
    unitPrice: 0
  });
};
// åˆ é™¤é€€è´§å•†å“
const removeReturnItem = (index) => {
  formData.returnItems.splice(index, 1);
};
// å…³é—­å¯¹è¯æ¡†
const handleClose = () => {
  emit('update:dialogFormVisible', false);
  formRef.value?.resetFields();
};
// æäº¤è¡¨å•
const handleSubmit = async () => {
  if (!formRef.value) return;
  try {
    await formRef.value.validate();
    // éªŒè¯é€€è´§å•†å“ä¿¡æ¯
    if (formData.returnItems.length === 0) {
      ElMessage.warning("请至少添加一个退货商品");
      return;
    }
    for (let item of formData.returnItems) {
      if (!item.coalId) {
        ElMessage.warning("请选择商品");
        return;
      }
      if (!item.quantity || item.quantity <= 0) {
        ElMessage.warning("请输入有效的商品数量");
        return;
      }
    }
    submitLoading.value = true;
    // æ¨¡æ‹Ÿæäº¤
    setTimeout(() => {
      submitLoading.value = false;
      ElMessage.success("提交成功");
      emit('submit', { ...formData });
      handleClose();
    }, 1000);
  } catch (error) {
    console.error('表单验证失败:', error);
  }
};
// è®¡ç®—总数量
const calculateTotalQuantity = () => {
  return formData.returnItems.reduce((total, item) => total + (item.quantity || 0), 0);
};
// è®¡ç®—总金额
const calculateTotalAmount = () => {
  return formData.returnItems.reduce((total, item) => {
    return total + ((item.quantity || 0) * (item.unitPrice || 0));
  }, 0);
};
// ç›‘听退货商品变化,自动计算总数量和总金额
watch(() => formData.returnItems, () => {
  formData.returnQuantity = calculateTotalQuantity();
  formData.returnAmount = calculateTotalAmount();
}, { deep: true });
</script>
<style scoped>
.purchase-return-form {
  padding: 20px 0;
}
.return-items-container {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  padding: 15px;
}
.return-items-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  font-weight: bold;
}
.dialog-footer {
  text-align: right;
}
.el-table {
  margin-top: 10px;
}
</style>
src/views/sales/components/PurchaseReturnViewDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,324 @@
<template>
  <el-dialog
    :model-value="dialogViewVisible"
    @update:model-value="$emit('update:dialogViewVisible', $event)"
    :title="title"
    width="900px"
    :close-on-click-modal="false"
  >
    <div class="view-container">
      <!-- åŸºæœ¬ä¿¡æ¯ -->
      <el-card class="info-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>基本信息</span>
          </div>
        </template>
        <el-descriptions :column="2" border>
          <el-descriptions-item label="退货单号">
            {{ form.returnNo || '-' }}
          </el-descriptions-item>
          <el-descriptions-item label="供应商">
            {{ form.supplierName || '-' }}
          </el-descriptions-item>
          <el-descriptions-item label="单据日期">
            {{ form.returnDate || '-' }}
          </el-descriptions-item>
          <el-descriptions-item label="操作员">
            {{ form.operatorName || '-' }}
          </el-descriptions-item>
          <el-descriptions-item label="退货原因">
            {{ form.returnReason || '-' }}
          </el-descriptions-item>
          <el-descriptions-item label="状态">
            <el-tag :type="getStatusType(form.status)">
              {{ getStatusText(form.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="退货数量">
            {{ form.returnQuantity || 0 }} å¨
          </el-descriptions-item>
          <el-descriptions-item label="退货金额">
            {{ form.returnAmount || 0 }} å…ƒ
          </el-descriptions-item>
          <el-descriptions-item label="创建时间" :span="2">
            {{ form.createTime || '-' }}
          </el-descriptions-item>
          <el-descriptions-item label="备注" :span="2">
            {{ form.remark || '-' }}
          </el-descriptions-item>
        </el-descriptions>
      </el-card>
      <!-- é€€è´§å•†å“æ˜Žç»† -->
      <el-card class="info-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>退货商品明细</span>
          </div>
        </template>
        <el-table :data="form.returnItems || []" border style="width: 100%">
          <el-table-column label="序号" type="index" width="60" />
          <el-table-column label="商品名称" prop="coalName" width="150" />
          <el-table-column label="规格型号" prop="specification" width="150" />
          <el-table-column label="数量" prop="quantity" width="100">
            <template #default="scope">
              {{ scope.row.quantity || 0 }} å¨
            </template>
          </el-table-column>
          <el-table-column label="单价" prop="unitPrice" width="120">
            <template #default="scope">
              {{ scope.row.unitPrice || 0 }} å…ƒ/吨
            </template>
          </el-table-column>
          <el-table-column label="小计" width="120">
            <template #default="scope">
              {{ ((scope.row.quantity || 0) * (scope.row.unitPrice || 0)).toFixed(2) }} å…ƒ
            </template>
          </el-table-column>
        </el-table>
        <div class="table-summary">
          <span class="summary-item">
            æ€»æ•°é‡ï¼š<strong>{{ getTotalQuantity() }} å¨</strong>
          </span>
          <span class="summary-item">
            æ€»é‡‘额:<strong>{{ getTotalAmount() }} å…ƒ</strong>
          </span>
        </div>
      </el-card>
      <!-- å®¡æ‰¹æµç¨‹ -->
      <el-card class="info-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>审批流程</span>
          </div>
        </template>
        <el-timeline>
          <el-timeline-item
            v-for="(activity, index) in approvalFlow"
            :key="index"
            :timestamp="activity.timestamp"
            :type="activity.type"
          >
            <h4>{{ activity.title }}</h4>
            <p>{{ activity.content }}</p>
            <p v-if="activity.operator">操作人:{{ activity.operator }}</p>
          </el-timeline-item>
        </el-timeline>
      </el-card>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">关闭</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { computed } from "vue";
// Props
const props = defineProps({
  dialogViewVisible: {
    type: Boolean,
    default: false
  },
  form: {
    type: Object,
    default: () => ({})
  },
  title: {
    type: String,
    default: "退货单详情"
  }
});
// Emits
const emit = defineEmits(['update:dialogViewVisible']);
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    draft: "",
    pending: "warning",
    approved: "success",
    rejected: "danger",
    completed: "info"
  };
  return statusMap[status] || "";
};
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    draft: "草稿",
    pending: "待审核",
    approved: "已审核",
    rejected: "已拒绝",
    completed: "已完成"
  };
  return statusMap[status] || status;
};
// è®¡ç®—总数量
const getTotalQuantity = () => {
  if (!props.form.returnItems || props.form.returnItems.length === 0) {
    return 0;
  }
  return props.form.returnItems.reduce((total, item) => total + (item.quantity || 0), 0);
};
// è®¡ç®—总金额
const getTotalAmount = () => {
  if (!props.form.returnItems || props.form.returnItems.length === 0) {
    return 0;
  }
  return props.form.returnItems.reduce((total, item) => {
    return total + ((item.quantity || 0) * (item.unitPrice || 0));
  }, 0).toFixed(2);
};
// å®¡æ‰¹æµç¨‹æ•°æ®
const approvalFlow = computed(() => {
  const flow = [];
  // åˆ›å»º
  flow.push({
    title: "创建退货单",
    content: "退货单已创建,等待提交审核",
    timestamp: props.form.createTime || new Date().toLocaleString(),
    operator: props.form.operatorName || "系统",
    type: "primary"
  });
  // æ ¹æ®çŠ¶æ€æ·»åŠ å®¡æ‰¹æµç¨‹
  if (props.form.status === "pending") {
    flow.push({
      title: "提交审核",
      content: "退货单已提交,等待审核",
      timestamp: new Date().toLocaleString(),
      operator: props.form.operatorName || "系统",
      type: "warning"
    });
  } else if (props.form.status === "approved") {
    flow.push({
      title: "提交审核",
      content: "退货单已提交,等待审核",
      timestamp: new Date().toLocaleString(),
      operator: props.form.operatorName || "系统",
      type: "warning"
    });
    flow.push({
      title: "审核通过",
      content: "退货单审核通过",
      timestamp: new Date().toLocaleString(),
      operator: "审核员",
      type: "success"
    });
  } else if (props.form.status === "rejected") {
    flow.push({
      title: "提交审核",
      content: "退货单已提交,等待审核",
      timestamp: new Date().toLocaleString(),
      operator: props.form.operatorName || "系统",
      type: "warning"
    });
    flow.push({
      title: "审核拒绝",
      content: "退货单审核被拒绝",
      timestamp: new Date().toLocaleString(),
      operator: "审核员",
      type: "danger"
    });
  } else if (props.form.status === "completed") {
    flow.push({
      title: "提交审核",
      content: "退货单已提交,等待审核",
      timestamp: new Date().toLocaleString(),
      operator: props.form.operatorName || "系统",
      type: "warning"
    });
    flow.push({
      title: "审核通过",
      content: "退货单审核通过",
      timestamp: new Date().toLocaleString(),
      operator: "审核员",
      type: "success"
    });
    flow.push({
      title: "退货完成",
      content: "退货流程已完成",
      timestamp: new Date().toLocaleString(),
      operator: "系统",
      type: "info"
    });
  }
  return flow;
});
// å…³é—­å¯¹è¯æ¡†
const handleClose = () => {
  emit('update:dialogViewVisible', false);
};
</script>
<style scoped>
.view-container {
  padding: 0;
}
.info-card {
  margin-bottom: 20px;
}
.info-card:last-child {
  margin-bottom: 0;
}
.card-header {
  font-weight: bold;
  font-size: 16px;
}
.table-summary {
  margin-top: 15px;
  text-align: right;
  padding: 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
}
.summary-item {
  margin-left: 20px;
  font-size: 14px;
}
.summary-item strong {
  color: #409eff;
  font-size: 16px;
}
.dialog-footer {
  text-align: right;
}
.el-timeline {
  padding: 20px;
}
.el-timeline-item h4 {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #303133;
}
.el-timeline-item p {
  margin: 4px 0;
  font-size: 12px;
  color: #606266;
}
</style>
src/views/sales/purchaseReturn.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,462 @@
<template>
  <div class="app-container">
    <el-form :inline="true" :model="queryParams" class="search-form">
      <el-form-item label="退货单号">
        <el-input
          v-model="queryParams.returnNo"
          placeholder="请输入退货单号"
          clearable
          :style="{ width: '200px' }"
        />
      </el-form-item>
      <el-form-item label="供应商">
        <el-select
          v-model="queryParams.supplierId"
          placeholder="请选择供应商"
          clearable
          :style="{ width: '200px' }"
        >
          <el-option
            :label="item.label"
            v-for="item in supplierList"
            :key="item.value"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="状态">
        <el-select
          v-model="queryParams.status"
          placeholder="请选择状态"
          clearable
          :style="{ width: '150px' }"
        >
          <el-option
            :label="item.label"
            v-for="item in statusList"
            :key="item.value"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="单据日期">
        <el-date-picker
          v-model="queryParams.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          format="YYYY-MM-DD"
          value-format="YYYY-MM-DD"
          :style="{ width: '240px' }"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <!-- æ“ä½œæŒ‰é’®åŒº -->
      <el-row :gutter="24" class="table-toolbar" justify="space-between">
        <el-button type="primary" :icon="Plus" @click="handleAdd">
          æ–°å¢žé€€è´§å•
        </el-button>
        <el-button type="success" :icon="Refresh" @click="handleGenerateReturn">
          ä¸€é”®ç”Ÿæˆé€€è´§å•
        </el-button>
        <el-button type="danger" :icon="Delete" @click="handleBatchDelete" :disabled="selectedIds.length === 0">
          æ‰¹é‡åˆ é™¤
        </el-button>
      </el-row>
      <!-- è¡¨æ ¼ç»„ä»¶ -->
      <el-table
        v-loading="loading"
        :data="tableData"
        @selection-change="handleSelectionChange"
        border
        style="width: 100%"
      >
        <el-table-column type="selection" width="55" />
        <el-table-column label="退货单号" prop="returnNo" width="180" />
        <el-table-column label="供应商" prop="supplierName" width="200" />
        <el-table-column label="单据日期" prop="returnDate" width="120" />
        <el-table-column label="操作员" prop="operatorName" width="120" />
        <el-table-column label="退货原因" prop="returnReason" width="200" show-overflow-tooltip />
        <el-table-column label="退货数量" prop="returnQuantity" width="120">
          <template #default="scope">
            {{ scope.row.returnQuantity }} å¨
          </template>
        </el-table-column>
        <el-table-column label="状态" prop="status" width="100">
          <template #default="scope">
            <el-tag :type="getStatusType(scope.row.status)">
              {{ getStatusText(scope.row.status) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="创建时间" prop="createTime" width="160" />
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="scope">
            <el-button
              size="small"
              type="primary"
              @click="handleView(scope.row)"
            >
              æŸ¥çœ‹
            </el-button>
            <el-button
              size="small"
              type="warning"
              @click="handleEdit(scope.row)"
              v-if="scope.row.status === 'draft'"
            >
              ç¼–辑
            </el-button>
            <el-button
              size="small"
              type="danger"
              @click="handleDelete(scope.row)"
              v-if="scope.row.status === 'draft'"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µç»„ä»¶ -->
      <pagination
        v-if="total > 0"
        :page="current"
        :limit="pageSize"
        :total="total"
        @pagination="handlePagination"
        :layout="'total, prev, pager, next, jumper'"
      />
    </el-card>
    <!-- æ–°å¢ž/编辑对话框 -->
    <PurchaseReturnDialog
      v-model:dialogFormVisible="dialogFormVisible"
      v-model:form="form"
      :title="title"
      :is-edit="isEdit"
      @submit="handleSubmit"
      @success="handleSuccess"
      ref="purchaseReturnDialog"
    />
    <!-- æŸ¥çœ‹è¯¦æƒ…对话框 -->
    <PurchaseReturnViewDialog
      v-model:dialogViewVisible="dialogViewVisible"
      :form="viewForm"
      title="退货单详情"
    />
    <!-- ä¸€é”®ç”Ÿæˆé€€è´§å•对话框 -->
    <GenerateReturnDialog
      v-model:dialogGenerateVisible="dialogGenerateVisible"
      @success="handleGenerateSuccess"
    />
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Edit, Delete, Refresh, View } from "@element-plus/icons-vue";
import Pagination from "@/components/Pagination";
import PurchaseReturnDialog from "./components/PurchaseReturnDialog.vue";
import PurchaseReturnViewDialog from "./components/PurchaseReturnViewDialog.vue";
import GenerateReturnDialog from "./components/GenerateReturnDialog.vue";
// å“åº”式数据
const loading = ref(false);
const tableData = ref([]);
const selectedIds = ref([]);
const current = ref(1);
const pageSize = ref(10);
const total = ref(0);
const dialogFormVisible = ref(false);
const dialogViewVisible = ref(false);
const dialogGenerateVisible = ref(false);
const isEdit = ref(false);
const title = ref("");
const form = ref({});
const viewForm = ref({});
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  returnNo: "",
  supplierId: "",
  status: "",
  dateRange: []
});
// ä¾›åº”商列表
const supplierList = ref([
  { value: "1", label: "供应商A" },
  { value: "2", label: "供应商B" },
  { value: "3", label: "供应商C" }
]);
// çŠ¶æ€åˆ—è¡¨
const statusList = ref([
  { value: "draft", label: "草稿" },
  { value: "pending", label: "待审核" },
  { value: "approved", label: "已审核" },
  { value: "rejected", label: "已拒绝" },
  { value: "completed", label: "已完成" }
]);
// æ¨¡æ‹Ÿæ•°æ®
const mockData = [
  {
    id: "1",
    returnNo: "TH20241201001",
    supplierName: "供应商A",
    returnDate: "2024-12-01",
    operatorName: "陈志强",
    returnReason: "质量不合格,煤质不符合要求",
    returnQuantity: 50,
    status: "pending",
    createTime: "2024-12-01 10:00:00"
  },
  {
    id: "2",
    returnNo: "TH20241201002",
    supplierName: "供应商B",
    returnDate: "2024-12-01",
    operatorName: "刘美玲",
    returnReason: "交货滞后,影响生产计划",
    returnQuantity: 30,
    status: "approved",
    createTime: "2024-12-01 14:30:00"
  }
];
// èŽ·å–çŠ¶æ€ç±»åž‹
const getStatusType = (status) => {
  const statusMap = {
    draft: "",
    pending: "warning",
    approved: "success",
    rejected: "danger",
    completed: "info"
  };
  return statusMap[status] || "";
};
// èŽ·å–çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    draft: "草稿",
    pending: "待审核",
    approved: "已审核",
    rejected: "已拒绝",
    completed: "已完成"
  };
  return statusMap[status] || status;
};
// æŸ¥è¯¢
const handleQuery = () => {
  current.value = 1;
  loadData();
};
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  Object.assign(queryParams, {
    returnNo: "",
    supplierId: "",
    status: "",
    dateRange: []
  });
  handleQuery();
};
// åŠ è½½æ•°æ®
const loadData = () => {
  loading.value = true;
  // æ¨¡æ‹ŸAPI调用
  setTimeout(() => {
    tableData.value = mockData;
    total.value = mockData.length;
    loading.value = false;
  }, 500);
};
// åˆ†é¡µå¤„理
const handlePagination = (pagination) => {
  current.value = pagination.page;
  pageSize.value = pagination.limit;
  loadData();
};
// é€‰æ‹©å˜åŒ–
const handleSelectionChange = (selection) => {
  selectedIds.value = selection.map(item => item.id);
};
// æ–°å¢ž
const handleAdd = () => {
  isEdit.value = false;
  title.value = "新增退货单";
  form.value = {
    supplierId: "",
    returnDate: "",
    operatorId: "",
    returnReason: "",
    returnQuantity: "",
    returnAmount: "",
    returnItems: [],
    remark: ""
  };
  dialogFormVisible.value = true;
};
// ç¼–辑
const handleEdit = (row) => {
  isEdit.value = true;
  title.value = "编辑退货单";
  form.value = { ...row };
  dialogFormVisible.value = true;
};
// æŸ¥çœ‹
const handleView = (row) => {
  viewForm.value = { ...row };
  dialogViewVisible.value = true;
};
// åˆ é™¤
const handleDelete = (row) => {
  ElMessageBox.confirm(
    `确定要删除退货单 ${row.returnNo} å—?`,
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    // æ¨¡æ‹Ÿåˆ é™¤
    const index = tableData.value.findIndex(item => item.id === row.id);
    if (index > -1) {
      tableData.value.splice(index, 1);
      total.value--;
      ElMessage.success("删除成功");
    }
  });
};
// æ‰¹é‡åˆ é™¤
const handleBatchDelete = () => {
  if (selectedIds.value.length === 0) {
    ElMessage.warning("请选择要删除的记录");
    return;
  }
  ElMessageBox.confirm(
    `确定要删除选中的 ${selectedIds.value.length} æ¡è®°å½•吗?`,
    "提示",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning"
    }
  ).then(() => {
    // æ¨¡æ‹Ÿæ‰¹é‡åˆ é™¤
    tableData.value = tableData.value.filter(item => !selectedIds.value.includes(item.id));
    total.value = tableData.value.length;
    selectedIds.value = [];
    ElMessage.success("批量删除成功");
  });
};
// ä¸€é”®ç”Ÿæˆé€€è´§å•
const handleGenerateReturn = () => {
  dialogGenerateVisible.value = true;
};
// æäº¤è¡¨å•
const handleSubmit = (formData) => {
  if (isEdit.value) {
    // ç¼–辑
    const index = tableData.value.findIndex(item => item.id === formData.id);
    if (index > -1) {
      tableData.value[index] = { ...formData };
      ElMessage.success("编辑成功");
    }
  } else {
    // æ–°å¢ž
    const newItem = {
      id: Date.now().toString(),
      returnNo: `TH${Date.now()}`,
      supplierName: supplierList.value.find(item => item.value === formData.supplierId)?.label || "",
      returnDate: formData.returnDate,
      operatorName: "当前用户",
      returnReason: formData.returnReason,
      returnQuantity: formData.returnQuantity,
      status: "draft",
      createTime: new Date().toLocaleString()
    };
    tableData.value.unshift(newItem);
    total.value++;
    ElMessage.success("新增成功");
  }
  dialogFormVisible.value = false;
};
// è¡¨å•成功回调
const handleSuccess = () => {
  loadData();
};
// ç”Ÿæˆé€€è´§å•成功回调
const handleGenerateSuccess = (returnOrder) => {
  dialogGenerateVisible.value = false;
  // å°†ç”Ÿæˆçš„退货单添加到列表中
  if (returnOrder) {
    const newItem = {
      id: Date.now().toString(),
      returnNo: returnOrder.returnNo,
      supplierName: returnOrder.supplierName,
      returnDate: returnOrder.returnDate,
      operatorName: returnOrder.operatorName,
      returnReason: returnOrder.returnReason,
      returnQuantity: returnOrder.returnQuantity,
      status: returnOrder.status,
      createTime: returnOrder.createTime,
      returnItems: returnOrder.returnItems
    };
    tableData.value.unshift(newItem);
    total.value++;
  }
  loadData();
  ElMessage.success("退货单生成成功");
};
// é¡µé¢åŠ è½½
onMounted(() => {
  loadData();
});
</script>
<style scoped>
.search-form {
  margin-bottom: 20px;
}
.table-toolbar {
  margin-bottom: 20px;
}
.el-card {
  margin-bottom: 20px;
}
</style>