zlwu 11 maanden geleden
bovenliggende
commit
6f77148fae
2 gewijzigde bestanden met toevoegingen van 256 en 3 verwijderingen
  1. 255 0
      web/src/views/crm/contract/index.vue
  2. 1 3
      web/src/views/crm/quote/index.vue

+ 255 - 0
web/src/views/crm/contract/index.vue

@@ -354,6 +354,34 @@
           </template>
         </el-table-column>
         <el-table-column v-if="isColumnVisible('renewalCount')" label="续签次数" align="center" prop="renewalCount" width="100" />
+        <el-table-column v-if="isColumnVisible('attachmentFiles')" label="合同附件" align="center" prop="attachmentFiles" width="200">
+          <template #default="scope">
+            <div v-if="getAttachmentCount(scope.row) > 0" class="attachment-list-cell">
+              <div
+                v-for="(file, index) in getAttachmentList(scope.row)"
+                :key="index"
+                class="attachment-thumb-item"
+                @click.stop="handleViewAttachment(scope.row, file)"
+              >
+                <!-- 图片缩略图 -->
+                <ImagePreview
+                  v-if="isListImageFile(file)"
+                  :src="file.ossId"
+                  class="attachment-thumb-image"
+                  :isOssId="true"
+                  width="50"
+                  height="50"
+                />
+                <!-- 非图片文件图标 -->
+                <div v-else class="attachment-file-icon">
+                  <el-icon><Document /></el-icon>
+                  <span class="file-ext">{{ getListFileExt(file.name) }}</span>
+                </div>
+              </div>
+            </div>
+            <span v-else class="text-muted">-</span>
+          </template>
+        </el-table-column>
         <el-table-column v-if="isColumnVisible('amount')" label="合同金额" align="right" prop="amount" width="130">
           <template #default="scope">
             <span class="text-primary font-weight-bold"
