From 05eb7f660c2968afcfa5d8c961a17d5bf9964d2f Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 27 三月 2026 13:46:39 +0800
Subject: [PATCH] feat(地区管理): 增加地区增删改查功能并优化客户档案页面

---
 src/views/basicData/customerFile/index.vue |  322 +++++++++++++++++++++++++++++++++++++++++++----------
 src/api/basicData/customerFile.js          |   28 ++++
 2 files changed, 286 insertions(+), 64 deletions(-)

diff --git a/src/api/basicData/customerFile.js b/src/api/basicData/customerFile.js
index be5a40d..da17201 100644
--- a/src/api/basicData/customerFile.js
+++ b/src/api/basicData/customerFile.js
@@ -13,11 +13,37 @@
 // 鏌ヨ鍦板尯鍒楄〃
 export function listCustomerRegions(query) {
     return request({
-        url: '/basic/customer/regions',
+        url: '/customerRegions/list',
         method: 'get',
         params: query
     })
 }
+
+// 鏂板鍦板尯
+export function addCustomerRegions(data) {
+    return request({
+        url: '/customerRegions/add',
+        method: 'post',
+        data
+    })
+}
+
+// 淇敼鍦板尯
+export function updateCustomerRegions(data) {
+    return request({
+        url: '/customerRegions/update',
+        method: 'put',
+        data
+    })
+}
+
+// 鍒犻櫎鍦板尯
+export function delCustomerRegions(id) {
+    return request({
+        url: '/customerRegions/' + id,
+        method: 'delete'
+    })
+}
 // 鏌ヨ瀹㈡埛妗f璇︾粏
 export function getCustomer(id) {
     return request({
diff --git a/src/views/basicData/customerFile/index.vue b/src/views/basicData/customerFile/index.vue
index bd65a22..cdb802b 100644
--- a/src/views/basicData/customerFile/index.vue
+++ b/src/views/basicData/customerFile/index.vue
@@ -4,9 +4,17 @@
       <div class="left-panel">
         <div class="left-header">
           <div class="left-title">鍦板尯</div>
-          <el-button type="primary"
-                     size="small"
-                     @click="openAddRegionDialog">鏂板鍦板尯</el-button>
+          <div class="left-actions">
+            <el-button type="primary"
+                       size="small"
+                       @click="openAddRegionDialog">鏂板</el-button>
+            <el-button size="small"
+                       @click="openEditRegionDialog">淇敼</el-button>
+            <el-button type="danger"
+                       plain
+                       size="small"
+                       @click="handleDeleteRegion">鍒犻櫎</el-button>
+          </div>
         </div>
 
         <div class="left-search">
@@ -22,15 +30,18 @@
                        animated />
           <template v-else>
             <div class="region-item"
-                 :class="{ active: selectedRegion === '' }"
+                 :class="{ active: selectedRegionId === 0 }"
                  @click="selectRegion('')">鍏ㄩ儴</div>
-            <div v-for="item in filteredRegions"
-                 :key="item.__key"
-                 class="region-item"
-                 :class="{ active: selectedRegion === item.regionName }"
-                 @click="selectRegion(item.regionName)"
-                 :title="item.regionName">{{ item.regionName }}</div>
-            <div v-if="filteredRegions.length === 0"
+            <el-tree ref="regionTreeRef"
+                     :data="regionTreeData"
+                     node-key="id"
+                     :props="regionTreeProps"
+                     :filter-node-method="filterRegionNode"
+                     highlight-current
+                     default-expand-all
+                     :expand-on-click-node="false"
+                     @node-click="handleRegionNodeClick" />
+            <div v-if="regionTreeData.length === 0"
                  class="empty-tip">鏆傛棤鍦板尯</div>
           </template>
         </div>
@@ -96,8 +107,16 @@
                @close="closeAddRegionDialog">
       <el-form :model="addRegionForm"
                label-width="90px">
+        <el-form-item label="涓婄骇鍦板尯">
+          <el-cascader v-model="addRegionForm.parentPath"
+                       :options="regionTreeData"
+                       :props="regionCascaderProps"
+                       clearable
+                       filterable
+                       placeholder="涓嶉�夊垯涓洪《绾у湴鍖�" />
+        </el-form-item>
         <el-form-item label="鍦板尯鍚嶇О">
-          <el-input v-model="addRegionForm.regionName"
+          <el-input v-model="addRegionForm.regionsName"
                     placeholder="璇疯緭鍏�"
                     clearable />
         </el-form-item>
@@ -107,6 +126,30 @@
           <el-button type="primary"
                      @click="submitAddRegion">纭</el-button>
           <el-button @click="closeAddRegionDialog">鍙栨秷</el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <el-dialog v-model="editRegionDialogVisible"
+               title="淇敼鍦板尯"
+               width="420px"
+               @close="closeEditRegionDialog">
+      <el-form :model="editRegionForm"
+               label-width="90px">
+        <el-form-item label="涓婄骇鍦板尯">
+          <el-input :value="editRegionParentLabel"
+                    disabled />
+        </el-form-item>
+        <el-form-item label="鍦板尯鍚嶇О">
+          <el-input v-model="editRegionForm.regionsName"
+                    placeholder="璇疯緭鍏�"
+                    clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary"
+                     @click="submitEditRegion">纭</el-button>
+          <el-button @click="closeEditRegionDialog">鍙栨秷</el-button>
         </div>
       </template>
     </el-dialog>
@@ -139,6 +182,19 @@
         </el-row>
         <el-row :gutter="30">
           <el-col :span="12">
+            <el-form-item label="瀹㈡埛鍦板尯锛�"
+                          prop="regions">
+              <el-cascader v-model="formRegionPath"
+                           :options="regionTreeData"
+                           :props="regionCascaderProps"
+                           clearable
+                           filterable
+                           style="width: 100%"
+                           placeholder="璇烽�夋嫨"
+                           @change="handleFormRegionChange" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
             <el-form-item label="瀹㈡埛鍦板潃锛�"
                           prop="companyAddress">
               <el-input v-model="form.companyAddress"
@@ -146,15 +202,6 @@
                         clearable />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="瀹㈡埛鍦板尯锛�"
-                          prop="regions">
-              <el-input v-model="form.regions"
-                        placeholder="璇疯緭鍏�"
-                        clearable />
-            </el-form-item>
-          </el-col>
-
         </el-row>
         <el-row :gutter="30">
           <el-col :span="12">
@@ -350,7 +397,7 @@
             </el-col>
             <el-col :span="12">
               <div class="info-item">
-                <span class="info-label">鍏徃鐢佃瘽锛�</span>
+                <span class="info-label">瀹㈡埛鐢佃瘽锛�</span>
                 <span class="info-value">{{ detailForm.companyPhone }}</span>
               </div>
             </el-col>
@@ -358,52 +405,60 @@
           <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
-                <span class="info-label">鍏徃鍦板潃锛�</span>
+                <span class="info-label">瀹㈡埛鍦板尯锛�</span>
+                <span class="info-value">{{ detailForm.regionsName || detailForm.regions }}</span>
+              </div>
+            </el-col>
+            <el-col :span="12">
+              <div class="info-item">
+                <span class="info-label">瀹㈡埛鍦板潃锛�</span>
                 <span class="info-value">{{ detailForm.companyAddress }}</span>
               </div>
             </el-col>
+          </el-row>
+          <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">閾惰鍩烘湰鎴凤細</span>
                 <span class="info-value">{{ detailForm.basicBankAccount }}</span>
               </div>
             </el-col>
-          </el-row>
-          <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">閾惰璐﹀彿锛�</span>
                 <span class="info-value">{{ detailForm.bankAccount }}</span>
               </div>
             </el-col>
+          </el-row>
+          <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">寮�鎴疯鍙凤細</span>
                 <span class="info-value">{{ detailForm.bankCode }}</span>
               </div>
             </el-col>
-          </el-row>
-          <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">鑱旂郴浜猴細</span>
                 <span class="info-value">{{ detailForm.contactPerson }}</span>
               </div>
             </el-col>
+          </el-row>
+          <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">鑱旂郴鐢佃瘽锛�</span>
                 <span class="info-value">{{ detailForm.contactPhone }}</span>
               </div>
             </el-col>
-          </el-row>
-          <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">缁存姢浜猴細</span>
                 <span class="info-value">{{ detailForm.maintainer }}</span>
               </div>
             </el-col>
+          </el-row>
+          <el-row :gutter="20"> 
             <el-col :span="12">
               <div class="info-item">
                 <span class="info-label">缁存姢鏃堕棿锛�</span>
@@ -428,7 +483,7 @@
 </template>
 
 <script setup>
-  import { onMounted, ref, reactive, getCurrentInstance, toRefs, computed } from "vue";
+  import { onMounted, ref, reactive, getCurrentInstance, toRefs, computed, watch } from "vue";
   import { Search } from "@element-plus/icons-vue";
   import {
     addCustomer,
@@ -436,6 +491,9 @@
     getCustomer,
     listCustomer,
     listCustomerRegions,
+    addCustomerRegions,
+    updateCustomerRegions,
+    delCustomerRegions,
     updateCustomer,
     // addCustomerFollow,
     // updateCustomerFollow,
@@ -497,6 +555,8 @@
   const detailDialogVisible = ref(false);
   const detailForm = reactive({
     customerName: "",
+    regionsName: "",
+    regions: "",
     customerType: "",
     taxpayerIdentificationNumber: "",
     companyPhone: "",
@@ -654,11 +714,13 @@
       customerName: "",
       customerType: "",
       regions: "",
+      regionsId: "",
     },
     form: {
       customerName: "",
       taxpayerIdentificationNumber: "",
       companyAddress: "",
+      regions: "",
       companyPhone: "",
       contactPerson: "",
       contactPhone: "",
@@ -675,6 +737,7 @@
         { required: true, message: "璇疯緭鍏�", trigger: "blur" },
       ],
       companyAddress: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+      regions: [{ required: true, message: "璇烽�夋嫨瀹㈡埛鍦板尯", trigger: "change" }],
       companyPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
       // contactPerson: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
       // contactPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
@@ -746,19 +809,48 @@
   const { searchForm, form, rules } = toRefs(data);
 
   // 宸︿晶鍦板尯鏍�
+  const regionTreeRef = ref();
   const regionsLoading = ref(false);
-  const regions = ref([]);
+  const regionTreeData = ref([]);
   const regionKeyword = ref("");
-  const selectedRegion = ref(""); // '' 琛ㄧず鍏ㄩ儴
+  const selectedRegionId = ref(0); // 0 琛ㄧず鍏ㄩ儴
+  const selectedRegionNode = ref(null);
+  const formRegionPath = ref([]);
   const addRegionDialogVisible = ref(false);
-  const addRegionForm = reactive({ regionName: "" });
+  const editRegionDialogVisible = ref(false);
+  const addRegionForm = reactive({ parentPath: [], regionsName: "" });
+  const editRegionForm = reactive({ id: undefined, parentId: 0, regionsName: "" });
+  const regionTreeProps = { label: "label", children: "children" };
+  const regionCascaderProps = {
+    value: "id",
+    label: "label",
+    children: "children",
+    checkStrictly: true,
+    emitPath: true,
+  };
+  const regionNodeMap = computed(() => {
+    const map = new Map();
+    const walk = list => {
+      (list || []).forEach(node => {
+        map.set(node.id, node);
+        walk(node.children || []);
+      });
+    };
+    walk(regionTreeData.value);
+    return map;
+  });
+  const editRegionParentLabel = computed(() => {
+    if (!editRegionForm.parentId) return "椤剁骇鍦板尯";
+    return regionNodeMap.value.get(editRegionForm.parentId)?.label || "鏈煡";
+  });
 
-  const normalizeRegionItem = (raw, index) => {
-    if (typeof raw === "string") {
-      return { regionName: raw, __key: `s_${raw}_${index}`, __local: false };
-    }
-    const name = raw?.regionName ?? raw?.regions ?? raw?.name ?? raw?.label ?? "";
-    return { ...raw, regionName: name, __key: raw?.id ?? `o_${name}_${index}` };
+  const normalizeRegionTree = list => {
+    return (list || []).map(item => ({
+      ...item,
+      label: item.label || item.regionsName || "",
+      regionsName: item.regionsName || item.label || "",
+      children: normalizeRegionTree(item.children || []),
+    }));
   };
 
   const fetchRegions = async () => {
@@ -766,50 +858,121 @@
     try {
       const res = await listCustomerRegions({});
       const list = res?.data ?? res?.rows ?? res ?? [];
-      regions.value = Array.isArray(list)
-        ? list.map(normalizeRegionItem).filter(i => i.regionName)
-        : [];
+      regionTreeData.value = Array.isArray(list) ? normalizeRegionTree(list) : [];
     } catch (e) {
       console.error("鍦板尯鏌ヨ澶辫触:", e);
-      regions.value = [];
+      regionTreeData.value = [];
     } finally {
       regionsLoading.value = false;
     }
   };
 
-  const filteredRegions = computed(() => {
-    const kw = (regionKeyword.value || "").trim();
-    if (!kw) return regions.value;
-    return regions.value.filter(r => (r.regionName || "").includes(kw));
-  });
+  const filterRegionNode = (value, data) => {
+    if (!value) return true;
+    return (data.label || "").includes(value);
+  };
 
   const selectRegion = regionName => {
-    selectedRegion.value = regionName ?? "";
-    searchForm.value.regions = selectedRegion.value || "";
+    selectedRegionId.value = 0;
+    selectedRegionNode.value = null;
+    searchForm.value.regions = regionName || "";
+    searchForm.value.regionsId = "";
+    handleQuery();
+  };
+  const handleRegionNodeClick = data => {
+    selectedRegionId.value = data.id;
+    selectedRegionNode.value = data;
+    searchForm.value.regions = data.regionsName || data.label || "";
+    searchForm.value.regionsId = data.id;
     handleQuery();
   };
 
   const openAddRegionDialog = () => {
-    addRegionForm.regionName = "";
+    addRegionForm.parentPath = [];
+    addRegionForm.regionsName = "";
     addRegionDialogVisible.value = true;
   };
   const closeAddRegionDialog = () => {
     addRegionDialogVisible.value = false;
-    addRegionForm.regionName = "";
+    addRegionForm.parentPath = [];
+    addRegionForm.regionsName = "";
   };
-  const submitAddRegion = () => {
-    const name = (addRegionForm.regionName || "").trim();
+  const submitAddRegion = async () => {
+    const name = (addRegionForm.regionsName || "").trim();
     if (!name) return proxy.$modal.msgWarning("璇疯緭鍏ュ湴鍖哄悕绉�");
-    const exists = regions.value.some(r => r.regionName === name);
-    if (!exists) {
-      regions.value.unshift({
-        regionName: name,
-        __key: `local_${Date.now()}`,
-        __local: true,
-      });
-    }
-    proxy.$modal.msgWarning("鍦板尯鏂板鎺ュ彛鏈彁渚涳紝宸叉湰鍦版柊澧烇紙鍒锋柊鍚庡け鏁堬級");
+    const parentPath = addRegionForm.parentPath || [];
+    const parentId = parentPath.length ? parentPath[parentPath.length - 1] : 0;
+    await addCustomerRegions({ parentId, regionsName: name });
+    proxy.$modal.msgSuccess("鏂板鎴愬姛");
+    await fetchRegions();
     closeAddRegionDialog();
+  };
+
+  const openEditRegionDialog = () => {
+    if (!selectedRegionNode.value || selectedRegionId.value === 0) {
+      return proxy.$modal.msgWarning("璇峰厛閫夋嫨瑕佷慨鏀圭殑鍦板尯");
+    }
+    editRegionForm.id = selectedRegionNode.value.id;
+    editRegionForm.parentId = selectedRegionNode.value.parentId || 0;
+    editRegionForm.regionsName =
+      selectedRegionNode.value.regionsName || selectedRegionNode.value.label || "";
+    editRegionDialogVisible.value = true;
+  };
+  const closeEditRegionDialog = () => {
+    editRegionDialogVisible.value = false;
+    editRegionForm.id = undefined;
+    editRegionForm.parentId = 0;
+    editRegionForm.regionsName = "";
+  };
+  const submitEditRegion = async () => {
+    const name = (editRegionForm.regionsName || "").trim();
+    if (!name) return proxy.$modal.msgWarning("璇疯緭鍏ュ湴鍖哄悕绉�");
+    await updateCustomerRegions({
+      id: editRegionForm.id,
+      parentId: editRegionForm.parentId,
+      regionsName: name,
+    });
+    proxy.$modal.msgSuccess("淇敼鎴愬姛");
+    await fetchRegions();
+    closeEditRegionDialog();
+  };
+  const handleDeleteRegion = () => {
+    if (!selectedRegionNode.value || selectedRegionId.value === 0) {
+      return proxy.$modal.msgWarning("璇峰厛閫夋嫨瑕佸垹闄ょ殑鍦板尯");
+    }
+    ElMessageBox.confirm("鍒犻櫎鍚庝笉鍙仮澶嶏紝鏄惁缁х画锛�", "鍒犻櫎鍦板尯", {
+      confirmButtonText: "纭",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    })
+      .then(async () => {
+        await delCustomerRegions(selectedRegionNode.value.id);
+        proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+        selectRegion("");
+        await fetchRegions();
+      })
+      .catch(() => {});
+  };
+
+  const handleFormRegionChange = value => {
+    const ids = value || [];
+    if (!ids.length) {
+      form.value.regions = "";
+      return;
+    }
+    const lastId = ids[ids.length - 1];
+    form.value.regions = regionNodeMap.value.get(lastId)?.regionsName || "";
+  };
+  const findRegionPathByName = (tree, targetName, parentPath = []) => {
+    for (const item of tree || []) {
+      const currentPath = [...parentPath, item.id];
+      if ((item.regionsName || item.label) === targetName) {
+        return currentPath;
+      }
+      const childResult = findRegionPathByName(item.children || [], targetName, currentPath);
+      if (childResult.length) return childResult;
+    }
+    return [];
   };
   const addNewContact = () => {
     formYYs.value.contactList.push({
@@ -864,6 +1027,7 @@
   const openForm = (type, row) => {
     operationType.value = type;
     form.value = {};
+    formRegionPath.value = [];
     form.value.maintainer = userStore.nickName;
     formYYs.value.contactList = [
       {
@@ -878,6 +1042,10 @@
     if (type === "edit") {
       getCustomer(row.id).then(res => {
         form.value = { ...res.data };
+        formRegionPath.value = findRegionPathByName(
+          regionTreeData.value,
+          form.value.regions || ""
+        );
         formYYs.value.contactList = res.data.contactPerson
           .split(",")
           .map((item, index) => {
@@ -936,6 +1104,7 @@
   // 鍏抽棴寮规
   const closeDia = () => {
     proxy.resetForm("formRef");
+    formRegionPath.value = [];
     dialogFormVisible.value = false;
   };
   // 瀵煎嚭
@@ -1039,6 +1208,10 @@
   onMounted(() => {
     fetchRegions();
     getList();
+  });
+
+  watch(regionKeyword, value => {
+    regionTreeRef.value?.filter((value || "").trim());
   });
 </script>
 
@@ -1153,6 +1326,16 @@
     color: #303133;
   }
 
+  .left-actions {
+    display: flex;
+    gap: 6px;
+  }
+
+  .left-actions :deep(.el-button) {
+    margin-left: 0;
+    padding: 5px 10px;
+  }
+
   .left-search {
     margin-bottom: 10px;
   }
@@ -1184,6 +1367,19 @@
     font-weight: 600;
   }
 
+  .left-list :deep(.el-tree) {
+    background: transparent;
+  }
+
+  .left-list :deep(.el-tree-node__content) {
+    height: 30px;
+    border-radius: 6px;
+  }
+
+  .left-list :deep(.el-tree-node__content:hover) {
+    background: #f5f7fa;
+  }
+
   .empty-tip {
     padding: 16px 10px;
     color: #909399;

--
Gitblit v1.9.3