From 04b1a9cfde4049be9a38b9832d5289d4a192c883 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 15 五月 2026 16:29:33 +0800
Subject: [PATCH] 加班申请模块和审批流程公共组件

---
 src/views/customerService/feedbackRegistration/components/formDia.vue |  650 +++++++++++++++++++++++++++++++++++++++++++++--------------
 1 files changed, 495 insertions(+), 155 deletions(-)

diff --git a/src/views/customerService/feedbackRegistration/components/formDia.vue b/src/views/customerService/feedbackRegistration/components/formDia.vue
index a37002b..790ddbe 100644
--- a/src/views/customerService/feedbackRegistration/components/formDia.vue
+++ b/src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -1,167 +1,507 @@
 <template>
   <div>
-    <el-dialog
-        v-model="dialogFormVisible"
-        title="鍞悗鐧昏"
-        width="70%"
-        @close="closeDia"
-    >
-			<el-form
-				:model="form"
-				label-width="140px"
-				label-position="top"
-				:rules="rules"
-				ref="formRef"
-			>
-				<el-row :gutter="30">
-					<el-col :span="12">
-						<el-form-item label="鍙嶉鏃堕棿锛�" prop="feedbackDate">
-							<el-date-picker
-								style="width: 100%"
-								v-model="form.feedbackDate"
-								value-format="YYYY-MM-DD"
-								format="YYYY-MM-DD"
-								type="date"
-								placeholder="璇烽�夋嫨"
-								clearable
-							/>
-						</el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="鐧昏浜猴細" prop="checkUserId">
-							<el-select
-								v-model="form.checkUserId"
-								placeholder="璇烽�夋嫨"
-								clearable
-							>
-								<el-option
-									v-for="item in userList"
-									:key="item.userId"
-									:label="item.nickName"
-									:value="item.userId"
-								></el-option>
-							</el-select>
-						</el-form-item>
-					</el-col>
-				</el-row>
-				<el-row :gutter="30">
-					<el-col :span="12">
-						<el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerName">
-							<el-input
-								v-model="form.customerName"
-								placeholder="璇疯緭鍏�"
-								clearable
-							/>
-						</el-form-item>
-					</el-col>
-					<el-col :span="12">
-						<el-form-item label="闂鎻忚堪锛�" prop="proDesc">
-							<el-input
-								v-model="form.proDesc"
-								placeholder="璇疯緭鍏�"
-								clearable
-								type="textarea"
-							/>
-						</el-form-item>
-					</el-col>
-				</el-row>
-			</el-form>
-			<template #footer>
-				<div class="dialog-footer">
-					<el-button type="primary" @click="submitForm">纭</el-button>
-					<el-button @click="closeDia">鍙栨秷</el-button>
-				</div>
-			</template>
+    <el-dialog v-model="dialogFormVisible"
+               title="鏂板鍞悗鍗�"
+               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="customerName">
+                <el-select v-model="form.customerName"
+                           filterable
+                           @change="customerNameChange">
+                  <el-option v-for="item in customerNameOptions"
+                             :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="serviceType">
+                <el-select v-model="form.serviceType"
+                           filterable>
+                  <el-option v-for="dict in serviceTypeOptions"
+                             :key="dict.value"
+                             :label="dict.label"
+                             :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="鍏宠仈閿�鍞崟鍙凤細"
+                            prop="salesContractNo">
+                <el-select v-model="form.salesContractNo"
+                           @change="associatedSalesOrderNumberChange"
+                           filterable>
+                  <el-option v-for="item in associatedSalesOrderNumberOptions"
+                             :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="urgency">
+                <el-select v-model="form.urgency"
+                           filterable>
+                  <el-option v-for="dict in urgencyOptions"
+                             :key="dict.value"
+                             :label="dict.label"
+                             :value="dict.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="4">
+              <el-form-item label="闂鎻忚堪锛�"
+                            prop="proDesc">
+                <el-input v-model="form.proDesc"
+                          placeholder="璇疯緭鍏ラ棶棰樻弿杩�" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </el-form>
+        <hr>
+        <div style="padding-top: 20px">
+          <div style="display: flex; justify-content: space-between">
+            <span class="descriptions">鍏宠仈浜у搧</span>
+            <el-button type="primary"
+                       style="margin-right: 12px; margin-bottom: 10px"
+                       @click="isShowProductSelectDialog = true">
+              閫夋嫨浜у搧
+            </el-button>
+          </div>
+          <PIMTable :isShowPagination="false"
+                    rowKey="id"
+                    :column="tableColumn"
+                    :tableData="tableData">
+            <template #approveStatus="{ row }">
+              <el-tag :type="getApproveStatusType(row)"
+                      size="small">
+                {{ getApproveStatusText(row) }}
+              </el-tag>
+            </template>
+            <template #shippingStatus="{ row }">
+              <el-tag :type="getShippingStatusType(row)"
+                      size="small">
+                {{ getShippingStatusText(row) }}
+              </el-tag>
+            </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>
+    <!-- 閫夋嫨浜у搧寮圭獥 -->
+    <ProductSelectDialog v-model="isShowProductSelectDialog"
+                         :products="currentSalesOrderProducts"
+                         :selected-ids="currentSelectedProductIds"
+                         @confirm="handleSelectProducts" />
   </div>
 </template>
 
 <script setup>
