From d39019790b7e15ab515ace9937c5e5d19eb31837 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 13 三月 2026 14:22:04 +0800
Subject: [PATCH] 军泰伟业 1.销售退货逻辑完善与联调

---
 src/views/salesManagement/returnOrder/components/formDia.vue   |  552 ++++++++++++++++++++++++++++++
 src/views/salesManagement/returnOrder/components/detailDia.vue |  146 ++++++++
 src/api/salesManagement/returnOrder.js                         |   82 ++++
 src/api/customerService/index.js                               |   57 ++-
 src/views/salesManagement/returnOrder/index.vue                |  234 +++++++++++++
 5 files changed, 1,052 insertions(+), 19 deletions(-)

diff --git a/src/api/customerService/index.js b/src/api/customerService/index.js
index 9f458b6..571601a 100644
--- a/src/api/customerService/index.js
+++ b/src/api/customerService/index.js
@@ -59,27 +59,17 @@
   })
 }
 // 鍞悗澶勭悊-闄勪欢鍒犻櫎
-export function afterSalesServiceFileDel(ids) {
+export function afterSalesServiceFileDel(id) {
   return request({
-    url: '/afterSalesService/file/del',
+    url: `/afterSalesService/file/del/${id}`,
     method: 'delete',
-    data: ids,
-  })
-}
-
-// 鍞悗澶勭悊-缁翠慨璁板綍鍒楄〃
-export function afterSalesServiceRepairListPage(query) {
-  return request({
-    url: '/afterSalesService/repair/listPage',
-    method: 'get',
-    params: query,
   })
 }
 
 // 涓存湡鍞悗绠$悊-鍒嗛〉鏌ヨ
 export function expiryAfterSalesListPage(query) {
   return request({
-    url: '/expiryAfterSales/listPage',
+    url: '/afterSalesNearExpiryService/listPage',
     method: 'get',
     params: query,
   })
@@ -88,7 +78,7 @@
 // 涓存湡鍞悗绠$悊-鏂板
 export function expiryAfterSalesAdd(query) {
   return request({
-    url: '/expiryAfterSales/add',
+    url: '/afterSalesNearExpiryService/add',
     method: 'post',
     data: query,
   })
@@ -97,17 +87,46 @@
 // 涓存湡鍞悗绠$悊-鏇存柊
 export function expiryAfterSalesUpdate(query) {
   return request({
-    url: '/expiryAfterSales/update',
+    url: '/afterSalesNearExpiryService/update',
     method: 'post',
     data: query,
   })
 }
 
 // 涓存湡鍞悗绠$悊-鍒犻櫎
-export function expiryAfterSalesDelete(query) {
+export function expiryAfterSalesDelete(ids) {
   return request({
-    url: '/expiryAfterSales/delete',
+    url: '/afterSalesNearExpiryService/delete?ids=' + ids,
     method: 'delete',
-    data: query,
   })
-}
\ No newline at end of file
+}
+
+// 鏌ヨ鎵�鏈夊鎴蜂俊鎭�
+// /basic/customer/list
+export function getAllCustomerList(query) {
+    return request({
+        url: '/basic/customer/list',
+        method: 'get',
+        params: query,
+    })
+}
+
+// 鏍规嵁瀹㈡埛鏌ヨ閿�鍞鍗曞彿
+// afterSalesService/listSalesLedger
+export function getSalesLedger(query) {
+    return request({
+        url: '/afterSalesService/listSalesLedger',
+        method: 'get',
+        params: query,
+    })
+}
+
+// 鏍规嵁閿�鍞鍗曞彿鏌ヨ閿�鍞鍗曡鎯�
+// afterSalesService/count
+export function getSalesLedgerDetail(query) {
+    return request({
+        url: '/afterSalesService/count',
+        method: 'get',
+        params: query,
+    })
+}
diff --git a/src/api/salesManagement/returnOrder.js b/src/api/salesManagement/returnOrder.js
new file mode 100644
index 0000000..f945fc9
--- /dev/null
+++ b/src/api/salesManagement/returnOrder.js
@@ -0,0 +1,82 @@
+import request from "@/utils/request";
+
+
+// 閿�鍞��璐�-鏌ヨ
+// /returnManagement/listPage
+export function returnManagementList(query) {
+  return request({
+    url: "/returnManagement/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 閿�鍞��璐�-娣诲姞
+// /returnManagement/add
+export function returnManagementAdd(data) {
+  return request({
+    url: "/returnManagement/add",
+    method: "post",
+    data: data,
+  });
+}
+
+// 閿�鍞��璐�-淇敼
+// /returnManagement/update
+export function returnManagementUpdate(data) {
+  return request({
+    url: "/returnManagement/update",
+    method: "post",
+    data: data,
+  });
+}
+
+// 閿�鍞��璐�-鍒犻櫎
+// /returnManagement/del
+export function returnManagementDel(data) {
+  return request({
+    url: "/returnManagement/del",
+    method: "delete",
+    data,
+  });
+}
+
+// 閿�鍞��璐�-鏌ヨ
+// /returnManagement/getById
+export function returnManagementGetById(query) {
+  return request({
+    url: "/returnManagement/getById",
+    method: "get",
+    params: query,
+  });
+}
+
+// 閿�鍞��璐�-鏍规嵁鍑哄簱鍗曟煡璇㈤攢鍞鍗曚互鍙婁骇鍝佷俊鎭�
+// /returnManagement/getByShippingId
+export function returnManagementGetByShippingId(query) {
+  return request({
+    url: "/returnManagement/getByShippingId",
+    method: "get",
+    params: query,
+  });
+}
+
+// 閫氳繃瀹㈡埛鍚嶇О鏌ヨ
+// /shippingInfo/getByCustomerName
+export function getSalesLedger(query) {
+    return request({
+        url: '/shippingInfo/getByCustomerName',
+        method: 'get',
+        params: query,
+    })
+}
+
+// 澶勭悊
+// /returnManagement/handle
+export function returnManagementHandle(data) {
+  return request({
+    url: "/returnManagement/handle",
+    method: "get",
+    params: data,
+  });
+}
diff --git a/src/views/salesManagement/returnOrder/components/detailDia.vue b/src/views/salesManagement/returnOrder/components/detailDia.vue
new file mode 100644
index 0000000..9ff4e89
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/components/detailDia.vue
@@ -0,0 +1,146 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="閫�璐у崟璇︽儏" width="90%" @close="closeDia">
+    <div v-loading="loading">
+      <span class="descriptions">鍩烘湰淇℃伅</span>
+      <el-descriptions :column="4" border>
+        <el-descriptions-item label="閫�璐у崟鍙�">{{ detail.returnNo }}</el-descriptions-item>
+        <el-descriptions-item label="鍗曟嵁鐘舵��">
+          <el-tag :type="getStatusType(detail.status)">{{ getStatusText(detail.status) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ detail.customerName }}</el-descriptions-item>
+        <el-descriptions-item label="閿�鍞崟鍙�">{{ detail.salesContractNo }}</el-descriptions-item>
+        <el-descriptions-item label="涓氬姟鍛�">{{ detail.salesman }}</el-descriptions-item>
+        <el-descriptions-item label="鍏宠仈鍑哄簱鍗曞彿">{{ detail.shippingNo }}</el-descriptions-item>
+        <el-descriptions-item label="椤圭洰鍚嶇О">{{ detail.projectName }}</el-descriptions-item>
+        <el-descriptions-item label="鍒跺崟浜�">{{ detail.maker }}</el-descriptions-item>
+        <el-descriptions-item label="鍒跺崟鏃堕棿">{{ detail.makeTime }}</el-descriptions-item>
+        <el-descriptions-item label="閫�璐у師鍥�">{{ detail.returnReason }}</el-descriptions-item>
+        <el-descriptions-item label="閫�娆炬�婚">{{ detail.refundAmount }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div style="padding-top: 20px">
+        <span class="descriptions">浜у搧鍒楄〃</span>
+        <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData" />
+      </div>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="closeDia">鍏抽棴</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { returnManagementGetById, returnManagementGetByShippingId } from "@/api/salesManagement/returnOrder.js";
+
+const dialogVisible = ref(false);
+const loading = ref(false);
+const detail = ref({});
+const tableData = ref([]);
+const availableProducts = ref([]);
+
+const tableColumn = [
+  {align: "center", label: "浜у搧澶х被", prop: "productCategory"},
+  {align: "center", label: "瑙勬牸鍨嬪彿", prop: "specificationModel"},
+  {align: "center", label: "鍗曚綅", prop: "unit", width: 80},
+  {align: "center", label: "鎬绘暟閲�", prop: "quantity", width: 120},
+  {align: "center", label: "宸查��璐ф暟閲�", prop: "totalReturnNum", width: 120},
+  {align: "center", label: "鏈��璐ф暟閲�", prop: "unQuantity", width: 120},
+  {align: "center", label: "閫�璐ф暟閲�", prop: "returnQuantity", width: 120},
+  {align: "center", label: "閫�璐т骇鍝佸崟浠�", prop: "price", width: 120},
+  {align: "center", label: "閫�璐т骇鍝侀噾棰�", prop: "amount", width: 120},
+  {align: "center", label: "鏄惁鏈夎川閲忛棶棰�", prop: "isQuality", width: 140, formatData: (v) => ({ "1": "鏄�", "2": "鍚�" }[String(v)] ?? v)},
+  {align: "center", label: "澶囨敞", prop: "remark", width: 150},
+];
+
+const getStatusType = (status) => {
+  const statusMap = {
+    0: "warning",
+    1: "success"
+  };
+  return statusMap[status] || "info";
+};
+
+const getStatusText = (status) => {
+  const statusMap = {
+    0: "寰呭鐞�",
+    1: "宸插鐞�"
+  };
+  return statusMap[status] || "鏈煡";
+};
+
+const openDialog = async (row) => {
+  if (!row?.id) return;
+  dialogVisible.value = true;
+  loading.value = true;
+  try {
+    const res = await returnManagementGetById({ returnManagementId: row.id });
+    detail.value = res?.data ?? res ?? {};
+    
+    if (detail.value.shippingId) {
+      const productRes = await returnManagementGetByShippingId({ shippingId: detail.value.shippingId });
+      if (productRes.code === 200) {
+        availableProducts.value = productRes.data.productDtoData || [];
+      }
+    }
+    
+    const list =
+      detail.value?.returnSaleProducts ||
+        detail.value?.returnSaleProductList ||
+        detail.value?.returnSaleProductDtoData ||
+        [];
+    
+    tableData.value = Array.isArray(list) ? list.map(raw => {
+      const productId = raw?.returnSaleLedgerProductId ?? raw?.saleLedgerProductId ?? raw?.id;
+      const product = availableProducts.value.find((p) => p.id === productId);
+      const normalized = {
+        ...raw,
+        id: productId,
+        returnQuantity: Number(raw?.num ?? raw?.returnQuantity ?? 0),
+        price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
+        amount: Number(raw?.amount ?? 0).toFixed(2),
+        isQuality: raw?.isQuality ?? 2,
+        remark: raw?.remark ?? "",
+      };
+      return product ? { ...product, ...normalized } : normalized;
+    }) : [];
+  } catch (e) {
+    console.error("Failed to load detail", e);
+  } finally {
+    loading.value = false;
+  }
+};
+
+const closeDia = () => {
+  dialogVisible.value = false;
+  detail.value = {};
+  tableData.value = [];
+  availableProducts.value = [];
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.descriptions {
+  margin-bottom: 20px;
+  display: inline-block;
+  font-size: 1rem;
+  font-weight: 600;
+  padding-left: 12px;
+  position: relative;
+}
+.descriptions::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 4px;
+  height: 1rem;
+  background-color: #002FA7;
+  border-radius: 2px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/components/formDia.vue b/src/views/salesManagement/returnOrder/components/formDia.vue
new file mode 100644
index 0000000..9f1bb9a
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/components/formDia.vue
@@ -0,0 +1,552 @@
+<template>
+  <div>
+    <el-dialog v-model="dialogFormVisible" :title="operationType === 'edit' ? '缂栬緫閫�璐у崟' : '鏂板閫�璐у崟'" width="90%" @close="closeDia">
+      <div>
+        <span class="descriptions">鍩烘湰淇℃伅</span>
+        <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+          <el-row :gutter="30">
+            <el-col :span="4">
+              <el-form-item label="閫�璐у崟鍙凤細" prop="returnNo">
+                <el-input
+                  :disabled="operationType === 'edit' || form.returnNoCheckbox"
+                  v-model="form.returnNo"
+                  placeholder="浣跨敤绯荤粺缂栧彿"
+                  class="input-with-select"
+                >
+                  <template v-if="operationType !== 'edit'" #append>
+                    <el-checkbox v-model="form.returnNoCheckbox" @change="handleReturnNoCheckboxChange"></el-checkbox>
+                  </template>
+                </el-input>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerId">
+                <el-select v-model="form.customerId" filterable placeholder="璇烽�夋嫨瀹㈡埛" @change="customerNameChange">
+                  <el-option
+                    v-for="item in customerNameOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="鍏宠仈鍑哄簱鍗曞彿锛�" prop="shippingId">
+                <el-select v-model="form.shippingId" filterable placeholder="璇烽�夋嫨鍑哄簱鍗曞彿" @change="outboundNoChange">
+                  <el-option
+                    v-for="item in outboundOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="鍒跺崟浜猴細" prop="maker">
+                <el-select v-model="form.maker" filterable placeholder="璇烽�夋嫨鍒跺崟浜�">
+                  <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="鍒跺崟鏃堕棿锛�" prop="makeTime">
+                <el-date-picker v-model="form.makeTime" type="datetime" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="鐘舵�侊細" prop="status">
+                <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+                  <el-option label="寰呭鐞�" :value="0" />
+                  <el-option label="宸插鐞�" :value="1" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="閫�璐у師鍥狅細" prop="returnReason">
+                <el-input v-model="form.returnReason" placeholder="璇疯緭鍏ラ��璐у師鍥�" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="閫�娆炬�婚锛�" prop="refundAmount">
+                <el-input v-model="form.refundAmount" disabled placeholder="鑷姩璁$畻" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+        <hr>
+        <div style="padding-top: 20px">
+          <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
+            <span class="descriptions" style="margin-bottom:0">浜у搧鍒楄〃</span>
+            <el-button type="primary" @click="openProductSelection" :disabled="!form.shippingId">娣诲姞浜у搧</el-button>
+          </div>
+          <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
+            <template #returnQuantity="{ row }">
+              <el-input 
+                v-model="row.returnQuantity" 
+                style="width:100px" 
+                placeholder="璇疯緭鍏�" 
+                type="number"
+                @input="(val) => handleReturnQuantityChange(val, row)"
+              />
+            </template>
+            <template #price="{ row }">
+              <el-input 
+                v-model="row.price" 
+                style="width:100px" 
+                placeholder="璇疯緭鍏�" 
+                type="number"
+                @input="(val) => handlePriceChange(val, row)"
+              />
+            </template>
+            <template #amount="{ row }">
+              <el-input 
+                v-model="row.amount" 
+                style="width:100px" 
+                placeholder="鑷姩璁$畻" 
+                type="number"
+                disabled
+              />
+            </template>
+            <template #isQuality="{ row }">
+              <el-select v-model="row.isQuality" placeholder="璇烽�夋嫨" style="width:120px">
+                <el-option label="鏄�" :value="1" />
+                <el-option label="鍚�" :value="2" />
+              </el-select>
+            </template>
+            <template #remark="{ row }">
+              <el-input 
+                v-model="row.remark" 
+                style="width:130px" 
+                placeholder="璇疯緭鍏�" 
+              />
+            </template>
+            <template #action="{ row, index }">
+              <el-button type="danger" link @click="deleteRow(index)">鍒犻櫎</el-button>
+            </template>
+          </PIMTable>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">纭</el-button>
+          <el-button @click="closeDia">鍙栨秷</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <el-dialog v-model="productSelectionVisible" title="閫夋嫨浜у搧" width="70%" append-to-body>
+      <el-table 
+        :data="availableProducts" 
+        style="width: 100%" 
+        @selection-change="handleSelectionChange"
+        ref="productTableRef"
+        row-key="id"
+      >
+        <el-table-column align="center" type="selection" width="55" />
+        <el-table-column align="center" prop="productCategory" label="浜у搧澶х被" />
+        <el-table-column align="center" prop="specificationModel" label="瑙勬牸鍨嬪彿" />
+        <el-table-column align="center" prop="unit" label="鍗曚綅" />
+        <el-table-column align="center" prop="quantity" label="鎬绘暟閲�" />
+        <el-table-column align="center" prop="unQuantity" label="鏈��璐ф暟閲�" />
+        <el-table-column align="center" label="宸查��璐ф暟閲�">
+          <template #default="{ row }">{{ calcAlreadyReturned(row) }}</template>
+        </el-table-column>
+
+      </el-table>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="confirmProductSelection">纭娣诲姞</el-button>
+          <el-button @click="productSelectionVisible = false">鍙栨秷</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, getCurrentInstance } from "vue";
+import { returnManagementAdd, returnManagementUpdate, returnManagementGetByShippingId, getSalesLedger, returnManagementGetById } from "@/api/salesManagement/returnOrder.js";
+import { getAllCustomerList } from "@/api/customerService/index.js";
+import useUserStore from "@/store/modules/user.js";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { listProject } from "@/api/oaSystem/projectManagement.js";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('');
+const formRef = ref(null);
+const userStore = useUserStore();
+
+const data = reactive({
+  form: {
+    returnNoCheckbox: true,
+    returnNo: "",
+    customerId: "",
+    shippingId: "",
+    projectId: "",
+    maker: "",
+    makeTime: "",
+    status: 0,
+    returnReason: "",
+    refundAmount: "",
+  },
+  rules: {
+    returnNo: [{
+      validator: (rule, value, callback) => {
+        if (form.value.returnNoCheckbox) return callback();
+        if (!value) return callback(new Error("璇疯緭鍏ラ��璐у崟鍙�"));
+        callback();
+      }, trigger: "blur"
+    }],
+    customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+    shippingId: [{ required: true, message: "璇烽�夋嫨鍏宠仈鍑哄簱鍗曞彿", trigger: "change" }],
+  }
+});
+const { form, rules } = toRefs(data);
+
+const calcAlreadyReturned = (row) => {
+  const total = Number(row?.quantity ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
+  const un = Number(row?.unQuantity ?? 0);
+  if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
+  return Math.max(total - un, 0);
+};
+
+const tableColumn = ref([
+  {align: "center", label: "浜у搧澶х被", prop: "productCategory" },
+  {align: "center", label: "瑙勬牸鍨嬪彿", prop: "specificationModel" },
+  {align: "center", label: "鍗曚綅", prop: "unit", width: 80 },
+  {align: "center", label: "鎬绘暟閲�", prop: "quantity", width: 120 },
+  {align: "center", label: "宸查��璐ф暟閲�", prop: "totalReturnNum", width: 120 },
+  {align: "center", label: "鏈��璐ф暟閲�", prop: "unQuantity", width: 120 },
+  {align: "center", label: "閫�璐ф暟閲�", prop: "returnQuantity", dataType: "slot", slot: "returnQuantity", width: 120 },
+  {align: "center", label: "閫�璐т骇鍝佸崟浠�", prop: "price", dataType: "slot", slot: "price", width: 120 },
+  {align: "center", label: "閫�璐т骇鍝侀噾棰�", prop: "amount", dataType: "slot", slot: "amount", width: 120 },
+  {align: "center", label: "鏄惁鏈夎川閲忛棶棰�", prop: "isQuality", dataType: "slot", slot: "isQuality", width: 140 },
+  {align: "center", label: "澶囨敞", prop: "remark", dataType: "slot", slot: "remark", width: 150 },
+  {align: "center", label: "鎿嶄綔" , prop: "action", dataType: "slot", slot: "action", width: 120 },
+]);
+const tableData = ref([]);
+const customerNameOptions = ref([]);
+const outboundOptions = ref([]);
+const userOptions = ref([]);
+const projectOptions = ref([]);
+
+const deleteRow = (index) => {
+  tableData.value.splice(index, 1);
+};
+
+const normalizeDetailRow = (raw) => {
+  const productId = raw?.returnSaleLedgerProductId ?? raw?.saleLedgerProductId ?? raw?.id;
+  const returnSaleProductId = raw?.returnSaleProductId ?? raw?.id;
+  const num = Number(raw?.num ?? raw?.returnQuantity ?? 0);
+  return {
+    ...raw,
+    id: productId,
+    returnSaleProductId,
+    returnSaleLedgerProductId: productId,
+    productModelId: raw?.productModelId,
+    num,
+    returnQuantity: Number.isFinite(num) ? num : 0,
+    price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
+    amount: Number(raw?.amount ?? 0).toFixed(2),
+    isQuality: raw?.isQuality ?? 2,
+    remark: raw?.remark ?? "",
+  };
+};
+
+const setFormForEdit = async (row) => {
+  const res = await returnManagementGetById({ returnManagementId: row?.id });
+  console.log("res", res);
+  const detail = res?.data ?? res ?? {};
+
+  Object.assign(form.value, detail);
+  form.value.returnNoCheckbox = true;
+
+  if (form.value.customerId) {
+    await customerNameChange(form.value.customerId, false);
+  }
+  if (form.value.shippingId) {
+    await outboundNoChange(form.value.shippingId, false);
+  }
+
+  const list =
+    detail?.returnSaleProducts ||
+    detail?.returnSaleProductList ||
+    detail?.returnSaleProductDtoData ||
+    [];
+
+  tableData.value = Array.isArray(list)
+    ? list.map((raw) => {
+        const normalized = normalizeDetailRow(raw);
+        const product = availableProducts.value.find((p) => p.id === normalized.id);
+        return product ? { ...product, ...normalized } : normalized;
+      })
+    : [];
+  
+  calculateTotalRefund();
+};
+
+const openDialog = async (type, row) => {
+  operationType.value = type;
+  dialogFormVisible.value = true;
+  proxy.resetForm("formRef");
+  await Promise.all([initCustomers(), initUsers(), initProjects()]);
+  if (type === "edit") {
+    await setFormForEdit(row);
+  } else {
+    tableData.value = [];
+    Object.assign(form.value, {
+      returnNoCheckbox: true,
+      returnNo: "",
+      customerId: "",
+      shippingId: "",
+      projectId: "",
+      maker: "",
+      makeTime: "",
+      status: 0,
+      returnReason: "",
+      refundAmount: "",
+    });
+    form.value.maker = userStore.nickName || userStore.name || "";
+    form.value.makeTime = new Date().toISOString().replace('T', ' ').split('.')[0]; // Default to now
+    form.value.status = 0; // Default status
+  }
+};
+
+const submitForm = () => {
+  proxy.$refs["formRef"].validate(valid => {
+    if (!valid) return;
+    const returnSaleProducts = (tableData.value || []).map(el => ({
+      returnSaleLedgerProductId: el.returnSaleLedgerProductId ?? el.id,
+      productModelId: el.productModelId,
+      unit: el.unit,
+      num: Number(el.num ?? el.returnQuantity ?? 0),
+      price: Number(el.price ?? 0),
+      amount: Number(el.amount ?? 0),
+      isQuality: el.isQuality ?? 2,
+      remark: el.remark ?? "",
+      id: operationType.value === "edit" ? (el.returnSaleProductId ?? "") : ""
+    }));
+    const payload = { ...form.value, returnSaleProducts };
+    delete payload.returnNoCheckbox;
+    if (operationType.value === "add" && form.value.returnNoCheckbox) delete payload.returnNo;
+    if (operationType.value === "add") {
+      returnManagementAdd(payload).then(() => {
+        proxy.$modal.msgSuccess("鏂板鎴愬姛");
+        closeDia();
+      });
+    } else {
+      returnManagementUpdate(payload).then(() => {
+        proxy.$modal.msgSuccess("淇敼鎴愬姛");
+        closeDia();
+      });
+    }
+  });
+};
+
+const closeDia = () => {
+  proxy.resetForm("formRef");
+  dialogFormVisible.value = false;
+  emit('close');
+};
+
+const initCustomers = async () => {
+  const res = await getAllCustomerList({});
+  if (res?.records) {
+    customerNameOptions.value = res.records.map(item => ({
+      label: item.customerName,
+      value: item.customerName, // Keep value as name if needed for other logic, but request says customerId
+      id: item.id,
+      code: item.customerCode
+    }));
+  }
+};
+
+const initUsers = async () => {
+  const res = await userListNoPageByTenantId();
+  if (res?.data) {
+    userOptions.value = res.data.map(u => ({ label: u.nickName || u.userName, value: u.nickName || u.userName }));
+  }
+};
+
+const initProjects = async () => {
+  try {
+    const res = await listProject({ pageSize: 1000 });
+    if (res?.rows) {
+      projectOptions.value = res.rows.map(p => ({ label: p.projectName, value: p.id }));
+    }
+  } catch (e) {
+    console.error("Failed to load projects", e);
+  }
+};
+
+const handleReturnNoCheckboxChange = (checked) => {
+  if (checked) form.value.returnNo = "";
+  formRef.value?.validateField('returnNo');
+};
+
+const customerNameChange = async (val, clearDownstream = true) => {
+  // val is customerId now
+  if (clearDownstream) {
+    form.value.shippingId = "";
+    outboundOptions.value = [];
+  }
+  
+  // Find customer name for getSalesLedger if it requires name
+  const customer = customerNameOptions.value.find(c => c.id === val);
+  if (!customer) return;
+
+  // Assuming getSalesLedger takes customerName. If it takes ID, adjust accordingly.
+  // Previous code used customerName. Let's try passing customerName.
+  getSalesLedger({
+    customerName: customer.label, 
+  }).then(res => {
+    if(res.code === 200){
+      outboundOptions.value = res.data.map(item => ({
+        label: item.salesContractNo, // Or whatever the outbound number field is
+        value: item.id,
+      }))
+    }
+  })
+};
+
+const outboundNoChange = async (val, clearTable = true) => {
+  // val is shippingId
+  let res = await returnManagementGetByShippingId({ shippingId: val });
+  if(res.code === 200){
+    // If backend returns project info, set it
+    if (res.data.projectId) form.value.projectId = res.data.projectId;
+    
+    // Store available products for selection
+    availableProducts.value = res.data.productDtoData || [];
+    if (clearTable) tableData.value = [];
+  }
+};
+
+const handleReturnQuantityChange = (val, row) => {
+  if (val === "" || val === null) return;
+  const max = row.unQuantity === undefined || row.unQuantity === null ? Infinity : Number(row.unQuantity || 0);
+  const current = Number(val);
+  
+  if (current > max) {
+    proxy.$nextTick(() => {
+      row.returnQuantity = max;
+      row.num = max;
+    });
+    proxy.$modal.msgWarning(`閫�璐ф暟閲忎笉鑳借秴杩囨湭閫�璐ф暟閲�(${max})`);
+  } else if (current < 0) {
+    proxy.$nextTick(() => {
+      row.returnQuantity = 0;
+      row.num = 0;
+    });
+  } else {
+    row.num = current;
+  }
+  calculateRowAmount(row);
+  calculateTotalRefund();
+};
+
+const handlePriceChange = (val, row) => {
+  if (val === "" || val === null) {
+    row.price = 0;
+  }
+  calculateRowAmount(row);
+  calculateTotalRefund();
+};
+
+const calculateRowAmount = (row) => {
+  const quantity = Number(row.returnQuantity || 0);
+  const price = Number(row.price || 0);
+  row.amount = (quantity * price).toFixed(2);
+};
+
+const calculateTotalRefund = () => {
+  const total = tableData.value.reduce((sum, row) => {
+    return sum + Number(row.amount || 0);
+  }, 0);
+  form.value.refundAmount = total.toFixed(2);
+};
+
+const availableProducts = ref([]);
+const productSelectionVisible = ref(false);
+const selectedProducts = ref([]);
+
+const openProductSelection = () => {
+  productSelectionVisible.value = true;
+  // Pre-select items already in tableData
+  proxy.$nextTick(() => {
+    if (proxy.$refs.productTableRef) {
+      proxy.$refs.productTableRef.clearSelection();
+      availableProducts.value.forEach(row => {
+        if (tableData.value.some(item => item.id === row.id)) {
+          proxy.$refs.productTableRef.toggleRowSelection(row, true);
+        }
+      });
+    }
+  });
+};
+
+const handleSelectionChange = (val) => {
+  selectedProducts.value = val;
+};
+
+// Removed checkSelectable to allow toggling existing items
+const confirmProductSelection = () => {
+  const newTableData = [];
+  
+  selectedProducts.value.forEach(product => {
+    const existing = tableData.value.find(item => item.id === product.id);
+    if (existing) {
+      newTableData.push(existing);
+    } else {
+      newTableData.push({
+        ...product,
+        returnSaleLedgerProductId: product.id, 
+        productModelId: product.productModelId,
+        returnQuantity: 0,
+        num: 0,
+        price: Number(product.taxInclusiveUnitPrice ?? 0),
+        amount: "0.00",
+        isQuality: 2,
+        remark: "",
+        productName: product.productName,
+        specificationModel: product.specificationModel,
+        unit: product.unit,
+        quantity: product.quantity,
+        totalReturnNum: product.totalReturnNum,
+        unQuantity: product.unQuantity
+      });
+    }
+  });
+  
+  tableData.value = newTableData;
+  productSelectionVisible.value = false;
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.descriptions {
+  margin-bottom: 20px;
+  display: inline-block;
+  font-size: 1rem;
+  font-weight: 600;
+  padding-left: 12px;
+  position: relative;
+}
+.descriptions::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 4px;
+  height: 1rem;
+  background-color: #002FA7;
+  border-radius: 2px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/index.vue b/src/views/salesManagement/returnOrder/index.vue
new file mode 100644
index 0000000..5a5aa05
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/index.vue
@@ -0,0 +1,234 @@
+<template>
+  <div class="app-container">
+    <div class="search-wrapper">
+      <el-form :model="searchForm" class="demo-form-inline">
+        <el-row :gutter="20">
+          <el-col :span="4">
+            <el-form-item label="閫�璐у崟鍙�">
+              <el-input v-model="searchForm.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item label="瀹㈡埛鍚嶇О">
+              <el-input v-model="searchForm.customerName" placeholder="瀹㈡埛鍚嶇О" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item label="閿�鍞崟鍙�">
+              <el-input v-model="searchForm.salesContractNo" placeholder="閿�鍞崟鍙�" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item label="鍏宠仈鍑哄簱鍗曞彿">
+              <el-input v-model="searchForm.shippingNo" placeholder="鍏宠仈鍑哄簱鍗曞彿" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="4">
+            <el-form-item>
+              <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+              <el-button @click="handleReset">閲嶇疆</el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+    <div class="table_list">
+      <div class="table_header" style="display: flex;justify-content: flex-end;margin-bottom: 10px;">
+        <el-button type="primary" @click="openForm('add')">鏂板缓閿�鍞��璐�</el-button>
+        <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+      </div>
+      <PIMTable
+        rowKey="id"
+        :column="tableColumn"
+        :tableData="tableData"
+        :page="page"
+        :isSelection="true"
+        @selection-change="handleSelectionChange"
+        :tableLoading="tableLoading"
+        @pagination="pagination"
+      >
+        <template #status="{ row }">
+          <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+        </template>
+      </PIMTable>
+    </div>
+    <form-dia ref="formDia" @close="handleQuery" />
+    <detail-dia ref="detailDia" />
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, computed, getCurrentInstance, nextTick, onMounted } from "vue";
+import { ElMessageBox } from "element-plus";
+import FormDia from "./components/formDia.vue";
+import DetailDia from "./components/detailDia.vue";
+import { returnManagementList, returnManagementDel, returnManagementHandle } from "@/api/salesManagement/returnOrder.js";
+const { proxy } = getCurrentInstance();
+
+const formDia = ref();
+const detailDia = ref();
+const openForm = (type, row) => {
+  nextTick(() => formDia.value?.openDialog(type, row));
+};
+
+const openDetail = (row) => {
+  nextTick(() => detailDia.value?.openDialog(row));
+};
+
+const handleRowDelete = (row) => {
+  if (!row?.id) return;
+  ElMessageBox.confirm("璇ラ��璐у崟灏嗚鍒犻櫎锛屾槸鍚︾‘璁ゅ垹闄わ紵", "鍒犻櫎鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    returnManagementDel([row.id]).then(() => {
+      proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      getList();
+    });
+  });
+};
+
+const handleRowHandle = (row) => {
+  if (!row?.id) return;
+  ElMessageBox.confirm("鏄惁澶勭悊璇ラ��璐у崟锛熷鐞嗗悗灏嗘棤娉曚慨鏀�", "澶勭悊鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    returnManagementHandle({ returnManagementId: String(row.id) }).then(() => {
+      proxy.$modal.msgSuccess("澶勭悊鎴愬姛");
+      getList();
+    });
+  });
+}
+
+const data = reactive({
+  searchForm: {
+    returnNo: "",
+    status: "",
+    customerName: "",
+    salesContractNo: "",
+    salesman: "",
+    shippingNo: "",
+    projectName: "",
+    salesLedgerId: "",
+    makeTime: ""
+  }
+});
+const { searchForm } = toRefs(data);
+
+const documentStatusOptions = ref([
+  { label: "寰呭鐞�", value: 0 },
+  { label: "宸插鐞�", value: 1 }
+]);
+
+const defaultColumns = [
+  { label: "閫�璐у崟鍙�", prop: "returnNo", width: 160 },
+  { label: "鍗曟嵁鐘舵��", prop: "status", width: 90, dataType: "slot", slot: "status" },
+  { label: "鍒跺崟鏃堕棿", prop: "makeTime", width: 170 },
+  { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: 220 },
+  { label: "閿�鍞崟鍙�", prop: "salesContractNo", width: 160 },
+  { label: "涓氬姟鍛�", prop: "salesman", width: 120 },
+  { label: "鍏宠仈鍑哄簱鍗曞彿", prop: "shippingNo", width: 170 },
+  { label: "椤圭洰鍚嶇О", prop: "projectName", width: 180 },
+  { label: "鍒跺崟浜�", prop: "maker", width: 120 },
+  {
+    label: "鎿嶄綔",
+    prop: "operation",
+    dataType: "action",
+    align: "center",
+    fixed: "right",
+    width: 240,
+    operation: [
+      { name: "缂栬緫", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => openForm("edit", row) },
+      { name: "閫�娆惧鐞�", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowHandle(row) },
+      { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+      { name: "鍒犻櫎", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowDelete(row) },
+    ],
+  },
+];
+const tableColumn = defaultColumns;
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({ current: 1, size: 10, total: 0 });
+const selectedRows = ref([]);
+const tableHeight = computed(() => "calc(100% - 80px)");
+
+const handleReset = () => {
+  Object.keys(searchForm.value).forEach(k => searchForm.value[k] = "");
+  handleQuery();
+};
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection;
+};
+const handleQuery = () => {
+  page.current = 1;
+  getList();
+};
+const pagination = (obj) => {
+  page.current = obj.page;
+  page.size = obj.limit;
+  getList();
+};
+const getList = () => {
+  tableLoading.value = true;
+  returnManagementList({ ...searchForm.value, ...page }).then(res => {
+    tableLoading.value = false;
+    tableData.value = res?.data?.records || [];
+    page.total = res?.data?.total || 0;
+  }).finally(() => tableLoading.value = false);
+};
+const handleOut = () => {
+  ElMessageBox.alert("瀵煎嚭鍔熻兘寰呮帴鍏ユ帴鍙�", "鎻愮ず");
+};
+const handleDelete = () => {
+  let ids = [];
+  if (selectedRows.value.length === 0) {
+    proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+    return;
+  }
+  ids = selectedRows.value.map(i => i.id);
+  console.log(ids);
+  ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+    confirmButtonText: "纭",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  }).then(() => {
+    returnManagementDel( ids ).then(() => {
+      proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      getList();
+    });
+  });
+};
+
+const getStatusType = (status) => {
+  const statusMap = {
+    0: "warning",
+    1: "success"
+  };
+  return statusMap[status] || "info";
+};
+
+const getStatusText = (status) => {
+  const statusMap = {
+    0: "寰呭鐞�",
+    1: "宸插鐞�"
+  };
+  return statusMap[status] || "鏈煡";
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped lang="scss">
+.search-wrapper {
+  background: white;
+  padding: 1rem 1rem 0 1rem;
+  border: 8px;
+  border-radius: 16px;
+}
+</style>

--
Gitblit v1.9.3