@@ -1402,6 +1430,7 @@ import { CircleCheck, CircleClose, ChatLineSquare, Clock, InfoFilled, VideoPlay,
 import request from '@/utils/request';
 import html2canvas from 'html2canvas';
 import jsPDF from 'jspdf';
+import { getFilePreviewUrl } from '@/api/system/file';
 
 const router = useRouter();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -1422,6 +1451,7 @@ const tableColumns = ref([
   { prop: 'contactMobile', label: '联系人手机', fixed: false },
   { prop: 'contractType', label: '合同类型', fixed: false },
   { prop: 'renewalCount', label: '续签次数', fixed: false },
+  { prop: 'attachmentFiles', label: '合同附件', fixed: false },
   { prop: 'amount', label: '合同金额', fixed: false },
   { prop: 'receivedAmount', label: '已回款', fixed: false },
   { prop: 'unreceivedAmount', label: '未回款', fixed: false },
@@ -1449,6 +1479,7 @@ const tableColumns = ref([
 const defaultVisibleColumns = [
   'customerName',
   'contractType',
+  'attachmentFiles',
   'amount',
   'receivedAmount',
   'receivedRate',
@@ -1623,11 +1654,33 @@ const getList = async () => {
 
     // 加载统计数据
     loadStatistics();
+
+    // 批量加载附件信息
+    loadAllAttachmentInfos();
   } finally {
     loading.value = false;
   }
 };
 
+/** 批量加载所有附件信息 */
+const loadAllAttachmentInfos = () => {
+  const allOssIds: string[] = [];
+  contractList.value.forEach((row) => {
+    if (row.attachmentFiles) {
+      const attachments = parseListAttachments(row.attachmentFiles);
+      attachments.forEach(att => {
+        if (att.ossId && !allOssIds.includes(att.ossId)) {
+          allOssIds.push(att.ossId);
+        }
+      });
+    }
+  });
+  
+  if (allOssIds.length > 0) {
+    loadAttachmentInfos(allOssIds);
+  }
+};
+
 /** 加载统计数据 - 基于当前tab页数据统计 */
 const loadStatistics = () => {
   let totalAmount = 0;
@@ -1644,6 +1697,145 @@ const loadStatistics = () => {
   };
 };
 
+/** 解析附件列表(支持JSON格式和逗号分隔格式) */
+const parseListAttachments = (fileStr: string): Array<{ ossId: string; category?: string }> => {
+  if (!fileStr) return [];
+
+  // 尝试解析为JSON格式
+  if (fileStr.trim().startsWith('[')) {
+    try {
+      const parsed = JSON.parse(fileStr);
+      if (Array.isArray(parsed)) {
+        return parsed.map((item: any) => ({
+          ossId: typeof item === 'string' ? item : item.ossId || item,
+          category: typeof item === 'object' ? (item.category || 'other') : 'other'
+        }));
+      }
+    } catch (e) {
+      console.warn('解析JSON格式附件失败,尝试其他格式:', e);
+    }
+  }
+
+  // 兼容旧格式:逗号分隔的ossId
+  return fileStr
+    .split(',')
+    .map((id: string) => id.trim())
+    .filter((id: string) => id)
+    .map(ossId => ({ ossId, category: 'other' }));
+};
+
+/** 获取附件数量 */
+const getAttachmentCount = (row: ContractVO): number => {
+  if (!row.attachmentFiles) return 0;
+  const attachments = parseListAttachments(row.attachmentFiles);
+  return attachments.length;
+};
+
+// 附件信息缓存(ossId -> fileInfo)
+const attachmentInfoCache = ref<Map<string, { name: string; url?: string }>>(new Map());
+
+/** 批量加载附件信息 */
+const loadAttachmentInfos = async (ossIds: string[]) => {
+  if (ossIds.length === 0) return;
+
+  // 过滤出未缓存的ossId
+  const uncachedIds = ossIds.filter(id => !attachmentInfoCache.value.has(id));
+  if (uncachedIds.length === 0) return;
+
+  try {
+    const response = await request({
+      url: `/resource/oss/listByIds/${uncachedIds.join(',')}`,
+      method: 'get'
+    });
+
+    if (response.code === 200 && response.data) {
+      response.data.forEach((item: any) => {
+        attachmentInfoCache.value.set(item.ossId, {
+          name: item.originalName || '附件',
+          url: item.url
+        });
+      });
+    }
+  } catch (error) {
+    console.error('加载附件信息失败:', error);
+  }
+};
+
+/** 获取附件列表(返回所有附件,包含文件名) */
+const getAttachmentList = (row: ContractVO): Array<{ ossId: string; name: string; url?: string }> => {
+  if (!row.attachmentFiles) return [];
+  const attachments = parseListAttachments(row.attachmentFiles);
+  
+  // 返回所有附件,优先使用缓存的文件名
+  return attachments.map((att, index) => {
+    const cachedInfo = attachmentInfoCache.value.get(att.ossId);
+    return {
+      ossId: att.ossId,
+      name: cachedInfo?.name || `附件${index + 1}`,
+      url: cachedInfo?.url || att.ossId
+    };
+  });
+};
+
+/** 判断列表中的附件是否为图片 */
+const isListImageFile = (file: { ossId: string; name: string; url?: string }): boolean => {
+  if (!file.name) return false;
+  const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
+  const ext = getListFileExt(file.name).toLowerCase();
+  return imageExts.includes(ext);
+};
+
+/** 获取列表附件的文件扩展名 */
+const getListFileExt = (fileName: string): string => {
+  if (!fileName) return 'FILE';
+  const ext = fileName.split('.').pop()?.toUpperCase();
+  return ext || 'FILE';
+};
+
+/** 获取短文件名(用于显示) */
+const getShortFileName = (fileName: string): string => {
+  if (!fileName) return '';
+  
+  const lastDotIndex = fileName.lastIndexOf('.');
+  if (lastDotIndex === -1) {
+    return fileName.length > 10 ? fileName.substring(0, 10) + '...' : fileName;
+  }
+  
+  const name = fileName.substring(0, lastDotIndex);
+  const ext = fileName.substring(lastDotIndex);
+  
+  if (name.length <= 8) {
+    return fileName;
+  }
+  
+  return name.substring(0, 8) + '...' + ext;
+};
+
+/** 查看附件 - 直接预览/打开文件 */
+const handleViewAttachment = async (row: ContractVO, file: { ossId: string; name: string; url?: string }) => {
+  if (!file.ossId) {
+    proxy?.$modal.msgError('无法获取文件信息');
+    return;
+  }
+
+  try {
+    // 获取预览URL(预览URL在响应的msg字段中)
+    const res = await getFilePreviewUrl(file.ossId);
+    const previewUrl = res.msg || res.data;
+
+    if (!previewUrl) {
+      proxy?.$modal.msgError('获取文件预览地址失败');
+      return;
+    }
+
+    // 直接在新窗口打开预览(浏览器会自动处理图片、PDF等文件类型)
+    window.open(previewUrl, '_blank');
+  } catch (error) {
+    console.error('预览文件失败:', error);
+    proxy?.$modal.msgError('预览文件失败,请稍后重试');
+  }
+};
+
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.value.pageNum = 1;
@@ -3488,4 +3680,67 @@ onMounted(() => {
     }
   }
 }
+
+.attachment-list-cell {
+  display: flex;
+  flex-wrap: nowrap;
+  gap: 6px;
+  justify-content: flex-start;
+  align-items: center;
+  overflow-x: auto;
+  overflow-y: hidden;
+  white-space: nowrap;
+  
+  /* 隐藏滚动条但保持滚动功能 */
+  scrollbar-width: none; /* Firefox */
+  -ms-overflow-style: none; /* IE and Edge */
+  
+  &::-webkit-scrollbar {
+    display: none; /* Chrome, Safari, Opera */
+  }
+
+  .attachment-thumb-item {
+    cursor: pointer;
+    transition: all 0.3s;
+    position: relative;
+
+    &:hover {
+      opacity: 0.8;
+      transform: translateY(-2px);
+    }
+
+    .attachment-thumb-image {
+      width: 50px;
+      height: 50px;
+      border-radius: 4px;
+      border: 1px solid #e0e0e0;
+      object-fit: cover;
+      display: block;
+    }
+
+    .attachment-file-icon {
+      width: 50px;
+      height: 50px;
+      border-radius: 4px;
+      border: 1px solid #e0e0e0;
+      background: #f5f7fa;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      gap: 4px;
+
+      .el-icon {
+        font-size: 20px;
+        color: #909399;
+      }
+
+      .file-ext {
+        font-size: 10px;
+        color: #606266;
+        font-weight: 500;
+      }
+    }
+  }
+}
 </style>

+ 1 - 3
web/src/views/crm/quote/index.vue

@@ -1636,9 +1636,7 @@ const generateQuoteTemplate = (quote: QuoteVO): string => {
             ${productRows}
             <!-- 备注行 -->
             <tr>
-              <td colspan="10" style="padding: 8px; border: 1px solid #000; background-color: #f9f9f9; text-align: center; font-size: 13px;">
-                备注:桶收押金25kg 桶15元/ 个 | 吨桶 450元/ 个
-              </td>
+              <td colspan="10" style="padding: 8px; border: 1px solid #000; background-color: #f9f9f9; text-align: center; font-size: 13px;">    </td>
             </tr>
             <!-- 合计行 -->
             <tr>