-import {ref} from "vue";
-import useUserStore from "@/store/modules/user.js";
-import {userListNoPageByTenantId} from "@/api/system/user.js";
-import {afterSalesServiceAdd, afterSalesServiceUpdate} from "@/api/customerService/index.js";
-const { proxy } = getCurrentInstance()
-const emit = defineEmits(['close'])
-const dialogFormVisible = ref(false);
-const operationType = ref('')
-const userStore = useUserStore();
+  import { ref, reactive, toRefs, getCurrentInstance, computed } from "vue";
+  import ProductSelectDialog from "./ProductSelectDialog.vue";
+  import useUserStore from "@/store/modules/user.js";
+  import { userListNoPageByTenantId } from "@/api/system/user.js";
+  import {
+    afterSalesServiceAdd,
+    afterSalesServiceUpdate,
+    getAllCustomerList,
+    getSalesLedger,
+  } from "@/api/customerService/index.js";
+  import { getCurrentDate } from "@/utils/index.js";
+  const { proxy } = getCurrentInstance();
+  const emit = defineEmits(["close"]);
+  const dialogFormVisible = ref(false);
+  const operationType = ref("");
+  const formRef = ref(null);
+  const customerNameOptions = ref([]);
+  const userStore = useUserStore();
 
-const data = reactive({
-	form: {
-		feedbackDate: "",
-		checkUserId: "",
-		customerName: "",
-		proDesc: "",
-	},
-	rules: {
-		feedbackDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
-		checkUserId: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
-		customerName: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
-		proDesc: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
-	}
-})
-const { form, rules } = toRefs(data);
-const userList = ref([])
+  const data = reactive({
+    form: {
+      topic: "",
+      serviceType: "",
+      urgency: "",
+      salesLedgerId: null,
+      productModelIds: "",
+      customerId: null,
+      salesContractNo: "",
+      proDesc: "",
+      customerName: "",
+    },
+    rules: {
+      customerName: [
+        { required: true, message: "璇烽�夋嫨瀹㈡埛鍚嶇О", trigger: "change" },
+      ],
+      serviceType: [
+        { required: true, message: "璇烽�夋嫨鍞悗绫诲瀷", trigger: "change" },
+      ],
+      urgency: [{ required: true, message: "璇烽�夋嫨绱ф�ョ▼搴�", trigger: "change" }],
+      feedbackDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+    },
+  });
 
