|
|
@@ -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>
|