Pārlūkot izejas kodu

feat(customer): 添加客户信息详情页面的各个功能模块组件

- 新增 AddressTab.vue 组件实现地址信息列表展示和增删改功能
- 新增 BasicInfoTab.vue 组件实现企业基本信息、工商信息和付款信息管理功能
- 新增 ContactTab.vue 组件实现业务联系人搜索和管理功能
- 新增 ContractTab.vue 组件实现合同信息查询和管理功能
- 新增 PurchaseInfoTab.vue 组件实现采购信息管理和保存功能
- 新增 SupplyInfoTab.vue 组件实现供货类目、品牌、区域及授权详情管理功能
- 新增 bussineInfo API 模块提供工商信息查询和管理接口
- 定义 BusinessInfoVO、BusinessInfoForm 和 BusinessInfoQuery 类型接口
Lijingyang 1 mēnesi atpakaļ
vecāks
revīzija
cc4523ab2b

+ 77 - 0
src/api/customer/bussineInfo/index.ts

@@ -0,0 +1,77 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { BusinessInfoVO, BusinessInfoForm, BusinessInfoQuery } from '@/api/customer/bussineInfo/types';
+
+/**
+ * 查询供应商工商注册信息列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listBusinessInfo = (query?: BusinessInfoQuery): AxiosPromise<BusinessInfoVO[]> => {
+  return request({
+    url: '/customer/sbusinessInfo/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询供应商工商注册信息详细
+ * @param supplierId
+ */
+export const getBusinessInfo = (supplierId: string | number): AxiosPromise<BusinessInfoVO> => {
+  return request({
+    url: '/customer/sbusinessInfo/' + supplierId,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增供应商工商注册信息
+ * @param data
+ */
+export const addBusinessInfo = (data: BusinessInfoForm) => {
+  return request({
+    url: '/customer/sbusinessInfo',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改供应商工商注册信息
+ * @param data
+ */
+export const updateBusinessInfo = (data: BusinessInfoForm) => {
+  return request({
+    url: '/customer/sbusinessInfo',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除供应商工商注册信息
+ * @param supplierId
+ */
+export const delBusinessInfo = (supplierId: string | number | Array<string | number>) => {
+  return request({
+    url: '/customer/sbusinessInfo/' + supplierId,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取工商信息
+ * @param enterpriseName 企业名称
+ */
+export const getBusinessInformation = (enterpriseName: string) => {
+  return request({
+    url: '/customer/sbusinessInfo/getinformation',
+    method: 'get',
+    params: {
+      enterpriseName
+    }
+  });
+};

+ 206 - 0
src/api/customer/bussineInfo/types.ts

@@ -0,0 +1,206 @@
+export interface BusinessInfoVO {
+  /**
+   * 关联客户ID
+   */
+  supplierId: string | number;
+
+  /**
+   * 工商全称
+   */
+  businessName: string;
+
+  /**
+   * 统一社会信用代码
+   */
+  socialCreditCode: string;
+
+  /**
+   * 法人姓名
+   */
+  legalPersonName: string;
+
+  /**
+   * 注册资本
+   */
+  registeredCapital: string;
+
+  /**
+   * 实缴资本
+   */
+  paidInCapital: string | number;
+
+  /**
+   * 成立日期
+   */
+  establishmentDate: string;
+
+  /**
+   * 吊销日期
+   */
+  revocationDate: string;
+
+  /**
+   * 登记状态
+   */
+  registrationStatus: string;
+
+  /**
+   * 登记机关
+   */
+  registrationAuthority: string;
+
+  /**
+   * 经营范围
+   */
+  bussinessRange: string;
+
+  /**
+   * 营业执照路径
+   */
+  businessLicense: string;
+
+  /**
+   * 工商地址-详细地址
+   */
+  businessAddress: string;
+
+}
+
+export interface BusinessInfoForm extends BaseEntity {
+  /**
+   * 关联客户ID
+   */
+  supplierId?: string | number;
+
+  /**
+   * 工商全称
+   */
+  businessName?: string;
+
+  /**
+   * 统一社会信用代码
+   */
+  socialCreditCode?: string;
+
+  /**
+   * 法人姓名
+   */
+  legalPersonName?: string;
+
+  /**
+   * 注册资本
+   */
+  registeredCapital?: string;
+
+  /**
+   * 实缴资本
+   */
+  paidInCapital?: string | number;
+
+  /**
+   * 成立日期
+   */
+  establishmentDate?: string;
+
+  /**
+   * 吊销日期
+   */
+  revocationDate?: string;
+
+  /**
+   * 登记状态
+   */
+  registrationStatus?: string;
+
+  /**
+   * 登记机关
+   */
+  registrationAuthority?: string;
+
+  /**
+   * 经营范围
+   */
+  bussinessRange?: string;
+
+  /**
+   * 营业执照路径
+   */
+  businessLicense?: string;
+
+  /**
+   * 工商地址-详细地址
+   */
+  businessAddress?: string;
+
+}
+
+export interface BusinessInfoQuery extends PageQuery {
+
+  /**
+   * 工商全称
+   */
+  businessName?: string;
+
+  /**
+   * 统一社会信用代码
+   */
+  socialCreditCode?: string;
+
+  /**
+   * 法人姓名
+   */
+  legalPersonName?: string;
+
+  /**
+   * 注册资本
+   */
+  registeredCapital?: string;
+
+  /**
+   * 实缴资本
+   */
+  paidInCapital?: string | number;
+
+  /**
+   * 成立日期
+   */
+  establishmentDate?: string;
+
+  /**
+   * 吊销日期
+   */
+  revocationDate?: string;
+
+  /**
+   * 登记状态
+   */
+  registrationStatus?: string;
+
+  /**
+   * 登记机关
+   */
+  registrationAuthority?: string;
+
+  /**
+   * 经营范围
+   */
+  bussinessRange?: string;
+
+  /**
+   * 营业执照路径
+   */
+  businessLicense?: string;
+
+  /**
+   * 工商地址-详细地址
+   */
+  businessAddress?: string;
+
+    /**
+     * 日期范围参数
+     */
+    params?: any;
+}
+
+
+

+ 3 - 4
src/api/system/addressarea/index.ts

@@ -23,12 +23,11 @@ export const listAddressarea = (query?: AddressareaQuery): AxiosPromise<Addressa
  * @param query
  * @returns {*}
  */
-
 export const chinaAreaList = (query?: AddressareaQuery): AxiosPromise<AddressareaVO[]> => {
   return request({
-    url: '/system/addressarea/getchina/area',
-    method: 'get',
-    params: query
+    url: '/system/addressarea/getChinaList',
+    method: 'get'
+    ,params: query
   });
 };
 

+ 109 - 5
src/components/RegionCascader/index.vue

@@ -9,12 +9,13 @@
             v-for="province in provinces" 
             :key="province.value"
             class="region-item"
-            :class="{ 'is-active': isProvinceSelected(province.value), 'is-checked': isProvinceChecked(province.value) }"
+            :class="{ 'is-active': isProvinceSelected(province.value), 'is-checked': getProvinceCheckState(province).checked || getProvinceCheckState(province).indeterminate }"
             @click="handleProvinceClick(province)"
           >
             <el-checkbox 
               v-if="multiple"
-              :model-value="isProvinceChecked(province.value)"
+              :model-value="getProvinceCheckState(province).checked"
+              :indeterminate="getProvinceCheckState(province).indeterminate"
               @click.stop
               @change="handleProvinceCheck(province, $event)"
             >
@@ -48,7 +49,7 @@
               {{ city.label }}
             </el-checkbox>
             <span v-else>{{ city.label }}</span>
-            <el-icon v-if="city.children && city.children.length > 0" class="arrow-icon">
+            <el-icon v-if="showDistrict && city.children && city.children.length > 0" class="arrow-icon">
               <ArrowRight />
             </el-icon>
           </div>
@@ -59,7 +60,7 @@
       </div>
       
       <!-- 右侧:区县列表 -->
-      <div class="region-list district-list">
+      <div v-if="showDistrict" class="region-list district-list">
         <div class="list-header">区县</div>
         <div class="list-body">
           <div 
@@ -101,6 +102,7 @@ interface Props {
   modelValue?: string[]; // 选中的区域代码数组
   multiple?: boolean; // 是否多选模式,默认 true
   options?: RegionData[]; // 省市区数据源(建议由后端接口提供)
+  showDistrict?: boolean; // 是否展示区县列(供货区域可关闭,仅省/市两级)
 }
 
 interface Emits {
@@ -110,7 +112,8 @@ interface Emits {
 const props = withDefaults(defineProps<Props>(), {
   modelValue: () => [],
   multiple: true,
-  options: () => []
+  options: () => [],
+  showDistrict: true
 });
 
 const emit = defineEmits<Emits>();
@@ -137,9 +140,18 @@ const currentDistricts = computed(() => {
 // 选中的区域代码
 const selectedRegions = ref<string[]>(props.modelValue || []);
 
+// 仅省/市模式下:记录“手动勾选”的省份(用于判断取消城市时是否自动取消父省)
+const manualCheckedProvinces = ref<Set<string>>(new Set());
+
 // 监听 props 变化
 watch(() => props.modelValue, (newVal) => {
   selectedRegions.value = newVal || [];
+
+  if (!props.showDistrict) {
+    // 在省/市模式下,v-model 会同时包含省ID和市ID。
+    // manualCheckedProvinces 仅用于区分“用户手动点省”与“由市联动带上省”。
+    // 这里不做强推断,避免误判;仅在 handleProvinceCheck 中维护。
+  }
 }, { immediate: true });
 
 watch(() => props.options, (newVal) => {
@@ -156,6 +168,9 @@ const isProvinceSelected = (provinceCode: string) => {
 
 /** 判断省份是否被勾选 */
 const isProvinceChecked = (provinceCode: string) => {
+  if (!props.showDistrict) {
+    return selectedRegions.value.includes(provinceCode);
+  }
   const province = provinces.value.find(p => p.value === provinceCode);
   if (!province || !province.children) return false;
   
@@ -179,6 +194,9 @@ const isCitySelected = (cityCode: string) => {
 
 /** 判断城市是否被勾选 */
 const isCityChecked = (cityCode: string) => {
+  if (!props.showDistrict) {
+    return selectedRegions.value.includes(cityCode);
+  }
   const city = currentCities.value.find(c => c.value === cityCode);
   if (!city || !city.children) return false;
   
@@ -191,6 +209,36 @@ const isDistrictChecked = (districtCode: string) => {
   return selectedRegions.value.includes(districtCode);
 };
 
+const findProvinceByCityValue = (cityValue: string): RegionData | null => {
+  for (const p of provinces.value || []) {
+    const cities = p.children || [];
+    if (cities.some((c) => c.value === cityValue)) return p;
+  }
+  return null;
+};
+
+const hasAnySelectedCityUnderProvince = (provinceValue: string) => {
+  const province = (provinces.value || []).find((p) => p.value === provinceValue);
+  if (!province) return false;
+  const cities = province.children || [];
+  return cities.some((c) => selectedRegions.value.includes(c.value));
+};
+
+const getProvinceCheckState = (province: RegionData) => {
+  if (!props.showDistrict) {
+    const cities = province.children || [];
+    const total = cities.length;
+    const selectedCount = cities.filter((c) => selectedRegions.value.includes(c.value)).length;
+
+    const checked = total > 0 ? selectedCount === total : selectedRegions.value.includes(province.value);
+    const indeterminate = total > 0 ? selectedCount > 0 && selectedCount < total : false;
+
+    return { checked, indeterminate };
+  }
+
+  return { checked: isProvinceChecked(province.value), indeterminate: false };
+};
+
 /** 点击省份 */
 const handleProvinceClick = (province: RegionData) => {
   currentProvince.value = province;
@@ -217,6 +265,31 @@ const handleDistrictClick = (district: RegionData) => {
 
 /** 勾选/取消省份 */
 const handleProvinceCheck = (province: RegionData, checked: boolean | string | number) => {
+  if (!props.showDistrict) {
+    const isChecked = !!checked;
+    const next = new Set(selectedRegions.value);
+
+    const cities = province.children || [];
+
+    if (isChecked) {
+      next.add(province.value);
+      manualCheckedProvinces.value.add(province.value);
+
+      // 省勾选:该省下所有市一起勾选
+      cities.forEach((c) => next.add(c.value));
+    } else {
+      // 省取消:该省下所有市一起取消
+      next.delete(province.value);
+      manualCheckedProvinces.value.delete(province.value);
+
+      cities.forEach((c) => next.delete(c.value));
+    }
+
+    selectedRegions.value = Array.from(next);
+    emit('update:modelValue', selectedRegions.value);
+    return;
+  }
+
   const isChecked = !!checked;
   if (!province.children) return;
   
@@ -249,6 +322,37 @@ const handleProvinceCheck = (province: RegionData, checked: boolean | string | n
 
 /** 勾选/取消城市 */
 const handleCityCheck = (city: RegionData, checked: boolean | string | number) => {
+  if (!props.showDistrict) {
+    const isChecked = !!checked;
+    const next = new Set(selectedRegions.value);
+
+    if (isChecked) {
+      next.add(city.value);
+
+      const parentProvince = findProvinceByCityValue(city.value);
+      if (parentProvince) {
+        next.add(parentProvince.value);
+      }
+    } else {
+      next.delete(city.value);
+
+      const parentProvince = findProvinceByCityValue(city.value);
+      if (parentProvince) {
+        const provinceValue = parentProvince.value;
+        // 若该省下已没有任何选中的城市,则自动取消父省
+        selectedRegions.value = Array.from(next);
+        if (!hasAnySelectedCityUnderProvince(provinceValue)) {
+          next.delete(provinceValue);
+          manualCheckedProvinces.value.delete(provinceValue);
+        }
+      }
+    }
+
+    selectedRegions.value = Array.from(next);
+    emit('update:modelValue', selectedRegions.value);
+    return;
+  }
+
   const isChecked = !!checked;
   if (!city.children) return;
   

+ 51 - 0
src/views/customer/info/components/AddressTab.vue

@@ -0,0 +1,51 @@
+<template>
+  <div class="tab-content">
+    <div class="info-section">
+      <div class="section-title-row">
+        <div class="section-title-left">
+          <span class="section-title-text">地址信息列表</span>
+        </div>
+        <el-button v-if="!isViewMode" icon="Plus" type="primary" @click="emit('add')">添加地址</el-button>
+      </div>
+      <el-table :data="addressList" border style="width: 100%">
+        <el-table-column prop="supplierNo" label="供应商编号" align="center" />
+        <el-table-column prop="addressNo" label="地址编号" align="center" />
+        <el-table-column prop="shipperName" label="姓名" align="center" />
+        <el-table-column prop="shipperPhone" label="手机号码" align="center" />
+        <el-table-column prop="shippingProvincial" label="省份" align="center" />
+        <el-table-column prop="shippingCity" label="市" align="center" />
+        <el-table-column prop="shippingCounty" label="区县" align="center" />
+        <el-table-column label="详细地址" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.shippingAddress || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="shippingPostCode" label="邮政编码" align="center" />
+        <el-table-column prop="isSelf" label="默认地址" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.isSelf === 1 || scope.row.isSelf === '1' ? '是' : '否' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150">
+          <template #default="scope">
+            <el-button v-if="!isViewMode" link type="primary" @click="emit('edit', scope.row)">编辑</el-button>
+            <el-button v-if="!isViewMode" link type="danger" @click="emit('delete', scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  addressList: any[];
+  isViewMode: boolean;
+}>();
+
+const emit = defineEmits<{
+  (e: 'add'): void;
+  (e: 'edit', row: any): void;
+  (e: 'delete', row: any): void;
+}>();
+</script>

+ 422 - 0
src/views/customer/info/components/BasicInfoTab.vue

@@ -0,0 +1,422 @@
+<template>
+  <div class="tab-content">
+    <div class="info-section">
+      <div class="section-title-row">
+        <div class="section-title-left">
+          <span class="section-title-text">企业基本信息</span>
+          <span class="section-title-divider">/</span>
+          <span class="supplier-no">供应商编码:{{ detailData.supplierNo }}</span>
+        </div>
+        <el-button v-if="!isViewMode" type="primary" icon="Document" @click="emit('save')">保存</el-button>
+      </div>
+
+      <el-form :model="detailData" label-width="120px" class="detail-form">
+        <el-row :gutter="12" class="form-row">
+          <el-col :span="8">
+            <el-form-item label="所属公司:" required>
+              <el-select v-model="detailData.ownedCompany" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="company in companyOptions" :key="company.id" :label="company.companyName" :value="company.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="企业名称:" required>
+              <el-input v-model="detailData.enterpriseName" placeholder="请输入企业名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="工商名称:" required>
+              <el-input
+                v-model="detailData.businessName"
+                placeholder="请输入工商名称"
+                @blur="isAddMode && !isBasicInfoSaved && !isViewMode && emit('getBusinessInfo')"
+                @keyup.enter="isAddMode && !isBasicInfoSaved && !isViewMode && emit('getBusinessInfo')"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="12" class="form-row">
+          <el-col :span="8">
+            <el-form-item label="企业简称:" required>
+              <el-input v-model="detailData.shortName" placeholder="请输入企业简称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="供应商等级:" required>
+              <el-select v-model="detailData.cooperateLevel" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="level in supplierLevelOptions" :key="level.id" :label="level.supplierLevelName" :value="String(level.id)" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="企业规模:" required>
+              <el-select v-model="detailData.membershipSize" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="scale in enterpriseScaleOptions" :key="scale.id" :label="scale.enterpriseScaleName" :value="scale.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="12" class="form-row">
+          <el-col :span="8">
+            <el-form-item label="行业类别:" required>
+              <el-select v-model="detailData.industrCategory" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="industry in industryCategoryOptions" :key="industry.id" :label="industry.industryCategoryName" :value="industry.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="供应商类型:" required>
+              <el-select v-model="detailData.supplierType" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="type in supplierTypeOptions" :key="type.id" :label="type.supplierTypeName" :value="type.id" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="固定电话:">
+              <el-input
+                v-model="detailData.fixedPhone"
+                placeholder="请输入固定电话"
+                type="tel"
+                @input="onFixedPhoneInput"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="12" class="form-row">
+          <el-col :span="8">
+            <el-form-item label="传真:">
+              <el-input v-model="detailData.fax" placeholder="请输入传真" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="企业邮箱:">
+              <el-input v-model="detailData.mailbox" placeholder="请输入企业邮箱" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="开始时间:">
+              <el-date-picker v-model="detailData.validityFromDate" type="date" placeholder="请选择" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="12" class="form-row">
+          <el-col :span="8">
+            <el-form-item label="结束时间:">
+              <el-date-picker v-model="detailData.validityToDate" type="date" placeholder="请选择" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="邮政编码:">
+              <el-input v-model="detailData.postCode" placeholder="请输入邮政编码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="网址:">
+              <el-input v-model="detailData.url" placeholder="请输入网址" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="12" class="form-row">
+          <el-col :span="8">
+            <el-form-item label="详细地址:" required>
+              <el-cascader v-model="selectedOfficeRegionProxy" :options="regionOptions" placeholder="请选择省市区" clearable filterable style="width: 100%;" @change="onOfficeRegionChange" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label=" " label-width="0">
+              <el-input v-model="detailData.officeAddress" placeholder="请输入详细地址" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </div>
+
+    <div class="info-section">
+      <div class="section-title">工商信息</div>
+
+      <el-row :gutter="12" class="form-row">
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">企业工商名称:</span>
+            <span class="value">{{ businessInfo.businessName || detailData.businessName || '' }}</span>
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">登记机关:</span>
+            <span class="value">{{ businessInfo.registrationAuthority || '' }}</span>
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">成立日期:</span>
+            <span class="value">{{ formatDate(businessInfo.establishmentDate) || '' }}</span>
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="12" class="form-row">
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">登记状态:</span>
+            <span class="value">{{ businessInfo.registrationStatus || '' }}</span>
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">实缴资本:</span>
+            <span class="value">{{ businessInfo.paidInCapital || '' }}</span>
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">社会信用代码:</span>
+            <span class="value">{{ businessInfo.socialCreditCode || detailData.socialCreditCode || '' }}</span>
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="12" class="form-row">
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">法人姓名:</span>
+            <span class="value">{{ businessInfo.legalPersonName || '' }}</span>
+          </div>
+        </el-col>
+        <el-col :span="8">
+          <div class="form-item">
+            <span class="label">注册资本:</span>
+            <span class="value">{{ businessInfo.registeredCapital || '' }}</span>
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="12" class="form-row">
+        <el-col :span="24">
+          <div class="form-item">
+            <span class="label">工商地址:</span>
+            <el-input v-model="businessInfo.businessAddress" placeholder="工商地址" style="width: 70%;" />
+          </div>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="12" class="form-row">
+        <el-col :span="12">
+          <div class="form-item">
+            <span class="label">营业执照:</span>
+            <ImageUpload
+              v-model="detailData.businessLicense"
+              :limit="1"
+              :disabled="isViewMode"
+              :file-size="5"
+              :file-type="['png', 'jpg', 'jpeg']"
+              :is-show-tip="false"
+            />
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="form-item">
+            <span class="label">法人身份证照片:</span>
+            <ImageUpload
+              v-model="detailData.personImageUrl"
+              :limit="1"
+              :disabled="isViewMode"
+              :file-size="5"
+              :file-type="['png', 'jpg', 'jpeg']"
+              :is-show-tip="false"
+            />
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
+    <div class="info-section">
+      <div class="section-title-row">
+        <div class="section-title-left">
+          <span class="section-title-text">付款信息</span>
+        </div>
+        <el-button v-if="!isViewMode" type="primary" icon="Plus" @click="emit('addPayment')">新增付款信息</el-button>
+      </div>
+      <el-table :data="paymentInfoList" border style="width: 100%">
+        <el-table-column prop="isture" label="是否主账号" align="center">
+          <template #default="scope">
+            <span>{{ Number(scope.row.isture) === 1 ? '是' : '否' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="bankName" label="开户银行" align="center" />
+        <el-table-column prop="bankNo" label="银行账户" align="center" />
+
+        <el-table-column label="操作" align="center" width="120">
+          <template #default="scope">
+            <el-button link type="primary" @click="emit('viewPayment', scope.row)">查看</el-button>
+            <el-button v-if="!isViewMode" link type="primary" @click="emit('editPayment', scope.row)">编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ImageUpload from '@/components/ImageUpload/index.vue';
+
+const props = defineProps<{
+  detailData: any;
+  companyOptions: any[];
+  enterpriseScaleOptions: any[];
+  industryCategoryOptions: any[];
+  supplierLevelOptions: any[];
+  supplierTypeOptions: any[];
+  isAddMode: boolean;
+  isViewMode: boolean;
+  isBasicInfoSaved: boolean;
+  businessInfo: any;
+  businessInfoLoading: boolean;
+  regionOptions: any[];
+  selectedOfficeRegion: string[];
+  paymentInfoList: any[];
+  formatDate: (date: any) => string;
+}>();
+
+const emit = defineEmits<{
+  (e: 'save'): void;
+  (e: 'getBusinessInfo'): void;
+  (e: 'officeRegionChange', value: string[]): void;
+  (e: 'update:selectedOfficeRegion', value: string[]): void;
+  (e: 'addPayment'): void;
+  (e: 'viewPayment', row: any): void;
+  (e: 'editPayment', row: any): void;
+}>();
+
+const selectedOfficeRegionProxy = computed({
+  get: () => props.selectedOfficeRegion,
+  set: (value) => emit('update:selectedOfficeRegion', value)
+});
+
+const onOfficeRegionChange = (val: unknown) => {
+  emit('officeRegionChange', (val || []) as string[]);
+};
+
+const onFixedPhoneInput = (val: string) => {
+  const next = (val || '').replace(/\D+/g, '');
+  if (next !== props.detailData.fixedPhone) {
+    props.detailData.fixedPhone = next;
+  }
+};
+</script>
+
+<style scoped>
+.tab-content {
+  --el-component-size: 24px;
+  padding: 16px;
+}
+
+.detail-form :deep(.el-form-item__label) {
+  white-space: nowrap;
+}
+
+.detail-form :deep(.form-row) {
+  margin-bottom: 2px;
+}
+
+.detail-form :deep(.el-form-item) {
+  margin-bottom: 8px;
+}
+
+.tab-content :deep(.form-row) {
+  margin-bottom: 2px;
+}
+
+.tab-content :deep(.el-form-item) {
+  margin-bottom: 8px;
+}
+
+.tab-content :deep(.info-section) {
+  margin-bottom: 20px;
+}
+
+.tab-content :deep(.section-title),
+.tab-content :deep(.section-title-row) {
+  margin-bottom: 12px;
+  padding-bottom: 8px;
+}
+
+.tab-content :deep(.section-title-text) {
+  font-size: 14px;
+}
+
+.tab-content :deep(.supplier-no) {
+  font-size: 13px;
+}
+
+.tab-content :deep(.el-form-item__label) {
+  font-size: 13px;
+  height: var(--el-component-size);
+  line-height: var(--el-component-size);
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.tab-content :deep(.form-item) {
+  line-height: 24px;
+  font-size: 13px;
+}
+
+.detail-form :deep(.el-input),
+.detail-form :deep(.el-select),
+.detail-form :deep(.el-date-editor),
+.detail-form :deep(.el-cascader) {
+  width: 100%;
+}
+
+.detail-form :deep(.el-input__wrapper),
+.detail-form :deep(.el-select__wrapper),
+.detail-form :deep(.el-date-editor),
+.detail-form :deep(.el-cascader) {
+  min-height: var(--el-component-size);
+}
+
+.detail-form :deep(.el-input__inner) {
+  height: var(--el-component-size);
+  line-height: var(--el-component-size);
+}
+
+.detail-form :deep(.el-form-item__content) {
+  align-items: center;
+  min-height: var(--el-component-size);
+}
+
+.detail-form :deep(.el-input__wrapper),
+.detail-form :deep(.el-select__wrapper),
+.detail-form :deep(.el-date-editor),
+.detail-form :deep(.el-cascader) {
+  height: var(--el-component-size);
+  align-items: center;
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.detail-form :deep(.el-select__selected-item),
+.detail-form :deep(.el-select__input),
+.detail-form :deep(.el-range-input) {
+  line-height: var(--el-component-size);
+}
+
+:deep(.component-upload-image .el-upload--picture-card),
+:deep(.component-upload-image .el-upload-list--picture-card .el-upload-list__item) {
+  width: 64px;
+  height: 64px;
+}
+
+:deep(.component-upload-image .el-upload-list--picture-card .el-upload-list__item-thumbnail) {
+  width: 64px;
+  height: 64px;
+}
+</style>

+ 65 - 0
src/views/customer/info/components/ContactTab.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="tab-content">
+    <div class="info-section">
+      <div class="section-title">业务联系人</div>
+
+      <el-form :model="contactSearchParams" :inline="true" style="margin-bottom: 20px;">
+        <el-form-item label="用户ID">
+          <el-input v-model="contactSearchParams.userNo" placeholder="用户ID" clearable style="width: 200px;" />
+        </el-form-item>
+        <el-form-item label="员工姓名">
+          <el-input v-model="contactSearchParams.userName" placeholder="员工姓名" clearable style="width: 200px;" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="emit('search')">搜索</el-button>
+          <el-button icon="Refresh" @click="emit('reset')">重置</el-button>
+          <el-button v-if="!isViewMode" type="primary" icon="Plus" @click="emit('add')">新增联系人</el-button>
+        </el-form-item>
+      </el-form>
+
+      <el-table v-loading="contactLoading" :data="contactList" border style="width: 100%">
+        <el-table-column prop="supplierName" label="所属供应商" align="center" />
+        <el-table-column prop="userNo" label="用户ID" align="center" />
+        
+        <el-table-column prop="userName" label="员工姓名" align="center" />
+        <el-table-column prop="phone" label="手机号" align="center" />
+        <el-table-column prop="roleNo" label="角色" align="center" />
+        <el-table-column prop="departmentNo" label="部门" align="center" />
+        <el-table-column prop="position" label="职位" align="center" />
+        <el-table-column prop="isPrimaryContact" label="主要联系人" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.isPrimaryContact === '1' ? '是' : '否' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="isRegister" label="允许登录供应商端" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.isRegister === '1' ? '是' : '否' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="emit('view', scope.row)">查看</el-button>
+            <el-button v-if="!isViewMode" link type="primary" @click="emit('edit', scope.row)">编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  contactSearchParams: { userNo: string; userName: string };
+  contactList: any[];
+  contactLoading: boolean;
+  isViewMode: boolean;
+}>();
+
+const emit = defineEmits<{
+  (e: 'search'): void;
+  (e: 'reset'): void;
+  (e: 'add'): void;
+  (e: 'view', row: any): void;
+  (e: 'edit', row: any): void;
+}>();
+</script>

+ 140 - 0
src/views/customer/info/components/ContractTab.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="tab-content">
+    <div class="info-section">
+      <el-form :model="contractSearchParams" label-width="70px" style="margin-bottom: 20px;">
+        <el-row :gutter="16">
+          <el-col :span="6">
+            <el-form-item label="合同编号">
+              <el-input v-model="contractSearchParams.contractNo" placeholder="请输入合同编号" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="合同名称">
+              <el-input v-model="contractSearchParams.contractName" placeholder="请输入合同名称" clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="合同类型">
+              <el-select v-model="contractSearchParams.contractType" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="item in contractTypeDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="开始时间">
+              <el-date-picker v-model="contractSearchParams.contractStartTime" type="date" placeholder="请选择" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="16">
+          <el-col :span="6">
+            <el-form-item label="结束时间">
+              <el-date-picker v-model="contractSearchParams.contractEndTime" type="date" placeholder="请选择" style="width: 100%;" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6">
+            <el-form-item label="合同状态">
+              <el-select v-model="contractSearchParams.contractStatus" placeholder="请选择" clearable filterable style="width: 100%;">
+                <el-option v-for="item in contractStatusDict" :key="item.dictValue" :label="item.dictLabel" :value="item.dictValue" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label-width="0">
+              <el-button type="primary" icon="Search" @click="emit('search')">搜索</el-button>
+              <el-button icon="Refresh" @click="emit('reset')">重置</el-button>
+              <el-button v-if="!isViewMode" icon="Plus" type="primary" @click="emit('add')">添加合同</el-button>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div class="section-title">合同信息列表</div>
+
+      <el-table :data="contractList" border style="width: 100%">
+        <el-table-column prop="contractNo" label="合同编号" align="center" />
+        <el-table-column prop="contractName" label="合同名称" align="center" />
+        <el-table-column prop="contractType" label="合同类型" align="center">
+          <template #default="scope">
+            <span>{{ getContractTypeText(scope.row.contractType) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="contractAmount" label="金额(万)" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.contractAmount || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="contractStartTime" label="起始时间" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.contractStartTime ? formatDate(scope.row.contractStartTime) : '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="contractEndTime" label="截止时间" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.contractEndTime ? formatDate(scope.row.contractEndTime) : '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createTime" label="上传时间" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.createTime ? formatDate(scope.row.createTime) : '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="contractAttachment" label="附件管理" align="center">
+          <template #default="scope">
+            <el-button v-if="scope.row.contractAttachment" link type="primary" @click="emit('viewAttachment', scope.row)">查看附件</el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="contractStatus" label="状态" align="center">
+          <template #default="scope">
+            <span>{{ getContractStatusText(scope.row.contractStatus) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="120">
+          <template #default="scope">
+            <el-button link type="primary" @click="emit('view', scope.row)">查看</el-button>
+            <el-button v-if="!isViewMode" link type="primary" @click="emit('edit', scope.row)">编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div style="margin-top: 20px; display: flex; justify-content: flex-end;">
+        <el-pagination
+          v-model:current-page="contractPagination.pageNum"
+          v-model:page-size="contractPagination.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="contractPagination.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="emit('sizeChange', $event)"
+          @current-change="emit('currentChange', $event)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+  contractSearchParams: any;
+  contractList: any[];
+  contractPagination: { pageNum: number; pageSize: number; total: number };
+  isViewMode: boolean;
+  contractTypeDict: any[];
+  contractStatusDict: any[];
+  formatDate: (date: any) => string;
+  getContractTypeText: (type: any) => string;
+  getContractStatusText: (status: any) => string;
+}>();
+
+const emit = defineEmits<{
+  (e: 'search'): void;
+  (e: 'reset'): void;
+  (e: 'add'): void;
+  (e: 'viewAttachment', row: any): void;
+  (e: 'view', row: any): void;
+  (e: 'edit', row: any): void;
+  (e: 'sizeChange', size: number): void;
+  (e: 'currentChange', page: number): void;
+}>();
+</script>

+ 54 - 0
src/views/customer/info/components/PurchaseInfoTab.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="tab-content">
+    <div class="info-section">
+      <div class="section-title" style="display: flex; align-items: center; justify-content: space-between;">
+        <span>采购信息</span>
+        <el-button v-if="!isViewMode" type="primary" icon="Document" @click="emit('save')">保存</el-button>
+      </div>
+
+      <el-row :gutter="40" class="form-row">
+        <el-col :span="12">
+          <div class="form-item">
+            <span class="label">产品经理:</span>
+            <el-select v-model="selectedProductManagerProxy" placeholder="请选择产品经理" filterable clearable style="width: 300px;">
+              <el-option v-for="item in staffOptions" :key="item.staffId" :label="item.displayText" :value="item.staffId" />
+            </el-select>
+          </div>
+        </el-col>
+        <el-col :span="12">
+          <div class="form-item">
+            <span class="label">采购员:</span>
+            <el-select v-model="selectedBuyerProxy" placeholder="请选择采购员" filterable clearable style="width: 300px;">
+              <el-option v-for="item in staffOptions" :key="item.staffId" :label="item.displayText" :value="item.staffId" />
+            </el-select>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+  staffOptions: any[];
+  selectedProductManager: number | null;
+  selectedBuyer: number | null;
+  isViewMode: boolean;
+}>();
+
+const emit = defineEmits<{
+  (e: 'save'): void;
+  (e: 'update:selectedProductManager', value: number | null): void;
+  (e: 'update:selectedBuyer', value: number | null): void;
+}>();
+
+const selectedProductManagerProxy = computed({
+  get: () => props.selectedProductManager,
+  set: (value) => emit('update:selectedProductManager', value)
+});
+
+const selectedBuyerProxy = computed({
+  get: () => props.selectedBuyer,
+  set: (value) => emit('update:selectedBuyer', value)
+});
+</script>

+ 131 - 0
src/views/customer/info/components/SupplyInfoTab.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="tab-content">
+    <div class="info-section">
+      <div class="section-title-row">
+        <div class="section-title-left">
+          <span class="section-title-text">供货类目</span>
+        </div>
+        <el-button v-if="!isViewMode" type="primary" icon="Document" @click="emit('saveCategories')">保存</el-button>
+      </div>
+
+      <el-checkbox-group v-model="selectedCategoriesProxy" class="category-group">
+        <el-checkbox v-for="category in productCategoryList" :key="category.id" :label="String(category.id)">
+          {{ category.categoryName }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+
+    <div class="info-section">
+      <div class="section-title-row">
+        <div class="section-title-left">
+          <span class="section-title-text">供货品牌</span>
+        </div>
+        <div>
+          <el-button v-if="!isViewMode" type="primary" icon="Plus" @click="emit('addBrand')">新增</el-button>
+        </div>
+      </div>
+      <div class="brand-display-wrapper">
+        <el-tag v-for="brand in selectedBrands" :key="brand.id" closable @close="emit('removeBrand', brand)" style="margin-right: 10px; margin-bottom: 10px;">
+          {{ brand.brandName }}
+        </el-tag>
+        <span v-if="selectedBrands.length === 0" style="color: #999;">暂无品牌信息</span>
+      </div>
+    </div>
+
+    <div class="info-section">
+      <div class="section-title-row">
+        <div class="section-title-left">
+          <span class="section-title-text">供货区域</span>
+        </div>
+        <el-button v-if="!isViewMode" type="primary" icon="Edit" @click="emit('editSupplyArea')">编辑</el-button>
+      </div>
+      <el-table :data="supplyAreaList" border style="width: 100%">
+        <el-table-column type="index" label="序号" align="center" width="80" />
+        <el-table-column prop="province" label="供货区域(省)" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.province || '' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="city" label="供货区域(市)" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.city || '' }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <div class="info-section">
+      <div class="section-title">授权详情信息列表</div>
+      <el-table :data="authorizationList" border style="width: 100%">
+        <el-table-column type="index" label="序号" align="center" width="80" />
+        <el-table-column prop="supplierName" label="供应商名称" align="center" />
+        <el-table-column prop="brandName" label="品牌名称" align="center" />
+        <el-table-column label="一级类目" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.categorysMap?.oneLevelName || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="二级类目" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.categorysMap?.twoLevelName || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="三级类目" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.categorysMap?.threeLevelName || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="authorizationEndTime" label="禁止时间" align="center">
+          <template #default="scope">
+            <span>{{ scope.row.authorizationEndTime ? formatDate(scope.row.authorizationEndTime) : '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="province" label="授权区域(省)" align="center" />
+        <el-table-column prop="city" label="授权区域(市)" align="center" />
+        <el-table-column prop="authorizedStatus" label="状态" align="center">
+          <template #default="scope">
+            <span>{{ getAuthorizedStatusText(scope.row.authorizedStatus) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="150">
+          <template #default>
+            <el-button link type="primary">查看</el-button>
+            <el-button link type="primary">编辑</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div style="margin-top: 20px; display: flex; justify-content: flex-end;">
+        <Pagination v-model:page="authorizationPagination.pageNum" v-model:limit="authorizationPagination.pageSize" :total="authorizationPagination.total" @pagination="emit('authorizationPagination')" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+  isViewMode: boolean;
+  productCategoryList: any[];
+  selectedCategories: string[];
+  selectedBrands: any[];
+  supplyAreaList: any[];
+  authorizationList: any[];
+  authorizationPagination: { pageNum: number; pageSize: number; total: number };
+  formatDate: (date: any) => string;
+  getAuthorizedStatusText: (status: string) => string;
+}>();
+
+const emit = defineEmits<{
+  (e: 'saveCategories'): void;
+  (e: 'addBrand'): void;
+  (e: 'removeBrand', brand: any): void;
+  (e: 'editSupplyArea'): void;
+  (e: 'authorizationPagination'): void;
+  (e: 'update:selectedCategories', value: string[]): void;
+}>();
+
+const selectedCategoriesProxy = computed({
+  get: () => props.selectedCategories,
+  set: (value) => emit('update:selectedCategories', value)
+});
+</script>

+ 106 - 105
src/views/customer/info/detail.vue

@@ -316,7 +316,7 @@
       width="700px"
       :close-on-click-modal="false"
     >
-      <RegionCascader v-model="selectedSupplyAreaCodes" :multiple="true" :options="supplyAreaOptions" />
+      <RegionCascader v-model="selectedSupplyAreaCodes" :multiple="true" :options="supplyAreaOptions" :show-district="false" />
 
       <template #footer>
         <div class="dialog-footer">
@@ -778,7 +778,7 @@ const authorizationPagination = ref({
 // 供货区域对话框相关
 const supplyAreaDialogVisible = ref(false);
 const supplyAreaSubmitLoading = ref(false);
-const selectedSupplyAreaCodes = ref<string[]>([]); // 选中的供货区域(区县ID数组)
+const selectedSupplyAreaCodes = ref<string[]>([]); // 选中的供货区域(省/市ID数组)
 const savedAreaData = ref<any[]>([]); // 已保存的供货区域数据
 
 // 供货区域选择器数据源(从接口获取)
@@ -1958,89 +1958,54 @@ const handleEditSupplyArea = async () => {
   selectedSupplyAreaCodes.value = buildSupplyAreaCodesFromSavedData(savedAreaData.value);
 };
 
-/** 根据已保存的省/市数据生成 RegionCascader 需要的区县编码列表 */
+/** 根据已保存的省/市数据生成 RegionCascader 需要的省/市ID列表 */
 const buildSupplyAreaCodesFromSavedData = (areaData: any[]) => {
   const codes = new Set<string>();
   if (!areaData || areaData.length === 0) return [];
 
-  const provinces: any[] = supplyAreaOptions.value || [];
-
-  const collectDistrictIdsUnderNode = (node: any) => {
-    const result: string[] = [];
-    const dfs = (n: any) => {
-      if (!n) return;
-      if (!n.children || n.children.length === 0) {
-        if (n.value) result.push(String(n.value));
-        return;
-      }
-      n.children.forEach((c: any) => dfs(c));
-    };
-    dfs(node);
-    return result;
-  };
-
-  const findNodeById = (id: string) => {
-    for (const p of provinces) {
-      if (String(p.value) === id) return p;
-      for (const c of p.children || []) {
-        if (String(c.value) === id) return c;
-      }
-    }
-    return null;
-  };
-
   areaData.forEach((area: any) => {
     const level = String(area.level);
-    const areaId = String(area.areaId ?? area.areaCode ?? '');
+    const areaId = String(area.areaId ?? area.id ?? '');
     if (!areaId) return;
-
-    // 已保存的供货区域通常只包含省(1)/市(2),回显时要勾选其下所有区县
     if (level === '1' || level === '2') {
-      const node = findNodeById(areaId);
-      if (node) {
-        collectDistrictIdsUnderNode(node).forEach((d) => codes.add(d));
-      }
+      codes.add(areaId);
     }
   });
 
   return Array.from(codes);
 };
 
-/** 根据区县编码在 regionOptions 中找到对应的省/市节点 */
-const findProvinceCityByDistrictCode = (districtCode: string, provinces: any[]) => {
-  for (const province of provinces) {
-    const cities = province?.children || [];
-    for (const city of cities) {
-      const districts = city?.children || [];
-      const hit = districts.find((d: any) => d.value === districtCode);
-      if (hit) {
-        return { province, city };
-      }
-    }
-  }
-  return null;
-};
-
-/** 根据区县编码提取供货区域(省/市)展示字符串 */
-const extractRegionDataFromDistrictCodes = (districtCodes: string[]) => {
+/** 根据省/市ID提取供货区域(省/市)展示字符串 */
+const extractRegionDataFromSelectedProvinceCityIds = (selectedIds: string[]) => {
   const provinces: any[] = supplyAreaOptions.value || [];
   const provinceNames: string[] = [];
   const cityNames: string[] = [];
   const addedProvinces = new Set<string>();
   const addedCities = new Set<string>();
 
-  districtCodes.forEach(code => {
-    const path = findProvinceCityByDistrictCode(code, provinces);
-    if (!path) return;
+  selectedIds.forEach((id) => {
+    for (const province of provinces) {
+      if (String(province.value) === String(id)) {
+        if (!addedProvinces.has(String(province.value))) {
+          provinceNames.push(province.label);
+          addedProvinces.add(String(province.value));
+        }
+        return;
+      }
 
-    const { province, city } = path;
-    if (province && !addedProvinces.has(province.value)) {
-      provinceNames.push(province.label);
-      addedProvinces.add(province.value);
-    }
-    if (city && !addedCities.has(city.value)) {
-      cityNames.push(city.label);
-      addedCities.add(city.value);
+      const cities = province?.children || [];
+      const hitCity = cities.find((c: any) => String(c.value) === String(id));
+      if (hitCity) {
+        if (!addedProvinces.has(String(province.value))) {
+          provinceNames.push(province.label);
+          addedProvinces.add(String(province.value));
+        }
+        if (!addedCities.has(String(hitCity.value))) {
+          cityNames.push(hitCity.label);
+          addedCities.add(String(hitCity.value));
+        }
+        return;
+      }
     }
   });
 
@@ -2051,37 +2016,57 @@ const extractRegionDataFromDistrictCodes = (districtCodes: string[]) => {
 };
 
 /** 构建 areaList 数组(提交给后端:省(1)/市(2)) */
-const buildAreaListFromDistrictCodes = (districtCodes: string[]) => {
+const buildAreaListFromSelectedProvinceCityIds = (selectedIds: string[]) => {
   const provinces: any[] = supplyAreaOptions.value || [];
   const areaList: any[] = [];
   const addedProvinces = new Set<string>();
   const addedCities = new Set<string>();
 
-  districtCodes.forEach(code => {
-    const path = findProvinceCityByDistrictCode(code, provinces);
-    if (!path) return;
-    const { province, city } = path;
-
-    if (province && !addedProvinces.has(province.value)) {
-      areaList.push({
-        areaId: Number(province.value),
-        areaCode: province.areaCode || province.value,
-        areaName: province.label,
-        level: 1,
-        parentId: 0
-      });
-      addedProvinces.add(province.value);
-    }
+  selectedIds.forEach((id) => {
+    for (const province of provinces) {
+      // 省
+      if (String(province.value) === String(id)) {
+        if (!addedProvinces.has(String(province.value))) {
+          areaList.push({
+            areaId: Number(province.value),
+            areaCode: province.areaCode || province.value,
+            areaName: province.label,
+            level: 1,
+            parentId: 0
+          });
+          addedProvinces.add(String(province.value));
+        }
+        return;
+      }
 
-    if (province && city && !addedCities.has(city.value)) {
-      areaList.push({
-        areaId: Number(city.value),
-        areaCode: city.areaCode || city.value,
-        areaName: city.label,
-        level: 2,
-        parentId: Number(province.value)
-      });
-      addedCities.add(city.value);
+      // 市
+      const cities = province?.children || [];
+      const hitCity = cities.find((c: any) => String(c.value) === String(id));
+      if (hitCity) {
+        // city 已选时确保 parent province 也存在(即使组件没回传,也兜底)
+        if (!addedProvinces.has(String(province.value))) {
+          areaList.push({
+            areaId: Number(province.value),
+            areaCode: province.areaCode || province.value,
+            areaName: province.label,
+            level: 1,
+            parentId: 0
+          });
+          addedProvinces.add(String(province.value));
+        }
+
+        if (!addedCities.has(String(hitCity.value))) {
+          areaList.push({
+            areaId: Number(hitCity.value),
+            areaCode: hitCity.areaCode || hitCity.value,
+            areaName: hitCity.label,
+            level: 2,
+            parentId: Number(province.value)
+          });
+          addedCities.add(String(hitCity.value));
+        }
+        return;
+      }
     }
   });
 
@@ -2104,30 +2089,51 @@ const handleSupplyAreaSubmit = async () => {
       return;
     }
 
-    console.log('提交前的选中区县编码:', selectedSupplyAreaCodes.value);
+    console.log('提交前的选中省/市ID:', selectedSupplyAreaCodes.value);
 
     // 构建 areaList 数组
-    const areaList = buildAreaListFromDistrictCodes(selectedSupplyAreaCodes.value);
+    const areaList = buildAreaListFromSelectedProvinceCityIds(selectedSupplyAreaCodes.value);
     console.log('构建的 areaList:', areaList);
 
+    // 前端兜底去重:避免后端按“追加插入”导致重复省/市
+    // 只提交“新增的”省/市(已存在的省/市不重复提交)
+    const existedKeys = new Set<string>();
+    (savedAreaData.value || []).forEach((a: any) => {
+      const level = String(a.level);
+      const areaId = String(a.areaId ?? a.id ?? '');
+      if (!areaId) return;
+      if (level === '1' || level === '2') {
+        existedKeys.add(`${level}-${areaId}`);
+      }
+    });
+
+    const areaListToSubmit = (areaList || []).filter((a: any) => {
+      const level = String(a.level);
+      const areaId = String(a.areaId ?? '');
+      if (!areaId) return false;
+      return !existedKeys.has(`${level}-${areaId}`);
+    });
+
     // 从选中的编码中提取区域信息(用于显示)
-    const regionData = extractRegionDataFromDistrictCodes(selectedSupplyAreaCodes.value);
+    const regionData = extractRegionDataFromSelectedProvinceCityIds(selectedSupplyAreaCodes.value);
     console.log('提取的区域数据:', regionData);
 
     // 构建提交数据
     const submitData: any = {
       supplierId: id,
       supplyNo: detailData.value.supplierNo,
-      areaList: areaList
+      areaList: areaListToSubmit
     };
-
-    // 只在编辑模式下添加 supplyStatus
-    if (isEditMode.value) {
-      submitData.supplyStatus = "4";
-    }
     
     console.log('提交的数据:', submitData);
 
+    if (!submitData.areaList || submitData.areaList.length === 0) {
+      ElMessage.success('保存成功');
+      supplyAreaDialogVisible.value = false;
+      await getSupplyAreaList();
+      return;
+    }
+
     await addArea(submitData);
 
     // 更新本地数据
@@ -2148,7 +2154,6 @@ const handleSupplyAreaSubmit = async () => {
             ...detailData.value,  // 当前页面的数据
             supplierType: String(detailData.value.supplierType),
             cooperateLevel: String(detailData.value.cooperateLevel),
-            supplyStatus: "4"
           };
         } else {
           // 临时表没有记录,使用当前数据
@@ -2156,7 +2161,6 @@ const handleSupplyAreaSubmit = async () => {
             ...detailData.value,
             supplierType: String(detailData.value.supplierType),
             cooperateLevel: String(detailData.value.cooperateLevel),
-            supplyStatus: "4"
           };
         }
         
@@ -2180,11 +2184,8 @@ const handleSupplyAreaSubmit = async () => {
 
     ElMessage.success('保存成功');
     supplyAreaDialogVisible.value = false;
-    
-    // add状态下保存后需要重新查询回显,edit状态不需要
-    if (isAddMode.value) {
-      await getSupplyAreaList();
-    }
+
+    await getSupplyAreaList();
   } catch (e) {
     console.error('保存供货区域失败:', e);
     ElMessage.error('保存失败');

+ 38 - 12
src/views/supplier/supplierauthorize/index.vue

@@ -50,24 +50,28 @@
         </el-row>
       </template>
 
-      <el-table v-loading="loading" border :data="supplierauthorizeList" highlight-current-row @current-change="handleCurrentChange">
-        <el-table-column label="供应商编号" align="center" prop="supplierNo" />
-        <el-table-column label="供应商名称" align="left" prop="supplierName" width="250" />
-        <el-table-column label="供货区域(省)" align="center" prop="province">
+      <el-table v-loading="loading" border :data="supplierauthorizeList" class="supplierauthorize-table" table-layout="fixed" highlight-current-row @current-change="handleCurrentChange">
+        <el-table-column label="供应商编号" align="left" prop="supplierNo" width="110" fixed="left" />
+        <el-table-column label="供应商名称" align="left" prop="supplierName" width="250" fixed="left" />
+        <el-table-column label="供货区域(省)" align="center" prop="province" width="180">
           <template #default="scope">
-            <span>{{ formatProvinceDisplay(scope.row.province) }}</span>
+            <el-tooltip :content="scope.row.province || ''" placement="top" :show-after="300">
+              <span class="ellipsis">{{ formatProvinceDisplay(scope.row.province) }}</span>
+            </el-tooltip>
           </template>
         </el-table-column>
-        <el-table-column label="供货区域(市)" align="center" prop="city">
+        <el-table-column label="供货区域(市)" align="center" prop="city" width="300">
           <template #default="scope">
-            <span>{{ formatCityDisplay(scope.row.city) }}</span>
+            <el-tooltip :content="scope.row.city || ''" placement="top" :show-after="300">
+              <span class="ellipsis">{{ formatCityDisplay(scope.row.city) }}</span>
+            </el-tooltip>
           </template>
         </el-table-column>
-        <el-table-column label="供货类目" align="center" prop="supplyProCate" />
-        <el-table-column label="可供货品牌" align="center" prop="brandName" />
-        <el-table-column label="有授权品牌" align="center" prop="authBrand" />
+        <el-table-column label="供货类目" align="center" prop="supplyProCate" width="140" />
+        <el-table-column label="可供货品牌" align="center" prop="brandName" width="140" />
+        <el-table-column label="有授权品牌" align="center" prop="authBrand" width="140" />
         
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <el-table-column label="操作" align="right" class-name="small-padding fixed-width" width="120" fixed="right">
           <template #default="scope">
             <el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
             <el-button link type="primary" @click="handleEdit(scope.row)" v-hasPermi="['supplier:supplierauthorize:edit']">编辑</el-button>
@@ -274,7 +278,8 @@ const formatProvinceDisplay = (province: string) => {
   const provinces = province.split(',').filter(p => p.trim());
   if (provinces.length === 0) return '';
   if (provinces.length === 1) return provinces[0];
-  return `${provinces[0]}...`;
+  if (provinces.length <= 3) return provinces.join(',');
+  return `${provinces.slice(0, 3).join(',')}...`;
 };
 
 /** 格式化城市显示:显示前10个城市,其余用...表示 */
@@ -293,4 +298,25 @@ onMounted(() => {
 
 <style scoped>
 
+.ellipsis {
+  display: inline-block;
+  max-width: 100%;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  vertical-align: middle;
+}
+
+.supplierauthorize-table :deep(.el-table__cell) {
+  padding-top: 14px;
+  padding-bottom: 14px;
+}
+
+.supplierauthorize-table :deep(.cell) {
+  line-height: 24px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
 </style>