-// 鎵撳紑寮规
-const openDialog = (type, row) => {
-  operationType.value = type;
-  dialogFormVisible.value = true;
-	form.value = {}
-	proxy.resetForm("formRef");
-	form.value.checkUserId = userStore.id;
-	form.value.feedbackDate = getCurrentDate();
-	userListNoPageByTenantId().then((res) => {
-		userList.value = res.data;
-	});
-	if (type === "edit") {
-		form.value = {...row}
-	}
-}
-// const setName = (code) => {
-// 	const index = userList.value.findIndex(item => item.deviceModel === code);
-// 	if (index > -1) {
-// 		console.log(userList)
-// 		form.value.name = userList.value[index].deviceName;
-// 	}
-// }
-const submitForm = () => {
-	proxy.$refs["formRef"].validate(valid => {
-		if (valid) {
-			if (operationType.value === "add") {
-				afterSalesServiceAdd(form.value).then(response => {
-					proxy.$modal.msgSuccess("鏂板鎴愬姛")
-					closeDia()
-				})
-			} else {
-				afterSalesServiceUpdate(form.value).then(response => {
-					proxy.$modal.msgSuccess("淇敼鎴愬姛")
-					closeDia()
-				})
-			}
-		}
-	})
-}
-// 鍏抽棴寮规
-const closeDia = () => {
-	proxy.resetForm("formRef");
-  dialogFormVisible.value = false;
-  emit('close')
-};
-// 鑾峰彇褰撳墠鏃ユ湡骞舵牸寮忓寲涓� YYYY-MM-DD
-function getCurrentDate() {
-	const today = new Date();
-	const year = today.getFullYear();
-	const month = String(today.getMonth() + 1).padStart(2, "0"); // 鏈堜唤浠�0寮�濮�
-	const day = String(today.getDate()).padStart(2, "0");
-	return `${year}-${month}-${day}`;
-}
-defineExpose({
-  openDialog,
-});
+  // 鑷畾涔夋牎楠屽嚱鏁帮細鍒ゆ柇鏄惁闇�瑕佹牎楠屽敭鍚庣紪鍙�
+
+  const { form, rules } = toRefs(data);
+  const userList = ref([]);
+
+  const formatCurrency = val => {
+    if (val === null || val === undefined || val === "") return "-";
+    const num = Number(val);
+    return Number.isFinite(num) ? num.toFixed(2) : "-";
+  };
+
+  const { post_sale_waiting_list, degree_of_urgency } = proxy.useDict(
+    "post_sale_waiting_list",
+    "degree_of_urgency"
+  );
+
+  const serviceTypeOptions = computed(() => post_sale_waiting_list?.value || []);
+  const urgencyOptions = computed(() => degree_of_urgency?.value || []);
+
+  const getProductRowId = row => {
+    return (
+      row?.id ??
+      row?.productModelId ??
+      row?.modelId ??
+      `${row?.productCategory || row?.productName || ""}-${
+        row?.specificationModel || row?.model || ""
+      }-${row?.unit || ""}`
+    );
+  };
+
+  const normalizeProductRow = row => {
+    return {
+      ...row,
+      id: getProductRowId(row),
+      productCategory: row?.productCategory ?? row?.productName ?? "",
+      specificationModel: row?.specificationModel ?? row?.model ?? "",
+      unit: row?.unit ?? "",
+      approveStatus: row?.approveStatus ?? null,
+      shippingStatus: row?.shippingStatus ?? "",
+      expressCompany: row?.expressCompany ?? "",
+      expressNumber: row?.expressNumber ?? "",
+      shippingCarNumber: row?.shippingCarNumber ?? "",
+      shippingDate: row?.shippingDate ?? "",
+      quantity: row?.quantity ?? 0,
+      taxRate: row?.taxRate ?? 0,
+      taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
+      taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
+      taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
+      noQuantity: row?.noQuantity ?? 0,
+    };
+  };
+
+  const tableColumn = ref([
+    { label: "浜у搧澶х被", prop: "productCategory" },
+    { label: "瑙勬牸鍨嬪彿", prop: "specificationModel" },
+    { label: "鍗曚綅", prop: "unit" },
+    {
+      label: "浜у搧鐘舵��",
+      prop: "approveStatus",
+      width: 100,
+      align: "center",
+      dataType: "slot",
+      slot: "approveStatus",
+    },
+    {
+      label: "鍙戣揣鐘舵��",
+      align: "center",
+      width: 140,
+      dataType: "slot",
+      slot: "shippingStatus",
+    },
+    { label: "蹇�掑叕鍙�", prop: "expressCompany", width: 140 },
+    { label: "蹇�掑崟鍙�", prop: "expressNumber", width: 160 },
+    {
+      label: "鍙戣揣杞︾墝",
+      prop: "shippingCarNumber",
+      minWidth: 100,
+      align: "center",
+    },
+    { label: "鍙戣揣鏃ユ湡", prop: "shippingDate", minWidth: 100, align: "center" },
+    { label: "鏁伴噺", prop: "quantity", width: 100 },
+    { label: "绋庣巼(%)", prop: "taxRate", width: 100 },
+    {
+      label: "鍚◣鍗曚环(鍏�)",
+      prop: "taxInclusiveUnitPrice",
+      width: 160,
+      formatData: formatCurrency,
+    },
+    {
+      label: "鍚◣鎬讳环(鍏�)",
+      prop: "taxInclusiveTotalPrice",
+      width: 160,
+      formatData: formatCurrency,
+    },
+    {
+      label: "涓嶅惈绋庢�讳环(鍏�)",
+      prop: "taxExclusiveTotalPrice",
+      width: 160,
+      formatData: formatCurrency,
+    },
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      operation: [
+        {
+          name: "鍒犻櫎",
+          type: "text",
+          clickFun: row => {
+            tableData.value = tableData.value.filter(
+              i => getProductRowId(i) !== getProductRowId(row)
+            );
+          },
+        },
+      ],
+    },
+  ]);
+  const tableData = ref([]);
+  // 閫夋嫨浜у搧寮圭獥
+  const isShowProductSelectDialog = ref(false);
+  const handleSelectProducts = rows => {
+    if (!Array.isArray(rows)) return;
+    const existingIds = new Set(
+      tableData.value.map(i => String(getProductRowId(i)))
+    );
+    const mapped = rows
+      .map(normalizeProductRow)
+      .filter(r => !existingIds.has(String(getProductRowId(r))));
+    tableData.value = tableData.value.concat(mapped);
+  };
+  const currentSelectedProductIds = computed(() => {
+    return tableData.value
+      .map(item => getProductRowId(item))
+      .filter(item => item !== undefined && item !== null && item !== "");
+  });
+
+  const associatedSalesOrderNumberChange = () => {
+    const opt = associatedSalesOrderNumberOptions.value.find(
+      item => item.value === form.value.salesContractNo
+    );
+    tableData.value = (opt?.productData || []).map(normalizeProductRow);
+    form.value.salesLedgerId = opt?.id || null;
+  };
+
+  const associatedSalesOrderNumberOptions = ref([]);
+
+  const currentSalesOrderProducts = computed(() => {
+    const opt = associatedSalesOrderNumberOptions.value.find(
+      item => item.value === form.value.salesContractNo
+    );
+    return (opt?.productData || []).map(normalizeProductRow);
+  });
+
+  const customerNameChange = val => {
+    form.value.salesContractNo = "";
+    form.value.salesLedgerId = null;
+    tableData.value = [];
+    associatedSalesOrderNumberOptions.value = [];
+    const opt = customerNameOptions.value.find(item => item.value === val);
+    if (opt) {
+      form.value.customerId = opt.id;
+    } else {
+      form.value.customerId = null;
+    }
+    getSalesLedger({
+      customerName: form.value.customerName,
+    }).then(res => {
+      if (res.code === 200) {
+        associatedSalesOrderNumberOptions.value = res.data.records.map(item => ({
+          label: item.salesContractNo,
+          value: item.salesContractNo,
+          productData: item.productData,
+          id: item.id,
+        }));
+      }
+    });
+  };
+
+  const getApproveStatusText = row => {
+    if (!row) return "涓嶈冻";
+    if (
+      row.approveStatus === 1 &&
+      (!row.shippingDate || !row.shippingCarNumber)
+    ) {
+      return "鍏呰冻";
+    }
+    if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) {
+      return "宸插嚭搴�";
+    }
+    return "涓嶈冻";
+  };
+
+  const getApproveStatusType = row => {
+    const statusText = getApproveStatusText(row);
+    return statusText === "涓嶈冻" ? "danger" : "success";
+  };
+
+  const getShippingStatusText = row => {
+    if (!row) return "寰呭彂璐�";
+    if (row.shippingDate || row.shippingCarNumber) {
+      return "宸插彂璐�";
+    }
+    const status = row.shippingStatus;
+    if (status === null || status === undefined || status === "") {
+      return "寰呭彂璐�";
+    }
+    const map = {
+      寰呭彂璐�: "寰呭彂璐�",
+      寰呭鏍�: "寰呭鏍�",
+      瀹℃牳涓�: "瀹℃牳涓�",
+      瀹℃牳鎷掔粷: "瀹℃牳鎷掔粷",
+      瀹℃牳閫氳繃: "瀹℃牳閫氳繃",
+      宸插彂璐�: "宸插彂璐�",
+    };
+    return map[String(status).trim()] || "寰呭彂璐�";
+  };
+
+  const getShippingStatusType = row => {
+    if (!row) return "info";
+    if (row.shippingDate || row.shippingCarNumber) {
+      return "success";
+    }
+    const status = row.shippingStatus;
+    if (status === null || status === undefined || status === "") {
+      return "info";
+    }
+    const map = {
+      寰呭彂璐�: "info",
+      寰呭鏍�: "warning",
+      瀹℃牳涓�: "warning",
+      瀹℃牳鎷掔粷: "danger",
+      瀹℃牳閫氳繃: "success",
+      宸插彂璐�: "success",
+    };
+    return map[String(status).trim()] || "info";
+  };
+
+  // 鎵撳紑寮规
+  const openDialog = async (type, row) => {
+    // 璇锋眰澶氫釜鎺ュ彛锛岃幏鍙栨暟鎹�
+    let res = await getAllCustomerList({
+      current: 1,
+      size: 1000,
+      total: 0,
+    });
+    console.log(res, "res");
+
+    if (res.data.records) {
+      customerNameOptions.value = res.data.records.map(item => ({
+        label: item.customerName,
+        value: item.customerName,
+        id: item.id,
+      }));
+    } else {
+    }
+
+    operationType.value = type;
+    dialogFormVisible.value = true;
+    form.value = {};
+    proxy.resetForm("formRef");
+    form.value.checkUserId = userStore.id;
+    form.value.feedbackDate = getCurrentDate();
+    // 鏂板鏃舵竻绌哄凡閫夊叧鑱斾骇鍝�
+    if (type === "add") {
+      tableData.value = [];
+    }
+    userListNoPageByTenantId().then(res => {
+      userList.value = res.data;
+    });
+    if (type === "edit") {
+      form.value = { ...row };
+      if (form.value.customerName) {
+        const res = await getSalesLedger({
+          customerName: form.value.customerName,
+        });
+        if (res?.code === 200) {
+          console.log(res);
+          associatedSalesOrderNumberOptions.value = (res.data?.records || []).map(
+            item => ({
+              label: item.salesContractNo,
+              value: item.salesContractNo,
+              productData: item.productData,
+              id: item.id,
+            })
+          );
+        }
+      }
+      console.log(form.value);
+    }
+  };
+  const submitForm = () => {
+    proxy.$refs["formRef"].validate(valid => {
+      if (valid) {
+        // 鍖归厤浜у搧鍨嬪彿IDs
+        form.value.productModelIds = tableData.value
+          .map(item => item.id)
+          .join(",");
+        if (operationType.value === "add") {
+          afterSalesServiceAdd(form.value).then(response => {
+            proxy.$modal.msgSuccess("鏂板鎴愬姛");
+            closeDia();
+          });
+        } else {
+          afterSalesServiceUpdate(form.value).then(response => {
+            proxy.$modal.msgSuccess("淇敼鎴愬姛");
+            closeDia();
+          });
+        }
+      }
+    });
+  };
+  // 鍏抽棴寮规
+  const closeDia = () => {
+    proxy.resetForm("formRef");
+    dialogFormVisible.value = false;
+    emit("close");
+  };
+  defineExpose({
+    openDialog,
+  });
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+  .descriptions {
+    margin-bottom: 20px;
+    display: inline-block;
+    font-size: 1rem;
+    font-weight: 600;
+    padding-left: 12px;
+    position: relative;
+  }
 
-</style>
\ No newline at end of file
+  .descriptions::before {
+    content: "";
+    position: absolute;
+    left: 0;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 4px;
+    height: 1rem;
+    background-color: #002fa7; /* Element 榛樿绾㈣壊 */
+    border-radius: 2px;
+  }
+</style>

--
Gitblit v1.9.3