Jelajahi Sumber

修复跟进记录

沐梦. 3 minggu lalu
induk
melakukan
9ce6764d04

+ 98 - 19
src/views/common/businessActivity.vue

@@ -27,7 +27,7 @@
                   <div class="user-tags">
                     <span v-if="item.izManager === 1" class="leader-tag-simple">负责人</span>
                     <span class="user-role-text">{{ item.roleName || '业务负责人' }}</span>
-                    <span v-if="item.updateAccredit === 1" class="permission-text-simple">修改权限</span>
+                    <span class="permission-text-simple">{{ item.updateAccredit === 1 ? '修改权限' : '仅查看' }}</span>
                   </div>
                 </div>
               </div>
@@ -59,13 +59,31 @@
           
           <div v-if="recordList.length" class="record-list">
             <div v-for="(record, index) in recordList" :key="index" class="record-item">
-               <div class="record-card">
-                 <div class="record-header">
-                   <span class="record-user">{{ record.visitorName || record.userName || '未知用户' }}</span>
-                   <span class="record-time">{{ record.callDate || record.time }}</span>
+                 <div class="record-card">
+                   <div class="record-header">
+                     <span class="record-user">{{ getRecordUser(record) }}</span>
+                     <span class="record-time">{{ (record.callDate || record.time || '').split(' ')[0] }}</span>
+                   </div>
+                   <div class="record-detail-info">
+                     <div class="info-row"><span class="label">拜访方式:</span><span class="value"><dict-tag :options="visitTypeDict" :value="record.callTypeCode" /></span></div>
+                     <div v-if="record.followPeopleName" class="info-row"><span class="label">随访人:</span><span class="value">{{ record.followPeopleName }}</span></div>
+                     <div v-if="record.nextCallDate" class="info-row"><span class="label">下次拜访时间:</span><span class="value">{{ (record.nextCallDate || '').split(' ')[0] }}</span></div>
+                     <div class="info-row"><span class="label">拜访目的:</span><span class="value">{{ record.callAim }}</span></div>
+                   </div>
+                   <div class="record-body">
+                     <span class="label">跟进情况:</span>
+                     <div class="value">{{ record.followUpCondition || record.content }}</div>
+                   </div>
+                   <div v-if="record.recordPicture" class="record-images">
+                     <el-image 
+                       :src="record.recordPicture" 
+                       :preview-src-list="[record.recordPicture]"
+                       fit="cover"
+                       class="record-img"
+                       preview-teleported
+                     />
+                   </div>
                  </div>
-                 <div class="record-body">{{ record.followUpCondition || record.content }}</div>
-               </div>
             </div>
           </div>
           <div v-else class="empty-status">暂无跟进记录</div>
@@ -269,16 +287,18 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="成员角色:" prop="roleCode">
+        <el-form-item label="团队角色:" prop="roleCode">
           <el-select v-model="memberForm.roleCode" placeholder="请选择" class="w100">
             <el-option v-for="dict in teamRoleDict" :key="dict.value" :label="dict.label" :value="dict.value" />
           </el-select>
         </el-form-item>
 
         <el-form-item label="权限:">
-          <div class="permission-checkbox">
-            <el-checkbox v-model="memberForm.updateAccredit" :true-value="1" :false-value="0">分配修改权限</el-checkbox>
-          </div>
+          <el-radio-group v-if="isEditMember" v-model="memberForm.updateAccredit">
+            <el-radio :label="0">仅查看</el-radio>
+            <el-radio :label="1">修改权限</el-radio>
+          </el-radio-group>
+          <el-checkbox v-else v-model="memberForm.updateAccredit" :true-value="1" :false-value="0">分配修改权限</el-checkbox>
         </el-form-item>
       </el-form>
 
@@ -616,18 +636,23 @@ const submitRecord = async () => {
         return staff ? staff.staffName : '';
       }).filter(n => n).join(',');
 
+      // 辅助函数:如果值是纯数字(ID),则视为无效,返回空字符串
+      const safeText = (val) => (val && !isNaN(Number(val)) ? '' : (val || ''));
+
       const data = { 
         ...recordForm,
         objectNo: objectNo,
         customerNo: objectNo,
         visitorName: visitorItem ? visitorItem.realName : '',
         followPeopleName: followNames,
-        // 自动补全列表所需字段
-        customerName: props.infoData?.customerName || props.infoData?.companyName || '',
-        goalObject: props.infoData?.projectName || props.infoData?.customerName || '',
-        salesman: props.infoData?.leaderName || props.infoData?.createByName || '',
-        department: props.infoData?.deptName || props.infoData?.department || '',
-        profession: props.infoData?.profession || props.infoData?.industry || '',
+        // 自动补全列表所需字段(过滤掉可能是数字 ID 的情况)
+        customerName: props.infoData?.customerName || props.infoData?.customName || props.infoData?.companyName || '',
+        goalObject: props.infoData?.projectName || props.infoData?.customerName || props.infoData?.customName || '',
+        salesman: props.infoData?.leaderName || props.infoData?.salesPersonName || props.infoData?.salesPerson || props.infoData?.staffName || props.infoData?.managerName || props.infoData?.salesman || '',
+        department: safeText(props.infoData?.deptName) || safeText(props.infoData?.department) || '',
+        deptName: safeText(props.infoData?.deptName) || safeText(props.infoData?.department) || '',
+        profession: safeText(props.infoData?.profession) || safeText(props.infoData?.industry) || '',
+        industryName: safeText(props.infoData?.industryName) || safeText(props.infoData?.industry) || '',
         dataType: computedDataType.value
       };
       if (Array.isArray(data.followPeople)) {
@@ -644,6 +669,15 @@ const submitRecord = async () => {
   });
 };
 
