yyb
5 天以前 05eb7f660c2968afcfa5d8c961a17d5bf9964d2f
feat(地区管理): 增加地区增删改查功能并优化客户档案页面

- 在customerFile.js中新增addCustomerRegions、updateCustomerRegions和delCustomerRegions接口以支持地区的增、改、删操作
- 在index.vue中添加地区修改和删除按钮,优化地区选择和新增地区对话框
- 调整地区相关表单和信息展示,提高用户体验
已修改2个文件
346 ■■■■ 文件已修改
src/api/basicData/customerFile.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 318 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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'
    })
}
// 查询客户档案详细
export function getCustomer(id) {
    return request({
src/views/basicData/customerFile/index.vue
@@ -4,9 +4,17 @@
      <div class="left-panel">
        <div class="left-header">
          <div class="left-title">地区</div>
          <div class="left-actions">
          <el-button type="primary"
                     size="small"
                     @click="openAddRegionDialog">新增地区</el-button>
                       @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;