沐梦. 2 settimane fa
parent
commit
b5680996e2
38 ha cambiato i file con 2734 aggiunte e 958 eliminazioni
  1. 4 3
      src/api/customer/addressArea.js
  2. 0 1
      src/api/customer/crmContact.js
  3. 18 2
      src/views/common/businessActivity.vue
  4. 3 6
      src/views/customer/care/add.vue
  5. 12 6
      src/views/customer/care/detail.vue
  6. 3 6
      src/views/customer/care/edit.vue
  7. 285 170
      src/views/customer/contactPerson/add.vue
  8. 74 28
      src/views/customer/contactPerson/detail.vue
  9. 429 152
      src/views/customer/contactPerson/edit.vue
  10. 4 4
      src/views/customer/contactPerson/index.vue
  11. 65 7
      src/views/customer/highseas/detail.vue
  12. 49 1
      src/views/customer/highseas/edit.vue
  13. 168 19
      src/views/customer/valid/detail.vue
  14. 47 1
      src/views/customer/valid/edit.vue
  15. 2 2
      src/views/sale/annualTask/detail.vue
  16. 2 2
      src/views/sale/monthlyTask/detail.vue
  17. 2 2
      src/views/sale/quarterlyTask/detail.vue
  18. 6 9
      src/views/saleManage/leads/add.vue
  19. 613 256
      src/views/saleManage/leads/detail.vue
  20. 3 6
      src/views/saleManage/leads/edit.vue
  21. 6 6
      src/views/saleManage/leads/index.vue
  22. 5 8
      src/views/saleManage/opportunity/add.vue
  23. 268 63
      src/views/saleManage/opportunity/detail.vue
  24. 3 6
      src/views/saleManage/opportunity/edit.vue
  25. 1 1
      src/views/saleManage/opportunity/index.vue
  26. 7 8
      src/views/saleManage/platformSelection/add.vue
  27. 362 63
      src/views/saleManage/platformSelection/detail.vue
  28. 7 8
      src/views/saleManage/platformSelection/edit.vue
  29. 5 0
      src/views/saleManage/platformSelection/index.vue
  30. 6 7
      src/views/saleManage/projectSelection/add.vue
  31. 186 60
      src/views/saleManage/projectSelection/detail.vue
  32. 6 7
      src/views/saleManage/projectSelection/edit.vue
  33. 1 1
      src/views/visit/plan/add.vue
  34. 1 1
      src/views/visit/plan/edit.vue
  35. 1 1
      src/views/visit/routine/add.vue
  36. 1 1
      src/views/visit/routine/edit.vue
  37. 1 1
      src/views/visit/routine/index.vue
  38. 78 33
      src/views/workbench/dashboard/index.vue

+ 4 - 3
src/api/customer/addressArea.js

@@ -1,10 +1,11 @@
 import request from '@/utils/request'
 
-// 获取省市两级联动列表(省级 + children 市级
+// 获取全量地区列表(扁平结构,需前端转树
 export function listProvinceWithCities() {
   return request({
-    url: '/customer/address/province-cities',
-    method: 'get'
+    url: '/system/addressarea/list',
+    method: 'get',
+    params: { pageSize: 5000 } // 获取全量数据
   })
 }
 

+ 0 - 1
src/api/customer/crmContact.js

@@ -42,4 +42,3 @@ export function delContact(ids) {
     method: 'delete'
   })
 }
-

+ 18 - 2
src/views/common/businessActivity.vue

@@ -116,7 +116,7 @@
                 <el-col :span="12">
                   <div class="m-item">
                     <span class="m-label">创建人</span>
-                    <span class="m-value link-style">{{ infoData.createByName || infoData.createBy || '' }}</span>
+                    <span class="m-value link-style">{{ (infoData.createByName && infoData.createByName !== 'admin') ? infoData.createByName : (infoData.createByStaffName || getStaffName(infoData.createBy || infoData.createByName)) }}</span>
                   </div>
                 </el-col>
                 <el-col :span="12">
@@ -134,7 +134,7 @@
                 <el-col :span="12">
                   <div class="m-item">
                     <span class="m-label">最后修改人</span>
-                    <span class="m-value">{{ infoData.updateByName || infoData.updateBy || '' }}</span>
+                    <span class="m-value">{{ (infoData.updateByName && infoData.updateByName !== 'admin') ? infoData.updateByName : (infoData.updateByStaffName || getStaffName(infoData.updateBy || infoData.updateByName)) }}</span>
                   </div>
                 </el-col>
               </el-row>
@@ -676,6 +676,22 @@ const submitRecord = async () => {
   });
 };
 