+// 获取记录显示的用户名
+const getRecordUser = (record) => {
+  if (record.visitorName) return record.visitorName;
+  if (record.userName) return record.userName;
+  // 如果字段缺失,尝试从团队成员列表中匹配
+  const member = teamList.value.find(m => String(m.userNo) === String(record.visitor));
+  return member ? (member.realName || member.userName) : '未知用户';
+};
+
 
 onMounted(() => {
   loadUsers();
@@ -754,15 +788,60 @@ onMounted(() => {
     display: flex;
     align-items: center;
   }
-  .permission-radio, .permission-checkbox {
+  :deep(.el-radio-group) {
     display: flex;
     align-items: center;
+    gap: 20px;
+  }
+  :deep(.el-radio) {
+    margin-right: 0;
+    font-weight: normal;
+    &.is-checked .el-radio__label {
+      color: #409eff;
+    }
   }
 }
 
+/* 弹窗关闭按钮颜色优化 (匹配图片) */
+:deep(.el-dialog__headerbtn:hover .el-dialog__close) {
+  color: #ff6a00;
+}
+:deep(.el-dialog__headerbtn .el-dialog__close) {
+  color: #ff6a00;
+  font-size: 20px;
+}
+
 /* 其他样式保持不变 */
 .record-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; .new-record-btn { color: #409eff; font-size: 14px; } .filter-item { display: flex; align-items: center; gap: 10px; .filter-label { font-size: 13px; color: #666; } } }
-.record-card { padding: 15px; background: #f8fafc; border-radius: 4px; margin-bottom: 15px; .record-header { display: flex; justify-content: space-between; margin-bottom: 8px; .record-user { font-size: 14px; font-weight: normal; color: #333; } .record-time { font-size: 12px; color: #999; } } .record-body { font-size: 13px; color: #666; line-height: 1.5; } }
+.record-card { 
+  padding: 15px; background: #f8fafc; border-radius: 4px; margin-bottom: 15px; 
+  .record-header { 
+    display: flex; justify-content: space-between; margin-bottom: 12px; 
+    .record-user { font-size: 14px; font-weight: 500; color: #333; } 
+    .record-time { font-size: 12px; color: #999; } 
+  } 
+  .record-detail-info {
+    margin-bottom: 10px;
+    .info-row {
+      font-size: 13px; line-height: 1.8; color: #666;
+      display: flex;
+      .label { color: #999; width: 100px; flex-shrink: 0; }
+      .value { color: #333; flex: 1; }
+    }
+  }
+  .record-body { 
+    font-size: 13px; color: #666; line-height: 1.5; margin-bottom: 10px;
+    display: flex;
+    .label { color: #999; width: 100px; flex-shrink: 0; }
+    .value { color: #333; flex: 1; }
+  } 
+  .record-images {
+    display: flex; flex-wrap: wrap; gap: 8px;
+    .record-img {
+      width: 80px; height: 80px; border-radius: 4px; border: 1px solid #eee;
+    }
+  }
+}
 .log-list-new { .log-row { font-size: 13px; line-height: 2.2; color: #666; display: flex; flex-wrap: wrap; align-items: center; .log-time { color: #999; margin-right: 12px; width: 140px; } .log-user { color: #409eff; margin-right: 8px; cursor: pointer; } .link-style { color: #409eff; cursor: pointer; &:hover { text-decoration: underline; } } } }
 .manage-info-list { padding-top: 10px; .m-item { display: flex; align-items: center; margin-bottom: 20px; font-size: 13px; .m-label { color: #999; width: 80px; } .m-value { color: #333; } } }
 .image-uploader { .uploader-icon { font-size: 28px; color: #8c939d; width: 80px; height: 80px; text-align: center; line-height: 80px; border: 1px dashed #d9d9d9; border-radius: 4px; } .uploaded-image { width: 80px; height: 80px; display: block; border-radius: 4px; object-fit: cover; } }

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

@@ -123,7 +123,12 @@
             v-if="form.id"
             :business-id="form.id" 
             business-type="care"
-            :info-data="form"
+            :info-data="{
+              ...form,
+              customerName: form.customerName,
+              department: getDeptName(form.department),
+              deptName: getDeptName(form.department)
+            }"
           >
             <template #management>
               <div class="sidebar-grid">

+ 10 - 1
src/views/customer/highseas/detail.vue

@@ -150,7 +150,14 @@
             v-if="customerData.id"
             :business-id="customerData.id" 
             business-type="customer"
-            :info-data="customerData"
+            :info-data="{
+              ...customerData,
+              customerName: customerData.customerName,
+              deptName: customerData.deptName || '',
+              department: customerData.deptName || '',
+              industryName: customerData.industryName || '',
+              profession: customerData.industryName || ''
+            }"
           />
         </div>
       </div>
@@ -405,6 +412,8 @@ const handleAddContact = () => {
   Object.assign(contactForm, {
     id: undefined,
     customerId: customerData.value.id,
+    customerName: customerData.value.customerName || '',
+    customerNo: customerData.value.customerNo || '',
     contactName: '', type: '1', gender: '1', age: '', nativePlace: '', birthday: '', remark: '',
     jobStatus: '1', phone: '', deptId: undefined, deptName: '', roleName: '', officePhone: '', jobContent: '',
     homeProvinceId: undefined, homeCityId: undefined, homeAreaId: undefined,

+ 13 - 1
src/views/customer/valid/detail.vue

@@ -358,7 +358,18 @@
               v-if="customerData"
               :business-id="customerData.id" 
               business-type="customer"
-              :info-data="customerData"
+              :info-data="{
+                ...customerData,
+                customerName: customerData.customerName,
+                customerNo: customerData.customerNo || customerData.customNo || '',
+                deptName: customerData.belongingDepartmentName || customerData.deptName || '',
+                department: customerData.belongingDepartmentName || customerData.deptName || '',
+                industryName: customerData.industryName || '',
+                profession: customerData.industryName || '',
+                /* 业务员:优先取销售信息中的 salesPersonName 或 salesPerson,兼容多种字段名 */
+                leaderName: customerData.customerSalesInfoVo?.salesPersonName || customerData.customerSalesInfoVo?.salesPerson || customerData.salesPersonName || customerData.leaderName || customerData.staffName || customerData.salesman || '',
+                staffName: customerData.customerSalesInfoVo?.salesPersonName || customerData.customerSalesInfoVo?.salesPerson || customerData.salesPersonName || customerData.leaderName || customerData.staffName || customerData.salesman || ''
+              }"
             />
           </div>
         </div>
@@ -961,6 +972,7 @@ const handleDeleteFile = (row) => {
 const handleAddContact = () => {
   resetContactForm();
   contactForm.customerId = customerData.value.id;
+  contactForm.customerName = customerData.value.customerName || '';
   contactVisible.value = true;
 };
 

+ 9 - 4
src/views/saleManage/leads/detail.vue

@@ -211,7 +211,12 @@
             v-if="form.id"
             :business-id="form.id" 
             business-type="leads"
-            :info-data="form"
+            :info-data="{
+              ...form,
+              industryName: findIndustryName(form.profession),
+              profession: findIndustryName(form.profession),
+              customerName: form.customerName
+            }"
           />
         </div>
       </div>
@@ -850,15 +855,15 @@ const handleDeleteFile = (row) => {
 <style scoped lang="scss">
 .detail-wrapper { height: 100%; display: flex; flex-direction: column; background: #f5f7fa; }
 .detail-header { 
-  height: 48px; 
+  height: 45px; 
   padding: 0 20px; 
   display: flex; 
   justify-content: space-between; 
   align-items: center; 
   background: #fff; 
   border-bottom: 1px solid #e4e7ed; 
-  .main-title { font-size: 16px; font-weight: normal; color: #303133; } 
-  .close-btn { cursor: pointer; font-size: 18px; color: #909399; &:hover { color: #409eff; } } 
+  .main-title { font-size: 16px; font-weight: normal; color: #303133; line-height: 45px; } 
+  .close-btn { cursor: pointer; font-size: 18px; color: #909399; line-height: 45px; &:hover { color: #409eff; } } 
 }
 .detail-main { flex: 1; display: flex; overflow: hidden; }
 .detail-left { flex: 1; display: flex; flex-direction: column; background: #fff; overflow: hidden; }

+ 93 - 24
src/views/saleManage/opportunity/add.vue

@@ -3,7 +3,7 @@
     title="项目商机(3个月内出结果、金额明确的项目信息,计入:项目商机)"
     v-model="visible"
     direction="rtl"
-    size="75%"
+    size="80%"
     :close-on-click-modal="false"
     class="opp-drawer"
   >
@@ -150,10 +150,19 @@
                 <el-button link type="primary" icon="Upload">上传附件</el-button>
               </el-upload>
             </div>
-            <el-table :data="fileList" border class="file-table">
-              <el-table-column label="文件名称" prop="name" show-overflow-tooltip />
-              <el-table-column label="操作" width="100" align="center">
+            <el-table :data="fileList" border stripe class="file-table">
+              <el-table-column label="文件名称" prop="name" show-overflow-tooltip min-width="200">
+                <template #default="scope">
+                  <el-link type="primary" :underline="false" @click="downloadFile(scope.row)" class="file-link">
+                    <el-icon style="margin-right: 4px;"><Document /></el-icon>
+                    {{ scope.row.name }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column label="文件类型" prop="type" width="100" align="center" />
+              <el-table-column label="操作" width="120" align="center">
                  <template #default="scope">
+                   <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
                    <el-button link type="danger" @click="handleDeleteFile(scope.$index)">删除</el-button>
                  </template>
               </el-table-column>
@@ -173,10 +182,11 @@
 
 <script setup>
 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 { globalHeaders } from '@/utils/request';
-import { Close, Upload } from '@element-plus/icons-vue';
+import { Close, Upload, Document } from '@element-plus/icons-vue';
 
 const props = defineProps({
   modelValue: Boolean,
@@ -208,7 +218,7 @@ const form = reactive({
   status: '0'
 });
 
-const remoteLoadCustomers = (query) => {
+const remoteLoadCustomers = useDebounceFn((query) => {
   customerLoading.value = true;
   listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
     const list = res.data || res.rows || [];
@@ -216,7 +226,7 @@ const remoteLoadCustomers = (query) => {
   }).finally(() => {
     customerLoading.value = false;
   });
-};
+}, 300);
 
 const rules = reactive({
   companyId: [{ required: true, message: '归属公司不能为空', trigger: 'change' }],
@@ -283,12 +293,30 @@ const handleLeaderChange = (val) => {
 
 const handleUploadSuccess = (res) => {
   if (res.code === 200) {
-    fileList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
+    const file = { 
+      name: res.data.fileName, 
+      url: res.data.url, 
+      ossId: res.data.ossId,
+      type: res.data.fileName.split('.').pop().toUpperCase() || '-',
+      time: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
+    };
+    fileList.value.push(file);
     form.fileNo = fileList.value.map(f => f.ossId).join(',');
     proxy.$modal.msgSuccess("上传成功");
   }
 };
 
+const downloadFile = (row) => {
+  if (row.url) {
+    proxy.$modal.msgInfo("正在启动下载...");
+    if (proxy.$download && proxy.$download.resource) {
+      proxy.$download.resource(row.url);
+    } else {
+      window.open(row.url, '_blank');
+    }
+  }
+};
+
 const handleDeleteFile = (index) => {
   fileList.value.splice(index, 1);
   form.fileNo = fileList.value.map(f => f.ossId).join(',');
@@ -299,17 +327,28 @@ const submitForm = () => {
     if (valid) {
       submitting.value = true;
       const data = {
-        ...form,
+        projectName: form.projectName,
         projectBudget: Number(form.amount),
         winRate: Number(form.winRate),
         approvalDate: form.setupTime,
         expectedCompletionTime: form.deadline,
         companyNo: form.companyId,
         leader: form.managerId,
+        leaderName: form.leaderName,
         activityNo: form.marketingActivity,
         procurementMethod: form.purchaseMethod,
         infoSource: form.source,
-        projectDescription: form.description
+        projectDescription: form.description,
+        competitor: form.competitor,
+        customerNo: form.customerNo,
+        customerName: form.customerName,
+        fileNo: form.fileNo,
+        projectLevel: form.projectLevel,
+        projectArea: form.projectArea,
+        profession: form.profession,
+        izClue: form.izClue || 0,
+        projectType: form.projectType || '销售商机',
+        status: form.status || '0'
       };
       addOpportunity(data).then(() => {
         proxy.$modal.msgSuccess("新增成功");
@@ -324,16 +363,28 @@ const submitForm = () => {
 <style scoped lang="scss">
 .opp-drawer {
   :deep(.el-drawer__header) {
-    margin-bottom: 0;
-    span, div { font-weight: normal !important; color: #333; }
+    margin: 0;
+    padding: 0 20px;
+    height: 48px;
+    line-height: 48px;
+    border-bottom: 1px solid #f0f0f0;
+    span {
+      font-size: 16px;
+      font-weight: normal;
+      color: #333;
+    }
   }
+  :deep(.el-drawer__body) { padding: 0 !important; background-color: #fff; }
 }
 
 .drawer-body {
-  padding: 10px 20px;
+  padding: 0;
+  height: 100%;
+  overflow-y: auto;
 }
 
 .opp-form {
+  padding: 15px 24px;
   :deep(.el-form-item__label) {
     font-weight: normal !important;
     color: #666;
@@ -341,24 +392,42 @@ const submitForm = () => {
 }
 
 .section-block {
-  margin-bottom: 15px;
+  margin-bottom: 24px;
   .section-title {
-    font-size: 14px;
+    padding: 8px 15px;
+    background-color: #f8fbff;
     color: #409eff;
-    margin-bottom: 10px;
-    padding-bottom: 6px;
-    border-bottom: 1px solid #f0f2f5;
+    font-size: 14px;
+    margin-bottom: 15px;
+    border-bottom: none;
+    &.attachment-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding-top: 4px;
+      padding-bottom: 4px;
+      margin-top: 0;
+    }
   }
 }
-.attachment-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+
+.file-table {
+  :deep(th.el-table__cell) {
+    background-color: #f8fafc !important;
+    color: #475569;
+    font-weight: 500;
+  }
+  .file-link {
+    font-weight: normal;
+    font-size: 13px;
+  }
 }
+
 .drawer-footer {
-  padding: 10px 20px;
+  padding: 12px 24px;
   border-top: 1px solid #f0f2f5;
   text-align: right;
-  .el-button { font-weight: normal !important; }
+  background-color: #fff;
+  .el-button { font-weight: normal !important; padding: 8px 20px; }
 }
 </style>

+ 42 - 18
src/views/saleManage/opportunity/detail.vue

@@ -70,10 +70,14 @@
                   <el-descriptions-item label="项目级别:">{{ findProjectLevelName(detailData.projectLevel) }}</el-descriptions-item>
                   <el-descriptions-item label="项目区域:">{{ detailData.projectArea }}</el-descriptions-item>
                   
-                  <el-descriptions-item label="采购方式:">{{ findProcurementMethodName(detailData.procurementMethod) }}</el-descriptions-item>
+                  <el-descriptions-item label="采购方式:" :span="3">{{ findProcurementMethodName(detailData.procurementMethod) }}</el-descriptions-item>
                   
-                  <el-descriptions-item label="项目描述:" :span="3">{{ detailData.projectDescription }}</el-descriptions-item>
-                  <el-descriptions-item label="竞争对手:" :span="3">{{ detailData.competitor }}</el-descriptions-item>
+                  <el-descriptions-item label="项目描述:" :span="3">
+                    <div class="text-content">{{ detailData.projectDescription }}</div>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="竞争对手:" :span="3">
+                    <div class="text-content">{{ detailData.competitor }}</div>
+                  </el-descriptions-item>
                 </el-descriptions>
               </div>
             </el-tab-pane>
@@ -204,8 +208,8 @@
                  <el-table-column label="上传时间" align="center" prop="uploadTime" width="160" />
                   <el-table-column label="操作" align="center" width="120">
                     <template #default="scope">
-                      <el-link :href="scope.row.url" :underline="false" target="_blank" type="primary" style="margin-right: 10px;">下载</el-link>
-                      <el-link type="danger" :underline="false" @click="handleDeleteFile(scope.row)">删除</el-link>
+                      <el-button link type="primary" @click="downloadFile(scope.row)" style="margin-right: 10px;">下载</el-button>
+                      <el-button link type="danger" @click="handleDeleteFile(scope.row)">删除</el-button>
                     </template>
                   </el-table-column>
                  <template #empty><span class="empty-text">暂无数据</span></template>
@@ -219,7 +223,11 @@
           <BusinessActivity 
             v-if="detailData.id"
             :businessId="detailData.id" 
-            :infoData="detailData" 
+            :infoData="{
+              ...detailData,
+              industryName: findIndustryName(detailData.industry),
+              profession: findIndustryName(detailData.industry)
+            }" 
             businessType="opportunity"
             @success="getList"
           />
@@ -408,6 +416,7 @@
 
 <script setup>
 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";
@@ -602,7 +611,7 @@ const handleAnalysisDeleteFile = (index) => {
   analysisFileList.value.splice(index, 1);
 };
 
-const handleSaveAnalysis = async () => {
+const handleSaveAnalysis = useDebounceFn(async () => {
   if (!detailData.value.projectNo) {
     proxy.$modal.msgWarning("项目编号缺失,无法保存");
     return;
@@ -631,11 +640,16 @@ const handleSaveAnalysis = async () => {
   } finally {
     proxy.$modal.closeLoading();
   }
-};
+}, 300);
 
 const downloadFile = (file) => {
   if (file.url) {
-    window.open(file.url, '_blank');
+    proxy.$modal.msgInfo("正在启动下载...");
+    if (proxy.$download && proxy.$download.resource) {
+      proxy.$download.resource(file.url);
+    } else {
+      window.open(file.url, '_blank');
+    }
   }
 };
 
@@ -696,7 +710,7 @@ const handleSelectionChange = (selection) => {
   selectedContacts.value = selection;
 };
 
-const confirmAssociate = async () => {
+const confirmAssociate = useDebounceFn(async () => {
   if (selectedContacts.value.length === 0) return;
   proxy.$modal.loading("正在关联...");
   try {
@@ -713,7 +727,7 @@ const confirmAssociate = async () => {
   } catch (e) {} finally {
     proxy.$modal.closeLoading();
   }
-};
+}, 300);
 
 const handleNewProjectContact = () => {
   Object.assign(projectContactForm, {
@@ -738,7 +752,7 @@ const handleEditContact = (row) => {
   projectContactDrawerOpen.value = true;
 };
 
-const submitProjectContact = () => {
+const submitProjectContact = useDebounceFn(() => {
   projectContactFormRef.value.validate(async (valid) => {
     if (valid) {
       contactSubmitting.value = true;
@@ -746,7 +760,10 @@ const submitProjectContact = () => {
         const payload = {
           ...projectContactForm,
           platformCode: String(detailData.value.id),
-          customerNo: detailData.value.companyNo
+          // 关联客户信息
+          customerName: detailData.value.customerName || '',
+          customerNo: detailData.value.companyNo || '',
+          customNo: detailData.value.companyNo || ''
         };
         if (projectContactForm.id) {
           await updateContact(payload);
@@ -761,15 +778,15 @@ const submitProjectContact = () => {
       }
     }
   });
-};
+}, 300);
 
-const handleDeleteContact = (row) => {
+const handleDeleteContact = useDebounceFn((row) => {
   ElMessageBox.confirm(`确认删除联系人「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
     await delContact(row.id);
     ElMessage.success('操作成功');
     fetchContactList(detailData.value.id);
   });
-};
+}, 300);
 
 const handleUploadSuccess = async (res) => {
   if (res.code === 200) {
@@ -786,7 +803,7 @@ const handleUploadSuccess = async (res) => {
   }
 };
 
-const handleDeleteFile = (row) => {
+const handleDeleteFile = useDebounceFn((row) => {
   ElMessageBox.confirm(`确认删除附件「${row.name}」吗?`, '提示', { type: 'warning' }).then(async () => {
     const currentFileNos = detailData.value.fileNo ? detailData.value.fileNo.split(',') : [];
     const newFileNo = currentFileNos.filter(id => String(id) !== String(row.ossId)).join(',');
@@ -801,7 +818,7 @@ const handleDeleteFile = (row) => {
       proxy.$modal.closeLoading();
     }
   });
-};
+}, 300);
 
 const handleStepClick = async (index) => {
   const targetSchedule = String(index + 1);
@@ -1019,4 +1036,11 @@ defineExpose({ open });
   -ms-overflow-style: none;
   scrollbar-width: none;
 }
+
+.text-content {
+  white-space: pre-wrap;
+  word-break: break-all;
+  line-height: 1.6;
+  color: #1D2129;
+}
 </style>

+ 95 - 25
src/views/saleManage/opportunity/edit.vue

@@ -3,7 +3,7 @@
     title="编辑项目商机"
     v-model="visible"
     direction="rtl"
-    size="80%"
+    size="85%"
     :close-on-click-modal="false"
     class="opp-drawer"
   >
@@ -150,10 +150,18 @@
                 <el-button link type="primary" icon="Upload">上传附件</el-button>
               </el-upload>
             </div>
-            <el-table :data="fileList" border class="file-table">
-              <el-table-column label="文件名称" prop="name" show-overflow-tooltip />
-              <el-table-column label="操作" width="100" align="center">
+            <el-table :data="fileList" border stripe class="file-table">
+              <el-table-column label="文件名称" prop="name" show-overflow-tooltip>
+                <template #default="scope">
+                  <el-link type="primary" :underline="false" @click="downloadFile(scope.row)" class="file-link">
+                    <el-icon style="margin-right: 4px;"><Document /></el-icon>
+                    {{ scope.row.name }}
+                  </el-link>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="120" align="center">
                  <template #default="scope">
+                   <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
                    <el-button link type="danger" @click="handleDeleteFile(scope.$index)">删除</el-button>
                  </template>
               </el-table-column>
@@ -173,11 +181,12 @@
 
 <script setup>
 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 { listByIds } from '@/api/system/oss/index';
 import { globalHeaders } from '@/utils/request';
-import { Close, Upload } from '@element-plus/icons-vue';
+import { Close, Upload, Document } from '@element-plus/icons-vue';
 
 const props = defineProps({
   modelValue: Boolean,
@@ -206,7 +215,7 @@ const form = reactive({});
 const customerLoading = ref(false);
 const localCustomerOptions = ref([]);
 
-const remoteLoadCustomers = (query) => {
+const remoteLoadCustomers = useDebounceFn((query) => {
   customerLoading.value = true;
   listCustomerOption({ customerName: query, isHighSeas: 'all' }).then(res => {
     const list = res.data || res.rows || [];
@@ -214,7 +223,7 @@ const remoteLoadCustomers = (query) => {
   }).finally(() => {
     customerLoading.value = false;
   });
-};
+}, 300);
 
 const rules = reactive({
   companyId: [{ required: true, message: '归属公司不能为空', trigger: 'change' }],
@@ -284,7 +293,11 @@ const loadDetail = (id) => {
 
     if (form.fileNo) {
       listByIds(form.fileNo).then(ossRes => {
-        fileList.value = ossRes.data.map(i => ({ name: i.originalName, url: i.url, ossId: i.ossId }));
+        fileList.value = ossRes.data.map(i => ({ 
+          name: i.originalName || i.fileName, 
+          url: i.url, 
+          ossId: i.ossId 
+        }));
       });
     } else {
       fileList.value = [];
@@ -309,12 +322,28 @@ const handleLeaderChange = (val) => {
 
 const handleUploadSuccess = (res) => {
   if (res.code === 200) {
-    fileList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
+    const file = res.data;
+    fileList.value.push({ 
+      name: file.originalName || file.fileName, 
+      url: file.url, 
+      ossId: file.ossId 
+    });
     form.fileNo = fileList.value.map(f => f.ossId).join(',');
     proxy.$modal.msgSuccess("上传成功");
   }
 };
 
+const downloadFile = (row) => {
+  if (row.url) {
+    proxy.$modal.msgInfo("正在启动下载...");
+    if (proxy.$download && proxy.$download.resource) {
+      proxy.$download.resource(row.url);
+    } else {
+      window.open(row.url, '_blank');
+    }
+  }
+};
+
 const handleDeleteFile = (index) => {
   fileList.value.splice(index, 1);
   form.fileNo = fileList.value.map(f => f.ossId).join(',');
@@ -325,17 +354,29 @@ const submitForm = () => {
     if (valid) {
       submitting.value = true;
       const data = {
-        ...form,
+        id: form.id,
+        projectName: form.projectName,
         projectBudget: Number(form.amount),
         winRate: Number(form.winRate),
         approvalDate: form.setupTime,
         expectedCompletionTime: form.deadline,
         companyNo: form.companyId,
         leader: form.managerId,
+        leaderName: form.leaderName,
         activityNo: form.marketingActivity,
         procurementMethod: form.purchaseMethod,
         infoSource: form.source,
-        projectDescription: form.description
+        projectDescription: form.description,
+        competitor: form.competitor,
+        customerNo: form.customerNo,
+        customerName: form.customerName,
+        fileNo: form.fileNo,
+        projectLevel: form.projectLevel,
+        projectArea: form.projectArea,
+        profession: form.profession,
+        izClue: form.izClue || 0,
+        projectType: form.projectType || '销售商机',
+        status: form.status || '0'
       };
       updateOpportunity(data).then(() => {
         proxy.$modal.msgSuccess("修改成功");
@@ -350,16 +391,28 @@ const submitForm = () => {
 <style scoped lang="scss">
 .opp-drawer {
   :deep(.el-drawer__header) {
-    margin-bottom: 0;
-    span, div { font-weight: normal !important; color: #333; }
+    margin: 0;
+    padding: 0 20px;
+    height: 48px;
+    line-height: 48px;
+    border-bottom: 1px solid #f0f0f0;
+    span {
+      font-size: 16px;
+      font-weight: normal;
+      color: #333;
+    }
   }
+  :deep(.el-drawer__body) { padding: 0 !important; background-color: #fff; }
 }
 
 .drawer-body {
-  padding: 10px 20px;
+  padding: 0;
+  height: 100%;
+  overflow-y: auto;
 }
 
 .opp-form {
+  padding: 15px 24px;
   :deep(.el-form-item__label) {
     font-weight: normal !important;
     color: #666;
@@ -367,24 +420,41 @@ const submitForm = () => {
 }
 
 .section-block {
-  margin-bottom: 15px;
+  margin-bottom: 24px;
   .section-title {
-    font-size: 14px;
+    padding: 8px 15px;
+    background-color: #f8fbff;
     color: #409eff;
-    margin-bottom: 10px;
-    padding-bottom: 6px;
-    border-bottom: 1px solid #f0f2f5;
+    font-size: 14px;
+    margin-bottom: 15px;
+    border-bottom: none;
+    &.attachment-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding-top: 4px;
+      padding-bottom: 4px;
+    }
   }
 }
-.attachment-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+
+.file-table {
+  :deep(th.el-table__cell) {
+    background-color: #f8fafc !important;
+    color: #475569;
+    font-weight: 500;
+  }
+  .file-link {
+    font-weight: normal;
+    font-size: 13px;
+  }
 }
+
 .drawer-footer {
-  padding: 10px 20px;
+  padding: 12px 24px;
   border-top: 1px solid #f0f2f5;
   text-align: right;
-  .el-button { font-weight: normal !important; }
+  background-color: #fff;
+  .el-button { font-weight: normal !important; padding: 8px 20px; }
 }
 </style>

+ 34 - 13
src/views/saleManage/opportunity/index.vue

@@ -87,8 +87,8 @@
           <el-col :span="10">
             <el-form-item label-width="0">
               <div class="search-btns">
-                <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-                <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+                <el-button type="primary" icon="Search" @click="handleQuery" :loading="loading">搜索</el-button>
+                <el-button icon="Refresh" @click="resetQuery" :disabled="loading">重置</el-button>
                 <el-button type="primary" icon="Plus" @click="handleAdd" class="btn-new">新增</el-button>
               </div>
             </el-form-item>
@@ -129,7 +129,11 @@
         <el-table-column label="项目名称" align="center" prop="projectName" min-width="240" show-overflow-tooltip fixed />
         <el-table-column label="所属公司" align="center" prop="companyName" min-width="150" />
         <el-table-column label="客户名称" align="center" prop="customerName" min-width="200" show-overflow-tooltip />
-        <el-table-column label="行业" align="center" prop="industry" width="100" />
+        <el-table-column label="行业" align="center" width="100">
+          <template #default="{ row }">
+            <span>{{ findIndustryName(row.industry || row.profession) }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="部门" align="center" prop="deptName" width="100" />
         <el-table-column label="金额(万)" align="center" prop="projectBudget" width="100">
           <template #default="{ row }">
@@ -146,7 +150,11 @@
             <span>{{ findProjectLevelName(row.projectLevel) }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="项目负责人" align="center" prop="leaderName" min-width="100" />
+        <el-table-column label="项目负责人" align="center" prop="leader" min-width="100">
+          <template #default="{ row }">
+            <span>{{ findUserName(row.leader) }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="产品支持" align="center" prop="productSupportName" min-width="100" />
         <el-table-column label="创建时间" align="center" prop="createTime" width="110">
           <template #default="scope">
@@ -163,10 +171,10 @@
             <span>{{ parseTime(scope.row.expectedCompletionTime, '{y}-{m}-{d}') }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="项目状态" align="center" prop="projectStatus" width="100">
+        <el-table-column label="项目状态" align="center" prop="status" width="100">
           <template #default="{ row }">
-            <span class="status-tag" :class="getStatusClass(row.projectStatus)">
-              {{ getSaleStatusLabel(row.projectStatus) }}
+            <span class="status-tag" :class="getStatusClass(row.status)">
+              {{ getSaleStatusLabel(row.status) }}
             </span>
           </template>
         </el-table-column>
@@ -294,6 +302,7 @@
 
 <script setup name="ProjectOpportunity">
 import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
+import { useDebounceFn } from '@vueuse/core';
 import { listOpportunity, delOpportunity, transferOpportunity } from '@/api/saleManage/opportunity/index';
 import { selectStaffOptionList } from "@/api/system/comStaff/index";
 import { listCompanyOption, listCustomerOption } from "@/api/customer/customerInfo/index";
@@ -406,8 +415,8 @@ const getList = async () => {
   finally { loading.value = false; }
 };
 
-const handleQuery = () => { queryParams.pageNum = 1; getList(); };
-const resetQuery = () => { dateRange.value = []; proxy.resetForm('queryRef'); handleQuery(); };
+const handleQuery = useDebounceFn(() => { queryParams.pageNum = 1; getList(); }, 300);
+const resetQuery = useDebounceFn(() => { dateRange.value = []; proxy.resetForm('queryRef'); handleQuery(); }, 300);
 const handleSelectionChange = (selection) => { multipleSelection.value = selection; };
 
 /* ========== 按钮操作 ========== */
@@ -540,6 +549,18 @@ const findProjectLevelName = (id) => {
 function getCompanyList() { listCompanyOption().then(r => companyOptions.value = r.data); }
 function getUserList() { selectStaffOptionList().then(r => userOptions.value = r.data); }
 function getDeptList() { listDept().then(r => deptOptions.value = proxy.handleTree(r.data, 'deptId')); }
+
+const findIndustryName = (val) => {
+  if (!val) return '';
+  return industryOptions.value.find(i => String(i.id) === String(val))?.industryCategoryName || val;
+};
+
+const findUserName = (id) => {
+  if (!id) return '';
+  const user = userOptions.value.find(u => String(u.staffId || u.userId) === String(id));
+  return user ? (user.staffName || user.nickName) : id;
+};
+
 function getIndustryList() { listIndustryCategory().then(r => industryOptions.value = r.data); }
 
 onMounted(() => {
@@ -743,11 +764,11 @@ onMounted(() => {
   border: 1px solid transparent;
 }
 
-/* 跟进中 - 色 */
+/* 跟进中 - 色 */
 .status-following {
-  background-color: #fff7e8;
-  color: #ff7d00;
-  border-color: #ffcf8b;
+  background-color: #eaf5ff;
+  color: #1890ff;
+  border-color: #badeff;
 }
 
 /* 结案/成功 - 绿色 */

+ 94 - 4
src/views/saleManage/platformSelection/detail.vue

@@ -56,8 +56,8 @@
                     <el-descriptions-item label="项目名称" :span="3">{{ drawerForm.projectName }}</el-descriptions-item>
                     <el-descriptions-item label="归属公司">{{ displayCompanyName }}</el-descriptions-item>
                     <el-descriptions-item label="客户名称">{{ drawerForm.customName || drawerForm.customerName }}</el-descriptions-item>
-                    <el-descriptions-item label="行业">{{ drawerForm.industry }}</el-descriptions-item>
-                    <el-descriptions-item label="部门">{{ drawerForm.deptName || drawerForm.createDept }}</el-descriptions-item>
+                    <el-descriptions-item label="行业">{{ displayIndustryName }}</el-descriptions-item>
+                    <el-descriptions-item label="部门">{{ displayDeptName }}</el-descriptions-item>
                     <el-descriptions-item label="项目状态">{{ getStatusLabel(drawerForm.projectStatus) }}</el-descriptions-item>
                     <el-descriptions-item label="项目级别">{{ displayProjectLevelName }}</el-descriptions-item>
                     <el-descriptions-item label="项目类型">{{ displayProjectTypeName }}</el-descriptions-item>
@@ -239,7 +239,16 @@
           <business-activity
             :business-id="props.id"
             business-type="platformSelection"
-            :info-data="drawerForm"
+            :info-data="{
+              ...drawerForm,
+              customerName: drawerForm.customName || drawerForm.customerName,
+              deptName: displayDeptName,
+              department: displayDeptName,
+              profession: displayIndustryName,
+              industryName: displayIndustryName,
+              /* 显式传入负责人姓名,兼容多种字段名,避免回退到系统创建人 */
+              leaderName: drawerForm.leaderName || drawerForm.managerName || drawerForm.staffName || ''
+            }"
           />
         </div>
       </div>
@@ -417,6 +426,7 @@ import { globalHeaders } from '@/utils/request';
 import { listProvinceWithCities } from "@/api/customer/addressArea";
 import BusinessActivity from '@/views/common/businessActivity.vue';
 import { ElMessageBox, ElMessage } from 'element-plus';
+import { listComStaff } from '@/api/system/comStaff/index';
 
 const proxy = getCurrentInstance().proxy;
 
@@ -438,6 +448,37 @@ const drawerForm = ref({ projectStatus: 1, fileList: [], memberList: [] });
 const drawerActiveTab = ref('info');
 const stepList = ['获取信息', '正式立项', '竞价投标', '项目跟进', '结案'];
 
+/**
+ * loadData 后调用:当后端详情接口未返回 leaderName / deptName 文字时,
+ * 通过 listComStaff(staffId) 查询 CRM 员工信息(含 staffName 和 deptName)进行补全。
+ */
+const enrichFormNames = async () => {
+  const data = drawerForm.value;
+  const leaderId = data.leader || data.managerId;
+
+  // leaderName 和 deptName 都已有文字,不需要查询
+  const hasLeaderName = data.leaderName && isNaN(Number(data.leaderName));
+  const hasDeptName = data.deptName && isNaN(Number(data.deptName));
+  if (hasLeaderName && hasDeptName) return;
+  if (!leaderId) return;
+
+  try {
+    const res = await listComStaff({ staffId: leaderId });
+    // listComStaff 返回分页结构,取第一条匹配的员工
+    const staff = (res.rows || res.data || [])[0];
+    if (!staff) return;
+
+    drawerForm.value = {
+      ...drawerForm.value,
+      // 只在当前字段为空时覆盖
+      leaderName: hasLeaderName ? data.leaderName : (staff.staffName || ''),
+      deptName: hasDeptName ? data.deptName : (staff.deptName || '')
+    };
+  } catch (e) {
+    // 静默失败,不影响主流程
+  }
+};
+
 const contactList = ref([]);
 const contactLoading = ref(false);
 const contactSubmitting = ref(false);
@@ -496,6 +537,8 @@ const loadData = async () => {
       industry: data.industry || data.industryName || data.professionName,
       deptName: data.deptName || data.createDeptName || data.dept?.deptName || data.createDept
     };
+    // 加载完数据后,立即补全负责人和部门的文字名称
+    enrichFormNames();
     fetchContactList(props.id);
     loadAnalysisData();
     loadFileList();
@@ -602,7 +645,10 @@ const submitProjectContact = () => {
         const payload = { 
           ...projectContactForm, 
           platformCode: String(props.id),
-          officeAddressArea: projectContactForm.region.join(',')
+          officeAddressArea: projectContactForm.region.join(','),
+          // 关联客户信息
+          customerName: drawerForm.value.customName || drawerForm.value.customerName || '',
+          customNo: drawerForm.value.customNo || drawerForm.value.customerNo || ''
         };
         if (projectContactForm.id) { await updateContact(payload); } 
         else { await addContact(payload); }
@@ -764,6 +810,50 @@ const displayCompanyName = computed(() => {
 const displayProjectLevelName = computed(() => props.options.level?.find(o => String(o.value) === String(drawerForm.value.projectLevel))?.label || '');
 const displayProjectTypeName = computed(() => props.options.type?.find(o => String(o.value) === String(drawerForm.value.businessType))?.label || '');
 
+const displayIndustryName = computed(() => {
+  // 优先取确认是文字名称的字段(排除纯数字 ID)
+  if (drawerForm.value.industryName && isNaN(Number(drawerForm.value.industryName))) return drawerForm.value.industryName;
+  if (drawerForm.value.industryCategoryName && isNaN(Number(drawerForm.value.industryCategoryName))) return drawerForm.value.industryCategoryName;
+  if (drawerForm.value.industry && isNaN(Number(drawerForm.value.industry))) return drawerForm.value.industry;
+  
+  // 否则通过 ID 查找
+  const professionId = drawerForm.value.profession || drawerForm.value.industry;
+  if (professionId && props.options.industryList) {
+    const flatten = (list) => {
+      let res = [];
+      list.forEach(i => {
+        res.push(i);
+        if (i.children) res = res.concat(flatten(i.children));
+      });
+      return res;
+    };
+    const flatList = flatten(props.options.industryList);
+    const found = flatList.find(i => String(i.id) === String(professionId))?.industryCategoryName || '';
+    if (found) return found;
+  }
+  return '';
+});
+
+const displayDeptName = computed(() => {
+  // 第一优先:后端直接返回的文字部门名
+  const name = drawerForm.value.deptName || drawerForm.value.createDeptName || '';
+  if (name && isNaN(Number(name))) return name;
+
+  // 第二兑底:通过部门 ID 在父组件传入的部门树中反查
+  const deptId = drawerForm.value.deptNo || drawerForm.value.deptId ||
+                 drawerForm.value.createDept || (isNaN(Number(name)) ? '' : name);
+  if (deptId && props.options?.dept?.length) {
+    const flatten = (list) => {
+      let res = [];
+      list.forEach(i => { res.push(i); if (i.children) res = res.concat(flatten(i.children)); });
+      return res;
+    };
+    const found = flatten(props.options.dept).find(d => String(d.id) === String(deptId));
+    if (found) return found.label || found.deptName || '';
+  }
+  return '';
+});
+
 </script>
 
 <style scoped lang="scss">

+ 50 - 4
src/views/saleManage/platformSelection/index.vue

@@ -219,9 +219,17 @@
              <el-tag :type="scope.row.projectStatus === 5 ? 'info' : 'warning'" plain>{{ statusOptions.find(o => String(o.value) === String(scope.row.projectStatus))?.label || '跟进中' }}</el-tag>
            </template>
         </el-table-column>
-        <el-table-column label="成交结果" align="center" prop="result" width="100">
+        <el-table-column label="成交结果" align="center" width="100">
           <template #default="scope">
-            {{ resultOptions.find(o => String(o.value) === String(scope.row.result))?.label || scope.row.result }}
+            <template v-if="String(scope.row.dealResult || scope.row.result) === '1'">
+              <span style="color: #67c23a;">赢单</span>
+            </template>
+            <template v-else-if="String(scope.row.dealResult || scope.row.result) === '2'">
+              <span style="color: #f56c6c;">丢单</span>
+            </template>
+            <template v-else>
+              {{ resultOptions.find(o => String(o.value) === String(scope.row.dealResult || scope.row.result))?.label || scope.row.dealResult || scope.row.result }}
+            </template>
           </template>
         </el-table-column>
         <el-table-column label="操作" align="center" width="180" fixed="right">
@@ -322,6 +330,7 @@ import { listRecord, addRecord } from "@/api/visit/record";
 import { selectStaffOptionList } from "@/api/system/comStaff/index";
 import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo/index";
 import { listIndustryCategory } from "@/api/customer/industryCategory";
+import { listSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
 import { deptTreeSelect } from "@/api/system/user";
 import { Search, Refresh, Plus, Delete, Switch } from '@element-plus/icons-vue';
 import AddDialog from './add.vue';
@@ -346,6 +355,10 @@ const publishLoading = ref(false);
 const progressList = ref([]);
 const progressListLoading = ref(false);
 const currentProjectNo = ref('');
+const currentCustomerName = ref('');
+const currentCustomerNo = ref('');
+const currentDeptName = ref('');
+const currentIndustry = ref('');
 const progressTotal = ref(0);
 const progressPage = ref(1);
 const progressPageSize = ref(5);
@@ -412,6 +425,7 @@ const drawerOptions = computed(() => ({
   shortlisted: R0001.value || [],
   material: ZBPL0001.value || [],
   status: J0001.value || [],
+  dept: deptOptions.value || [],
 }));
 
 /** 查询列表 */
@@ -419,7 +433,30 @@ const getList = async () => {
   loading.value = true;
   try {
     const res = await listPlatformSelection(proxy.addDateRange(queryParams, dateRange.value));
-    dataList.value = res.rows || [];
+    const list = res.rows || [];
+    
+    // 补全成交结果数据
+    if (list.length > 0) {
+      const projectNos = list.map(i => i.projectNo).filter(Boolean);
+      if (projectNos.length > 0) {
+        try {
+          const analysisRes = await listSalesResultAnalyze({ objectNos: projectNos.join(',') });
+          const analysisMap = {};
+          (analysisRes.rows || analysisRes.data || []).forEach(a => {
+            analysisMap[a.objectNo] = a.dealResult;
+          });
+          list.forEach(item => {
+            if (item.projectNo && analysisMap[item.projectNo]) {
+              item.dealResult = analysisMap[item.projectNo];
+            }
+          });
+        } catch (e) {
+          console.error('补全成交结果失败', e);
+        }
+      }
+    }
+
+    dataList.value = list;
     total.value = res.total || 0;
     totalAmount.value = dataList.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
   } catch (err) {
@@ -463,6 +500,10 @@ const handleEdit = (id) => {
 const handleProgress = (row) => {
   currentId.value = row.id;
   currentProjectNo.value = row.projectNo;
+  currentCustomerName.value = row.customName || row.customerName;
+  currentCustomerNo.value = row.customNo || row.companyNo || '';
+  currentDeptName.value = row.deptName || row.department || '';
+  currentIndustry.value = row.industryCategoryName || row.industryName || row.industry || row.profession || '';
   progressContent.value = '';
   progressPage.value = 1;
   progressVisible.value = true;
@@ -492,8 +533,13 @@ const submitProgress = async () => {
   try {
     await addRecord({
       objectNo: currentProjectNo.value,
+      customerNo: currentCustomerNo.value,
+      customerName: currentCustomerName.value,
+      department: currentDeptName.value,
+      deptName: currentDeptName.value,
+      profession: currentIndustry.value,
       followUpCondition: progressContent.value,
-      dataType: '1', // 平台入围一般用 dataType 1 或根据后端约定
+      dataType: '3', // 对应年度入围类型
       callDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
       callTypeCode: '1',
       callAim: '项目进度跟进'

+ 2 - 2
src/views/saleManage/projectSelection/add.vue

@@ -236,7 +236,7 @@ const headers = globalHeaders();
 
 const form = reactive({
   bidPeriodType: 1,
-  projectStatus: '1',
+  projectStatus: '0',
   finalizationType: 2,
   customName: undefined,
   customNo: undefined,
@@ -300,7 +300,7 @@ const reset = () => {
     condition: undefined,
     projectDesc: undefined,
     fileNo: '',
-    projectStatus: '1'
+    projectStatus: '0'
   });
   fileList.value = [];
   customerOptions.value = [];

+ 56 - 13
src/views/saleManage/projectSelection/detail.vue

@@ -46,8 +46,8 @@
                   <el-descriptions-item label="归属公司">{{ findCompanyName(detailData.companyNo || detailData.CompanyNo) }}</el-descriptions-item>
                   <el-descriptions-item label="客户名称" :span="2">{{ detailData.customerName || detailData.customName }}</el-descriptions-item>
                   
-                  <el-descriptions-item label="行业">{{ findIndustryName(detailData.profession) }}</el-descriptions-item>
-                  <el-descriptions-item label="部门">{{ findLeaderDeptName(detailData.leaderId) }}</el-descriptions-item>
+                  <el-descriptions-item label="行业">{{ findIndustryLabel() }}</el-descriptions-item>
+                  <el-descriptions-item label="部门">{{ findLeaderDeptName(detailData.leader) }}</el-descriptions-item>
                   <el-descriptions-item label="负责人">{{ detailData.leaderName || detailData.leader }}</el-descriptions-item>
                   
                   <el-descriptions-item label="项目状态">{{ getStatusLabel(detailData.projectStatus) }}</el-descriptions-item>
@@ -74,7 +74,7 @@
                   <el-descriptions-item label="服务时间段">{{ detailData.serviceTime }}</el-descriptions-item>
  
                   <el-descriptions-item label="入围类型">{{ getShortlistedLabel(detailData.shortlistedType) }}</el-descriptions-item>
-                  <el-descriptions-item label="物资类目">{{ findIndustryName(detailData.profession) }}</el-descriptions-item>
+                  <el-descriptions-item label="物资类目">{{ findProfessionLabel(detailData.profession) }}</el-descriptions-item>
                   <el-descriptions-item label="标期类型">{{ detailData.bidPeriodType === 1 ? '单项目入围' : '周期性框架' }}</el-descriptions-item>
  
                   <el-descriptions-item label="招标链接" :span="3">
@@ -83,7 +83,7 @@
                         {{ detailData.biddingLink || detailData.BiddingLink }}
                       </el-link>
                     </template>
-                    <span v-else>--</span>
+                    <span v-else></span>
                   </el-descriptions-item>
                   
                   <el-descriptions-item label="入围要求" :span="3">{{ detailData.condition }}</el-descriptions-item>
@@ -232,7 +232,14 @@
           <BusinessActivity 
             v-if="detailData.id"
             :businessId="detailData.id" 
-            :infoData="detailData" 
+            :infoData="{
+              ...detailData,
+              customerName: detailData.customerName || detailData.customName,
+              deptName: findLeaderDeptName(detailData.leader),
+              department: findLeaderDeptName(detailData.leader),
+              industryName: findIndustryLabel(),
+              profession: findIndustryLabel()
+            }" 
             businessType="projectSelection"
             @success="getList"
           />
@@ -408,6 +415,7 @@ import { listContact, addContact, delContact, updateContact } from "@/api/custom
 import { getSalesResultAnalyzeByObjectNo, addSalesResultAnalyze, updateSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
 import { listProvinceWithCities } from "@/api/customer/addressArea";
 import { listByIds } from "@/api/system/oss/index";
+import { getCustomerInfo } from "@/api/customer/customerInfo/index";
 import { globalHeaders } from '@/utils/request';
 import { ElMessageBox, ElMessage } from 'element-plus';
 import BusinessActivity from '@/views/common/businessActivity.vue';
@@ -420,7 +428,8 @@ const props = defineProps({
   projectTypeOptions: Array,
   shortlistedTypeOptions: Array,
   statusOptions: Array,
-  professionOptions: Array
+  professionOptions: Array,
+  deptOptions: Array
 });
 
 const emit = defineEmits(['update:modelValue', 'edit', 'success']);
@@ -480,10 +489,28 @@ const open = async (row) => {
         ...detailData.value, 
         ...serverData,
         biddingLink: serverData.biddingLink || serverData.BiddingLink || detailData.value.biddingLink,
-        companyName: serverData.companyName || detailData.value.companyName
+        companyName: serverData.companyName || detailData.value.companyName,
+        deptName: serverData.deptName || serverData.departmentName || detailData.value.deptName || row.deptName,
+        industryName: serverData.industryName || detailData.value.industryName || row.industryName
       };
       fetchContactList(row.id);
       loadAnalysisData();
+      
+      // 动态补全行业信息 (如果仍然缺失)
+      if (!detailData.value.industryName && (detailData.value.customNo || detailData.value.customerNo)) {
+        getCustomerInfo(detailData.value.customNo || detailData.value.customerNo).then(cRes => {
+          const cData = cRes.data || cRes;
+          const industryId = cData.industryCategoryId || cData.industryId;
+          const industryName = cData.industryName || props.professionOptions?.find(o => String(o.id) === String(industryId))?.industryCategoryName;
+          if (industryName) {
+            detailData.value = { ...detailData.value, industryName: industryName };
+          }
+          // 如果部门还是空的,尝试从客户那看看(虽然可能性小,但也算个来源)
+          if (!detailData.value.deptName && (cData.deptName || cData.departmentName)) {
+            detailData.value = { ...detailData.value, deptName: cData.deptName || cData.departmentName };
+          }
+        }).catch(() => {});
+      }
     } catch (error) {}
   }
 };
@@ -585,7 +612,13 @@ const submitProjectContact = () => {
     if (valid) {
       contactSubmitting.value = true;
       try {
-        const payload = { ...projectContactForm, platformCode: String(detailData.value.id) };
+        const payload = { 
+          ...projectContactForm, 
+          platformCode: String(detailData.value.id),
+          // 关联客户信息
+          customerName: detailData.value.customerName || detailData.value.customName || '',
+          customNo: detailData.value.customNo || detailData.value.companyNo || ''
+        };
         if (projectContactForm.id) { await updateContact(payload); } 
         else { await addContact(payload); }
         ElMessage.success('保存成功');
@@ -715,17 +748,27 @@ const getStatusLabel = (val) => props.statusOptions?.find(o => String(o.value) =
 const getLevelLabel = (val) => props.projectLevelOptions?.find(o => String(o.value) === String(val))?.label || val;
 const getTypeLabel = (val) => props.projectTypeOptions?.find(o => String(o.value) === String(val))?.label || val;
 const getShortlistedLabel = (val) => props.shortlistedTypeOptions?.find(o => String(o.value) === String(val))?.label || val;
-const findIndustryName = (val) => props.professionOptions?.find(o => String(o.id || o.dictValue) === String(val))?.industryCategoryName || val;
+const findProfessionLabel = (val) => {
+  if (!val) return '';
+  return props.professionOptions?.find(o => String(o.id || o.value || o.dictValue) === String(val))?.label || val;
+};
+const findIndustryLabel = () => {
+  return detailData.value.industryName || 
+         props.professionOptions?.find(o => String(o.id) === String(detailData.value.industryId || detailData.value.industry || detailData.value.profession))?.industryCategoryName || 
+         '';
+};
 const findLeaderDeptName = (val) => {
-  const leaderId = val || detailData.value.leaderId || detailData.value.leader;
-  if (!leaderId) return detailData.value.deptName || detailData.value.DeptName || '--';
+  const leaderId = val || detailData.value.leader;
+  // 优先从人员选项中匹配部门
   const user = props.userOptions?.find(u => String(u.staffId || u.userId || u.id) === String(leaderId));
-  return user ? (user.deptName || user.departmentName) : (detailData.value.deptName || detailData.value.DeptName || '--');
+  const userDept = user ? (user.deptName || user.departmentName) : null;
+  // 如果人员档案没带部门,则显示项目数据中存储的部门名称
+  return userDept || detailData.value.deptName || '';
 };
 
 const findCompanyName = (val) => {
   const value = val || detailData.value.companyNo || detailData.value.CompanyNo;
-  if (!value) return detailData.value.companyName || '--';
+  if (!value) return detailData.value.companyName || '';
   
   // 广泛匹配:deptId (add页面用), companyCode (edit页面用), id (常规)
   const company = props.companyOptions?.find(o => 

+ 4 - 0
src/views/saleManage/projectSelection/edit.vue

@@ -347,6 +347,10 @@ const handleCustomerChange = (val) => {
   if (customer) {
     form.customName = customer.customerName || customer.customName;
     form.customNo = String(customer.customerNo || customer.id);
+    // 联动带出行业信息
+    if (customer.industryCategoryId) {
+      form.profession = String(customer.industryCategoryId);
+    }
   } else {
     form.customName = undefined;
     form.customNo = undefined;

+ 38 - 19
src/views/saleManage/projectSelection/index.vue

@@ -194,7 +194,7 @@
         </el-table-column>
         <el-table-column label="行业" align="center" prop="industryName" width="120" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ row.industryName || professionOptions.find(o => String(o.id) === String(row.profession))?.industryCategoryName || row.profession }}
+            {{ row.industryName || industryOptions.find(o => String(o.id) === String(row.industryId || row.industry || row.profession))?.industryCategoryName || '' }}
           </template>
         </el-table-column>
         <el-table-column label="创建时间" align="center" prop="createTime" width="120">
@@ -253,7 +253,7 @@
       :projectLevelOptions="projectLevelOptions"
       :projectTypeOptions="projectTypeOptions"
       :shortlistedTypeOptions="shortlistedTypeOptions"
-      :professionOptions="professionOptions"
+      :professionOptions="industryOptions"
       @success="getList"
     />
     
@@ -265,7 +265,7 @@
       :projectLevelOptions="projectLevelOptions"
       :projectTypeOptions="projectTypeOptions"
       :shortlistedTypeOptions="shortlistedTypeOptions"
-      :professionOptions="professionOptions"
+      :professionOptions="industryOptions"
       @success="getList"
     />
 
@@ -278,7 +278,8 @@
       :projectLevelOptions="projectLevelOptions"
       :projectTypeOptions="projectTypeOptions"
       :shortlistedTypeOptions="shortlistedTypeOptions"
-      :professionOptions="professionOptions"
+      :professionOptions="industryOptions"
+      :deptOptions="deptOptions"
       @edit="handleEdit"
       @success="getList"
     />
@@ -356,7 +357,7 @@ import { listSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyz
 import { listRecord, addRecord } from "@/api/visit/record";
 import { listIndustryCategory } from "@/api/customer/industryCategory";
 import { selectStaffOptionList } from "@/api/customer/crmStaff";
-import { listCompanyOption } from "@/api/customer/customerInfo/index";
+import { listCompanyOption, getCustomerInfo } from "@/api/customer/customerInfo/index";
 import { deptTreeSelect } from "@/api/system/user";
 
 import SelectionAdd from './add.vue';
@@ -370,8 +371,9 @@ const {
   R0001: shortlistedTypeOptions,
   J0001: statusOptions,
   deal_result: resultOptions,
-  time_query_type: timeTypeOptions
-} = toRefs(reactive(proxy.useDict('XMJB0001', 'L0001', 'R0001', 'J0001', 'deal_result', 'time_query_type')));
+  time_query_type: timeTypeOptions,
+  ZBPL0001: professionOptions
+} = toRefs(reactive(proxy.useDict('XMJB0001', 'L0001', 'R0001', 'J0001', 'deal_result', 'time_query_type', 'ZBPL0001')));
 
 const loading = ref(false);
 const progressVisible = ref(false);
@@ -404,7 +406,7 @@ const progressPageSize = ref(5);
 const userOptions = ref([]);
 const companyOptions = ref([]);
 const deptOptions = ref([]);
-const professionOptions = ref([]);
+const industryOptions = ref([]);
 
 const queryParams = reactive({
   pageNum: 1, pageSize: 10,
@@ -442,6 +444,32 @@ const getList = async () => {
           console.error('补全成交结果失败', e);
         }
       }
+      
+      // 异步补全行业信息 (根据客户动态显示)
+      const missingIndustryList = list.filter(i => (i.customNo || i.customerNo) && !i.industryName);
+      if (missingIndustryList.length > 0) {
+        const uniqueNos = [...new Set(missingIndustryList.map(i => i.customNo || i.customerNo))];
+        uniqueNos.forEach(async (no) => {
+          try {
+            const cRes = await getCustomerInfo(no);
+            const cData = cRes.data || cRes;
+            const industryId = cData.industryCategoryId || cData.industryId;
+            // 尝试从返回的行业名称或根据 ID 从选项中查找
+            const industryName = cData.industryName || industryOptions.value.find(o => String(o.id) === String(industryId))?.industryCategoryName;
+            if (industryName) {
+              list.forEach(item => {
+                if ((item.customNo || item.customerNo) === no) {
+                  item.industryName = industryName;
+                }
+              });
+              // 触发列表强制更新
+              dataList.value = [...list];
+            }
+          } catch (e) {
+            console.error('动态补全行业失败', e);
+          }
+        });
+      }
     }
 
     dataList.value = list;
@@ -559,17 +587,8 @@ const handleBatchDelete = () => {
 };
 
 const initOptions = () => {
-  listIndustryCategory().then(r => {
-    // 行业数据通常是树形结构,扁平化处理以便列表查找
-    const flatten = (list) => {
-      let res = [];
-      list.forEach(item => {
-        res.push(item);
-        if (item.children) res = res.concat(flatten(item.children));
-      });
-      return res;
-    };
-    professionOptions.value = flatten(r.data || []);
+  listIndustryCategory().then(res => {
+    industryOptions.value = res.data || [];
   });
   selectStaffOptionList().then(r => userOptions.value = r.data);
   listCompanyOption().then(r => companyOptions.value = r.data);

+ 21 - 3
src/views/visit/record/index.vue

@@ -46,12 +46,16 @@
             <span>{{ scope.row.visitorDeptName || scope.row.deptName || scope.row.department || '' }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="行业" align="center" prop="profession" width="100" />
+        <el-table-column label="行业" align="center" width="100">
+          <template #default="scope">
+            <span>{{ industryOptions.find(o => String(o.id) === String(scope.row.profession))?.industryCategoryName || scope.row.profession || '' }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="业务员" align="center" prop="salesman" width="100" show-overflow-tooltip />
         <el-table-column label="对象类型" align="center" prop="dataType" width="120">
            <template #default="scope">
-             <el-tag v-if="String(scope.row.dataType) === '1'" type="primary">项目</el-tag>
-             <el-tag v-else-if="String(scope.row.dataType) === '2'" type="success">项目商机</el-tag>
+             <el-tag v-if="String(scope.row.dataType) === '1'" type="primary">项目商机</el-tag>
+             <el-tag v-else-if="String(scope.row.dataType) === '2'" type="success">项目筛选</el-tag>
              <el-tag v-else-if="String(scope.row.dataType) === '3'" type="warning">年度入围</el-tag>
              <el-tag v-else type="info">通用</el-tag>
            </template>
@@ -89,6 +93,7 @@
 import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
 import { listRecord, delRecord } from "@/api/visit/record";
 import { selectStaffOptionList } from "@/api/customer/crmStaff";
+import { listIndustryCategory } from "@/api/customer/industryCategory";
 import Detail from './detail.vue';
 
 const proxy = getCurrentInstance().proxy;
@@ -99,6 +104,7 @@ const total = ref(0);
 const detailOpen = ref(false);
 const selectedId = ref(null);
 const staffOptions = ref([]);
+const industryOptions = ref([]);
 
 const queryParams = reactive({
   pageNum: 1,
@@ -156,6 +162,18 @@ onMounted(() => {
   selectStaffOptionList().then(res => {
     staffOptions.value = res.data || [];
   });
+  
+  listIndustryCategory().then(res => {
+    const flatten = (list) => {
+      let result = [];
+      list.forEach(item => {
+        result.push(item);
+        if (item.children) result = result.concat(flatten(item.children));
+      });
+      return result;
+    };
+    industryOptions.value = flatten(res.data || []);
+  });
 });
 </script>
 

+ 5 - 2
src/views/visit/routine/detail.vue

@@ -257,12 +257,15 @@ const handleNewRecord = () => {
 const submitRecord = () => {
   recordFormRef.value.validate(valid => {
     if (valid) {
+      // 辅助函数:纯数字视为 ID,不写入
+      const safeText = (val) => (val && !isNaN(Number(val)) ? '' : (val || ''));
       const data = {
         objectNo: detailData.value.id,
         customerName: detailData.value.customerName,
         customerNo: detailData.value.customerNo,
-        department: detailData.value.department || '',
-        profession: detailData.value.profession || '',
+        department: safeText(detailData.value.department),
+        deptName: safeText(detailData.value.department),
+        profession: safeText(detailData.value.profession),
         salesman: detailData.value.callPeopleName || '',
         visitor: detailData.value.callPeopleName || '',
         goalObject: detailData.value.objectName || detailData.value.customerName,