From 1edc99c8d6fc4ab71961c23aaf3dd5fecc076908 Mon Sep 17 00:00:00 2001
From: ZN <zhang_12370@163.com>
Date: 星期五, 06 三月 2026 16:56:04 +0800
Subject: [PATCH] feat(销售管理): 新增销售退货单管理功能
---
src/views/salesManagement/returnOrder/components/formDia.vue | 489 ++++++++++++++++++++++++++++++++++
src/api/salesManagement/returnOrder.js | 72 +++++
src/views/salesManagement/returnOrder/index.vue | 279 +++++++++++++++++++
3 files changed, 840 insertions(+), 0 deletions(-)
diff --git a/src/api/salesManagement/returnOrder.js b/src/api/salesManagement/returnOrder.js
new file mode 100644
index 0000000..81a8ae2
--- /dev/null
+++ b/src/api/salesManagement/returnOrder.js
@@ -0,0 +1,72 @@
+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(query) {
+ return request({
+ url: "/returnManagement/del",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閿�鍞��璐�-鏌ヨ
+// /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,
+ })
+}
\ No newline at end of file
diff --git a/src/views/salesManagement/returnOrder/components/formDia.vue b/src/views/salesManagement/returnOrder/components/formDia.vue
new file mode 100644
index 0000000..82b3766
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/components/formDia.vue
@@ -0,0 +1,489 @@
+<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="projectStage">
+ <el-input v-model="form.projectStage" placeholder="椤圭洰闃舵" />
+ </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="settler">
+ <el-select v-model="form.settler" 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="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="瀹℃牳涓�" :value="1" />
+ <el-option label="宸插鏍�" :value="2" />
+ </el-select>
+ </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 #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: "",
+ projectStage: "",
+ maker: "",
+ makeTime: "",
+ settler: "",
+ status: 0,
+ },
+ 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: "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,
+ num,
+ returnQuantity: Number.isFinite(num) ? num : 0,
+ };
+};
+
+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;
+ })
+ : [];
+};
+
+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: "",
+ projectStage: "",
+ maker: "",
+ makeTime: "",
+ settler: "",
+ status: 0,
+ });
+ 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,
+ num: Number(el.num ?? el.returnQuantity ?? 0),
+ 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;
+ if (res.data.projectStage) form.value.projectStage = res.data.projectStage;
+
+ // 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) {
+ // Need nextTick to ensure update if user typed too fast or pasted
+ 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;
+ }
+};
+
+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 = () => {
+ // Rebuild tableData based on selection, preserving existing data (returnQuantity)
+ const newTableData = [];
+
+ selectedProducts.value.forEach(product => {
+ // Check if product was already in tableData to preserve user input
+ const existing = tableData.value.find(item => item.id === product.id);
+ if (existing) {
+ newTableData.push(existing);
+ } else {
+ // Create new entry
+ newTableData.push({
+ ...product, // Keep all product display fields (productName, model, unit, etc.)
+
+ // Map to backend entity structure for submission
+ returnSaleLedgerProductId: product.id,
+ returnQuantity: 0, // Default input
+ num: 0, // Backend quantity field
+
+ // Ensure display fields are available if they come from 'product'
+ // If product has different field names than tableColumn expects, map them here
+ 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..c4f23a1
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/index.vue
@@ -0,0 +1,279 @@
+<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>
+ <el-input v-model="searchForm.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input v-model="searchForm.customerName" placeholder="瀹㈡埛鍚嶇О" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <el-input v-model="searchForm.salesContractNo" placeholder="閿�鍞崟鍙�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item>
+ <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:space-between;align-items:center;">
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板缓閿�鍞��璐�</el-button>
+ </div>
+ <div>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ <el-button @click="columnsDialogVisible = true">鍒楄〃瀛楁</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="visibleColumns"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ />
+ </div>
+ <form-dia ref="formDia" @close="handleQuery" />
+
+ <el-dialog v-model="columnsDialogVisible" title="鑷畾涔夋樉绀哄垪椤�" width="600px">
+ <div class="columns-tip">娉細鍒楄〃椤规樉绀轰笉寰楀皯浜�5椤癸紱鎷栧姩鍙充晶鎶婃墜鍙皟鏁存樉绀洪『搴�</div>
+ <ul class="columns-list">
+ <li v-for="(col, idx) in allColumns" :key="col.prop"
+ class="columns-item"
+ draggable="true"
+ @dragstart="onDragStart(idx)"
+ @dragover.prevent
+ @drop="onDrop(idx)">
+ <el-checkbox v-model="col.selected">{{ col.label }}</el-checkbox>
+ <span class="drag-handle">鈮�</span>
+ </li>
+ </ul>
+ <template #footer>
+ <el-button @click="resetColumns">鎭㈠榛樿</el-button>
+ <el-button type="primary" @click="saveColumns">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+ </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 { returnManagementList, returnManagementDel } from "@/api/salesManagement/returnOrder.js";
+const { proxy } = getCurrentInstance();
+
+const formDia = ref();
+const openForm = (type, row) => {
+ nextTick(() => formDia.value?.openDialog(type, row));
+};
+
+const handleRowDelete = (row) => {
+ if (!row?.id) return;
+ ElMessageBox.confirm("璇ラ��璐у崟灏嗚鍒犻櫎锛屾槸鍚︾‘璁ゅ垹闄わ紵", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementDel({ ids: 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 },
+ { label: "宸插鏍�", value: 2 }
+]);
+
+const defaultColumns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: 160 },
+ { label: "鍗曟嵁鐘舵��", prop: "status", minWidth: 120, formatData: (v) => ({ "0": "寰呭鏍�", "1": "瀹℃牳涓�", "2": "宸插鏍�" }[String(v)] ?? v) },
+ { label: "鍒跺崟鏃堕棿", prop: "makeTime", minWidth: 170 },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: 220 },
+ { label: "閿�鍞崟鍙�", prop: "salesContractNo", minWidth: 160 },
+ { label: "涓氬姟鍛�", prop: "salesman", minWidth: 120 },
+ { label: "鍏宠仈鍑哄簱鍗曞彿", prop: "shippingNo", minWidth: 170 },
+ { label: "椤圭洰鍚嶇О", prop: "projectName", minWidth: 180 },
+ { label: "椤圭洰闃舵", prop: "projectStage", minWidth: 120 },
+ { label: "鍒跺崟浜�", prop: "maker", minWidth: 120 },
+ { label: "缁撶畻浜�", prop: "settler", minWidth: 120 },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "action",
+ align: "center",
+ fixed: "right",
+ width: 140,
+ operation: [
+ { name: "缂栬緫", type: "text", clickFun: (row) => openForm("edit", row) },
+ { name: "鍒犻櫎", type: "text", clickFun: (row) => handleRowDelete(row) },
+ ],
+ },
+];
+const COLUMNS_KEY = "return_order_columns_v2";
+const columnsDialogVisible = ref(false);
+const allColumns = ref([]);
+
+const initColumns = () => {
+ const saved = localStorage.getItem(COLUMNS_KEY);
+ if (saved) {
+ try {
+ const parsed = JSON.parse(saved);
+ // 鍚堝苟榛樿鍒椾笌宸蹭繚瀛橀厤缃紝閬垮厤鍚庣画鏂板鍒椾涪澶�
+ const map = new Map(parsed.map(c => [c.prop, c]));
+ allColumns.value = defaultColumns.map(d => {
+ const found = map.get(d.prop);
+ return { ...d, selected: found ? !!found.selected : true };
+ });
+ // 浠ヤ繚瀛樼殑椤哄簭涓哄噯
+ const order = parsed.map(p => p.prop);
+ allColumns.value.sort((a, b) => order.indexOf(a.prop) - order.indexOf(b.prop));
+ return;
+ } catch {}
+ }
+ allColumns.value = defaultColumns.map(c => ({ ...c, selected: true }));
+};
+initColumns();
+
+const visibleColumns = computed(() => allColumns.value.filter(c => c.selected));
+
+let dragFrom = -1;
+const onDragStart = (idx) => {
+ dragFrom = idx;
+};
+const onDrop = (to) => {
+ if (dragFrom < 0 || dragFrom === to) return;
+ const arr = [...allColumns.value];
+ const [moved] = arr.splice(dragFrom, 1);
+ arr.splice(to, 0, moved);
+ allColumns.value = arr;
+ dragFrom = -1;
+};
+
+const resetColumns = () => {
+ allColumns.value = defaultColumns.map(c => ({ ...c, selected: true }));
+ localStorage.removeItem(COLUMNS_KEY);
+};
+const saveColumns = () => {
+ const toSave = allColumns.value.map(({ label, prop, width, selected }) => ({ label, prop, width, selected }));
+ localStorage.setItem(COLUMNS_KEY, JSON.stringify(toSave));
+ columnsDialogVisible.value = false;
+};
+
+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] = "");
+};
+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);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementDel({ ids: ids.join(",") }).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.search-wrapper {
+ background: white;
+ padding: 1rem 1rem 0 1rem;
+ border: 8px;
+ border-radius: 16px;
+}
+.table_list {
+ height: calc(100vh - 230px);
+ min-height: 360px;
+ background: #fff;
+ margin-top: 20px;
+ display: flex;
+ flex-direction: column;
+}
+.columns-tip{color:#909399;margin-bottom:10px;font-size:12px;}
+.columns-list{list-style:none;padding:0;margin:0;max-height:360px;overflow:auto;}
+.columns-item{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border:1px solid #f0f0f0;border-radius:6px;margin-bottom:8px;cursor:move;background:#fff;}
+.columns-item .drag-handle{color:#909399;padding-left:12px;user-select:none;}
+.table_header {
+ margin-bottom: 15px;
+}
+</style>
--
Gitblit v1.9.3