+const getStaffName = (id) => {
+  if (!id) return '';
+  const strId = String(id).trim();
+  if (strId.toLowerCase() === 'admin' || strId === '1') return '超级管理员';
+  // 避免将非 ID 字符串(已是姓名)再次查找
+  if (id && isNaN(Number(id)) && strId.length > 4) return id; 
+
+  const staff = staffOptions.value.find(item => 
+    String(item.staffId) === strId || 
+    String(item.userId) === strId || 
+    String(item.userName).toLowerCase() === strId.toLowerCase() ||
+    String(item.id) === strId
+  );
+  return staff ? (staff.staffName || staff.userName || staff.nickName) : id;
+};
+
 // 获取记录显示的用户名
 const getRecordUser = (record) => {
   if (record.visitorName) return record.visitorName;

+ 3 - 6
src/views/customer/care/add.vue

@@ -20,10 +20,7 @@
                   placeholder="请选择" 
                   style="width: 100%" 
                   filterable 
-                  remote 
-                  :remote-method="remoteLoadCustomers" 
-                  :loading="selectLoading" 
-                  @focus="remoteLoadCustomers('')" 
+                  clearable
                   @change="handleCustomerChange"
                 >
                   <el-option v-for="item in customerOptions" :key="item.value" :label="item.label" :value="item.label" />
@@ -238,10 +235,10 @@ const rules = reactive({
   concernArgument: [{ required: true, message: "关怀理由不能为空", trigger: "blur" }]
 });
 
-/** 远程加载客户列表 */
+/** 加载客户列表 (500条) */
 const remoteLoadCustomers = (query) => {
   selectLoading.value = true;
-  listCustomerInfo({ customerName: query || undefined, pageSize: 20 }).then(res => {
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
     customerOptions.value = (res.rows || []).map(item => ({
       label: item.customerName,
       value: item.customerName,

+ 12 - 6
src/views/customer/care/detail.vue

@@ -138,7 +138,7 @@
                 </div>
                 <div class="sidebar-item">
                   <span class="label">创建人</span>
-                  <span class="value">{{ form.createByName || form.createBy || '超级管理员' }}</span>
+                  <span class="value">{{ (form.createByName && form.createByName !== 'admin') ? form.createByName : getStaffName(form.createBy || form.createByName) || '超级管理员' }}</span>
                 </div>
                 <div class="sidebar-item">
                   <span class="label">修改日期</span>
@@ -146,7 +146,7 @@
                 </div>
                 <div class="sidebar-item">
                   <span class="label">最后修改人</span>
-                  <span class="value">{{ form.updateByName || form.updateBy || '' }}</span>
+                  <span class="value">{{ (form.updateByName && form.updateByName !== 'admin') ? form.updateByName : getStaffName(form.updateBy || form.updateByName) || '' }}</span>
                 </div>
               </div>
             </template>
@@ -237,10 +237,16 @@ const getDeptName = (deptId) => {
 
 const getStaffName = (staffId) => {
   if (!staffId) return '';
-  if (form.value.salesPersonName) return form.value.salesPersonName;
-  if (staffId && isNaN(Number(staffId))) return staffId;
-  const staff = staffOptions.value.find(item => item.staffId == staffId || item.id == staffId);
-  return staff ? (staff.staffName || staff.userName) : staffId;
+  const strId = String(staffId).trim();
+  if (strId.toLowerCase() === 'admin' || strId === '1') return '超级管理员';
+  if (staffId && isNaN(Number(staffId)) && strId.length > 4) return staffId;
+  const staff = staffOptions.value.find(item => 
+    String(item.staffId) === strId || 
+    String(item.userId) === strId || 
+    String(item.userName).toLowerCase() === strId.toLowerCase() ||
+    String(item.id) === strId
+  );
+  return staff ? (staff.staffName || staff.userName || staff.nickName) : staffId;
 };
 
 /** 加载基础选项 */

+ 3 - 6
src/views/customer/care/edit.vue

@@ -20,10 +20,7 @@
                   placeholder="请选择" 
                   style="width: 100%" 
                   filterable 
-                  remote 
-                  :remote-method="remoteLoadCustomers" 
-                  :loading="selectLoading" 
-                  @focus="remoteLoadCustomers('')" 
+                  clearable
                   @change="handleCustomerChange"
                 >
                   <el-option v-for="item in customerOptions" :key="item.value" :label="item.label" :value="item.label" />
@@ -225,10 +222,10 @@ function getDetail() {
   }
 }
 
-/** 远程加载客户列表 */
+/** 加载客户列表 (500条) */
 const remoteLoadCustomers = (query) => {
   selectLoading.value = true;
-  listCustomerInfo({ customerName: query || undefined, pageSize: 20 }).then(res => {
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
     customerOptions.value = (res.rows || []).map(item => ({
       label: item.customerName,
       value: item.customerName,

+ 285 - 170
src/views/customer/contactPerson/add.vue

@@ -1,147 +1,174 @@
 <template>
   <el-drawer
     v-model="visible"
-    title="新增联系人"
+    :with-header="false"
     size="80%"
     @close="handleClose"
     destroy-on-close
     class="custom-drawer"
   >
+    <div class="drawer-header">
+      <div class="header-title">新增联系人</div>
+      <el-icon class="close-icon" @click="handleClose"><Close /></el-icon>
+    </div>
     <div class="drawer-content">
-      <el-form ref="contactPersonRef" :model="form" :rules="rules" label-width="110px">
-        
+      <el-form 
+        ref="contactPersonRef" 
+        :model="form" 
+        :rules="rules" 
+        label-width="85px" 
+        label-position="right"
+        size="default"
+      >
+        <!-- 个人资料部分 -->
         <div class="section-title">个人资料</div>
-        <el-row :gutter="20">
+        <el-row :gutter="15">
           <el-col :span="8">
-            <el-form-item label="姓名" prop="contactName">
-              <el-input v-model="form.contactName" placeholder="请输入姓名" />
+            <el-form-item label="姓名" prop="contactName">
+              <el-input v-model="form.contactName" placeholder="姓名" clearable />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="联系人类型" prop="roleId">
-              <el-radio-group v-model="form.roleId">
-                <el-radio :label="'1'">公司职员</el-radio>
-                <el-radio :label="'2'">关系资源人</el-radio>
+            <el-form-item label="类型:" prop="type">
+              <el-radio-group v-model="form.type">
+                <el-radio label="1">公司职员</el-radio>
+                <el-radio label="2">关系资源人</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="年龄" prop="age">
-              <el-input v-model="form.age" placeholder="请输入" />
+            <el-form-item label="年龄" prop="age">
+              <el-input v-model="form.age" placeholder="年龄" clearable />
             </el-form-item>
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
+        <el-row :gutter="15">
           <el-col :span="8">
-            <el-form-item label="性别" prop="gender">
+            <el-form-item label="性别" prop="gender">
               <el-radio-group v-model="form.gender">
-                <el-radio :label="'0'">男</el-radio>
-                <el-radio :label="'1'">女</el-radio>
+                <el-radio label="0">男</el-radio>
+                <el-radio label="1">女</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="籍贯" prop="nativePlace">
-              <el-input v-model="form.nativePlace" placeholder="请输入籍贯" />
+            <el-form-item label="籍贯" prop="nativePlace">
+              <el-input v-model="form.nativePlace" placeholder="请输入籍贯" clearable />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="生日" prop="birthday">
-              <el-date-picker clearable
+            <el-form-item label="生日" prop="birthday">
+              <el-date-picker
                 v-model="form.birthday"
                 type="date"
                 value-format="YYYY-MM-DD"
-                placeholder="请选择生日"
-                style="width: 100%">
-              </el-date-picker>
+                placeholder="选择日期"
+                style="width: 100%"
+                clearable
+              />
             </el-form-item>
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
+        <el-row :gutter="15">
           <el-col :span="24">
-            <el-form-item label="描述" prop="remark">
-              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" :rows="4" />
+            <el-form-item label="描述:" prop="remark">
+              <el-input 
+                v-model="form.remark" 
+                type="textarea" 
+                placeholder="请输入描述内容" 
+                :rows="2" 
+                maxlength="200" 
+                show-word-limit 
+              />
             </el-form-item>
           </el-col>
         </el-row>
 
-        <div class="section-title" style="margin-top: 10px;">办公信息</div>
-        <el-row :gutter="20">
+        <!-- 办公信息部分 -->
+        <div class="section-title compact-title">办公信息</div>
+        <el-row :gutter="15">
           <el-col :span="8">
-            <el-form-item label="归属公司" prop="belongCompanyId">
-              <el-select v-model="form.belongCompanyId" placeholder="请选择" style="width: 100%">
-                <el-option v-for="item in companyOptions" :key="item.id" :label="item.companyName" :value="item.id" />
+            <el-form-item label="归属公司:" prop="deptId">
+              <el-select v-model="form.deptId" placeholder="归属公司" style="width: 100%" clearable filterable>
+                <el-option v-for="item in companyOptions" :key="item.companyCode || item.id" :label="item.companyName" :value="item.id" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="客户名称" prop="customerId">
-              <el-select v-model="form.customerId" placeholder="请选择" style="width: 100%" filterable clearable @change="handleCustomerChange">
-                <el-option v-for="item in customerOptions" :key="item.id" :label="item.customerName" :value="item.id" />
+            <el-form-item label="客户名称" prop="customerId">
+              <el-select v-model="form.customerId" placeholder="客户名称" style="width: 100%" clearable filterable>
+                <el-option v-for="item in customerOptions" :key="item.value" :label="item.label" :value="item.value" />
               </el-select>
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="在职状态" prop="status">
-              <el-radio-group v-model="form.status">
-                <el-radio :label="'0'">在职</el-radio>
-                <el-radio :label="'1'">离职</el-radio>
+            <el-form-item label="在职状态:" prop="jobStatus">
+              <el-radio-group v-model="form.jobStatus">
+                <el-radio label="1">在职</el-radio>
+                <el-radio label="2">离职</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
+        <el-row :gutter="15">
           <el-col :span="8">
-            <el-form-item label="部门" prop="deptName">
-              <el-input v-model="form.deptName" placeholder="请输入部门" />
+            <el-form-item label="部门" prop="deptName">
+              <el-input v-model="form.deptName" placeholder="部门" clearable />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="职位" prop="roleName">
-              <el-input v-model="form.roleName" placeholder="请输入职位" />
+            <el-form-item label="职位:" prop="position">
+              <el-input v-model="form.position" placeholder="职位" clearable />
             </el-form-item>
           </el-col>
           <el-col :span="8">
-            <el-form-item label="手机号码" prop="phone">
-              <el-input v-model="form.phone" placeholder="请输入手机号码" />
+            <el-form-item label="手机号码" prop="phone">
+              <el-input v-model="form.phone" placeholder="手机号码" clearable />
             </el-form-item>
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
+        <el-row :gutter="15">
           <el-col :span="8">
-            <el-form-item label="办公座机" prop="officePhone">
-              <el-input v-model="form.officePhone" placeholder="请输入办公座机" />
+            <el-form-item label="办公座机" prop="officePhone">
+              <el-input v-model="form.officePhone" placeholder="座机" clearable />
             </el-form-item>
           </el-col>
           <el-col :span="16">
-            <el-form-item label="办公地址" prop="addressDetail">
-              <div style="display: flex; gap: 10px; width: 100%">
+            <el-form-item label="办公地址" prop="addressDetail">
+              <div style="display: flex; gap: 8px; width: 100%">
                 <el-cascader
                   v-model="form.region"
+                  ref="regionCascader"
                   :options="areaOptions"
                   :props="{ label: 'areaName', value: 'id', children: 'children' }"
-                  placeholder="请选择省/市/县"
+                  placeholder="省/市/区"
                   style="width: 200px"
                   clearable
                 />
-                <el-input v-model="form.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                <el-input v-model="form.addressDetail" placeholder="详细地址" style="flex: 1" clearable />
               </div>
             </el-form-item>
           </el-col>
         </el-row>
 
-        <el-row :gutter="20">
+        <el-row :gutter="15">
           <el-col :span="24">
-            <el-form-item label="工作内容" prop="jobContent">
-              <el-input v-model="form.jobContent" type="textarea" placeholder="请输入内容" :rows="4" />
+            <el-form-item label="工作内容:" prop="jobContent">
+              <el-input 
+                v-model="form.jobContent" 
+                type="textarea" 
+                placeholder="工作职责描述" 
+                :rows="2" 
+                maxlength="200" 
+                show-word-limit 
+              />
             </el-form-item>
           </el-col>
         </el-row>
-
       </el-form>
     </div>
     <template #footer>
@@ -154,76 +181,71 @@
 </template>
 
 <script setup name="ContactPersonAdd">
-import { ref, reactive, toRefs, getCurrentInstance, onMounted } from 'vue';
-import { useRouter } from 'vue-router';
-import { addContactPerson } from "@/api/customer/contactPerson";
-import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo/index";
+import { ref, reactive, getCurrentInstance, onMounted } from 'vue';
+import { Close } from '@element-plus/icons-vue';
+import { addContact } from "@/api/customer/crmContact";
+import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo";
 import { listProvinceWithCities } from "@/api/customer/addressArea";
 
-const proxy = getCurrentInstance().proxy;
-const router = useRouter();
-
+const { proxy } = getCurrentInstance();
 const visible = ref(false);
-
+const areaOptions = ref([]);
 const companyOptions = ref([]);
 const customerOptions = ref([]);
-const areaOptions = ref([]);
+const regionCascader = ref(null);
 
 const form = reactive({
   id: null,
-  contactNo: null,
-  contactName: null,
-  roleId: null,
-  roleName: null,
-  gender: null,
-  age: null,
-  nativePlace: null,
-  birthday: null,
-  belongCompanyId: null,
-  customerId: null,
-  customerName: null,
-  status: null,
+  contactName: '',
+  type: '1',
+  age: '',
+  gender: '0',
+  nativePlace: '',
+  birthday: '',
+  remark: '',
   deptId: null,
-  deptName: null,
-  phone: null,
-  officePhone: null,
+  customerId: null,
+  jobStatus: '1',
+  deptName: '',
+  position: '',
+  phone: '',
+  officePhone: '',
   region: [],
-  homeProvinceId: null,
-  homeCityId: null,
-  homeAreaId: null,
-  homeAddressDetail: null,
-  remark: null,
-  jobContent: null
+  addressDetail: '',
+  jobContent: ''
 });
 
 const rules = reactive({
-  contactName: [
-    { required: true, message: "姓名不能为空", trigger: "blur" }
-  ],
-  belongCompanyId: [
-    { required: true, message: "归属公司不能为空", trigger: "change" }
-  ],
-  customerId: [
-    { required: true, message: "客户名称不能为空", trigger: "change" }
-  ],
-  deptName: [
-    { required: true, message: "部门不能为空", trigger: "blur" }
-  ],
+  contactName: [{ required: true, message: "姓名不能为空", trigger: "blur" }],
+  age: [{ required: true, message: "年龄不能为空", trigger: "blur" }],
+  deptName: [{ required: true, message: "部门不能为空", trigger: "blur" }],
   phone: [
-    { required: true, message: "手机号码不能为空", trigger: "blur" }
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号码", trigger: "blur" }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: "座机号请输入正确的11位手机号码", trigger: "blur" }
   ]
 });
 
 const initOptions = async () => {
   try {
-    const resCompany = await listCompanyOption();
-    companyOptions.value = resCompany.data || [];
-
-    const resCustomer = await listCustomerInfo({ pageNum: 1, pageSize: 1000 });
-    customerOptions.value = resCustomer.rows || resCustomer.data || [];
-    
     const resArea = await listProvinceWithCities();
-    areaOptions.value = resArea.data || [];
+    const list = resArea.rows || [];
+    areaOptions.value = handleTree(list, "id", "parentId");
+    
+    // 还原归属公司选项加载
+    listCompanyOption().then(res => {
+      companyOptions.value = res.data || [];
+    });
+
+    // 改为查询前 500 条记录,平衡数据量和性能
+    listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
+      customerOptions.value = (res.rows || []).map(item => ({ 
+        label: item.customerName, 
+        value: item.id,
+        customerNo: item.customerNo
+      }));
+    });
   } catch (error) {
     console.error("加载选项失败", error);
   }
@@ -233,61 +255,79 @@ onMounted(() => {
   initOptions();
 });
 
-const handleCustomerChange = (val) => {
-  const customer = customerOptions.value.find(item => item.id === val);
-  if (customer) {
-    form.customerName = customer.customerName;
-  } else {
-    form.customerName = '';
+const open = (initData) => {
+  visible.value = true;
+  reset();
+  
+  // 处理传入参数:支持直接传 ID 或 传对象
+  if (initData) {
+    if (typeof initData === 'object') {
+      const cId = initData.customerId ? Number(initData.customerId) : null;
+      const dId = initData.deptId ? Number(initData.deptId) : null;
+      
+      if (cId) {
+        form.customerId = cId;
+        // 如果选项中没有当前客户,手动添加一个以确保回显名称
+        if (initData.customerName && !customerOptions.value.find(o => o.value === cId)) {
+          customerOptions.value.push({ label: initData.customerName, value: cId });
+        }
+      }
+      
+      if (dId) {
+        form.deptId = dId;
+        // 归属公司回显处理
+        if (initData.deptName && !companyOptions.value.find(o => o.id === dId)) {
+          companyOptions.value.push({ companyName: initData.deptName, id: dId });
+        }
+      }
+    } else {
+      form.customerId = Number(initData);
+    }
   }
 };
 
-const open = () => {
-  visible.value = true;
+function reset() {
   Object.assign(form, {
     id: null,
-    contactNo: null,
-    contactName: null,
-    roleId: null,
-    roleName: null,
-    gender: null,
-    age: null,
-    nativePlace: null,
-    birthday: null,
-    belongCompanyId: null,
-    customerId: null,
-    customerName: null,
-    status: null,
+    contactName: '',
+    type: '1',
+    age: '',
+    gender: '0',
+    nativePlace: '',
+    birthday: '',
+    remark: '',
     deptId: null,
-    deptName: null,
-    phone: null,
-    officePhone: null,
+    customerId: null,
+    jobStatus: '1',
+    deptName: '',
+    position: '',
+    phone: '',
+    officePhone: '',
     region: [],
-    homeProvinceId: null,
-    homeCityId: null,
-    homeAreaId: null,
-    homeAddressDetail: null,
-    remark: null,
-    jobContent: null
+    addressDetail: '',
+    jobContent: ''
   });
-};
+  if (proxy.$refs["contactPersonRef"]) {
+    proxy.$refs["contactPersonRef"].resetFields();
+  }
+}
 
 const emit = defineEmits(['success']);
 
-/** 提交按钮 */
 function submitForm() {
-  proxy.$refs["contactPersonRef"].validate(valid => {
+  proxy.$refs["contactPersonRef"].validate(async valid => {
     if (valid) {
-      // 转换地理位置 ID
-      if (form.region && form.region.length > 0) {
-        form.homeProvinceId = form.region[0];
-        form.homeCityId = form.region[1];
-        form.homeAreaId = form.region[2] || null;
-      }
-      // 这里的 addressDetail 实际上对应后端扩展表的 homeAddressDetail
-      form.homeAddressDetail = form.addressDetail;
-      
-      addContactPerson(form).then(response => {
+      const selectedCustomer = customerOptions.value.find(c => c.value === form.customerId);
+      const payload = {
+        ...form,
+        customerNo: selectedCustomer ? selectedCustomer.customerNo : null,
+        addressProvince: form.region?.[0] ? String(form.region[0]) : null,
+        addressCity: form.region?.[1] ? String(form.region[1]) : null,
+        addressCounty: form.region?.[2] ? String(form.region[2]) : null,
+        age: form.age ? parseInt(form.age) : null,
+        platformCode: 'crm'
+      };
+      addContact(payload).then(response => {
         proxy.$modal.msgSuccess("新增成功");
         visible.value = false;
         emit('success');
@@ -296,52 +336,127 @@ function submitForm() {
   });
 }
 
-/** 关闭抽屉 */
 function handleClose() {
   visible.value = false;
 }
 
 defineExpose({ open });
+
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
 </script>
 
 <style scoped lang="scss">
+/* 核心:消除抽屉默认的巨大空隙 */
+.drawer-header {
+  height: 40px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 20px;
+  background-color: #fff;
+  border-bottom: 1px solid #e2e8f0;
+  .header-title {
+    font-size: 15px;
+    font-weight: 600;
+    color: #333;
+  }
+  .close-icon {
+    font-size: 18px;
+    color: #94a3b8;
+    cursor: pointer;
+    &:hover { color: #409eff; }
+  }
+}
+
 .drawer-content {
-  padding: 0;
+  padding: 10px 20px; 
 }
 
 .section-title {
-  font-size: 15px;
+  font-size: 14px;
+  font-weight: 600;
   color: #409eff;
-  margin-bottom: 12px;
-  padding-bottom: 8px;
-  border-bottom: 1px solid #ebeef5;
+  margin-bottom: 15px;
+  padding: 0 0 8px 0;
+  border-bottom: 1px solid #f0f2f5;
+  
+  &.compact-title {
+    margin-top: 10px;
+  }
 }
 
 :deep(.el-form-item) {
-  margin-bottom: 14px;
+  margin-bottom: 18px; // 调大间距,不再极致紧凑
+}
+
+:deep(.el-form-item__label) {
+  padding-right: 6px;
+  font-size: 13px;
+  color: #606266;
+}
+
+:deep(.el-radio-group) {
+  height: 30px;
 }
 
 .drawer-footer {
-  padding: 15px 25px;
+  padding: 10px 20px;
   text-align: right;
-  border-top: 1px solid #ebeef5;
+  border-top: 1px solid #f2f6fc;
 }
 
-/* 强制所有字体不加粗 */
-:deep(*) {
-  font-weight: 400 !important;
+:deep(.el-input__wrapper), :deep(.el-textarea__inner) {
+  padding: 1px 10px;
 }
-</style>
 
-<style lang="scss">
-.custom-drawer {
-  .el-drawer__header {
-    margin-bottom: 10px !important;
-    padding-bottom: 0 !important;
-  }
-  .el-drawer__body {
-    padding-top: 10px !important;
-    padding-bottom: 0 !important;
+/* 适配窄屏幕 */
+@media (max-width: 1200px) {
+  :deep(.el-col-8) {
+    max-width: 50%;
+    flex: 0 0 50%;
   }
 }
 </style>

+ 74 - 28
src/views/customer/contactPerson/detail.vue

@@ -1,23 +1,24 @@
 <template>
   <el-drawer
     v-model="visible"
-    size="85%"
-    :modal="false"
-    @close="handleClose"
+    size="80%"
+    :with-header="false"
     destroy-on-close
     class="custom-detail-drawer"
   >
-    <template #header>
+    <div class="detail-container" v-loading="loading">
+      <!-- 自定义页眉 (放入 Body 以实现 0 间距控制) -->
       <div class="detail-header">
         <div class="company-name">{{ form.customerName }}</div>
+        <div class="header-ops">
+          <el-icon class="close-icon" @click="handleClose"><Close /></el-icon>
+        </div>
       </div>
-    </template>
 
-    <div class="detail-container" v-loading="loading">
       <div class="main-content">
         <!-- 左侧可切换内容区 -->
         <div class="info-left">
-          <!-- 顶部 Tab 栏 (移入左侧区域,确保与右侧对齐) -->
+          <!-- 顶部 Tab 栏 (紧贴 detail-header) -->
           <div class="tabs-bar">
             <div class="tabs-left-area">
               <div class="tab-item" :class="{ active: activeTab === 'info' }" @click="activeTab = 'info'">资料</div>
@@ -37,7 +38,7 @@
                 <el-row class="info-row">
                   <el-col :span="8"><span class="label">人员编号</span><span class="value">{{ form.contactNo }}</span></el-col>
                   <el-col :span="8"><span class="label">姓名</span><span class="value">{{ form.contactName }}</span></el-col>
-                  <el-col :span="8"><span class="label">联系人类型</span><span class="value">{{ form.roleId == '1' ? '公司职员' : (form.roleId == '2' ? '关系资源人' : '') }}</span></el-col>
+                  <el-col :span="8"><span class="label">联系人类型</span><span class="value">{{ form.type == '1' ? '公司职员' : (form.type == '2' ? '关系资源人' : '') }}</span></el-col>
                 </el-row>
                 <el-row class="info-row">
                   <el-col :span="8"><span class="label">性别</span><span class="value">{{ form.gender == '0' ? '男' : (form.gender == '1' ? '女' : '') }}</span></el-col>
@@ -59,16 +60,16 @@
                   <el-col :span="8"><span class="label">客户名称</span><span class="value">{{ form.customerName }}</span></el-col>
                 </el-row>
                 <el-row class="info-row">
-                  <el-col :span="8"><span class="label">在职状态</span><span class="value">{{ form.status == '0' ? '在职' : (form.status == '1' ? '离职' : '') }}</span></el-col>
+                  <el-col :span="8"><span class="label">在职状态</span><span class="value">{{ form.jobStatus == '1' ? '在职' : (form.jobStatus == '2' ? '离职' : '') }}</span></el-col>
                   <el-col :span="8"><span class="label">手机号码</span><span class="value">{{ form.phone }}</span></el-col>
                   <el-col :span="8"><span class="label">部门</span><span class="value">{{ form.deptName }}</span></el-col>
                 </el-row>
                 <el-row class="info-row">
-                  <el-col :span="8"><span class="label">职位</span><span class="value">{{ form.roleName }}</span></el-col>
+                  <el-col :span="8"><span class="label">职位</span><span class="value">{{ form.position }}</span></el-col>
                   <el-col :span="8"><span class="label">办公座机</span><span class="value">{{ form.officePhone }}</span></el-col>
                   <el-col :span="8">
                     <span class="label">办公地址</span>
-                    <span class="value">{{ form.addressDetail }}</span>
+                    <span class="value">{{ (form.provincialCityCounty || '') + (form.addressDetail || '') }}</span>
                   </el-col>
                 </el-row>
                 <el-row class="info-row">
@@ -79,12 +80,12 @@
               <div class="info-section">
                 <div class="section-title">家庭信息</div>
                 <el-row class="info-row">
-                  <el-col :span="24"><span class="label">家庭住址</span><span class="value">{{ form.homeAddress }}</span></el-col>
+                  <el-col :span="24"><span class="label">家庭住址</span><span class="value">{{ form.homeAddressDetail }}</span></el-col>
                 </el-row>
                 <el-row class="info-row">
                   <el-col :span="8"><span class="label">家庭情况</span><span class="value">{{ form.familyStatus }}</span></el-col>
                   <el-col :span="8"><span class="label">爱好</span><span class="value">{{ form.hobby }}</span></el-col>
-                  <el-col :span="8"><span class="label">性格特征</span><span class="value">{{ form.character }}</span></el-col>
+                  <el-col :span="8"><span class="label">性格特征</span><span class="value">{{ form.characterTrait }}</span></el-col>
                 </el-row>
                 <el-row class="info-row">
                   <el-col :span="8"><span class="label">是否抽烟</span><span class="value">{{ form.isSmoke == '1' ? '是' : '否' }}</span></el-col>
@@ -140,7 +141,7 @@
               </div>
               <div class="sidebar-item">
                 <span class="label">创建人</span>
-                <span class="value">{{ form.createByName || form.createBy || '超级管理员' }}</span>
+                <span class="value">{{ (form.createByName && form.createByName !== 'admin') ? form.createByName : getStaffName(form.createBy || form.createByName) || '超级管理员' }}</span>
               </div>
               <div class="sidebar-item">
                 <span class="label">修改日期</span>
@@ -148,7 +149,7 @@
               </div>
               <div class="sidebar-item">
                 <span class="label">最后修改人</span>
-                <span class="value">{{ form.updateByName || form.updateBy || '' }}</span>
+                <span class="value">{{ (form.updateByName && form.updateByName !== 'admin') ? form.updateByName : getStaffName(form.updateBy || form.updateByName) || '' }}</span>
               </div>
             </div>
           </template>
@@ -165,8 +166,9 @@
 </template>
 
 <script setup name="ContactPersonDetail">
-import { ref, onMounted, getCurrentInstance, toRefs } from 'vue';
+import { ref, onMounted, getCurrentInstance, toRefs, reactive } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
+import { Close } from '@element-plus/icons-vue';
 import { getContactPerson } from "@/api/customer/contactPerson";
 import { getCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listCare } from "@/api/customer/crmCare";
@@ -174,6 +176,7 @@ import BusinessActivity from '../../common/businessActivity.vue';
 import ContactEdit from './edit.vue';
 import CareAdd from '../care/add.vue';
 import CareDetail from '../care/detail.vue';
+import { listComStaff } from "@/api/system/comStaff/index";
 
 const proxy = getCurrentInstance().proxy;
 const { care_type: careTypeOptions } = toRefs(reactive(proxy.useDict("care_type")));
@@ -188,6 +191,7 @@ const careList = ref([]);
 const editRef = ref(null);
 const careAddRef = ref(null);
 const careDetailRef = ref(null);
+const staffOptions = ref([]);
 
 const open = (id) => {
   visible.value = true;
@@ -271,7 +275,24 @@ function translateDict(value, options) {
   return item ? item.label : value;
 }
 
+const getStaffName = (staffId) => {
+  if (!staffId) return '';
+  const strId = String(staffId).trim();
+  if (strId.toLowerCase() === 'admin' || strId === '1') return '超级管理员';
+  if (staffId && isNaN(Number(staffId)) && strId.length > 4) return staffId;
+  const staff = staffOptions.value.find(item => 
+    String(item.staffId) === strId || 
+    String(item.userId) === strId || 
+    String(item.userName).toLowerCase() === strId.toLowerCase() ||
+    String(item.id) === strId
+  );
+  return staff ? (staff.staffName || staff.userName || staff.nickName) : staffId;
+};
+
 onMounted(() => {
+  listComStaff({ pageSize: 1000 }).then(res => {
+    staffOptions.value = res.rows || res.data || [];
+  });
 });
 
 defineExpose({ open });
@@ -280,24 +301,46 @@ defineExpose({ open });
 <style scoped lang="scss">
 .custom-detail-drawer {
   :deep(.el-drawer__header) {
-    margin-bottom: 0;
-    padding: 0 20px;
-    height: 40px;
-    min-height: 40px;
+    margin-bottom: 0 !important;
+    padding: 0 !important;
+    height: 64px;
     display: flex;
     align-items: center;
-    border-bottom: 1px solid #f0f0f0;
     color: #333;
+    border-bottom: none !important;
   }
   :deep(.el-drawer__body) {
-    padding: 0;
+    padding: 0 !important;
   }
 }
 
 .detail-header {
+  width: 100%;
+  height: 40px; // 大幅减小高度
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 16px;
+  background-color: #fff;
+  border-bottom: 1px solid #e2e8f0;
+  flex-shrink: 0;
   .company-name {
-    font-size: 16px;
-    color: #333;
+    font-size: 16px; // 略微调小字号以适应紧凑布局
+    font-weight: 600;
+    color: #1e293b;
+    margin: 0; // 彻底移除边距
+    padding: 0;
+    line-height: 40px;
+  }
+  .header-ops {
+    display: flex;
+    align-items: center;
+    .close-icon {
+      font-size: 18px;
+      color: #94a3b8;
+      cursor: pointer;
+      &:hover { color: #409eff; }
+    }
   }
 }
 
@@ -305,15 +348,18 @@ defineExpose({ open });
   height: 100%;
   display: flex;
   flex-direction: column;
+  padding: 0;
+  margin: 0;
 }
 
 .tabs-bar {
   display: flex;
-  padding: 0 30px; // 与 BusinessActivity 的 padding 对齐
-  border-bottom: 1px solid #f0f0f0;
+  padding: 0 16px;
+  border-bottom: 1px solid #f1f5f9;
   background-color: #fff;
-  height: 56px; // 统一页签高度,对齐右侧 56px
+  height: 56px;
   align-items: center;
+  flex-shrink: 0;
   
   .tabs-left-area {
     flex: 1;
@@ -387,7 +433,7 @@ defineExpose({ open });
 
 .info-content-scroll {
   flex: 1;
-  padding: 20px 30px;
+  padding: 20px 16px;
   overflow-y: auto;
 }
 

+ 429 - 152
src/views/customer/contactPerson/edit.vue

@@ -2,151 +2,219 @@
   <el-drawer
     v-model="visible"
     title="编辑联系人"
-    size="70%"
+    size="80%"
     :modal="false"
     @close="handleClose"
     destroy-on-close
   >
     <div class="drawer-content">
-      <el-form ref="contactPersonRef" :model="form" :rules="rules" label-width="100px" v-loading="loading">
-        <el-tabs v-model="activeTab">
-          <el-tab-pane label="基本信息" name="basic">
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="人员编号" prop="contactNo">
-                  <el-input v-model="form.contactNo" placeholder="请输入人员编号" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="姓名" prop="contactName">
-                  <el-input v-model="form.contactName" placeholder="请输入姓名" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="性别" prop="gender">
-                  <el-select v-model="form.gender" placeholder="请选择性别" style="width: 100%">
-                    <el-option label="男" value="0" />
-                    <el-option label="女" value="1" />
-                  </el-select>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="生日" prop="birthday">
-                  <el-date-picker clearable
-                    v-model="form.birthday"
-                    type="date"
-                    value-format="YYYY-MM-DD"
-                    placeholder="请选择生日"
-                    style="width: 100%">
-                  </el-date-picker>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="角色" prop="roleName">
-                  <el-input v-model="form.roleName" placeholder="请输入角色" />
-                </el-form-item>
-              </el-col>
-            </el-row>
-          </el-tab-pane>
-          <el-tab-pane label="工作信息" name="work">
-             <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="客户名称" prop="customerName">
-                  <el-input v-model="form.customerName" placeholder="请输入客户名称" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="部门" prop="deptName">
-                  <el-input v-model="form.deptName" placeholder="请输入部门" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="24">
-                <el-form-item label="工作内容" prop="jobContent">
-                  <el-input v-model="form.jobContent" type="textarea" placeholder="请输入内容" :rows="4" />
-                </el-form-item>
-              </el-col>
-            </el-row>
-          </el-tab-pane>
-          <el-tab-pane label="扩展信息" name="extra">
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="年龄" prop="age">
-                  <el-input v-model="form.age" placeholder="请输入年龄" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="籍贯" prop="nativePlace">
-                  <el-input v-model="form.nativePlace" placeholder="请输入籍贯" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="是否抽烟" prop="isSmoke">
-                  <el-radio-group v-model="form.isSmoke">
-                    <el-radio value="1">是</el-radio>
-                    <el-radio value="0">否</el-radio>
-                  </el-radio-group>
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="是否喝酒" prop="isDrink">
-                  <el-radio-group v-model="form.isDrink">
-                    <el-radio value="1">是</el-radio>
-                    <el-radio value="0">否</el-radio>
-                  </el-radio-group>
-                </el-form-item>
-              </el-col>
-              <el-col :span="24">
-                <el-form-item label="家庭地址" prop="region">
-                  <div style="display: flex; gap: 10px; width: 100%">
-                    <el-cascader
-                      v-model="form.region"
-                      :options="areaOptions"
-                      :props="{ label: 'areaName', value: 'id', children: 'children' }"
-                      placeholder="请选择省/市/县"
-                      style="width: 250px"
-                      clearable
-                    />
-                    <el-input v-model="form.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
-                  </div>
-                </el-form-item>
-              </el-col>
-            </el-row>
-          </el-tab-pane>
-          <el-tab-pane label="联系方式" name="contact">
-            <el-row :gutter="20">
-              <el-col :span="12">
-                <el-form-item label="手机号码" prop="phone">
-                  <el-input v-model="form.phone" placeholder="请输入手机号码" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="12">
-                <el-form-item label="座机" prop="officePhone">
-                  <el-input v-model="form.officePhone" placeholder="请输入座机" />
-                </el-form-item>
-              </el-col>
-              <el-col :span="24">
-                <el-form-item label="办公地址" prop="addressDetail">
-                  <el-input v-model="form.addressDetail" placeholder="请输入办公地址" />
-                </el-form-item>
-              </el-col>
-            </el-row>
-          </el-tab-pane>
-          <el-tab-pane label="其他" name="other">
-             <el-row>
-              <el-col :span="24">
-                <el-form-item label="备注" prop="remark">
-                  <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" :rows="4" />
-                </el-form-item>
-              </el-col>
-            </el-row>
-          </el-tab-pane>
-        </el-tabs>
+      <el-form ref="contactPersonRef" :model="form" :rules="rules" label-width="110px" v-loading="loading">
+        <div class="section-title">个人资料</div>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="姓名" prop="contactName">
+              <el-input v-model="form.contactName" placeholder="请输入姓名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="联系人类型" prop="type">
+              <el-radio-group v-model="form.type">
+                <el-radio :label="'1'">公司职员</el-radio>
+                <el-radio :label="'2'">关系资源人</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="性别" prop="gender">
+              <el-radio-group v-model="form.gender">
+                <el-radio :label="'0'">男</el-radio>
+                <el-radio :label="'1'">女</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="年龄" prop="age">
+              <el-input v-model="form.age" placeholder="请输入" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="籍贯" prop="nativePlace">
+              <el-input v-model="form.nativePlace" placeholder="请输入籍贯" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="生日" prop="birthday">
+              <el-date-picker clearable
+                v-model="form.birthday"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择生日"
+                style="width: 100%">
+              </el-date-picker>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="描述" prop="remark">
+              <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" :rows="3" maxlength="200" show-word-limit />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <div class="section-title" style="margin-top: 10px;">职业信息</div>
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="在职状态" prop="jobStatus">
+              <el-radio-group v-model="form.jobStatus">
+                <el-radio :label="'1'">在职</el-radio>
+                <el-radio :label="'2'">离职</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="手机号码" prop="phone">
+              <el-input v-model="form.phone" placeholder="请输入手机号码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="部门" prop="deptName">
+              <el-input v-model="form.deptName" placeholder="请输入部门" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="职位" prop="position">
+              <el-input v-model="form.position" placeholder="请输入职位" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="办公座机" prop="officePhone">
+              <el-input v-model="form.officePhone" placeholder="请输入办公座机" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="办公地址" prop="addressDetail">
+              <div style="display: flex; gap: 10px; width: 100%">
+                <el-cascader
+                  v-model="form.region"
+                  ref="regionCascader"
+                  :options="areaOptions"
+                  :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                  placeholder="请选择省/市/县"
+                  style="width: 250px"
+                  clearable
+                />
+                <el-input v-model="form.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="工作内容" prop="jobContent">
+              <el-input v-model="form.jobContent" type="textarea" placeholder="请输入内容" :rows="2" maxlength="200" show-word-limit />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="项目角色" prop="projectRole">
+              <el-select v-model="form.projectRole" placeholder="请选择项目角色" style="width: 100%" clearable>
+                <el-option v-for="dict in projectRoleOptions" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="是否关键人" prop="isKeyPerson">
+              <el-select v-model="form.isKeyPerson" placeholder="请选择" style="width: 100%" clearable>
+                <el-option label="是" :value="1" />
+                <el-option label="否" :value="0" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="公关情况" prop="prStatus">
+              <el-input v-model="form.prStatus" type="textarea" placeholder="请输入内容" :rows="3" maxlength="200" show-word-limit />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <div class="section-title" style="margin-top: 10px;">家庭信息</div>
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="家庭住址" prop="homeAddressDetail">
+              <div style="display: flex; gap: 10px; width: 100%">
+                <el-cascader
+                  v-model="form.homeRegion"
+                  :options="areaOptions"
+                  :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                  placeholder="请选择省/市/县"
+                  style="width: 250px"
+                  clearable
+                />
+                <el-input v-model="form.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+              </div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="家庭情况" prop="familyStatus">
+              <el-input v-model="form.familyStatus" placeholder="请输入内容" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="爱好" prop="hobby">
+              <el-input v-model="form.hobby" placeholder="请输入内容" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="性格特征" prop="characterTrait">
+              <el-input v-model="form.characterTrait" placeholder="请输入内容" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="8">
+            <el-form-item label="是否抽烟" prop="isSmoke">
+              <el-radio-group v-model="form.isSmoke">
+                <el-radio value="1">是</el-radio>
+                <el-radio value="0">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="8">
+            <el-form-item label="是否喝酒" prop="isDrink">
+              <el-radio-group v-model="form.isDrink">
+                <el-radio value="1">是</el-radio>
+                <el-radio value="0">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
       </el-form>
     </div>
     <template #footer>
       <div class="drawer-footer">
-        <el-button @click="handleClose">取 消</el-button>
         <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="handleClose">取 消</el-button>
       </div>
     </template>
   </el-drawer>
@@ -155,10 +223,11 @@
 <script setup name="ContactPersonEdit">
 import { ref, reactive, toRefs, getCurrentInstance, onMounted } from 'vue';
 import { useRouter, useRoute } from 'vue-router';
-import { getContactPerson, updateContactPerson } from "@/api/customer/contactPerson";
+import { getContactPerson } from "@/api/customer/contactPerson";
+import { updateContact } from "@/api/customer/crmContact";
 import { listProvinceWithCities } from "@/api/customer/addressArea";
-
-const proxy = getCurrentInstance().proxy;
+const { proxy } = getCurrentInstance();
+const { LXRJE0001: projectRoleOptions } = proxy.useDict("LXRJE0001");
 const router = useRouter();
 const route = useRoute();
 
@@ -167,27 +236,110 @@ const loading = ref(false);
 const activeTab = ref("basic");
 
 const areaOptions = ref([]);
+const regionCascader = ref(null);
 
 const data = reactive({
   form: {
-    region: []
+    id: null,
+    customerId: null,
+    contactNo: null,
+    contactName: null,
+    type: '1',
+    gender: '0',
+    age: null,
+    nativePlace: null,
+    birthday: null,
+    jobStatus: '1',
+    deptName: null,
+    position: null,
+    phone: null,
+    officePhone: null,
+    region: [],
+    addressDetail: null,
+    jobContent: null,
+    projectRole: null,
+    isKeyPerson: 0,
+    prStatus: null,
+    homeRegion: [],
+    homeAddressDetail: null,
+    familyStatus: null,
+    hobby: null,
+    characterTrait: null,
+    isSmoke: '0',
+    isDrink: '0',
+    remark: null
   },
   rules: {
     contactName: [
       { required: true, message: "姓名不能为空", trigger: "blur" }
     ],
+    type: [
+      { required: true, message: "联系人类型不能为空", trigger: "change" }
+    ],
+    jobStatus: [
+      { required: true, message: "在职状态不能为空", trigger: "change" }
+    ],
+    phone: [
+      { required: true, message: "手机号码不能为空", trigger: "blur" }
+    ],
+    deptName: [
+      { required: true, message: "部门不能为空", trigger: "blur" }
+    ],
+    position: [
+      { required: true, message: "职位不能为空", trigger: "blur" }
+    ]
   }
 });
 
 const { form, rules } = toRefs(data);
 
-const open = (id) => {
+const open = (id, customerId) => {
+  reset();
   visible.value = true;
   if (id) {
     loadData(id);
+  } else if (customerId) {
+    form.value.customerId = customerId;
   }
 };
 
+/** 重置表单 */
+function reset() {
+  form.value = {
+    id: null,
+    customerId: null,
+    contactNo: null,
+    contactName: null,
+    type: '1',
+    gender: '0',
+    age: null,
+    nativePlace: null,
+    birthday: null,
+    jobStatus: '1',
+    deptName: null,
+    position: null,
+    phone: null,
+    officePhone: null,
+    region: [],
+    addressDetail: null,
+    jobContent: null,
+    projectRole: null,
+    isKeyPerson: 0,
+    prStatus: null,
+    homeRegion: [],
+    homeAddressDetail: null,
+    familyStatus: null,
+    hobby: null,
+    characterTrait: null,
+    isSmoke: '0',
+    isDrink: '0',
+    remark: null
+  };
+  if (proxy.$refs["contactPersonRef"]) {
+    proxy.$refs["contactPersonRef"].resetFields();
+  }
+}
+
 const emit = defineEmits(['success']);
 
 /** 获取详情 */
@@ -195,11 +347,43 @@ function loadData(id) {
   loading.value = true;
   getContactPerson(id).then(response => {
     const resData = response.data;
-    // 初始化 region 数组用于回显
+    
+    // 区域反查逻辑 (适配代码转换)
+    const findIdByCode = (code) => {
+        if (!code) return null;
+        const findInTree = (nodes, targetCode) => {
+            for (const node of nodes) {
+                if (String(node.areaCode) === String(targetCode)) return node.id;
+                if (node.children) {
+                    const found = findInTree(node.children, targetCode);
+                    if (found) return found;
+                }
+            }
+            return null;
+        };
+        return findInTree(areaOptions.value, code);
+    };
+
+    // 初始化办公地址 region (使用 addressProvince/City/County)
     resData.region = [];
-    if (resData.homeProvinceId) resData.region.push(resData.homeProvinceId);
-    if (resData.homeCityId) resData.region.push(resData.homeCityId);
-    if (resData.homeAreaId) resData.region.push(resData.homeAreaId);
+    if (resData.addressProvince) {
+        const pid = /^\d+$/.test(resData.addressProvince) && resData.addressProvince.length < 5 ? resData.addressProvince : findIdByCode(resData.addressProvince);
+        if (pid) resData.region.push(Number(pid));
+    }
+    if (resData.addressCity) {
+        const cid = /^\d+$/.test(resData.addressCity) && resData.addressCity.length < 7 ? resData.addressCity : findIdByCode(resData.addressCity);
+        if (cid) resData.region.push(Number(cid));
+    }
+    if (resData.addressCounty) {
+        const aid = /^\d+$/.test(resData.addressCounty) && resData.addressCounty.length < 9 ? resData.addressCounty : findIdByCode(resData.addressCounty);
+        if (aid) resData.region.push(Number(aid));
+    }
+
+    // 初始化家庭地址 homeRegion
+    resData.homeRegion = [];
+    if (resData.homeProvinceId) resData.homeRegion.push(Number(resData.homeProvinceId));
+    if (resData.homeCityId) resData.homeRegion.push(Number(resData.homeCityId));
+    if (resData.homeAreaId) resData.homeRegion.push(Number(resData.homeAreaId));
     
     form.value = resData;
     loading.value = false;
@@ -208,21 +392,104 @@ function loadData(id) {
 
 onMounted(async () => {
   const resArea = await listProvinceWithCities();
-  areaOptions.value = resArea.data || [];
+  const list = resArea.rows || [];
+  if (list.length > 0) {
+    areaOptions.value = handleTree(list, "id", "parentId");
+  }
 });
 
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
 /** 提交按钮 */
 function submitForm() {
   proxy.$refs["contactPersonRef"].validate(valid => {
     if (valid) {
-      // 转换地理位置 ID
-      if (form.value.region && form.value.region.length > 0) {
-        form.value.homeProvinceId = form.value.region[0];
-        form.value.homeCityId = form.value.region[1];
-        form.value.homeAreaId = form.value.region[2] || null;
+      // 自动映射 roleId
+      form.value.roleId = form.value.type === '1' ? 1 : 2;
+
+      // 获取地址名称
+      const checkedNodes = regionCascader.value?.getCheckedNodes();
+      let provincialCityCounty = form.value.provincialCityCounty;
+      if (checkedNodes && checkedNodes.length > 0) {
+        provincialCityCounty = checkedNodes[0].pathLabels.join("");
       }
+
+      // 获取选中的 AreaCode 列表 (用于办公地址转换)
+      const findCodeById = (id) => {
+          const findInTree = (nodes, targetId) => {
+              for (const node of nodes) {
+                  if (Number(node.id) === Number(targetId)) return node.areaCode;
+                  if (node.children) {
+                      const found = findInTree(node.children, targetId);
+                      if (found) return found;
+                  }
+              }
+              return null;
+          };
+          return findInTree(areaOptions.value, id);
+      };
+
+      const payload = {
+        ...form.value,
+        // 办公地址 ID (Long)
+        provinceId: form.value.region?.[0] || null,
+        cityId: form.value.region?.[1] || null,
+        districtId: form.value.region?.[2] || null,
+        // 办公地址编码 (String) - 转换为后端需要的 AreaCode
+        addressProvince: form.value.region?.[0] ? findCodeById(form.value.region[0]) : null,
+        addressCity: form.value.region?.[1] ? findCodeById(form.value.region[1]) : null,
+        addressCounty: form.value.region?.[2] ? findCodeById(form.value.region[2]) : null,
+        provincialCityCounty: provincialCityCounty,
+        // 家庭地址 ID
+        homeProvinceId: form.value.homeRegion?.[0] || null,
+        homeCityId: form.value.homeRegion?.[1] || null,
+        homeAreaId: form.value.homeRegion?.[2] || null
+      };
       
-      updateContactPerson(form.value).then(response => {
+      updateContact(payload).then(response => {
         proxy.$modal.msgSuccess("修改成功");
         visible.value = false;
         emit('success');
@@ -241,9 +508,19 @@ defineExpose({ open });
 
 <style scoped lang="scss">
 .drawer-content {
-  padding: 20px;
+  padding: 0;
+}
 
+.section-title {
+  font-size: 15px;
+  color: #409eff;
+  margin-bottom: 12px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #ebeef5;
+}
 
+:deep(.el-form-item) {
+  margin-bottom: 14px;
 }
 .drawer-footer {
   padding: 20px;

+ 4 - 4
src/views/customer/contactPerson/index.vue

@@ -49,7 +49,7 @@
         </el-table-column>
         <el-table-column label="部门" align="center" prop="deptName" />
         <el-table-column label="客户名称" align="center" prop="customerName" show-overflow-tooltip />
-        <el-table-column label="职位" align="center" prop="roleName" />
+        <el-table-column label="职位" align="center" prop="position" />
         <el-table-column label="手机号码" align="center" prop="phone" />
         <el-table-column label="办公电话" align="center" prop="officePhone" />
         <el-table-column label="操作" align="center" width="160" fixed="right">
@@ -75,7 +75,7 @@
 <script setup name="ContactPerson">
 import { ref, reactive, toRefs, getCurrentInstance, onMounted, onActivated } from 'vue';
 import { useRouter } from 'vue-router';
-import { listContactPerson, delContactPerson } from "@/api/customer/contactPerson";
+import { listContact, delContact } from "@/api/customer/crmContact";
 import ContactDetail from './detail.vue';
 import ContactAdd from './add.vue';
 
@@ -105,7 +105,7 @@ const { queryParams } = toRefs(data);
 /** 查询联系人列表 */
 function getList() {
   loading.value = true;
-  listContactPerson(queryParams.value).then(response => {
+  listContact(queryParams.value).then(response => {
     contactPersonList.value = response.rows;
     total.value = response.total;
     loading.value = false;
@@ -138,7 +138,7 @@ function handleDetail(row) {
 function handleDelete(row) {
   const _id = row.id;
   proxy.$modal.confirm('是否确认删除联系人编号为"' + _id + '"的数据项?').then(function() {
-    return delContactPerson(_id);
+    return delContact(_id);
   }).then(() => {
     getList();
     proxy.$modal.msgSuccess("删除成功");

+ 65 - 7
src/views/customer/highseas/detail.vue

@@ -164,7 +164,11 @@
     </div>
 
     <!-- 客户联系人抽屉 -->
-    <el-drawer v-model="contactDrawerVisible" :title="contactTitle" size="70%" class="contact-form-drawer" append-to-body>
+    <el-drawer v-model="contactDrawerVisible" direction="rtl" size="80%" destroy-on-close :with-header="false" class="contact-form-drawer" append-to-body>
+      <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
+        <span style="font-size: 15px; font-weight: 600; color: #333;">{{ contactForm.id ? '编辑客户联系人' : '新建客户联系人' }}</span>
+        <el-icon @click="contactDrawerVisible = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
+      </div>
       <el-form :model="contactForm" :rules="contactRules" ref="contactFormRef" label-width="100px" label-position="right" label-suffix=":">
         <!-- 基本信息 Section -->
         <div class="form-section-header">基本信息</div>
@@ -369,11 +373,15 @@ const contactForm = reactive({
   familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0', homeAddressDetail: ''
 });
 const contactRules = {
-  contactName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
-  age: [{ required: true, message: "请输入年龄", trigger: "blur" }],
-  phone: [{ required: true, message: "请输入手机号码", trigger: "blur" }],
-  deptId: [{ required: true, message: "请选择部门", trigger: "change" }],
-  officePhone: [{ required: true, message: "请输入办公座机", trigger: "blur" }],
+  contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
+  deptName: [{ required: true, message: "部门不能为空", trigger: "blur" }],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号码", trigger: "blur" }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: "座机号请输入正确的11位手机号码", trigger: "blur" }
+  ],
   addressDetail: [{ required: true, message: "请输入办公地址", trigger: "blur" }]
 };
 
@@ -522,12 +530,60 @@ const handleEdit = () => { editRef.value.open(customerData.value.id); };
 const handleEditSuccess = () => { loadData(customerData.value.id); proxy.$emit('success'); };
 
 onMounted(() => {
-  listProvinceWithCities().then(res => areaOptions.value = res.data || []);
+  listProvinceWithCities().then(res => {
+    const list = res.rows || [];
+    areaOptions.value = handleTree(list, "id", "parentId");
+  });
   listDept().then(res => {
     deptOptions.value = proxy.handleTree(res.data, "deptId");
   });
 });
 
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
 defineExpose({ open });
 </script>
 
@@ -641,6 +697,8 @@ defineExpose({ open });
   .drawer-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 20px; border-top: 1px solid #f1f5f9; }
   
   /* 强制覆盖标签和输入框文字加粗 */
+  .el-form-item { margin-bottom: 18px !important; }
+  :deep(*) { font-weight: normal !important; }
   .el-form-item__label, .el-radio__label, .el-input__inner, .el-textarea__inner { 
     font-weight: normal !important; 
     color: #64748b; 

+ 49 - 1
src/views/customer/highseas/edit.vue

@@ -495,10 +495,58 @@ onMounted(() => {
   listCompanyOption().then(res => companyOptions.value = res.data || []);
   listLevelOption().then(res => levelOptions.value = res.data || []);
   listComStaff({ pageSize: 1000 }).then(res => staffOptions.value = res.rows || res.data || []);
-  listProvinceWithCities().then(res => areaOptions.value = res.data || []);
+  listProvinceWithCities().then(res => {
+    const list = res.rows || [];
+    areaOptions.value = handleTree(list, "id", "parentId");
+  });
 });
 
 defineExpose({ open });
+
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
 </script>
 
 <style scoped lang="scss">

+ 168 - 19
src/views/customer/valid/detail.vue

@@ -252,7 +252,7 @@
                   </el-table-column>
                   <el-table-column label="操作" align="center" width="100">
                     <template #default="scope">
-                      <el-button link type="primary">详情</el-button>
+                      <el-button link type="primary" @click="handleOpportunityDetail(scope.row)">详情</el-button>
                     </template>
                   </el-table-column>
                   <template #empty>暂无数据</template>
@@ -288,7 +288,7 @@
                   </el-table-column>
                   <el-table-column label="操作" align="center" width="100">
                     <template #default="scope">
-                      <el-button link type="primary">详情</el-button>
+                      <el-button link type="primary" @click="handleAnnualDetail(scope.row)">详情</el-button>
                     </template>
                   </el-table-column>
                   <template #empty>暂无数据</template>
@@ -379,6 +379,25 @@
     <!-- 客户编辑抽屉 -->
     <customer-edit ref="editRef" @success="loadData(customerData.id)" />
 
+    <!-- 项目商机详情抽屉 -->
+    <opportunity-detail 
+      v-model="oppDetailVisible" 
+      :id="currentOppId"
+      :sale-status-options="dictOptions.opportunityLevel"
+      :project-level-options="dictOptions.opportunityLevel"
+    />
+
+    <!-- 年度入围(平台优选)详情抽屉 -->
+    <platform-detail 
+      v-model="annualDetailVisible" 
+      :id="currentAnnualId"
+      :options="{
+        level: dictOptions.annualLevel,
+        type: dictOptions.projectType,
+        status: dictOptions.annualLevel
+      }"
+    />
+
     <!-- 联系人编辑抽屉 -->
     <el-drawer
       v-model="contactVisible"
@@ -389,16 +408,16 @@
       class="contact-edit-drawer"
     >
       <template #header>
-        <div class="drawer-header-custom">
-          <span class="header-title">{{ contactForm.id ? '编辑客户联系人' : '新建客户联系人' }}</span>
-          <div class="header-ops">
-            <el-button type="primary" icon="Check" @click="submitContact" :loading="contactSubmitting">保存</el-button>
-            <el-button @click="contactVisible = false">取消</el-button>
-          </div>
+        <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
+          <span style="font-size: 15px; font-weight: 600; color: #333;">{{ contactForm.id ? '编辑客户联系人' : '新建客户联系人' }}</span>
+          <el-icon @click="contactVisible = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
         </div>
       </template>
 
       <div class="drawer-body-custom" v-loading="contactLoading">
+        <style>
+          .custom-form :deep(.el-form-item) { margin-bottom: 18px !important; }
+        </style>
         <el-form :model="contactForm" :rules="contactRules" ref="contactFormRef" label-width="100px" class="custom-form">
           <div class="form-title-blue">基本信息</div>
           <div class="form-section-custom">
@@ -709,6 +728,8 @@ import { globalHeaders } from "@/utils/request";
 import BusinessActivity from '../../common/businessActivity.vue';
 import CustomerEdit from "./edit.vue";
 import CareDetail from "../care/detail.vue";
+import OpportunityDetail from "../../saleManage/opportunity/detail.vue";
+import PlatformDetail from "../../saleManage/platformSelection/detail.vue";
 
 const proxy = getCurrentInstance().proxy;
 const { 
@@ -735,6 +756,12 @@ const fileList = ref([]);
 const activeMainTab = ref('profile');
 const editRef = ref(null);
 const careDetailRef = ref(null);
+
+// 详情控制
+const oppDetailVisible = ref(false);
+const currentOppId = ref(null);
+const annualDetailVisible = ref(false);
+const currentAnnualId = ref(null);
 const dictOptions = reactive({
   opportunityLevel: opportunityLevelOptions,
   annualLevel: annualLevelOptions,
@@ -806,8 +833,14 @@ const careRules = {
 
 const contactRules = reactive({
   contactName: [{ required: true, message: "姓名不能为空", trigger: "blur" }],
-  phone: [{ required: true, message: "手机号码不能为空", trigger: "blur" }],
-  deptName: [{ required: true, message: "部门不能为空", trigger: "blur" }]
+  age: [{ required: true, message: "年龄不能为空", trigger: "blur" }],
+  deptName: [{ required: true, message: "部门不能为空", trigger: "blur" }],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的11位手机号码", trigger: "blur" }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: "座机号请输入正确的11位手机号码", trigger: "blur" }
+  ]
 });
 
 // 上传相关
@@ -895,15 +928,17 @@ const refreshContacts = () => {
 };
 
 const refreshOpportunities = () => {
-  if (!customerData.value?.customerName) return;
-  listOpportunity({ customerName: customerData.value.customerName }).then(res => {
+  const cNo = customerData.value?.customerNo || customerData.value?.customNo;
+  if (!cNo) return;
+  listOpportunity({ customNo: cNo }).then(res => {
     opportunityList.value = res.rows || res.data || [];
   });
 };
 
 const refreshAnnualList = () => {
-  if (!customerData.value?.customerName) return;
-  listProjectSelection({ customerName: customerData.value.customerName }).then(res => {
+  const cNo = customerData.value?.customerNo || customerData.value?.customNo;
+  if (!cNo) return;
+  listProjectSelection({ customNo: cNo }).then(res => {
     annualList.value = res.rows || res.data || [];
   });
 };
@@ -988,10 +1023,44 @@ const handleEditContact = (row) => {
     if (contactForm.status) contactForm.status = String(contactForm.status);
     if (contactForm.isSmoke) contactForm.isSmoke = String(contactForm.isSmoke);
     if (contactForm.isDrink) contactForm.isDrink = String(contactForm.isDrink);
+    // 区域反查逻辑 (适配代码转换)
+    const findIdByCode = (code) => {
+        if (!code) return null;
+        const findInTree = (nodes, targetCode) => {
+            for (const node of nodes) {
+                if (String(node.areaCode) === String(targetCode)) return node.id;
+                if (node.children) {
+                    const found = findInTree(node.children, targetCode);
+                    if (found) return found;
+                }
+            }
+            return null;
+        };
+        return findInTree(areaOptions.value, code);
+    };
+
     // 地区回显
-    if (res.data.officeProvinceId) {
-      contactForm.officeAreaArray = [String(res.data.officeProvinceId), String(res.data.officeCityId), String(res.data.officeAreaId)].filter(i => i && i !== 'null');
+    if (res.data.officeProvinceId || res.data.addressProvince) {
+      const p = res.data.officeProvinceId || res.data.addressProvince;
+      const c = res.data.officeCityId || res.data.addressCity;
+      const a = res.data.officeAreaId || res.data.addressCounty;
+      
+      const officeArr = [];
+      if (p) {
+          const pid = /^\d+$/.test(p) && String(p).length < 5 ? p : findIdByCode(p);
+          if (pid) officeArr.push(String(pid));
+      }
+      if (c) {
+          const cid = /^\d+$/.test(c) && String(c).length < 7 ? c : findIdByCode(c);
+          if (cid) officeArr.push(String(cid));
+      }
+      if (a) {
+          const aid = /^\d+$/.test(a) && String(a).length < 9 ? a : findIdByCode(a);
+          if (aid) officeArr.push(String(aid));
+      }
+      contactForm.officeAreaArray = officeArr;
     }
+    
     if (res.data.homeProvinceId) {
       contactForm.homeAreaArray = [String(res.data.homeProvinceId), String(res.data.homeCityId), String(res.data.homeAreaId)].filter(i => i && i !== 'null');
     }
@@ -1004,12 +1073,32 @@ const submitContact = () => {
     if (valid) {
       contactSubmitting.value = true;
       // 处理地区
-      if (contactForm.officeAreaArray?.length >= 2) {
+      if (contactForm.officeAreaArray?.length >= 1) {
+        // 获取选中的 AreaCode 列表 (用于办公地址转换)
+        const findCodeById = (id) => {
+            const findInTree = (nodes, targetId) => {
+                for (const node of nodes) {
+                    if (Number(node.id) === Number(targetId)) return node.areaCode;
+                    if (node.children) {
+                        const found = findInTree(node.children, targetId);
+                        if (found) return found;
+                    }
+                }
+                return null;
+            };
+            return findInTree(areaOptions.value, id);
+        };
+
         contactForm.officeProvinceId = contactForm.officeAreaArray[0];
         contactForm.officeCityId = contactForm.officeAreaArray[1];
         contactForm.officeAreaId = contactForm.officeAreaArray[2] || null;
+        
+        // 兼容新字段 (代码形式)
+        contactForm.addressProvince = findCodeById(contactForm.officeAreaArray[0]);
+        contactForm.addressCity = findCodeById(contactForm.officeAreaArray[1]);
+        contactForm.addressCounty = findCodeById(contactForm.officeAreaArray[2]);
       }
-      if (contactForm.homeAreaArray?.length >= 2) {
+      if (contactForm.homeAreaArray?.length >= 1) {
         contactForm.homeProvinceId = contactForm.homeAreaArray[0];
         contactForm.homeCityId = contactForm.homeAreaArray[1];
         contactForm.homeAreaId = contactForm.homeAreaArray[2] || null;
@@ -1098,10 +1187,58 @@ onMounted(() => {
     loadData(id);
   }
   listProvinceWithCities().then(res => {
-    areaOptions.value = res.data || [];
+    const list = res.rows || [];
+    if (list.length > 0) {
+      areaOptions.value = handleTree(list, "id", "parentId");
+    }
   });
 });
 
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
 /** 客户关怀相关方法 */
 const handleAddCare = () => {
   Object.assign(careForm, {
@@ -1155,6 +1292,18 @@ const handleCareDetail = (row) => {
   careDetailRef.value.open(row.id);
 };
 
+/** 查看项目商机详情 */
+const handleOpportunityDetail = (row) => {
+  currentOppId.value = row.id;
+  oppDetailVisible.value = true;
+};
+
+/** 查看年度入围详情 */
+const handleAnnualDetail = (row) => {
+  currentAnnualId.value = row.id;
+  annualDetailVisible.value = true;
+};
+
 /** 字典翻译 */
 function translateDict(value, options) {
   if (!options || !value) return value;

+ 47 - 1
src/views/customer/valid/edit.vue

@@ -361,7 +361,8 @@ const initOptions = async () => {
     console.error("客户类别接口加载失败:", e);
   });
   listProvinceWithCities().then(res => {
-    areaOptions.value = res.data || [];
+    const list = res.rows || [];
+    areaOptions.value = handleTree(list, "id", "parentId");
   }).catch(e => console.error("省市联动接口出错", e));
   deptTreeSelect().then(res => {
     const data = res.data || res.rows || [];
@@ -491,6 +492,51 @@ onMounted(() => {
 });
 
 defineExpose({ open });
+
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
 </script>
 
 <style lang="scss">

+ 2 - 2
src/views/sale/annualTask/detail.vue

@@ -123,8 +123,8 @@ const getDeptTree = async () => {
 };
 
 const getCustomerList = async () => {
-  const res = await listCustomerInfo();
-  customerOptions.value = res.rows || res.data || [];
+  const res = await listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' });
+  customerOptions.value = res.rows || [];
 };
 
 const form = ref({

+ 2 - 2
src/views/sale/monthlyTask/detail.vue

@@ -118,8 +118,8 @@ const getDeptTree = async () => {
 };
 
 const getCustomerList = async () => {
-  const res = await listCustomerInfo();
-  customerOptions.value = res.rows || res.data || [];
+  const res = await listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' });
+  customerOptions.value = res.rows || [];
 };
 
 const form = ref({

+ 2 - 2
src/views/sale/quarterlyTask/detail.vue

@@ -150,8 +150,8 @@ const getDeptTree = async () => {
 };
 
 const getCustomerList = async () => {
-  const res = await listCustomerInfo();
-  customerOptions.value = res.rows || res.data || [];
+  const res = await listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' });
+  customerOptions.value = res.rows || [];
 };
 
 const form = ref({

+ 6 - 9
src/views/saleManage/leads/add.vue

@@ -38,9 +38,6 @@
                   v-model="form.customerNo" 
                   placeholder="请输入关键字搜索客户" 
                   filterable 
-                  remote
-                  :remote-method="remoteLoadCustomers"
-                  :loading="customerLoading"
                   style="width: 100%" 
                   @change="handleCustomerChange"
                 >
@@ -154,21 +151,21 @@
           <el-row :gutter="30">
             <el-col :span="24">
               <el-form-item label="采购内容" prop="purchaseContent">
-                <el-input v-model="form.purchaseContent" type="textarea" :rows="3" placeholder="请输入采购内容" />
+                <el-input v-model="form.purchaseContent" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入采购内容" />
               </el-form-item>
             </el-col>
           </el-row>
           <el-row :gutter="30">
             <el-col :span="24">
               <el-form-item label="项目描述" prop="projectDescription">
-                <el-input v-model="form.projectDescription" type="textarea" :rows="3" placeholder="请输入项目描述" />
+                <el-input v-model="form.projectDescription" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入项目描述" />
               </el-form-item>
             </el-col>
           </el-row>
           <el-row :gutter="30">
             <el-col :span="24">
               <el-form-item label="竞争对手" prop="competitor">
-                <el-input v-model="form.competitor" placeholder="请输入竞争对手" />
+                <el-input v-model="form.competitor" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入竞争对手" />
               </el-form-item>
             </el-col>
           </el-row>
@@ -213,7 +210,7 @@
 <script setup>
 import { ref, reactive, watch, getCurrentInstance } from 'vue';
 import { addLeads } from '@/api/saleManage/leads/index';
-import { listCustomerOption } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { globalHeaders } from '@/utils/request';
 import { Close, Upload } from '@element-plus/icons-vue';
 
@@ -266,8 +263,8 @@ const rules = reactive({
 
 const remoteLoadCustomers = (query) => {
   customerLoading.value = true;
-  listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
-    const list = res.data || res.rows || [];
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
+    const list = res.rows || [];
     localCustomerOptions.value = list.map(i => ({ ...i, customerNo: String(i.customerNo) }));
   }).finally(() => {
     customerLoading.value = false;

+ 613 - 256
src/views/saleManage/leads/detail.vue

@@ -122,12 +122,19 @@
                 </el-table-column>
                 <el-table-column label="项目角色" align="center" width="120">
                   <template #default="scope">
-                    {{ scope.row.projectRole || scope.row.role || '' }}
+                    <dict-tag :options="projectRoleOptions" :value="scope.row.projectRole" />
                   </template>
                 </el-table-column>
-                <el-table-column label="是否关键人" align="center" width="110">
+                <el-table-column label="公关情况" align="center" width="150" show-overflow-tooltip>
                   <template #default="scope">
-                    <span>{{ (scope.row.isKeyPerson || scope.row.keyPerson) ? '是' : '否' }}</span>
+                    {{ scope.row.prStatus }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="是否关键人" align="center" width="100">
+                  <template #default="scope">
+                    <el-tag :type="scope.row.isKeyPerson == 1 ? 'danger' : 'info'">
+                      {{ scope.row.isKeyPerson == 1 ? '是' : '否' }}
+                    </el-tag>
                   </template>
                 </el-table-column>
                 <el-table-column label="操作" align="center" width="140" fixed="right">
@@ -270,212 +277,230 @@
     </el-dialog>
 
 
-    <!-- 编辑项目联系人抽屉 -->
-    <el-drawer 
-      title="编辑项目联系人" 
-      v-model="editContactVisible" 
-      size="80%" 
-      append-to-body
-      destroy-on-close
-      class="edit-contact-drawer"
-    >
-      <el-form :model="editContactForm" ref="editContactRef" label-width="100px" size="default">
-        <!-- 基本信息 -->
-        <div class="form-section">
-          <div class="form-section-title">基本信息</div>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="姓名" prop="contactName" required>
-                <el-input v-model="editContactForm.contactName" placeholder="请输入姓名" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="联系人类型" prop="roleId" required>
-                <el-radio-group v-model="editContactForm.roleId">
-                  <el-radio value="1">公司职员</el-radio>
-                  <el-radio value="2">关系资源人</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="性别" prop="gender" required>
-                <el-radio-group v-model="editContactForm.gender">
-                  <el-radio value="0">男</el-radio>
-                  <el-radio value="1">女</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="年龄" prop="age">
-                <el-input-number v-model="editContactForm.age" :min="0" :max="150" style="width:100%" controls-position="right" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="籍贯" prop="nativePlace">
-                <el-input v-model="editContactForm.nativePlace" placeholder="请输入籍贯" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="生日" prop="birthday">
-                <el-date-picker v-model="editContactForm.birthday" type="date" placeholder="选择日期" style="width:100%" value-format="YYYY-MM-DD" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="24">
-              <el-form-item label="描述" prop="remark">
-                <el-input v-model="editContactForm.remark" type="textarea" :rows="3" placeholder="请输入内容" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-        </div>
+    <!-- 编辑/新建项目联系人抽屉 (完全对齐商机模块) -->
+    <el-drawer v-model="projectContactDrawerOpen" direction="rtl" size="80%" destroy-on-close :with-header="false">
+      <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
+        <span style="font-size: 15px; font-weight: 600; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
+        <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
+      </div>
 
-        <!-- 办公信息 -->
-        <div class="form-section">
-          <div class="form-section-title">办公信息</div>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="在职状态" prop="status" required>
-                <el-radio-group v-model="editContactForm.status">
-                  <el-radio value="0">在职</el-radio>
-                  <el-radio value="1">离职</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="手机号码" prop="phone" required>
-                <el-input v-model="editContactForm.phone" placeholder="请输入手机号码" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="部门" prop="deptName" required>
-                <el-input v-model="editContactForm.deptName" placeholder="请输入部门" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="职位" prop="roleName" required>
-                <el-input v-model="editContactForm.roleName" placeholder="请输入职位" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="办公座机" prop="officePhone">
-                <el-input v-model="editContactForm.officePhone" placeholder="请输入办公座机" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="24">
-              <el-form-item label="办公地址" prop="addressDetail">
-                <el-input v-model="editContactForm.addressDetail" placeholder="请输入详细办公地址" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="24">
-              <el-form-item label="工作内容" prop="workContent">
-                <el-input v-model="editContactForm.workContent" type="textarea" :rows="3" placeholder="请输入工作内容" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-        </div>
+      <div class="drawer-body-custom" style="padding: 0; overflow-y: auto; height: calc(100% - 110px);">
+        <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" class="no-bold-label">
+          <!-- 基本信息 -->
+          <div class="form-section-group">
+            <div class="section-group-title">基本信息</div>
+            <div class="section-group-content">
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="姓名" prop="contactName">
+                    <el-input v-model="projectContactForm.contactName" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="联系人类型" prop="type">
+                    <el-radio-group v-model="projectContactForm.type">
+                      <el-radio value="1">公司职员</el-radio>
+                      <el-radio value="2">关系资源人</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="性别" prop="gender">
+                    <el-radio-group v-model="projectContactForm.gender">
+                      <el-radio value="0">男</el-radio>
+                      <el-radio value="1">女</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="年龄" prop="age">
+                    <el-input v-model="projectContactForm.age" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="籍贯" prop="nativePlace">
+                    <el-input v-model="projectContactForm.nativePlace" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="生日" prop="birthday">
+                    <el-date-picker v-model="projectContactForm.birthday" type="date" placeholder="请选择" style="width: 100%" value-format="YYYY-MM-DD" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="24">
+                  <el-form-item label="描述" prop="remark">
+                    <el-input v-model="projectContactForm.remark" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
 
-        <!-- 项目决策 -->
-        <div class="form-section">
-          <div class="form-section-title">项目决策</div>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="项目角色" prop="projectRole">
-                <el-select v-model="editContactForm.projectRole" placeholder="请选择" style="width:100%" clearable>
-                  <el-option
-                    v-for="dict in projectRoleOptions"
-                    :key="dict.value"
-                    :label="dict.label"
-                    :value="dict.value"
-                  />
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="是否关键人" prop="isKeyPerson">
-                <el-select v-model="editContactForm.isKeyPerson" placeholder="请选择" style="width:100%">
-                  <el-option label="是" :value="1" />
-                  <el-option label="否" :value="0" />
-                </el-select>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="24">
-              <el-form-item label="公关情况" prop="prStatus">
-                <el-input v-model="editContactForm.prStatus" type="textarea" :rows="3" placeholder="请输入内容" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-        </div>
+          <!-- 办公信息 -->
+          <div class="form-section-group">
+            <div class="section-group-title">办公信息</div>
+            <div class="section-group-content">
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="在职状态" prop="jobStatus">
+                    <el-radio-group v-model="projectContactForm.jobStatus">
+                      <el-radio value="1">在职</el-radio>
+                      <el-radio value="2">离职</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="手机号码" prop="phone">
+                    <el-input v-model="projectContactForm.phone" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="部门" prop="deptName">
+                    <el-input v-model="projectContactForm.deptName" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="职位" prop="position">
+                    <el-input v-model="projectContactForm.position" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="办公座机" prop="officePhone">
+                    <el-input v-model="projectContactForm.officePhone" placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="16">
+                  <el-form-item label="办公地址" prop="addressDetail">
+                    <div style="display: flex; gap: 10px; width: 100%">
+                      <el-cascader
+                        v-model="projectContactForm.officeRegion"
+                        ref="officeRegionCascader"
+                        :options="areaOptions"
+                        :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                        placeholder="请选择省/市/县"
+                        style="width: 220px"
+                        clearable
+                      />
+                      <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                    </div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="24">
+                  <el-form-item label="工作内容" prop="jobContent">
+                    <el-input v-model="projectContactForm.jobContent" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
 
-        <!-- 家庭信息 -->
-        <div class="form-section">
-          <div class="form-section-title">家庭信息</div>
-          <el-row :gutter="20">
-            <el-col :span="24">
-              <el-form-item label="家庭住址" prop="homeAddress">
-                <el-input v-model="editContactForm.homeAddress" placeholder="请输入详细家庭地址" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="家庭情况" prop="familyStatus">
-                <el-input v-model="editContactForm.familyStatus" placeholder="请输入家庭情况" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="爱好" prop="hobby">
-                <el-input v-model="editContactForm.hobby" placeholder="请输入爱好" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="性格特征" prop="character">
-                <el-input v-model="editContactForm.character" placeholder="请输入性格特征" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="8">
-              <el-form-item label="是否抽烟" prop="isSmoking">
-                <el-radio-group v-model="editContactForm.isSmoking">
-                  <el-radio value="1">是</el-radio>
-                  <el-radio value="0">否</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-              <el-form-item label="是否喝酒" prop="isDrinking">
-                <el-radio-group v-model="editContactForm.isDrinking">
-                  <el-radio value="1">是</el-radio>
-                  <el-radio value="0">否</el-radio>
-                </el-radio-group>
-              </el-form-item>
-            </el-col>
-            <el-col :span="8">
-            </el-col>
-          </el-row>
-        </div>
-      </el-form>
-      <template #footer>
-        <div style="flex: auto; text-align: right; padding: 10px 20px; border-top: 1px solid #f0f0f0;">
-          <el-button type="primary" @click="submitEditContact">保 存</el-button>
-          <el-button @click="editContactVisible = false">取 消</el-button>
-        </div>
-      </template>
+          <!-- 项目决策 -->
+          <div class="form-section-group">
+            <div class="section-group-title">项目决策</div>
+            <div class="section-group-content">
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="项目角色" prop="projectRole">
+                    <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable>
+                      <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="parseInt(item.value)" />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="是否关键人" prop="isKeyPerson">
+                    <el-select v-model="projectContactForm.isKeyPerson" style="width: 100%" placeholder="请选择">
+                      <el-option label="是" :value="1" />
+                      <el-option label="否" :value="0" />
+                    </el-select>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="24">
+                  <el-form-item label="公关情况" prop="prStatus">
+                    <el-input v-model="projectContactForm.prStatus" type="textarea" :rows="2" maxlength="500" show-word-limit placeholder="请输入详细公关情况" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
+
+          <!-- 家庭信息 -->
+          <div class="form-section-group">
+            <div class="section-group-title">家庭信息</div>
+            <div class="section-group-content">
+              <el-row :gutter="20">
+                <el-col :span="24">
+                  <el-form-item label="家庭住址" prop="homeAddressDetail">
+                    <div style="display: flex; gap: 10px; width: 100%">
+                      <el-cascader
+                        v-model="projectContactForm.homeRegion"
+                        ref="homeRegionCascader"
+                        :options="areaOptions"
+                        :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                        placeholder="请选择省/市/县"
+                        style="width: 220px"
+                        clearable
+                      />
+                      <el-input v-model="projectContactForm.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                    </div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="家庭情况" prop="familyStatus">
+                    <el-input v-model="projectContactForm.familyStatus" placeholder="请输入家庭情况" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="爱好" prop="hobby">
+                    <el-input v-model="projectContactForm.hobby" placeholder="请输入爱好" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="性格特征" prop="characterTrait">
+                    <el-input v-model="projectContactForm.characterTrait" placeholder="请输入性格特征" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="是否抽烟" prop="isSmoke">
+                    <el-radio-group v-model="projectContactForm.isSmoke">
+                      <el-radio value="1">是</el-radio>
+                      <el-radio value="0">否</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="是否喝酒" prop="isDrink">
+                    <el-radio-group v-model="projectContactForm.isDrink">
+                      <el-radio value="1">是</el-radio>
+                      <el-radio value="0">否</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
+        </el-form>
+      </div>
+
+      <div class="drawer-footer-standard" style="padding: 15px 20px; border-top: 1px solid #f0f0f0; text-align: right;">
+        <el-button @click="projectContactDrawerOpen = false">取 消</el-button>
+        <el-button type="primary" @click="submitProjectContact" :loading="contactSubmitting">确 定</el-button>
+      </div>
     </el-drawer>
 
     <!-- 联系人详情预览弹窗 -->
@@ -503,6 +528,22 @@
             <el-descriptions-item label="办公地址">{{ contactForm.addressDetail }}</el-descriptions-item>
           </el-descriptions>
         </div>
+        <div class="preview-section" style="margin-top: 20px;">
+          <div class="preview-title">项目决策</div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="项目角色">
+              <dict-tag :options="projectRoleOptions" :value="contactForm.projectRole" />
+            </el-descriptions-item>
+            <el-descriptions-item label="是否关键人">
+              <el-tag :type="contactForm.isKeyPerson == 1 ? 'danger' : 'info'">
+                {{ contactForm.isKeyPerson == 1 ? '是' : '否' }}
+              </el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="公关情况" :span="2">
+              {{ contactForm.prStatus }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
       </div>
       <template #footer>
         <el-button @click="contactDetailVisible = false">关 闭</el-button>
@@ -514,13 +555,15 @@
 <script setup>
 import { ref, reactive, watch, getCurrentInstance, toRefs } from 'vue';
 import { getLeads, updateLeads } from '@/api/saleManage/leads/index';
-import { listContact, updateContact, delContact } from '@/api/customer/crmContact';
-import { getContactPerson } from "@/api/customer/contactPerson";
+import { listContact, addContact, updateContact, delContact, getContact } from '@/api/customer/crmContact';
 import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
 import { listByIds } from '@/api/system/oss/index';
+import { listProvinceWithCities } from "@/api/customer/addressArea";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { globalHeaders } from '@/utils/request';
 import BusinessActivity from '../../common/businessActivity.vue';
 import { Close, Edit, Plus, Upload, CircleCheck, ArrowDown } from '@element-plus/icons-vue';
+import { onMounted } from 'vue';
 
 const props = defineProps({
   modelValue: Boolean,
@@ -553,9 +596,54 @@ const selectedContacts = ref([]);
 const contactDetailVisible = ref(false);
 const contactDetailLoading = ref(false);
 const contactForm = ref({});
-const editContactVisible = ref(false);
-const editContactLoading = ref(false);
-const editContactForm = ref({});
+const areaOptions = ref([]);
+const projectContactDrawerOpen = ref(false);
+const projectContactFormRef = ref(null);
+const officeRegionCascader = ref(null);
+const homeRegionCascader = ref(null);
+const contactSubmitting = ref(false);
+const currentId = ref(props.id);
+const projectContactForm = reactive({
+  id: undefined,
+  customerId: undefined,
+  contactName: '',
+  type: '1',
+  gender: '0',
+  age: null,
+  nativePlace: '',
+  birthday: null,
+  remark: null,
+  jobStatus: '1',
+  phone: '',
+  deptName: '',
+  position: '',
+  officePhone: '',
+  officeRegion: [],
+  provincialCityCounty: '',
+  addressDetail: '',
+  jobContent: null,
+  projectRole: null,
+  isKeyPerson: 0,
+  prStatus: null,
+  familyStatus: null,
+  hobby: null,
+  characterTrait: null,
+  isSmoke: '0',
+  isDrink: '0',
+  homeRegion: [],
+  homeAddressDetail: null
+});
+const projectContactRules = {
+  contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
+  deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '座机号请输入正确的11位手机号码', trigger: 'blur' }
+  ]
+};
 const proxy = getCurrentInstance().proxy;
 const { 
   LXRJE0001: projectRoleOptions,
@@ -570,26 +658,99 @@ watch(() => props.modelValue, (val) => {
 }, { immediate: true });
 
 watch(() => visible.value, (val) => {
-  emit('update:modelValue', val);
-});
-
-watch(() => [props.id, visible.value], ([newId, isVisible]) => {
-  if (isVisible) {
-    // 加载字典
+  if (val) {
     loadDicts();
-    // 优先使用列表传入的基础数据,实现秒开体验
+    loadAreaOptions();
     if (props.infoData && Object.keys(props.infoData).length > 0) {
       form.value = { ...props.infoData };
     }
-    if (newId) {
-      getDetail(newId);
+    const targetId = props.id || currentId.value;
+    if (targetId) {
+      getDetail(targetId);
     }
+    // 强制触发一次地址加载
+    loadAreaOptions();
   }
-}, { immediate: true });
+  emit('update:modelValue', val);
+});
+
+const open = (id, options = {}) => {
+  visible.value = true;
+  activeTab.value = options.activeTab || 'info';
+  currentId.value = id;
+  // 这种情况下,watch 也会触发,但我们可以显式调用以防万一
+  if (id) {
+    getDetail(id);
+  }
+  
+  if (options.autoCreateContact) {
+    // 延迟确保详情加载和抽屉组件就绪
+    setTimeout(() => {
+      handleNewProjectContact();
+    }, 500);
+  }
+};
 
 const loadDicts = () => {
 };
 
+const loadAreaOptions = () => {
+  listProvinceWithCities().then(res => {
+    // 接口返回的是分页对象 res.rows
+    const list = res.rows || [];
+    if (list.length > 0) {
+      // 这里的 props 映射是 { label: 'areaName', value: 'id', children: 'children' }
+      // 我们通过 handleTree 构建树形结构
+      areaOptions.value = handleTree(list, "id", "parentId");
+    }
+  });
+};
+
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
 const getDetail = (id) => {
   if (!id) return;
   loading.value = true;
@@ -598,9 +759,25 @@ const getDetail = (id) => {
     const data = res.data || res;
     if (data && typeof data === 'object') {
       form.value = data;
-      loadProjectContacts();
-      loadAnalysisData();
-      loadOssFiles();
+      // 如果数据中已经带了 customerId,直接作为 realCustomerId
+      if (data.customerId) {
+        form.value.realCustomerId = data.customerId;
+      }
+      // 通过客户编号查询真实的客户 ID (作为补充校验)
+      if (data.customerNo) {
+        listCustomerInfo({ customerNo: data.customerNo }).then(res => {
+          if (res.rows && res.rows.length > 0) {
+            form.value.realCustomerId = res.rows[0].id;
+          }
+          loadProjectContacts();
+          loadAnalysisData();
+          loadOssFiles();
+        });
+      } else {
+        loadProjectContacts();
+        loadAnalysisData();
+        loadOssFiles();
+      }
     } else {
       proxy.$modal.msgWarning("未获取到详情数据");
     }
@@ -618,9 +795,12 @@ const findUserName = (id) => {
 };
 
 const loadProjectContacts = () => {
-  if (!form.value.id) return;
   contactLoading.value = true;
-  listContact({ platformCode: String(form.value.id) }).then(res => {
+  // 按照客户 ID 和项目编号查询(即便 realCustomerId 为空,后端也支持通过 projectNo 进行备注匹配回显)
+  listContact({ 
+    customerId: form.value.realCustomerId || form.value.customerId,
+    projectNo: form.value.projectNo 
+  }).then(res => {
     contactList.value = res.rows || [];
   }).finally(() => {
     contactLoading.value = false;
@@ -734,10 +914,24 @@ const handleContactCommand = (command) => {
   if (command === 'associate') {
     openAssociateDialog();
   } else if (command === 'create') {
-    proxy.$modal.msgInfo("新建联系人功能开发中...");
+    handleNewProjectContact();
   }
 };
 
+const handleNewProjectContact = () => {
+  Object.assign(projectContactForm, {
+    id: undefined,
+    customerId: form.value.customerId,
+    contactName: '', type: '1', gender: '0', age: null, deptName: '',
+    position: '', projectRole: null, isKeyPerson: 0, jobStatus: '1', phone: '',
+    nativePlace: '', birthday: null, remark: null, officePhone: '', officeRegion: [], provincialCityCounty: '', addressDetail: '',
+    jobContent: null, prStatus: null,
+    familyStatus: null, hobby: null, characterTrait: null, isSmoke: '0', isDrink: '0',
+    homeRegion: [], homeAddressDetail: null
+  });
+  projectContactDrawerOpen.value = true;
+};
+
 // 打开关联联系人弹窗
 const openAssociateDialog = () => {
   if (!form.value.customerName) {
@@ -770,11 +964,12 @@ const submitAssociate = async () => {
   
   try {
     proxy.$modal.loading("正在关联...");
-    // 遍历选中的联系人,更新其 platformCode 为当前线索 ID
     const promises = selectedContacts.value.map(contact => {
       return updateContact({
         ...contact,
-        platformCode: String(form.value.id)
+        customerId: form.value.realCustomerId || form.value.customerId || contact.customerId,
+        platformCode: form.value.projectNo || '',
+        projectNo: form.value.projectNo || ''
       });
     });
     
@@ -791,11 +986,11 @@ const submitAssociate = async () => {
 
 // 查看联系人详情
 const viewContactDetail = (row) => {
-  const contactId = row.contactId || row.id;
+  const contactId = row.id;
   if (!contactId) return;
   contactDetailVisible.value = true;
   contactDetailLoading.value = true;
-  getContactPerson(contactId).then(res => {
+  getContact(contactId).then(res => {
     contactForm.value = res.data || {};
   }).finally(() => {
     contactDetailLoading.value = false;
@@ -805,41 +1000,172 @@ const viewContactDetail = (row) => {
 const handleEditContact = (row) => {
   const contactId = row.contactId || row.id;
   if (!contactId) return;
-  editContactLoading.value = true;
-  getContactPerson(contactId).then(res => {
-    editContactForm.value = res.data || {};
-    // 确保单选框的值是字符串,以便正确匹配 radio
-    if (editContactForm.value.roleId) editContactForm.value.roleId = String(editContactForm.value.roleId);
-    if (editContactForm.value.gender) editContactForm.value.gender = String(editContactForm.value.gender);
-    if (editContactForm.value.status) editContactForm.value.status = String(editContactForm.value.status);
-    if (editContactForm.value.isSmoking) editContactForm.value.isSmoking = String(editContactForm.value.isSmoking);
-    if (editContactForm.value.isDrinking) editContactForm.value.isDrinking = String(editContactForm.value.isDrinking);
-    editContactVisible.value = true;
+
+  // 1. 重置表单,确保不残留上次的数据
+  Object.assign(projectContactForm, {
+    id: undefined,
+    customerId: undefined,
+    contactName: '', type: '1', gender: '0', age: null, deptName: '',
+    position: '', projectRole: null, isKeyPerson: 0, jobStatus: '1', phone: '',
+    nativePlace: '', birthday: null, remark: null, officePhone: '', officeRegion: [], provincialCityCounty: '', addressDetail: '',
+    jobContent: null, prStatus: null,
+    familyStatus: null, hobby: null, characterTrait: null, isSmoke: '0', isDrink: '0',
+    homeRegion: [], homeAddressDetail: null
+  });
+
+  // 2. 请求详情数据
+  proxy.$modal.loading("正在获取详情...");
+  getContact(contactId).then(res => {
+    const data = res.data || {};
+    if (!data.id) {
+      proxy.$modal.msgError("获取联系人详情失败");
+      return;
+    }
+
+    // 3. 赋值回显
+    projectContactForm.id = data.id;
+    projectContactForm.customerId = data.customerId;
+    projectContactForm.contactName = data.contactName;
+    projectContactForm.gender = String(data.gender || '0');
+    projectContactForm.deptName = data.deptName;
+    projectContactForm.position = data.position;
+    projectContactForm.projectRole = data.projectRole !== null && data.projectRole !== undefined ? Number(data.projectRole) : null;
+    projectContactForm.isKeyPerson = data.isKeyPerson !== null && data.isKeyPerson !== undefined ? Number(data.isKeyPerson) : 0;
+    projectContactForm.prStatus = data.prStatus;
+    projectContactForm.phone = data.phone;
+    projectContactForm.type = String(data.type || '1');
+    projectContactForm.jobStatus = String(data.jobStatus || '1');
+    projectContactForm.age = data.age;
+    projectContactForm.nativePlace = data.nativePlace;
+    projectContactForm.birthday = data.birthday;
+    projectContactForm.remark = data.remark;
+    projectContactForm.officePhone = data.officePhone;
+    projectContactForm.addressDetail = data.addressDetail;
+    projectContactForm.jobContent = data.jobContent;
+    projectContactForm.familyStatus = data.familyStatus;
+    projectContactForm.hobby = data.hobby;
+    projectContactForm.characterTrait = data.characterTrait;
+    projectContactForm.isSmoke = String(data.isSmoke || '0');
+    projectContactForm.isDrink = String(data.isDrink || '0');
+    projectContactForm.homeAddressDetail = data.homeAddressDetail;
+    
+    // 区域级联处理 (由于办公地址数据库存的是代码 String,而家庭住址存的是 ID Long,这里需要做特殊转换)
+    const findIdByCode = (code) => {
+        if (!code) return null;
+        // 在扁平的原始列表中查找(由于 loadAreaOptions 是异步的,这里从 areaOptions 树中递归找或者从接口缓存中找更稳)
+        // 简单处理:我们通过深度优先搜索在已有的树中查找
+        const findInTree = (nodes, targetCode) => {
+            for (const node of nodes) {
+                if (String(node.areaCode) === String(targetCode)) return node.id;
+                if (node.children) {
+                    const found = findInTree(node.children, targetCode);
+                    if (found) return found;
+                }
+            }
+            return null;
+        };
+        return findInTree(areaOptions.value, code);
+    };
+
+    const officeArr = [];
+    if (data.addressProvince) {
+        // 如果是代码,转为 ID
+        const pid = /^\d+$/.test(data.addressProvince) && data.addressProvince.length < 5 ? data.addressProvince : findIdByCode(data.addressProvince);
+        if (pid) officeArr.push(Number(pid));
+    }
+    if (data.addressCity) {
+        const cid = /^\d+$/.test(data.addressCity) && data.addressCity.length < 7 ? data.addressCity : findIdByCode(data.addressCity);
+        if (cid) officeArr.push(Number(cid));
+    }
+    if (data.addressCounty) {
+        const aid = /^\d+$/.test(data.addressCounty) && data.addressCounty.length < 9 ? data.addressCounty : findIdByCode(data.addressCounty);
+        if (aid) officeArr.push(Number(aid));
+    }
+    projectContactForm.officeRegion = officeArr;
+    
+    const homeArr = [];
+    if (data.homeProvinceId) homeArr.push(Number(data.homeProvinceId));
+    if (data.homeCityId) homeArr.push(Number(data.homeCityId));
+    if (data.homeAreaId) homeArr.push(Number(data.homeAreaId));
+    projectContactForm.homeRegion = homeArr;
+
+    // 4. 全部数据就绪后再打开抽屉
+    projectContactDrawerOpen.value = true;
   }).finally(() => {
-    editContactLoading.value = false;
+    proxy.$modal.closeLoading();
   });
 };
 
-const submitEditContact = () => {
-  proxy.$refs.editContactRef.validate(valid => {
+const submitProjectContact = () => {
+  projectContactFormRef.value.validate(async (valid) => {
     if (valid) {
-      proxy.$modal.loading("正在保存...");
-      updateContact(editContactForm.value).then(() => {
-        proxy.$modal.closeLoading();
-        proxy.$modal.msgSuccess("修改成功");
-        editContactVisible.value = false;
+      contactSubmitting.value = true;
+      try {
+        // 获取省市区名称
+        const checkedNodes = officeRegionCascader.value?.getCheckedNodes();
+        let provincialCityCounty = '';
+        if (checkedNodes && checkedNodes.length > 0) {
+          provincialCityCounty = checkedNodes[0].pathLabels.join("");
+        }
+
+        // 获取选中的 AreaCode 列表 (用于办公地址)
+        const findCodeById = (id) => {
+            const findInTree = (nodes, targetId) => {
+                for (const node of nodes) {
+                    if (Number(node.id) === Number(targetId)) return node.areaCode;
+                    if (node.children) {
+                        const found = findInTree(node.children, targetId);
+                        if (found) return found;
+                    }
+                }
+                return null;
+            };
+            return findInTree(areaOptions.value, id);
+        };
+
+        const payload = {
+          ...projectContactForm,
+          roleId: projectContactForm.type === '1' ? 1 : 2,
+          // 优先使用异步查询到的 ID,其次使用主表 ID,最后使用表单内的 ID
+          customerId: form.value.realCustomerId || form.value.customerId || projectContactForm.customerId,
+          platformCode: form.value.projectNo || '',
+          customerName: form.value.customerName || '',
+          customerNo: form.value.customerNo || '',
+          projectNo: form.value.projectNo || '',
+          // age 字段转换
+          age: projectContactForm.age ? parseInt(projectContactForm.age, 10) : null,
+          // 办公地址存储为代码 (String)
+          addressProvince: projectContactForm.officeRegion?.[0] ? findCodeById(projectContactForm.officeRegion[0]) : null,
+          addressCity: projectContactForm.officeRegion?.[1] ? findCodeById(projectContactForm.officeRegion[1]) : null,
+          addressCounty: projectContactForm.officeRegion?.[2] ? findCodeById(projectContactForm.officeRegion[2]) : null,
+          provincialCityCounty: provincialCityCounty,
+          // 家庭地址存储为 ID (Long)
+          homeProvinceId: projectContactForm.homeRegion?.[0] || null,
+          homeCityId: projectContactForm.homeRegion?.[1] || null,
+          homeAreaId: projectContactForm.homeRegion?.[2] || null,
+        };
+        if (projectContactForm.id) {
+          await updateContact(payload);
+        } else {
+          await addContact(payload);
+        }
+        proxy.$modal.msgSuccess("保存成功");
+        projectContactDrawerOpen.value = false;
         loadProjectContacts();
-      }).catch(() => {
-        proxy.$modal.closeLoading();
-      });
+      } catch (e) {
+        // 显示真实错误,避免静默失败
+        proxy.$modal.msgError(e?.message || "保存失败,请检查控制台");
+        console.error("保存联系人失败:", e);
+      } finally {
+        contactSubmitting.value = false;
+      }
     }
   });
 };
 
 const handleDeleteContact = (row) => {
-  proxy.$modal.confirm('确认要删除该联系人与本项目的关联吗?').then(() => {
-    // 这里其实是取消关联,将 platformCode 置空
-    return updateContact({ ...row, platformCode: '' });
+  proxy.$modal.confirm('确认要删除该联系人吗?').then(() => {
+    return delContact(row.id);
   }).then(() => {
     proxy.$modal.msgSuccess("删除成功");
     loadProjectContacts();
@@ -862,6 +1188,12 @@ const handleDeleteFile = (row) => {
     }
   });
 };
+
+onMounted(() => {
+  loadAreaOptions();
+});
+
+defineExpose({ open });
 </script>
 
 <style scoped lang="scss">
@@ -1002,13 +1334,38 @@ const handleDeleteFile = (row) => {
   padding: 0 10px;
   .preview-title {
     font-size: 14px;
-    font-weight: 500;
+    font-weight: normal;
     color: #409eff;
     margin-bottom: 12px;
     padding-left: 8px;
     border-left: 3px solid #409eff;
   }
 }
+
+.form-section-group {
+  margin-bottom: 20px;
+  .section-group-title {
+    padding: 8px 15px;
+    background-color: #f8fbff;
+    color: #409eff;
+    font-size: 14px;
+    margin-bottom: 15px;
+  }
+  .section-group-content { padding: 0 15px; }
+}
+
+.no-bold-label {
+  :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
+  :deep(.el-form-item) { margin-bottom: 18px !important; }
+}
+
+.drawer-body-custom {
+  &::-webkit-scrollbar {
+    display: none;
+  }
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
 :deep(.el-descriptions__label) {
   width: 100px;
   background-color: #f8f9fb !important;

+ 3 - 6
src/views/saleManage/leads/edit.vue

@@ -31,9 +31,6 @@
                   v-model="form.customerNo" 
                   placeholder="请输入关键字检索" 
                   filterable 
-                  remote
-                  :remote-method="remoteLoadCustomers"
-                  :loading="customerLoading"
                   style="width: 100%" 
                   @change="handleCustomerChange"
                 >
@@ -162,7 +159,7 @@
 <script setup>
 import { ref, reactive, watch, computed, getCurrentInstance } from 'vue';
 import { getLeads, updateLeads } from '@/api/saleManage/leads/index';
-import { listCustomerOption } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listByIds } from '@/api/system/oss/index';
 import { globalHeaders } from '@/utils/request';
 import { Close, Upload } from '@element-plus/icons-vue';
@@ -279,8 +276,8 @@ const loadDetail = (id) => {
 
 const remoteLoadCustomers = (query) => {
   customerLoading.value = true;
-  listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
-    const list = res.data || res.rows || [];
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
+    const list = res.rows || [];
     localCustomerOptions.value = list.map(i => ({ ...i, customerNo: String(i.customerNo) }));
     // 确保当前选中的客户始终在列表中
     if (form.customerNo && form.customerName) {

+ 6 - 6
src/views/saleManage/leads/index.vue

@@ -23,9 +23,6 @@
                 style="width: 100%" 
                 clearable 
                 filterable
-                remote
-                :remote-method="remoteLoadCustomers"
-                :loading="customerLoading"
               >
                 <el-option v-for="item in localCustomerOptions" :key="item.customerNo" :label="item.customerName" :value="item.customerName" />
               </el-select>
@@ -238,6 +235,7 @@
       @success="getList" 
     />
     <LeadsDetail 
+      ref="leadsDetailRef"
       v-model="detailVisible" 
       :id="currentId"
       :info-data="currentRow"
@@ -305,7 +303,7 @@
 import { ref, reactive, onMounted, computed, getCurrentInstance, toRefs } from 'vue';
 import { listLeads, delLeads, transferLeads, claimLeads } from '@/api/saleManage/leads/index';
 import { listComStaff } from "@/api/system/comStaff/index";
-import { listCompanyOption, listCustomerOption } from "@/api/customer/customerInfo/index";
+import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listIndustryCategory } from "@/api/customer/industryCategory";
 import { deptTreeSelect } from "@/api/system/user";
 import { Search, Refresh, Plus, Switch, Link, InfoFilled } from '@element-plus/icons-vue';
@@ -315,6 +313,8 @@ import LeadsAdd from './add.vue';
 import LeadsEdit from './edit.vue';
 import LeadsDetail from './detail.vue';
 
+const leadsDetailRef = ref(null);
+
 const proxy = getCurrentInstance().proxy;
 const loading = ref(false);
 const total = ref(0);
@@ -423,8 +423,8 @@ const getDicts = () => {
 
 const remoteLoadCustomers = (query) => {
   customerLoading.value = true;
-  listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
-    const list = res.data || res.rows || [];
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
+    const list = res.rows || [];
     localCustomerOptions.value = list.map(i => ({ ...i, customerNo: String(i.customerNo) }));
   }).finally(() => {
     customerLoading.value = false;

+ 5 - 8
src/views/saleManage/opportunity/add.vue

@@ -30,9 +30,6 @@
                     style="width: 100%" 
                     clearable 
                     filterable 
-                    remote
-                    :remote-method="remoteLoadCustomers"
-                    :loading="customerLoading"
                     @change="handleCustomerChange"
                   >
                     <el-option v-for="item in localCustomerOptions" :key="item.id"
@@ -128,10 +125,10 @@
               </el-col>
             </el-row>
             <el-form-item label="项目描述" prop="description">
-              <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入项目描述" />
+              <el-input v-model="form.description" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入项目描述" />
             </el-form-item>
             <el-form-item label="竞争对手" prop="competitor">
-              <el-input v-model="form.competitor" type="textarea" :rows="3" placeholder="请输入竞争对手" />
+              <el-input v-model="form.competitor" type="textarea" :rows="3" maxlength="200" show-word-limit placeholder="请输入竞争对手" />
             </el-form-item>
           </div>
 
@@ -184,7 +181,7 @@
 import { ref, reactive, watch, getCurrentInstance } from 'vue';
 import { useDebounceFn } from '@vueuse/core';
 import { addOpportunity } from '@/api/saleManage/opportunity/index';
-import { listCustomerOption } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { globalHeaders } from '@/utils/request';
 import { Close, Upload, Document } from '@element-plus/icons-vue';
 
@@ -220,8 +217,8 @@ const form = reactive({
 
 const remoteLoadCustomers = useDebounceFn((query) => {
   customerLoading.value = true;
-  listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
-    const list = res.data || res.rows || [];
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
+    const list = res.rows || [];
     localCustomerOptions.value = list.map(i => ({ ...i, customerNo: String(i.customerNo) }));
   }).finally(() => {
     customerLoading.value = false;

+ 268 - 63
src/views/saleManage/opportunity/detail.vue

@@ -262,9 +262,9 @@
 
     <!-- 编辑/新建项目联系人抽屉 (完全对齐原型图) -->
     <el-drawer v-model="projectContactDrawerOpen" direction="rtl" size="80%" destroy-on-close :with-header="false">
-      <div class="drawer-header-standard" style="padding: 15px 20px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center;">
-        <span style="font-size: 16px; font-weight: normal; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
-        <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 20px;"><Close /></el-icon>
+      <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
+        <span style="font-size: 15px; font-weight: 600; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
+        <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
       </div>
 
       <div class="drawer-body-custom" style="padding: 0; overflow-y: auto; height: calc(100% - 110px);">
@@ -275,21 +275,21 @@
             <div class="section-group-content">
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="姓名" prop="name">
-                    <el-input v-model="projectContactForm.name" placeholder="请输入" />
+                  <el-form-item label="姓名" prop="contactName">
+                    <el-input v-model="projectContactForm.contactName" placeholder="请输入" />
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="联系人类型" prop="contactType">
-                    <el-radio-group v-model="projectContactForm.contactType">
+                  <el-form-item label="联系人类型" prop="type">
+                    <el-radio-group v-model="projectContactForm.type">
                       <el-radio value="1">公司职员</el-radio>
                       <el-radio value="2">关系资源人</el-radio>
                     </el-radio-group>
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="性别" prop="sex">
-                    <el-radio-group v-model="projectContactForm.sex">
+                  <el-form-item label="性别" prop="gender">
+                    <el-radio-group v-model="projectContactForm.gender">
                       <el-radio value="0">男</el-radio>
                       <el-radio value="1">女</el-radio>
                     </el-radio-group>
@@ -316,7 +316,7 @@
               <el-row :gutter="20">
                 <el-col :span="24">
                   <el-form-item label="描述" prop="remark">
-                    <el-input v-model="projectContactForm.remark" type="textarea" :rows="2" placeholder="请输入" />
+                    <el-input v-model="projectContactForm.remark" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="请输入" />
                   </el-form-item>
                 </el-col>
               </el-row>
@@ -329,16 +329,16 @@
             <div class="section-group-content">
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="在职状态" prop="status">
-                    <el-radio-group v-model="projectContactForm.status">
+                  <el-form-item label="在职状态" prop="jobStatus">
+                    <el-radio-group v-model="projectContactForm.jobStatus">
                       <el-radio value="0">在职</el-radio>
                       <el-radio value="1">离职</el-radio>
                     </el-radio-group>
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="手机号码" prop="phonenumber">
-                    <el-input v-model="projectContactForm.phonenumber" placeholder="请输入" />
+                  <el-form-item label="手机号码" prop="phone">
+                    <el-input v-model="projectContactForm.phone" placeholder="请输入" />
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
@@ -353,26 +353,26 @@
                     <el-input v-model="projectContactForm.position" placeholder="请输入" />
                   </el-form-item>
                 </el-col>
-                <el-col :span="8">
-                  <el-form-item label="办公座机" prop="officePhone">
-                    <el-input v-model="projectContactForm.officePhone" placeholder="请输入" />
-                  </el-form-item>
-                </el-col>
-              </el-row>
-              <el-row :gutter="20">
-                <el-col :span="24">
-                  <el-form-item label="办公地址" prop="address">
-                    <div style="display: flex; gap: 10px;">
-                      <el-input v-model="projectContactForm.provincialCityCounty" placeholder="请选择" style="width: 200px" readonly />
-                      <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" />
+                <el-col :span="16">
+                  <el-form-item label="办公地址" prop="addressDetail">
+                    <div style="display: flex; gap: 10px; width: 100%">
+                      <el-cascader
+                        v-model="projectContactForm.officeRegion"
+                        :options="areaOptions"
+                        :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                        placeholder="请选择省/市/县"
+                        style="width: 220px"
+                        clearable
+                      />
+                      <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
                     </div>
                   </el-form-item>
                 </el-col>
               </el-row>
               <el-row :gutter="20">
-                <el-col :span="24">
-                  <el-form-item label="工作内容" prop="jobContent">
-                    <el-input v-model="projectContactForm.jobContent" type="textarea" :rows="2" placeholder="请输入" />
+                <el-col :span="8">
+                  <el-form-item label="办公座机" prop="officePhone">
+                    <el-input v-model="projectContactForm.officePhone" placeholder="请输入" />
                   </el-form-item>
                 </el-col>
               </el-row>
@@ -387,7 +387,7 @@
                 <el-col :span="8">
                   <el-form-item label="项目角色" prop="projectRole">
                     <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable>
-                      <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
+                      <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="parseInt(item.value)" />
                     </el-select>
                   </el-form-item>
                 </el-col>
@@ -403,7 +403,66 @@
               <el-row :gutter="20">
                 <el-col :span="24">
                   <el-form-item label="公关情况" prop="prStatus">
-                    <el-input v-model="projectContactForm.prStatus" type="textarea" :rows="2" placeholder="请输入" />
+                    <el-input v-model="projectContactForm.prStatus" type="textarea" :rows="2" maxlength="200" show-word-limit placeholder="请输入" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+            </div>
+          </div>
+
+          <!-- 家庭信息 -->
+          <div class="form-section-group">
+            <div class="section-group-title">家庭信息</div>
+            <div class="section-group-content">
+              <el-row :gutter="20">
+                <el-col :span="24">
+                  <el-form-item label="家庭住址" prop="homeAddressDetail">
+                    <div style="display: flex; gap: 10px; width: 100%">
+                      <el-cascader
+                        v-model="projectContactForm.homeRegion"
+                        :options="areaOptions"
+                        :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                        placeholder="请选择省/市/县"
+                        style="width: 220px"
+                        clearable
+                      />
+                      <el-input v-model="projectContactForm.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                    </div>
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="家庭情况" prop="familyStatus">
+                    <el-input v-model="projectContactForm.familyStatus" placeholder="请输入家庭情况" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="爱好" prop="hobby">
+                    <el-input v-model="projectContactForm.hobby" placeholder="请输入爱好" />
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="性格特征" prop="characterTrait">
+                    <el-input v-model="projectContactForm.characterTrait" placeholder="请输入性格特征" />
+                  </el-form-item>
+                </el-col>
+              </el-row>
+              <el-row :gutter="20">
+                <el-col :span="8">
+                  <el-form-item label="是否抽烟" prop="isSmoke">
+                    <el-radio-group v-model="projectContactForm.isSmoke">
+                      <el-radio value="1">是</el-radio>
+                      <el-radio value="0">否</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="是否喝酒" prop="isDrink">
+                    <el-radio-group v-model="projectContactForm.isDrink">
+                      <el-radio value="1">是</el-radio>
+                      <el-radio value="0">否</el-radio>
+                    </el-radio-group>
                   </el-form-item>
                 </el-col>
               </el-row>
@@ -424,11 +483,13 @@ import { ref, computed, reactive, watch, getCurrentInstance, toRefs } from 'vue'
 import { useDebounceFn } from '@vueuse/core';
 import { Plus, Search, Close, Upload, Edit, ArrowDown, CircleCheck } from '@element-plus/icons-vue';
 import { getOpportunity, updateOpportunity } from '@/api/saleManage/opportunity/index';
-import { listContact, addContact, delContact, updateContact } from "@/api/customer/crmContact";
-import { getContactPerson } from "@/api/customer/contactPerson";
+import { getProjectSelection, updateProjectSelection } from '@/api/saleManage/projectSelection/index';
+import { listContact, addContact, delContact, updateContact, getContact } from "@/api/customer/crmContact";
+import { listProvinceWithCities } from "@/api/customer/addressArea";
 import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
 import { listByIds } from "@/api/system/oss/index";
 import { listIndustryCategory } from "@/api/customer/industryCategory";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { globalHeaders } from '@/utils/request';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import BusinessActivity from '@/views/common/businessActivity.vue';
@@ -483,28 +544,43 @@ const projectContactDrawerOpen = ref(false);
 const projectContactFormRef = ref(null);
 const projectContactForm = reactive({
   id: undefined,
-  name: '',
-  contactType: '1',
-  sex: '0',
+  contactName: '',
+  type: '1',
+  gender: '0',
   age: '',
   nativePlace: '',
   birthday: '',
   remark: '',
-  status: '0',
-  phonenumber: '',
+  jobStatus: '0',
+  phone: '',
   deptName: '',
   position: '',
   officePhone: '',
+  officeRegion: [],
   provincialCityCounty: '',
   addressDetail: '',
   jobContent: '',
   projectRole: '',
   isKeyPerson: 0,
-  prStatus: ''
+  prStatus: '',
+  familyStatus: '',
+  hobby: '',
+  characterTrait: '',
+  isSmoke: '0',
+  isDrink: '0',
+  homeRegion: [],
+  homeAddressDetail: ''
 });
 const projectContactRules = {
-  name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
-  phonenumber: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }],
+  contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
+  deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '座机号请输入正确的11位手机号码', trigger: 'blur' }
+  ]
 };
 
 // 结果分析相关
@@ -523,6 +599,7 @@ const associateLoading = ref(false);
 const associateList = ref([]);
 const selectedContacts = ref([]);
 const associateQuery = reactive({ contactName: '', phone: '' });
+const areaOptions = ref([]);
 const { LXRJE0001: projectRoleOptions } = toRefs(reactive(proxy.useDict("LXRJE0001")));
 
 const getFileType = (name) => {
@@ -555,6 +632,7 @@ const open = async (row, tab) => {
   
   // 预加载字典
   loadDicts();
+  loadAreaOptions();
   
   try {
     const id = row.id;
@@ -562,6 +640,14 @@ const open = async (row, tab) => {
       const res = await getOpportunity(id);
       if (res.data) {
         Object.assign(detailData.value, res.data);
+        // 通过客户编号查询真实的客户 ID
+        if (detailData.value.customerNo) {
+          listCustomerInfo({ customerNo: detailData.value.customerNo }).then(cRes => {
+            if (cRes.rows && cRes.rows.length > 0) {
+              detailData.value.realCustomerId = cRes.rows[0].id;
+            }
+          });
+        }
       }
       fetchContactList(id);
       getIndustryList();
@@ -635,6 +721,7 @@ const handleSaveAnalysis = useDebounceFn(async () => {
   const payload = {
     id: analysisForm.id,
     objectNo: detailData.value.projectNo,
+    dataType: 2, // 2代表商机
     dealResult: analysisForm.resultType === 'win' ? 1 : 2,
     winSumUp: analysisForm.summary,
     loseReason: analysisForm.loseReason,
@@ -670,6 +757,58 @@ const downloadFile = (file) => {
 const loadDicts = () => {
 };
 
+const loadAreaOptions = () => {
+  listProvinceWithCities().then(res => {
+    const list = res.rows || [];
+    areaOptions.value = handleTree(list, "id", "parentId");
+  });
+};
+
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
 const getIndustryList = async () => {
   try {
     const res = await listIndustryCategory();
@@ -708,7 +847,6 @@ const loadAssociateList = async () => {
     proxy.$modal.msgWarning("当前商机未绑定客户,无法关联联系人");
     return;
   }
-  associateVisible.value = true;
   associateLoading.value = true;
   // 根据客户名称查询联系人 (注意:后端参数名为 customName,对齐线索模块逻辑)
   listContact({ customName: detailData.value.companyName }).then(res => {
@@ -726,44 +864,92 @@ const handleSelectionChange = (selection) => {
 
 const confirmAssociate = useDebounceFn(async () => {
   if (selectedContacts.value.length === 0) return;
+  
   proxy.$modal.loading("正在关联...");
   try {
     const promises = selectedContacts.value.map(contact => {
-      return updateContact({
+      return addContact({
         ...contact,
-        platformCode: String(detailData.value.id)
+        id: undefined, // 避免使用联系人ID作为关联表主键
+        contactId: contact.id, // 记录原联系人ID
+        platformCode: String(detailData.value.id),
+        customerId: detailData.value.realCustomerId || detailData.value.customerNo,
+        customerName: detailData.value.customerName,
+        customerNo: detailData.value.customerNo,
+        projectNo: detailData.value.projectNo
       });
     });
     await Promise.all(promises);
     ElMessage.success('关联成功');
     associateVisible.value = false;
     fetchContactList(detailData.value.id);
-  } catch (e) {} finally {
+  } catch (e) {
+    console.error('关联失败:', e);
+  } finally {
     proxy.$modal.closeLoading();
   }
 }, 300);
 
 const handleNewProjectContact = () => {
+  if (!detailData.value.customerNo) {
+    proxy.$modal.msgWarning("当前商机未关联客户,无法创建联系人");
+    return;
+  }
   Object.assign(projectContactForm, {
-    id: undefined, name: '', contactType: '1', sex: '0', age: '', deptName: '',
-    position: '', projectRole: '', isKeyPerson: 0, status: '1', phonenumber: ''
+    id: undefined, contactName: '', type: '1', gender: '0', age: '',
+    jobStatus: '0', phone: '', deptName: '', position: '',
+    customerId: detailData.value.realCustomerId || detailData.value.customerNo,
+    customerName: detailData.value.customerName,
+    nativePlace: '', birthday: '', remark: '', officePhone: '', officeRegion: [], provincialCityCounty: '', addressDetail: '',
+    jobContent: '', prStatus: '',
+    familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0',
+    homeRegion: [], homeAddressDetail: ''
   });
   projectContactDrawerOpen.value = true;
 };
 
 const handleEditContact = (row) => {
-  Object.assign(projectContactForm, {
-    id: row.id,
-    name: row.contactName || row.name,
-    sex: row.gender || row.sex || '0',
-    deptName: row.deptName || row.department,
-    position: row.position || row.roleName || row.role,
-    projectRole: row.projectRole || row.role,
-    isKeyPerson: row.isKeyPerson || (row.keyPerson ? 1 : 0),
-    phonenumber: row.phonenumber || row.phone,
-    contactType: row.contactType || '1'
+  const contactId = row.contactId || row.id;
+  if (!contactId) return;
+  proxy.$modal.loading("加载中...");
+  getContact(contactId).then(res => {
+    const data = res.data || {};
+    Object.assign(projectContactForm, {
+      id: data.id,
+      contactName: data.contactName,
+      type: String(data.type || '1'),
+      gender: String(data.gender || '0'),
+      age: data.age,
+      customerId: data.customerId || detailData.value.realCustomerId || detailData.value.customerNo,
+      customerName: data.customerName || detailData.value.customerName,
+      nativePlace: data.nativePlace,
+      birthday: data.birthday,
+      remark: data.remark,
+      jobStatus: String(data.jobStatus !== null && data.jobStatus !== undefined ? data.jobStatus : '0'),
+      phone: data.phone,
+      deptName: data.deptName,
+      position: data.position,
+      officePhone: data.officePhone,
+      officeRegion: (data.addressProvince && data.addressCity && data.addressCounty) 
+        ? [data.addressProvince, data.addressCity, data.addressCounty] : [],
+      addressDetail: data.addressDetail,
+      jobContent: data.jobContent,
+      projectRole: data.projectRole !== null && data.projectRole !== undefined ? Number(data.projectRole) : null,
+      isKeyPerson: data.isKeyPerson !== null && data.isKeyPerson !== undefined ? Number(data.isKeyPerson) : 0,
+      prStatus: data.prStatus,
+      familyStatus: data.familyStatus,
+      hobby: data.hobby,
+      characterTrait: data.characterTrait,
+      isSmoke: String(data.isSmoke || '0'),
+      isDrink: String(data.isDrink || '0'),
+      homeRegion: (data.homeProvinceId && data.homeCityId && data.homeAreaId) 
+        ? [data.homeProvinceId, data.homeCityId, data.homeAreaId] : [],
+      homeAddressDetail: data.homeAddressDetail
+    });
+    projectContactDrawerOpen.value = true;
+  }).finally(() => {
+    proxy.$modal.closeLoading();
   });
-  projectContactDrawerOpen.value = true;
 };
 
 const submitProjectContact = useDebounceFn(() => {
@@ -771,13 +957,30 @@ const submitProjectContact = useDebounceFn(() => {
     if (valid) {
       contactSubmitting.value = true;
       try {
+        if (!detailData.value.customerNo && !projectContactForm.customerId) {
+          proxy.$modal.msgError("当前商机未关联客户,无法提交联系人");
+          return;
+        }
+
         const payload = {
           ...projectContactForm,
-          platformCode: String(detailData.value.id),
+          roleId: projectContactForm.type === '1' ? 1 : 2,
           // 关联客户信息
-          customerName: detailData.value.customerName || '',
-          customerNo: detailData.value.companyNo || '',
-          customNo: detailData.value.companyNo || ''
+          customerId: projectContactForm.customerId || detailData.value.realCustomerId || detailData.value.customerNo,
+          platformCode: String(detailData.value.id),
+          customerName: projectContactForm.customerName || detailData.value.customerName || '',
+          customerNo: detailData.value.customerNo || '',
+          projectNo: detailData.value.projectNo || '',
+          // age 字段转换
+          age: projectContactForm.age ? parseInt(projectContactForm.age, 10) : null,
+          // 办公地址编码 (String)
+          addressProvince: projectContactForm.officeRegion?.[0] ? String(projectContactForm.officeRegion[0]) : null,
+          addressCity: projectContactForm.officeRegion?.[1] ? String(projectContactForm.officeRegion[1]) : null,
+          addressCounty: projectContactForm.officeRegion?.[2] ? String(projectContactForm.officeRegion[2]) : null,
+          // 家庭地址 ID (Long)
+          homeProvinceId: projectContactForm.homeRegion?.[0] || null,
+          homeCityId: projectContactForm.homeRegion?.[1] || null,
+          homeAreaId: projectContactForm.homeRegion?.[2] || null,
         };
         if (projectContactForm.id) {
           await updateContact(payload);
@@ -795,7 +998,8 @@ const submitProjectContact = useDebounceFn(() => {
 }, 300);
 
 const handleDeleteContact = useDebounceFn((row) => {
-  ElMessageBox.confirm(`确认删除联系人「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
+  const name = row.contactName || row.name || '';
+  ElMessageBox.confirm(`确认删除联系人「${name}」吗?`, '提示', { type: 'warning' }).then(async () => {
     await delContact(row.id);
     ElMessage.success('操作成功');
     fetchContactList(detailData.value.id);
@@ -975,6 +1179,7 @@ defineExpose({ open });
 
 .no-bold-label {
   :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
+  :deep(.el-form-item) { margin-bottom: 18px !important; }
 }
 
 .contact-table {

+ 3 - 6
src/views/saleManage/opportunity/edit.vue

@@ -30,9 +30,6 @@
                     style="width: 100%" 
                     clearable 
                     filterable 
-                    remote
-                    :remote-method="remoteLoadCustomers"
-                    :loading="customerLoading"
                     @change="handleCustomerChange"
                   >
                     <el-option v-for="item in computedCustomerOptions" :key="item.id || item.customerNo"
@@ -183,7 +180,7 @@
 import { ref, reactive, watch, computed, getCurrentInstance } from 'vue';
 import { useDebounceFn } from '@vueuse/core';
 import { getOpportunity, updateOpportunity } from '@/api/saleManage/opportunity/index';
-import { listCustomerOption } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listByIds } from '@/api/system/oss/index';
 import { globalHeaders } from '@/utils/request';
 import { Close, Upload, Document } from '@element-plus/icons-vue';
@@ -217,8 +214,8 @@ const localCustomerOptions = ref([]);
 
 const remoteLoadCustomers = useDebounceFn((query) => {
   customerLoading.value = true;
-  listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
-    const list = res.data || res.rows || [];
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
+    const list = res.rows || [];
     localCustomerOptions.value = list.map(i => ({ ...i, id: String(i.id || i.customerNo) }));
   }).finally(() => {
     customerLoading.value = false;

+ 1 - 1
src/views/saleManage/opportunity/index.vue

@@ -309,7 +309,7 @@ import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
 import { useDebounceFn } from '@vueuse/core';
 import { listOpportunity, delOpportunity, transferOpportunity } from '@/api/saleManage/opportunity/index';
 import { listComStaff } from "@/api/system/comStaff/index";
-import { listCompanyOption, listCustomerOption } from "@/api/customer/customerInfo/index";
+import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listDept } from "@/api/system/dept/index";
 import { listIndustryCategory } from "@/api/customer/industryCategory";
 import { listRecord, addRecord } from "@/api/visit/record";

+ 7 - 8
src/views/saleManage/platformSelection/add.vue

@@ -13,7 +13,7 @@
           <el-row :gutter="20">
             <el-col :span="8">
               <el-form-item label="归属公司" prop="companyNo">
-                <el-select v-model="drawerForm.companyNo" style="width:100%" placeholder="请选择">
+                <el-select v-model="drawerForm.companyNo" style="width:100%" placeholder="请选择" filterable>
                   <el-option v-for="item in options.company" :key="item.companyCode" :label="item.companyName" :value="item.companyCode" />
                 </el-select>
               </el-form-item>
@@ -25,9 +25,6 @@
                   style="width:100%" 
                   placeholder="搜索客户" 
                   filterable 
-                  remote
-                  :remote-method="remoteSearchCustomer"
-                  :loading="customerLoading"
                   clearable
                 >
                   <el-option 
@@ -244,7 +241,7 @@
 <script setup>
 import { ref, reactive, computed, watch, onMounted, getCurrentInstance } from 'vue';
 import { addPlatformSelection } from '@/api/saleManage/platformSelection/index';
-import { listCustomerInfo, listCustomerListPage } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { getToken } from "@/utils/auth";
 import { Upload } from '@element-plus/icons-vue';
 
@@ -283,10 +280,12 @@ const computedCustomerOptions = computed(() => {
 const remoteSearchCustomer = async (query) => {
   customerLoading.value = true;
   try {
-    const res = await listCustomerListPage({ 
-      customerName: query || undefined
+    const res = await listCustomerInfo({ 
+      pageNum: 1,
+      pageSize: 500,
+      isHighSeas: 'all'
     });
-    customerOptions.value = res.rows || res.data || [];
+    customerOptions.value = res.rows || [];
   } catch (e) {
     customerOptions.value = [];
   } finally {

+ 362 - 63
src/views/saleManage/platformSelection/detail.vue

@@ -42,7 +42,7 @@
         <!-- 左侧核心信息 -->
         <div class="content-left-panel">
           <div class="detail-content-card">
-            <el-tabs v-model="drawerActiveTab" class="business-tabs">
+            <el-tabs v-model="drawerActiveTab" class="custom-tabs sticky-tabs">
               <el-tab-pane label="项目信息" name="info">
                 <!-- 基本信息 -->
                 <div class="info-section">
@@ -260,32 +260,33 @@
     v-model="projectContactDrawerOpen"
     direction="rtl"
     size="80%"
+    destroy-on-close
     :with-header="false"
   >
-    <div class="drawer-header-custom" style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center;">
-      <span style="font-size: 16px; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
-      <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 20px;"><Close /></el-icon>
+    <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
+      <span style="font-size: 15px; font-weight: 600; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
+      <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
     </div>
     <div class="drawer-body-custom" style="padding: 24px; overflow-y: auto; height: calc(100% - 120px);">
-      <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" label-position="right">
+      <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" label-position="right" class="no-bold-label">
         <!-- 基本信息 -->
         <div class="form-section-group">
           <div class="section-group-title-bar">基本信息</div>
           <el-row :gutter="20">
             <el-col :span="8">
-              <el-form-item label="姓名" prop="name"><el-input v-model="projectContactForm.name" placeholder="请输入" /></el-form-item>
+              <el-form-item label="姓名" prop="contactName"><el-input v-model="projectContactForm.contactName" placeholder="请输入" /></el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item label="联系人类型" prop="contactType">
-                <el-radio-group v-model="projectContactForm.contactType">
+              <el-form-item label="联系人类型" prop="type">
+                <el-radio-group v-model="projectContactForm.type">
                   <el-radio value="1">公司职员</el-radio>
                   <el-radio value="2">关系资源人</el-radio>
                 </el-radio-group>
               </el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item label="性别" prop="sex">
-                <el-radio-group v-model="projectContactForm.sex">
+              <el-form-item label="性别" prop="gender">
+                <el-radio-group v-model="projectContactForm.gender">
                   <el-radio value="0">男</el-radio><el-radio value="1">女</el-radio>
                 </el-radio-group>
               </el-form-item>
@@ -306,7 +307,7 @@
           </el-row>
           <el-row :gutter="20">
             <el-col :span="24">
-              <el-form-item label="描述" prop="description"><el-input v-model="projectContactForm.description" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+              <el-form-item label="描述" prop="remark"><el-input v-model="projectContactForm.remark" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
             </el-col>
           </el-row>
         </div>
@@ -316,15 +317,15 @@
           <div class="section-group-title-bar">办公信息</div>
           <el-row :gutter="20">
             <el-col :span="8">
-              <el-form-item label="在职状态" prop="status">
-                <el-radio-group v-model="projectContactForm.status">
-                  <el-radio value="0">在职</el-radio>
-                  <el-radio value="1">离职</el-radio>
+              <el-form-item label="在职状态" prop="jobStatus">
+                <el-radio-group v-model="projectContactForm.jobStatus">
+                  <el-radio value="1">在职</el-radio>
+                  <el-radio value="2">离职</el-radio>
                 </el-radio-group>
               </el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item label="手机号码" prop="phonenumber"><el-input v-model="projectContactForm.phonenumber" placeholder="请输入" /></el-form-item>
+              <el-form-item label="手机号码" prop="phone"><el-input v-model="projectContactForm.phone" placeholder="请输入" /></el-form-item>
             </el-col>
             <el-col :span="8">
               <el-form-item label="部门" prop="deptName"><el-input v-model="projectContactForm.deptName" placeholder="请输入" /></el-form-item>
@@ -340,24 +341,24 @@
           </el-row>
           <el-row :gutter="20">
             <el-col :span="24">
-              <el-form-item label="办公地址" prop="officeAddressDetail">
+              <el-form-item label="办公地址" prop="addressDetail">
                 <div style="display: flex; gap: 10px;">
                   <el-cascader
-                    v-model="projectContactForm.region"
+                    v-model="projectContactForm.officeRegion"
                     :options="areaOptions"
                     :props="{ label: 'areaName', value: 'id', children: 'children' }"
                     placeholder="请选择省/市/区"
                     style="width: 250px"
                     clearable
                   />
-                  <el-input v-model="projectContactForm.officeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                  <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
                 </div>
               </el-form-item>
             </el-col>
           </el-row>
           <el-row :gutter="20">
             <el-col :span="24">
-              <el-form-item label="工作内容" prop="workContent"><el-input v-model="projectContactForm.workContent" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+              <el-form-item label="工作内容" prop="jobContent"><el-input v-model="projectContactForm.jobContent" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
             </el-col>
           </el-row>
         </div>
@@ -369,7 +370,7 @@
             <el-col :span="8">
               <el-form-item label="项目角色" prop="projectRole">
                 <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable clearable>
-                  <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
+                  <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="parseInt(item.value)" />
                 </el-select>
               </el-form-item>
             </el-col>
@@ -383,7 +384,56 @@
           </el-row>
           <el-row :gutter="20">
             <el-col :span="24">
-              <el-form-item label="公关情况" prop="publicRelations"><el-input v-model="projectContactForm.publicRelations" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+              <el-form-item label="公关情况" prop="prStatus"><el-input v-model="projectContactForm.prStatus" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+
+        <!-- 家庭信息 -->
+        <div class="form-section-group">
+          <div class="section-group-title-bar">家庭信息</div>
+          <el-row :gutter="20">
+            <el-col :span="24">
+              <el-form-item label="家庭住址" prop="homeAddressDetail">
+                <div style="display: flex; gap: 10px;">
+                  <el-cascader
+                    v-model="projectContactForm.homeRegion"
+                    :options="areaOptions"
+                    :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                    placeholder="请选择省/市/区"
+                    style="width: 250px"
+                    clearable
+                  />
+                  <el-input v-model="projectContactForm.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                </div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item label="家庭情况" prop="familyStatus"><el-input v-model="projectContactForm.familyStatus" placeholder="请输入" /></el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="爱好" prop="hobby"><el-input v-model="projectContactForm.hobby" placeholder="请输入" /></el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="性格特征" prop="characterTrait"><el-input v-model="projectContactForm.characterTrait" placeholder="请输入" /></el-form-item>
+            </el-col>
+          </el-row>
+          <el-row :gutter="20">
+            <el-col :span="8">
+              <el-form-item label="是否抽烟" prop="isSmoke">
+                <el-radio-group v-model="projectContactForm.isSmoke">
+                  <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+            <el-col :span="8">
+              <el-form-item label="是否喝酒" prop="isDrink">
+                <el-radio-group v-model="projectContactForm.isDrink">
+                  <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
+                </el-radio-group>
+              </el-form-item>
             </el-col>
           </el-row>
         </div>
@@ -416,17 +466,16 @@
 <script setup>
 import { ref, reactive, computed, watch, getCurrentInstance, toRefs } from 'vue';
 import { getPlatformSelection, updatePlatformSelection } from '@/api/saleManage/platformSelection/index';
-import { listContact } from '@/api/customer/crmContact';
-import { Plus, Close, Edit, ArrowDown, Upload, CircleCheck } from '@element-plus/icons-vue';
-import { parseTime } from "@/utils/ruoyi";
-import { addContact, delContact, updateContact } from "@/api/customer/crmContact";
+import { listContact, addContact, delContact, updateContact, getContact } from '@/api/customer/crmContact';
 import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
 import { listByIds } from "@/api/system/oss/index";
 import { globalHeaders } from '@/utils/request';
 import { listProvinceWithCities } from "@/api/customer/addressArea";
+import { useDebounceFn } from '@vueuse/core';
 import BusinessActivity from '@/views/common/businessActivity.vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import { listComStaff } from '@/api/system/comStaff/index';
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 
 const proxy = getCurrentInstance().proxy;
 
@@ -485,15 +534,43 @@ const contactSubmitting = ref(false);
 const projectContactDrawerOpen = ref(false);
 const projectContactFormRef = ref(null);
 const projectContactForm = reactive({
-  id: undefined, name: '', contactType: '1', sex: '0', age: '', nativePlace: '', birthday: '', description: '',
-  status: '0', phonenumber: '', deptName: '', position: '', officePhone: '', region: [], officeAddressDetail: '', workContent: '',
-  projectRole: '', isKeyPerson: 0, publicRelations: ''
+  id: undefined,
+  contactName: '',
+  type: '1',
+  gender: '0',
+  age: '',
+  nativePlace: '',
+  birthday: '',
+  remark: '',
+  jobStatus: '1',
+  phone: '',
+  deptName: '',
+  position: '',
+  officePhone: '',
+  officeRegion: [],
+  addressDetail: '',
+  jobContent: '',
+  projectRole: null,
+  isKeyPerson: 0,
+  prStatus: '',
+  familyStatus: '',
+  hobby: '',
+  characterTrait: '',
+  isSmoke: '0',
+  isDrink: '0',
+  homeRegion: [],
+  homeAddressDetail: ''
 });
 const projectContactRules = {
-  name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
   age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
-  phonenumber: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }],
   deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
+  phone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '座机号请输入正确的11位手机号码', trigger: 'blur' }
+  ]
 };
 const { LXRJE0001: projectRoleOptions } = toRefs(reactive(proxy.useDict("LXRJE0001")));
 const areaOptions = ref([]);
@@ -537,6 +614,17 @@ const loadData = async () => {
       industry: data.industry || data.industryName || data.professionName,
       deptName: data.deptName || data.createDeptName || data.dept?.deptName || data.createDept
     };
+
+    // 通过客户编号查询真实的客户 ID
+    const cNo = drawerForm.value.customNo || drawerForm.value.companyNo || drawerForm.value.customerNo;
+    if (cNo) {
+      listCustomerInfo({ customerNo: cNo }).then(cRes => {
+        if (cRes.rows && cRes.rows.length > 0) {
+          drawerForm.value.realCustomerId = cRes.rows[0].id;
+        }
+      });
+    }
+
     // 加载完数据后,立即补全负责人和部门的文字名称
     enrichFormNames();
     fetchContactList(props.id);
@@ -576,9 +664,59 @@ const fetchContactList = async (id) => {
 
 const loadDicts = () => {
   if (areaOptions.value.length === 0) {
-    listProvinceWithCities().then(res => { areaOptions.value = res.data || []; });
+    listProvinceWithCities().then(res => { 
+      const list = res.rows || [];
+      if (list.length > 0) {
+        areaOptions.value = handleTree(list, "id", "parentId");
+      }
+    });
   }
 };
+
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
 loadDicts();
 
 // --- 项目联系人交互 ---
@@ -588,12 +726,14 @@ const handleContactCommand = (command) => {
 };
 
 const handleAssociate = () => {
-  if (!drawerForm.value.customNo && !drawerForm.value.companyNo) {
+  if (!drawerForm.value.customNo && !drawerForm.value.companyNo && !drawerForm.value.customerNo) {
     return ElMessage.warning("缺少客户信息,无法关联联系人");
   }
   associateVisible.value = true;
   associateLoading.value = true;
-  listContact({ customNo: drawerForm.value.customNo || drawerForm.value.companyNo }).then(res => {
+  // 统一使用客户编号进行查询
+  const cNo = drawerForm.value.customNo || drawerForm.value.companyNo || drawerForm.value.customerNo;
+  listContact({ customNo: cNo }).then(res => {
     const currentIds = contactList.value.map(c => c.id);
     associateList.value = (res.rows || []).filter(item => !currentIds.includes(item.id));
   }).finally(() => { associateLoading.value = false; });
@@ -616,25 +756,96 @@ const confirmAssociate = async () => {
 
 const handleNewProjectContact = () => {
   Object.assign(projectContactForm, {
-    id: undefined, name: '', contactType: '1', sex: '0', age: '', nativePlace: '', birthday: '', description: '',
-    status: '0', phonenumber: '', deptName: '', position: '', officePhone: '', region: [], officeAddressDetail: '', workContent: '',
-    projectRole: '', isKeyPerson: 0, publicRelations: ''
+    id: undefined, contactName: '', type: '1', gender: '0', age: '', nativePlace: '', birthday: '', remark: '',
+    jobStatus: '1', phone: '', deptName: '', position: '', officePhone: '', officeRegion: [], addressDetail: '', jobContent: '',
+    projectRole: null, isKeyPerson: 0, prStatus: '', familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0',
+    homeRegion: [], homeAddressDetail: ''
   });
   projectContactDrawerOpen.value = true;
 };
 
 const handleEditContact = (row) => {
-  Object.assign(projectContactForm, {
-    id: row.id, name: row.contactName || row.name, contactType: row.contactType || '1', sex: row.gender || row.sex || '0',
-    age: row.age, nativePlace: row.nativePlace, birthday: row.birthday, description: row.description || row.remark,
-    status: row.status || '0', phonenumber: row.phonenumber || row.phone, deptName: row.deptName || row.department,
-    position: row.position || row.roleName || row.role, officePhone: row.officePhone || row.landline,
-    region: row.officeAddressArea ? row.officeAddressArea.split(',').map(Number) : [], 
-    officeAddressDetail: row.officeAddressDetail || row.address,
-    workContent: row.workContent, projectRole: row.projectRole || row.role,
-    isKeyPerson: row.isKeyPerson || (row.keyPerson ? 1 : 0), publicRelations: row.publicRelations
+  const contactId = row.contactId || row.id;
+  if (!contactId) return;
+  proxy.$modal.loading("加载中...");
+  getContact(contactId).then(res => {
+    const data = res.data || {};
+    // 防御性合并:优先使用详情接口数据,但如果详情接口返回 null,则保留列表行中的有效数据
+    const mergedData = { ...row };
+    for (const key in data) {
+      if (data[key] !== null && data[key] !== undefined && data[key] !== '') {
+        mergedData[key] = data[key];
+      }
+    }
+    
+    Object.assign(projectContactForm, {
+      id: data.id || row.id,
+      contactName: mergedData.contactName,
+      type: String(mergedData.type || '1'),
+      gender: String(mergedData.gender || '0'),
+      age: mergedData.age,
+      nativePlace: mergedData.nativePlace,
+      birthday: mergedData.birthday,
+      remark: mergedData.remark,
+      jobStatus: String(mergedData.jobStatus !== null && mergedData.jobStatus !== undefined ? mergedData.jobStatus : '1'),
+      phone: mergedData.phone,
+      deptName: mergedData.deptName,
+      position: mergedData.position,
+      officePhone: mergedData.officePhone,
+      officeRegion: (mergedData.addressProvince != null && mergedData.addressCity != null && mergedData.addressCounty != null) 
+        ? [String(mergedData.addressProvince), String(mergedData.addressCity), String(mergedData.addressCounty)] : [],
+      addressDetail: mergedData.addressDetail,
+      jobContent: mergedData.jobContent,
+      projectRole: mergedData.projectRole !== null && mergedData.projectRole !== undefined ? Number(mergedData.projectRole) : null,
+      isKeyPerson: mergedData.isKeyPerson !== null && mergedData.isKeyPerson !== undefined ? Number(mergedData.isKeyPerson) : 0,
+      prStatus: mergedData.prStatus,
+      familyStatus: mergedData.familyStatus || mergedData.familyInfo || '',
+      hobby: mergedData.hobby || mergedData.hobbies || '',
+      characterTrait: mergedData.characterTrait || mergedData.character || '',
+      isSmoke: String(mergedData.isSmoke !== null && mergedData.isSmoke !== undefined ? mergedData.isSmoke : '0'),
+      isDrink: String(mergedData.isDrink !== null && mergedData.isDrink !== undefined ? mergedData.isDrink : '0'),
+      homeRegion: (mergedData.homeProvinceId != null && mergedData.homeCityId != null && mergedData.homeAreaId != null) 
+        ? [Number(mergedData.homeProvinceId), Number(mergedData.homeCityId), Number(mergedData.homeAreaId)] : [],
+      homeAddressDetail: mergedData.homeAddressDetail
+    });
+
+    // 区域级联处理 (适配代码转换)
+    const findIdByCode = (code) => {
+        if (!code) return null;
+        const findInTree = (nodes, targetCode) => {
+            for (const node of nodes) {
+                if (String(node.areaCode) === String(targetCode)) return node.id;
+                if (node.children) {
+                    const found = findInTree(node.children, targetCode);
+                    if (found) return found;
+                }
+            }
+            return null;
+        };
+        return findInTree(areaOptions.value, code);
+    };
+
+    if (mergedData.addressProvince || mergedData.addressCity || mergedData.addressCounty) {
+        const officeArr = [];
+        if (mergedData.addressProvince) {
+            const pid = /^\d+$/.test(mergedData.addressProvince) && mergedData.addressProvince.length < 5 ? mergedData.addressProvince : findIdByCode(mergedData.addressProvince);
+            if (pid) officeArr.push(Number(pid));
+        }
+        if (mergedData.addressCity) {
+            const cid = /^\d+$/.test(mergedData.addressCity) && mergedData.addressCity.length < 7 ? mergedData.addressCity : findIdByCode(mergedData.addressCity);
+            if (cid) officeArr.push(Number(cid));
+        }
+        if (mergedData.addressCounty) {
+            const aid = /^\d+$/.test(mergedData.addressCounty) && mergedData.addressCounty.length < 9 ? mergedData.addressCounty : findIdByCode(mergedData.addressCounty);
+            if (aid) officeArr.push(Number(aid));
+        }
+        projectContactForm.officeRegion = officeArr;
+    }
+    
+    projectContactDrawerOpen.value = true;
+  }).finally(() => {
+    proxy.$modal.closeLoading();
   });
-  projectContactDrawerOpen.value = true;
 };
 
 const submitProjectContact = () => {
@@ -642,13 +853,36 @@ const submitProjectContact = () => {
     if (valid) {
       contactSubmitting.value = true;
       try {
+        // 获取选中的 AreaCode 列表 (用于办公地址转换)
+        const findCodeById = (id) => {
+            const findInTree = (nodes, targetId) => {
+                for (const node of nodes) {
+                    if (Number(node.id) === Number(targetId)) return node.areaCode;
+                    if (node.children) {
+                        const found = findInTree(node.children, targetId);
+                        if (found) return found;
+                    }
+                }
+                return null;
+            };
+            return findInTree(areaOptions.value, id);
+        };
+
         const payload = { 
           ...projectContactForm, 
           platformCode: String(props.id),
-          officeAddressArea: projectContactForm.region.join(','),
+          // 转换办公地址编码为代码 String
+          addressProvince: projectContactForm.officeRegion?.[0] ? findCodeById(projectContactForm.officeRegion[0]) : null,
+          addressCity: projectContactForm.officeRegion?.[1] ? findCodeById(projectContactForm.officeRegion[1]) : null,
+          addressCounty: projectContactForm.officeRegion?.[2] ? findCodeById(projectContactForm.officeRegion[2]) : null,
+          // 转换家庭地址为 ID Long
+          homeProvinceId: projectContactForm.homeRegion?.[0] || null,
+          homeCityId: projectContactForm.homeRegion?.[1] || null,
+          homeAreaId: projectContactForm.homeRegion?.[2] || null,
           // 关联客户信息
           customerName: drawerForm.value.customName || drawerForm.value.customerName || '',
-          customNo: drawerForm.value.customNo || drawerForm.value.customerNo || ''
+          customNo: drawerForm.value.customNo || drawerForm.value.customerNo || '',
+          customerId: drawerForm.value.realCustomerId || null
         };
         if (projectContactForm.id) { await updateContact(payload); } 
         else { await addContact(payload); }
@@ -690,12 +924,16 @@ const loadAnalysisData = async () => {
   } catch (e) {}
 };
 
-const handleSaveAnalysis = async () => {
+const handleSaveAnalysis = useDebounceFn(async () => {
   const ossIds = analysisFileList.value.map(f => f.ossId).join(',');
   const payload = {
-    id: analysisForm.id, objectNo: drawerForm.value.projectNo,
+    id: analysisForm.id, 
+    objectNo: drawerForm.value.projectNo,
+    dataType: 4, // 1:线索, 2:商机, 3:项目优选, 4:平台优选
     dealResult: analysisForm.resultType === 'win' ? 1 : 2,
-    winSumUp: analysisForm.summary, loseReason: analysisForm.loseReason, fileNo: ossIds
+    winSumUp: analysisForm.summary, 
+    loseReason: analysisForm.loseReason, 
+    fileNo: ossIds
   };
   try {
     if (analysisForm.id) { await updateSalesResultAnalyze(payload); } 
@@ -703,7 +941,7 @@ const handleSaveAnalysis = async () => {
     ElMessage.success("保存成功");
     loadAnalysisData();
   } catch (e) {}
-};
+}, 300);
 
 const handleAnalysisUploadSuccess = (res) => {
   if (res.code === 200) {
@@ -735,7 +973,15 @@ const handleUploadSuccess = async (res) => {
   }
 };
 
-const downloadFile = (file) => { if (file.url) window.open(file.url, '_blank'); };
+const downloadFile = (file) => { 
+  if (file.url) {
+    if (proxy.$download && proxy.$download.resource) {
+      proxy.$download.resource(file.url);
+    } else {
+      window.open(file.url, '_blank');
+    }
+  } 
+};
 
 const handleDeleteFile = (row) => {
   ElMessageBox.confirm(`确认删除附件「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
@@ -789,7 +1035,7 @@ const openLink = (link) => {
   }
 };
 
-const formatDate = (date) => date ? parseTime(date, '{y}-{m}-{d}') : '';
+const formatDate = (date) => date ? proxy.parseTime(date, '{y}-{m}-{d}') : '';
 const getStatusLabel = (s) => props.options.status?.find(o => String(o.value) === String(s))?.label || '';
 const getProjectRoleLabel = (val) => {
   return projectRoleOptions.value.find(o => String(o.value) === String(val))?.label || val || '';
@@ -909,17 +1155,66 @@ const displayDeptName = computed(() => {
 }
 
 .detail-content-layout { display: flex; gap: 24px; align-items: stretch; margin-top: 0; }
-.content-left-panel { flex: 7; display: flex; flex-direction: column; overflow: hidden; }
-.content-right-panel { flex: 3; background: #fff; overflow: hidden; display: flex; flex-direction: column; }
+.content-left-panel { flex: 6.5; display: flex; flex-direction: column; overflow: hidden; }
+.content-right-panel { flex: 3.5; background: #fff; overflow: hidden; display: flex; flex-direction: column; }
 
 .detail-content-card {
-  background: #fff; padding: 0; flex: 1;
-  .business-tabs {
-    :deep(.el-tabs__header) { margin: 0; padding: 0; background: #fff; border-bottom: 1px solid #f0f1f3; height: 56px; }
-    :deep(.el-tabs__nav-wrap::after) { display: none; }
-    :deep(.el-tabs__item) { height: 56px; line-height: 56px; font-size: 14px; color: #666; &.is-active { color: #409eff; font-weight: normal; } }
-    :deep(.el-tabs__content) { padding: 20px 0; overflow-y: auto; }
+  background: #fff; padding: 0; flex: 1; display: flex; flex-direction: column;
+}
+
+.custom-tabs {
+  :deep(.el-tabs__header) { 
+    margin: 0 !important; 
+    padding: 0 16px !important; 
+    background: #fff !important; 
+    height: 48px !important; 
+    box-sizing: border-box !important;
+    position: relative !important;
+    border-bottom: 1px solid #dcdfe6 !important;
+  }
+  :deep(.el-tabs__nav-wrap::after) { display: none !important; }
+  :deep(.el-tabs__item) { 
+    height: 48px !important; 
+    line-height: 48px !important; 
+    font-size: 14px; 
+    color: #666; 
+    &.is-active { color: #409eff; font-weight: 500 !important; } 
+  }
+  :deep(.el-tabs__active-bar) { 
+    height: 2px !important; 
+    border-radius: 1px !important;
+    bottom: 0 !important;
+    z-index: 5;
+  }
+  :deep(.el-tabs__content) { padding: 24px; }
+}
+
+.content-right-panel {
+  flex: 3; background: #fff; overflow: hidden; display: flex; flex-direction: column;
+  :deep(.el-tabs__header) {
+    margin: 0 !important;
+    padding: 0 16px !important;
+    background: #fff !important;
+    height: 48px !important;
+    box-sizing: border-box !important;
+    position: relative !important;
+    border-bottom: 1px solid #dcdfe6 !important;
+  }
+  :deep(.el-tabs__item) { 
+    height: 48px !important; 
+    line-height: 48px !important; 
+    padding: 0 10px !important; 
+    min-width: auto !important;
   }
+  :deep(.el-tabs__nav-wrap::after) { display: none !important; }
+  :deep(.el-tabs__active-bar) { 
+    height: 2px !important; 
+    bottom: 0 !important;
+    z-index: 5;
+  }
+  /* 隐藏右侧可能出现的滚动箭头 */
+  :deep(.el-tabs__nav-prev), :deep(.el-tabs__nav-next) { display: none !important; }
+  :deep(.el-tabs__nav-scroll) { overflow: visible !important; }
 }
 
 .info-section {
@@ -932,6 +1227,10 @@ const displayDeptName = computed(() => {
 .mt-24 { margin-top: 24px; }
 :deep(.hidden-label) { display: none !important; }
 :deep(.hidden-value) { display: none !important; }
+.no-bold-label {
+  :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
+  :deep(.el-form-item) { margin-bottom: 18px !important; }
+}
 :deep(*) { font-weight: normal !important; }
 .mt-20 { margin-top: 20px; }
 .tab-toolbar { margin-bottom: 16px; display: flex; justify-content: flex-end; }

+ 7 - 8
src/views/saleManage/platformSelection/edit.vue

@@ -13,7 +13,7 @@
           <el-row :gutter="20">
             <el-col :span="8">
               <el-form-item label="归属公司" prop="companyNo">
-                <el-select v-model="drawerForm.companyNo" style="width:100%" placeholder="请选择">
+                <el-select v-model="drawerForm.companyNo" style="width:100%" placeholder="请选择" filterable>
                   <el-option v-for="item in options.company" :key="item.companyCode" :label="item.companyName" :value="item.companyCode" />
                 </el-select>
               </el-form-item>
@@ -25,9 +25,6 @@
                   style="width:100%" 
                   placeholder="搜索客户" 
                   filterable 
-                  remote
-                  :remote-method="remoteSearchCustomer"
-                  :loading="customerLoading"
                   clearable
                 >
                   <el-option 
@@ -245,7 +242,7 @@
 <script setup>
 import { ref, reactive, computed, watch, onMounted, getCurrentInstance } from 'vue';
 import { getPlatformSelection, updatePlatformSelection } from '@/api/saleManage/platformSelection/index';
-import { listCustomerInfo, listCustomerListPage } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { getToken } from "@/utils/auth";
 import { Upload } from '@element-plus/icons-vue';
 
@@ -306,10 +303,12 @@ const computedUserOptions = computed(() => {
 const remoteSearchCustomer = async (query) => {
   customerLoading.value = true;
   try {
-    const res = await listCustomerListPage({ 
-      customerName: query || undefined
+    const res = await listCustomerInfo({ 
+      pageNum: 1,
+      pageSize: 500,
+      isHighSeas: 'all'
     });
-    customerOptions.value = res.rows || res.data || [];
+    customerOptions.value = res.rows || [];
   } catch (e) {
     customerOptions.value = [];
   } finally {

+ 5 - 0
src/views/saleManage/platformSelection/index.vue

@@ -623,6 +623,11 @@ const initData = () => {
   deptTreeSelect().then(res => deptOptions.value = res.data);
 };
 
+const getOptions = () => {
+  listCompanyOption().then(res => { companyOptions.value = res.data || []; });
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => { customerList.value = res.rows || []; });
+};
+
 const findUserName = (id) => {
   if (!id) return '';
   const user = userOptions.value.find(u => String(u.staffId || u.userId) === String(id));

+ 6 - 7
src/views/saleManage/projectSelection/add.vue

@@ -29,9 +29,6 @@
                     placeholder="搜索客户" 
                     style="width: 100%" 
                     filterable 
-                    remote 
-                    :remote-method="remoteLoadCustomers" 
-                    :loading="customerLoading"
                     clearable
                     @change="handleCustomerChange"
                   >
@@ -208,7 +205,7 @@
 <script setup>
 import { ref, reactive, watch, getCurrentInstance } from 'vue';
 import { addProjectSelection } from '@/api/saleManage/projectSelection/index';
-import { listCustomerInfo, listCustomerListPage } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { globalHeaders } from '@/utils/request';
 import { Upload } from '@element-plus/icons-vue';
 
@@ -310,10 +307,12 @@ const reset = () => {
 const remoteLoadCustomers = async (query) => {
   customerLoading.value = true;
   try {
-    const res = await listCustomerListPage({ 
-      customerName: query || undefined
+    const res = await listCustomerInfo({ 
+      pageNum: 1,
+      pageSize: 500,
+      isHighSeas: 'all'
     });
-    customerOptions.value = res.rows || res.data || [];
+    customerOptions.value = res.rows || [];
   } catch (e) {
     customerOptions.value = [];
   } finally {

+ 186 - 60
src/views/saleManage/projectSelection/detail.vue

@@ -248,31 +248,31 @@
     </div>
   </el-drawer>
 <el-drawer v-model="projectContactDrawerOpen" direction="rtl" size="80%" destroy-on-close :with-header="false" append-to-body>
-      <div class="drawer-header-standard" style="padding: 15px 20px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center;">
-        <span style="font-size: 16px; font-weight: normal; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
-        <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 20px;"><Close /></el-icon>
+      <div class="drawer-header-compact" style="height: 40px; padding: 0 16px; border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; background: #fff;">
+        <span style="font-size: 15px; font-weight: 600; color: #333;">{{ projectContactForm.id ? '编辑项目联系人' : '新建项目联系人' }}</span>
+        <el-icon @click="projectContactDrawerOpen = false" style="cursor: pointer; font-size: 18px; color: #94a3b8;"><Close /></el-icon>
       </div>
       <div class="drawer-body-custom" style="padding: 20px; overflow-y: auto; height: calc(100% - 110px);">
-        <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" label-position="right">
+        <el-form :model="projectContactForm" :rules="projectContactRules" ref="projectContactFormRef" label-width="100px" label-position="right" class="no-bold-label">
           <!-- 基本信息 -->
           <div class="form-section-group">
             <div class="section-group-title-bar">基本信息</div>
             <div class="section-group-content">
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="姓名" prop="name"><el-input v-model="projectContactForm.name" placeholder="请输入" /></el-form-item>
+                  <el-form-item label="姓名" prop="contactName"><el-input v-model="projectContactForm.contactName" placeholder="请输入" /></el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="联系人类型" prop="contactType">
-                    <el-radio-group v-model="projectContactForm.contactType">
+                  <el-form-item label="联系人类型" prop="type">
+                    <el-radio-group v-model="projectContactForm.type">
                       <el-radio value="1">公司职员</el-radio>
                       <el-radio value="2">关系资源人</el-radio>
                     </el-radio-group>
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="性别" prop="sex">
-                    <el-radio-group v-model="projectContactForm.sex">
+                  <el-form-item label="性别" prop="gender">
+                    <el-radio-group v-model="projectContactForm.gender">
                       <el-radio value="0">男</el-radio><el-radio value="1">女</el-radio>
                     </el-radio-group>
                   </el-form-item>
@@ -293,7 +293,7 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="24">
-                  <el-form-item label="描述" prop="description"><el-input v-model="projectContactForm.description" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+                  <el-form-item label="描述" prop="remark"><el-input v-model="projectContactForm.remark" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
                 </el-col>
               </el-row>
             </div>
@@ -305,15 +305,15 @@
             <div class="section-group-content">
               <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="在职状态" prop="status">
-                    <el-radio-group v-model="projectContactForm.status">
-                      <el-radio value="0">在职</el-radio>
-                      <el-radio value="1">离职</el-radio>
+                  <el-form-item label="在职状态" prop="jobStatus">
+                    <el-radio-group v-model="projectContactForm.jobStatus">
+                      <el-radio value="1">在职</el-radio>
+                      <el-radio value="2">离职</el-radio>
                     </el-radio-group>
                   </el-form-item>
                 </el-col>
                 <el-col :span="8">
-                  <el-form-item label="手机号码" prop="phonenumber"><el-input v-model="projectContactForm.phonenumber" placeholder="请输入" /></el-form-item>
+                  <el-form-item label="手机号码" prop="phone"><el-input v-model="projectContactForm.phone" placeholder="请输入" /></el-form-item>
                 </el-col>
                 <el-col :span="8">
                   <el-form-item label="部门" prop="deptName"><el-input v-model="projectContactForm.deptName" placeholder="请输入" /></el-form-item>
@@ -329,52 +329,75 @@
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="24">
-                  <el-form-item label="办公地址" prop="officeAddressDetail">
+                  <el-form-item label="办公地址" prop="addressDetail">
                     <div style="display: flex; gap: 10px;">
                       <el-cascader
-                        v-model="projectContactForm.region"
+                        v-model="projectContactForm.officeRegion"
                         :options="areaOptions"
                         :props="{ label: 'areaName', value: 'id', children: 'children' }"
                         placeholder="请选择省/市/区"
                         style="width: 200px"
                         clearable
                       />
-                      <el-input v-model="projectContactForm.officeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                      <el-input v-model="projectContactForm.addressDetail" placeholder="请输入详细地址" style="flex: 1" />
                     </div>
                   </el-form-item>
                 </el-col>
               </el-row>
               <el-row :gutter="20">
                 <el-col :span="24">
-                  <el-form-item label="工作内容" prop="workContent"><el-input v-model="projectContactForm.workContent" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+                  <el-form-item label="工作内容" prop="jobContent"><el-input v-model="projectContactForm.jobContent" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
                 </el-col>
               </el-row>
             </div>
           </div>
 
-          <!-- 项目决策 -->
+          <!-- 家庭信息 -->
           <div class="form-section-group">
-            <div class="section-group-title-bar">项目决策</div>
+            <div class="section-group-title-bar">家庭信息</div>
             <div class="section-group-content">
               <el-row :gutter="20">
-                <el-col :span="8">
-                  <el-form-item label="项目角色" prop="projectRole">
-                    <el-select v-model="projectContactForm.projectRole" style="width: 100%" placeholder="请选择" filterable clearable>
-                      <el-option v-for="item in projectRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
-                    </el-select>
+                <el-col :span="24">
+                  <el-form-item label="家庭住址" prop="homeAddressDetail">
+                    <div style="display: flex; gap: 10px;">
+                      <el-cascader
+                        v-model="projectContactForm.homeRegion"
+                        :options="areaOptions"
+                        :props="{ label: 'areaName', value: 'id', children: 'children' }"
+                        placeholder="请选择省/市/区"
+                        style="width: 200px"
+                        clearable
+                      />
+                      <el-input v-model="projectContactForm.homeAddressDetail" placeholder="请输入详细地址" style="flex: 1" />
+                    </div>
                   </el-form-item>
                 </el-col>
+              </el-row>
+              <el-row :gutter="20">
                 <el-col :span="8">
-                  <el-form-item label="是否关键人" prop="isKeyPerson">
-                    <el-select v-model="projectContactForm.isKeyPerson" style="width: 100%" placeholder="请选择">
-                      <el-option label="是" :value="1" /><el-option label="否" :value="0" />
-                    </el-select>
-                  </el-form-item>
+                  <el-form-item label="家庭情况" prop="familyStatus"><el-input v-model="projectContactForm.familyStatus" placeholder="请输入" /></el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="爱好" prop="hobby"><el-input v-model="projectContactForm.hobby" placeholder="请输入" /></el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="性格特征" prop="characterTrait"><el-input v-model="projectContactForm.characterTrait" placeholder="请输入" /></el-form-item>
                 </el-col>
               </el-row>
               <el-row :gutter="20">
-                <el-col :span="24">
-                  <el-form-item label="公关情况" prop="publicRelations"><el-input v-model="projectContactForm.publicRelations" type="textarea" :rows="3" placeholder="请输入" /></el-form-item>
+                <el-col :span="8">
+                  <el-form-item label="是否抽烟" prop="isSmoke">
+                    <el-radio-group v-model="projectContactForm.isSmoke">
+                      <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                </el-col>
+                <el-col :span="8">
+                  <el-form-item label="是否喝酒" prop="isDrink">
+                    <el-radio-group v-model="projectContactForm.isDrink">
+                      <el-radio value="1">是</el-radio><el-radio value="0">否</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
                 </el-col>
               </el-row>
             </div>
@@ -411,7 +434,7 @@
 import { ref, computed, watch, reactive, getCurrentInstance, toRefs } from 'vue';
 import { Edit, Upload, Plus, Close, CircleCheck, ArrowDown } from '@element-plus/icons-vue';
 import { getProjectSelection, updateProjectSelection } from '@/api/saleManage/projectSelection/index';
-import { listContact, addContact, delContact, updateContact } from "@/api/customer/crmContact";
+import { listContact, addContact, delContact, updateContact, getContact } from "@/api/customer/crmContact";
 import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
 import { listProvinceWithCities } from "@/api/customer/addressArea";
 import { listByIds } from "@/api/system/oss/index";
@@ -522,15 +545,22 @@ const contactSubmitting = ref(false);
 const projectContactDrawerOpen = ref(false);
 const projectContactFormRef = ref(null);
 const projectContactForm = reactive({
-  id: undefined, name: '', contactType: '1', sex: '0', age: '', nativePlace: '', birthday: '', description: '',
-  status: '0', phonenumber: '', deptName: '', position: '', officePhone: '', officeAddressArea: '', officeAddressDetail: '', workContent: '',
-  projectRole: '', isKeyPerson: 0, publicRelations: ''
+  id: undefined, customerId: undefined, contactName: '', type: '1', gender: '0', age: '', nativePlace: '', birthday: '', remark: '',
+  jobStatus: '1', phone: '', deptName: '', position: '', officePhone: '', officeRegion: [], provincialCityCounty: '', addressDetail: '', jobContent: '',
+  projectRole: null, isKeyPerson: 0, prStatus: '', familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0',
+  homeRegion: [], homeAddressDetail: ''
 });
 const projectContactRules = {
-  name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  contactName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
   age: [{ required: true, message: '年龄不能为空', trigger: 'blur' }],
-  phonenumber: [{ required: true, message: '手机号码不能为空', trigger: 'blur' }],
   deptName: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
+  phone: [
+    { required: true, message: '手机号码不能为空', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位手机号码', trigger: 'blur' }
+  ],
+  officePhone: [
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的11位座机号码', trigger: 'blur' }
+  ]
 };
 const areaOptions = ref([]);
 const associateVisible = ref(false);
@@ -540,10 +570,58 @@ const selectedContacts = ref([]);
 
 const loadDicts = () => {
   if (areaOptions.value.length === 0) {
-    listProvinceWithCities().then(res => { areaOptions.value = res.data || []; });
+    listProvinceWithCities().then(res => {
+      const list = res.rows || [];
+      areaOptions.value = handleTree(list, "id", "parentId");
+    });
   }
 };
 
+/** 构造树型结构数据 */
+function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (childrenListMap[pId] == null) {
+      childrenListMap[pId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[pId].push(d);
+  }
+
+  for (let d of data) {
+    let pId = d[config.parentId];
+    if (nodeIds[pId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
 const fetchContactList = async (id) => {
   if (!id) return;
   contactLoading.value = true;
@@ -587,24 +665,54 @@ const confirmAssociate = async () => {
 
 const handleNewProjectContact = () => {
   Object.assign(projectContactForm, {
-    id: undefined, name: '', contactType: '1', sex: '0', age: '', nativePlace: '', birthday: '', description: '',
-    status: '0', phonenumber: '', deptName: '', position: '', officePhone: '', officeAddressArea: '', officeAddressDetail: '', workContent: '',
-    projectRole: '', isKeyPerson: 0, publicRelations: ''
+    id: undefined, customerId: undefined, contactName: '', type: '1', gender: '0', age: '', nativePlace: '', birthday: '', remark: '',
+    jobStatus: '1', phone: '', deptName: '', position: '', officePhone: '', officeRegion: [], provincialCityCounty: '', addressDetail: '', jobContent: '',
+    projectRole: null, isKeyPerson: 0, prStatus: '', familyStatus: '', hobby: '', characterTrait: '', isSmoke: '0', isDrink: '0',
+    homeRegion: [], homeAddressDetail: ''
   });
   projectContactDrawerOpen.value = true;
 };
 
 const handleEditContact = (row) => {
-  Object.assign(projectContactForm, {
-    id: row.id, name: row.contactName || row.name, contactType: row.contactType || '1', sex: row.gender || row.sex || '0',
-    age: row.age, nativePlace: row.nativePlace, birthday: row.birthday, description: row.description || row.remark,
-    status: row.status || '0', phonenumber: row.phonenumber || row.phone, deptName: row.deptName || row.department,
-    position: row.position || row.roleName || row.role, officePhone: row.officePhone || row.landline,
-    officeAddressArea: row.officeAddressArea, officeAddressDetail: row.officeAddressDetail || row.address,
-    workContent: row.workContent, projectRole: row.projectRole || row.role,
-    isKeyPerson: row.isKeyPerson || (row.keyPerson ? 1 : 0), publicRelations: row.publicRelations
+  const contactId = row.contactId || row.id;
+  if (!contactId) return;
+  proxy.$modal.loading("加载中...");
+  getContact(contactId).then(res => {
+    const data = res.data || {};
+    Object.assign(projectContactForm, {
+      id: data.id,
+      customerId: data.customerId,
+      contactName: data.contactName,
+      type: String(data.type || '1'),
+      gender: String(data.gender || '0'),
+      age: data.age,
+      nativePlace: data.nativePlace,
+      birthday: data.birthday,
+      remark: data.remark,
+      jobStatus: String(data.jobStatus || '1'),
+      phone: data.phone,
+      deptName: data.deptName,
+      position: data.position,
+      officePhone: data.officePhone,
+      addressDetail: data.addressDetail,
+      jobContent: data.jobContent,
+      projectRole: data.projectRole !== null && data.projectRole !== undefined ? Number(data.projectRole) : null,
+      isKeyPerson: data.isKeyPerson !== null && data.isKeyPerson !== undefined ? Number(data.isKeyPerson) : 0,
+      prStatus: data.prStatus,
+      familyStatus: data.familyStatus,
+      hobby: data.hobby,
+      characterTrait: data.characterTrait,
+      isSmoke: String(data.isSmoke || '0'),
+      isDrink: String(data.isDrink || '0'),
+      officeRegion: (data.addressProvince && data.addressCity && data.addressCounty) ? [data.addressProvince, data.addressCity, data.addressCounty] : [],
+      homeRegion: (data.homeProvinceId && data.homeCityId && data.homeAreaId) 
+        ? [data.homeProvinceId, data.homeCityId, data.homeAreaId] : [],
+      homeAddressDetail: data.homeAddressDetail
+    });
+    projectContactDrawerOpen.value = true;
+  }).finally(() => {
+    proxy.$modal.closeLoading();
   });
-  projectContactDrawerOpen.value = true;
 };
 
 const submitProjectContact = () => {
@@ -614,10 +722,23 @@ const submitProjectContact = () => {
       try {
         const payload = { 
           ...projectContactForm, 
-          platformCode: String(detailData.value.id),
+          roleId: projectContactForm.type === '1' ? 1 : 2,
           // 关联客户信息
-          customerName: detailData.value.customerName || detailData.value.customName || '',
-          customNo: detailData.value.customNo || detailData.value.companyNo || ''
+          customerId: detailData.value.customerId || detailData.value.realCustomerId || projectContactForm.customerId,
+          platformCode: 'crm',
+          customerName: detailData.value.customerName || '',
+          customerNo: detailData.value.companyNo || detailData.value.customNo || '',
+          projectNo: detailData.value.projectNo || '',
+          // age 字段转换
+          age: projectContactForm.age ? parseInt(projectContactForm.age, 10) : null,
+          // 地址处理
+          addressProvince: projectContactForm.officeRegion?.[0] ? String(projectContactForm.officeRegion[0]) : null,
+          addressCity: projectContactForm.officeRegion?.[1] ? String(projectContactForm.officeRegion[1]) : null,
+          addressCounty: projectContactForm.officeRegion?.[2] ? String(projectContactForm.officeRegion[2]) : null,
+          // 转换家庭地址编码
+          homeProvinceId: projectContactForm.homeRegion?.[0] || null,
+          homeCityId: projectContactForm.homeRegion?.[1] || null,
+          homeAreaId: projectContactForm.homeRegion?.[2] || null
         };
         if (projectContactForm.id) { await updateContact(payload); } 
         else { await addContact(payload); }
@@ -665,9 +786,13 @@ const loadAnalysisData = async () => {
 const handleSaveAnalysis = async () => {
   const ossIds = analysisFileList.value.map(f => f.ossId).join(',');
   const payload = {
-    id: analysisForm.id, objectNo: detailData.value.projectNo,
-    dealResult: analysisForm.resultType,
-    winSumUp: analysisForm.summary, loseReason: analysisForm.loseReason, fileNo: ossIds
+    id: analysisForm.id,
+    objectNo: detailData.value.projectNo,
+    dataType: 1, // 1代表年度入围
+    dealResult: analysisForm.resultType === 'win' ? 1 : 2,
+    winSumUp: analysisForm.summary,
+    loseReason: analysisForm.loseReason,
+    fileNo: ossIds
   };
   try {
     if (analysisForm.id) { await updateSalesResultAnalyze(payload); } 
@@ -857,8 +982,9 @@ defineExpose({ open });
   }
 }
 
-:deep(.el-form-item__label) {
-  font-weight: normal !important;
+.no-bold-label {
+  :deep(.el-form-item__label) { font-weight: normal !important; color: #666; }
+  :deep(.el-form-item) { margin-bottom: 18px !important; }
 }
 
 :deep(.el-radio__label) {

+ 6 - 7
src/views/saleManage/projectSelection/edit.vue

@@ -29,9 +29,6 @@
                     placeholder="搜索客户" 
                     style="width: 100%" 
                     filterable 
-                    remote 
-                    :remote-method="remoteLoadCustomers" 
-                    :loading="customerLoading"
                     clearable
                     @change="handleCustomerChange"
                   >
@@ -208,7 +205,7 @@
 <script setup>
 import { ref, reactive, watch, computed, getCurrentInstance } from 'vue';
 import { getProjectSelection, updateProjectSelection } from '@/api/saleManage/projectSelection/index';
-import { listCustomerInfo, listCustomerListPage } from "@/api/customer/customerInfo/index";
+import { listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listByIds } from "@/api/system/oss/index";
 import { globalHeaders } from '@/utils/request';
 import { Upload } from '@element-plus/icons-vue';
@@ -327,10 +324,12 @@ const loadDetail = (id) => {
 const remoteLoadCustomers = async (query) => {
   customerLoading.value = true;
   try {
-    const res = await listCustomerListPage({ 
-      customerName: query || undefined
+    const res = await listCustomerInfo({ 
+      pageNum: 1,
+      pageSize: 500,
+      isHighSeas: 'all'
     });
-    customerOptions.value = res.rows || res.data || [];
+    customerOptions.value = res.rows || [];
   } catch (e) {
     customerOptions.value = [];
   } finally {

+ 1 - 1
src/views/visit/plan/add.vue

@@ -324,7 +324,7 @@ onMounted(() => {
   listComStaff({ pageSize: 1000 }).then(response => {
     staffOptions.value = response.rows || response.data || [];
   });
-  listCustomerInfo({ pageSize: 500 }).then(response => {
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(response => {
     customerOptions.value = response.rows || [];
   });
   listOpportunity({ pageSize: 500 }).then(response => {

+ 1 - 1
src/views/visit/plan/edit.vue

@@ -311,7 +311,7 @@ onMounted(() => {
   listComStaff({ pageSize: 1000 }).then(response => {
     staffOptions.value = response.rows || response.data || [];
   });
-  listCustomerInfo({ pageSize: 500 }).then(response => {
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(response => {
     customerOptions.value = response.rows || [];
   });
   listOpportunity({ pageSize: 500 }).then(response => {

+ 1 - 1
src/views/visit/routine/add.vue

@@ -210,7 +210,7 @@ const reset = () => {
 
 onMounted(() => {
   listPlan({ pageSize: 1000 }).then(res => { planOptions.value = res.rows || []; });
-  listCustomerInfo({ pageSize: 1000 }).then(res => { customerOptions.value = res.rows || []; });
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => { customerOptions.value = res.rows || []; });
   listOpportunity({ pageSize: 1000 }).then(res => { opportunityOptions.value = res.rows || []; });
   listProjectSelection({ pageSize: 1000 }).then(res => { annualOptions.value = res.rows || []; });
   listComStaff({ pageSize: 1000 }).then(res => { staffOptions.value = res.rows || res.data || []; });

+ 1 - 1
src/views/visit/routine/edit.vue

@@ -193,7 +193,7 @@ const cancel = () => {
 
 onMounted(() => {
   listPlan({ pageSize: 1000 }).then(res => { planOptions.value = res.rows || []; });
-  listCustomerInfo({ pageSize: 1000 }).then(res => { customerOptions.value = res.rows || []; });
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => { customerOptions.value = res.rows || []; });
   listOpportunity({ pageSize: 1000 }).then(res => { opportunityOptions.value = res.rows || []; });
   listProjectSelection({ pageSize: 1000 }).then(res => { annualOptions.value = res.rows || []; });
   listComStaff({ pageSize: 1000 }).then(res => { staffOptions.value = res.rows || res.data || []; });

+ 1 - 1
src/views/visit/routine/index.vue

@@ -228,7 +228,7 @@ const handleDelete = (row) => {
 const getOptions = () => {
   listComStaff({ pageSize: 1000 }).then(res => { staffOptions.value = res.rows || res.data || []; });
   listIndustryCategory({ pageSize: 1000 }).then(res => { industryOptions.value = res.rows || []; });
-  listCustomerInfo({ pageSize: 1000 }).then(res => { customerList.value = res.rows || []; });
+  listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => { customerList.value = res.rows || []; });
 };
 
 onMounted(() => { 

+ 78 - 33
src/views/workbench/dashboard/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="dashboard-container" v-loading="loading">
     <!-- 第一排:客户统计 & 客户跟进 -->
-    <el-row :gutter="20">
+    <el-row :gutter="15">
       <el-col :span="12">
         <div class="stat-card">
           <div class="card-header"><span class="title">客户</span></div>
@@ -43,8 +43,8 @@
     </el-row>
 
     <!-- 第二排:项目商机 & 跟进中的项目商机 -->
-    <el-row :gutter="20" class="mt20">
-      <el-col :span="13">
+    <el-row :gutter="15" class="mt15">
+      <el-col :span="12">
         <div class="stat-card">
           <div class="card-header"><span class="title">项目商机</span></div>
           <div class="card-body">
@@ -63,7 +63,7 @@
           </div>
         </div>
       </el-col>
-      <el-col :span="11">
+      <el-col :span="12">
         <div class="stat-card">
           <div class="card-header"><span class="title">跟进中的项目商机</span></div>
           <div class="card-body">
@@ -85,8 +85,8 @@
     </el-row>
 
     <!-- 第三排:年度入围 & 跟进中的年度入围 -->
-    <el-row :gutter="20" class="mt20">
-      <el-col :span="13">
+    <el-row :gutter="15" class="mt15">
+      <el-col :span="12">
         <div class="stat-card">
           <div class="card-header"><span class="title">年度入围</span></div>
           <div class="card-body">
@@ -105,7 +105,7 @@
           </div>
         </div>
       </el-col>
-      <el-col :span="11">
+      <el-col :span="12">
         <div class="stat-card">
           <div class="card-header"><span class="title">跟进中的年度入围</span></div>
           <div class="card-body">
@@ -306,30 +306,58 @@ onUnmounted(() => {
 
 <style lang="scss" scoped>
 .dashboard-container {
-  padding: 20px; background-color: #f5f7fa; min-height: calc(100vh - 84px);
-  .mt20 { margin-top: 20px; }
-  .stat-card {
-    background: #fff;
-    border-radius: 6px;
-    border: 1px solid #e6ebf5;
-    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
-    height: 100%;
-    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
-    
-    &:hover {
-      transform: translateY(-5px);
-      box-shadow: 0 12px 20px -10px rgba(0, 0, 0, 0.15);
-      border-color: #409eff55;
+  padding: 15px; background-color: #f5f7fa; min-height: calc(100vh - 84px);
+  .mt15 { margin-top: 15px; }
+    .el-row {
+      display: flex;
+      flex-wrap: wrap;
+      align-items: stretch; /* 确保所有列等高 */
+    }
+
+    .el-col {
+      display: flex;
+      flex-direction: column;
     }
 
+    .stat-card {
+      background: #fff;
+      border-radius: 8px;
+      border: 1px solid #f0f0f0;
+      /* 使用更清晰的多层阴影 */
+      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.05);
+      width: 100%;
+      height: 100%;
+      display: flex;
+      flex-direction: column;
+      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+      position: relative;
+      overflow: hidden;
+      
+      &:hover {
+        transform: translateY(-4px);
+        box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
+        border-color: #409eff44;
+      }
+
     .card-header {
-      padding: 12px 20px;
-      border-bottom: 1px solid #f0f0f0;
-      .title { font-size: 15px; font-weight: bold; color: #333; }
+      padding: 12px 16px;
+      border-bottom: 1px solid #f8f8f8;
+      background-color: #fafbfc; /* 头部微背景色,增加层次感 */
+      .title { 
+        font-size: 14px; 
+        font-weight: 700; 
+        color: #1a1a1a; 
+        letter-spacing: 0.5px;
+      }
     }
 
     .card-body {
-      padding: 15px 20px;
+      padding: 16px;
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      
       .stat-grid {
         display: flex;
         flex-wrap: wrap;
@@ -337,27 +365,44 @@ onUnmounted(() => {
         .stat-item {
           flex: 1;
           min-width: 100px;
-          background-color: #f8f9fb;
+          height: 90px;
+          background-color: #ffffff;
+          border: 1px solid #f2f2f2; /* 给子项添加细边框增加清晰度 */
           padding: 12px;
-          border-radius: 4px;
+          border-radius: 6px;
           display: flex;
           flex-direction: column;
           transition: all 0.2s ease;
           cursor: default;
 
           &:hover {
-            background-color: #eff5ff;
+            background-color: #f5f9ff;
+            border-color: #d9ecff;
             transform: scale(1.02);
           }
 
-          .label { font-size: 12px; color: #666; margin-bottom: 8px; }
-          .value { font-size: 22px; font-weight: bold; color: #333; margin-bottom: 4px; }
+          .label { 
+            font-size: 12px; 
+            color: #8c8c8c; 
+            margin-bottom: 8px; 
+            font-weight: 500;
+          }
+          .value { 
+            font-size: 24px; 
+            font-weight: 800; 
+            color: #262626; 
+            margin-bottom: 6px;
+            line-height: 1;
+            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+          }
           .trend {
             font-size: 11px;
-            color: #999;
+            color: #bfbfbf;
             white-space: nowrap;
-            .up { color: #67C23A; margin-left: 2px; font-weight: bold; }
-            .down { color: #F56C6C; margin-left: 2px; font-weight: bold; }
+            display: flex;
+            align-items: center;
+            .up { color: #52c41a; margin-left: 4px; font-weight: bold; }
+            .down { color: #f5222d; margin-left: 4px; font-weight: bold; }
           }
         }
       }