/** * CRM 附件解析与下载 — 门户 MinIO(fileUrl 即存储键/URL,不再走 CRM /resource/oss/listByIds) */ import { downloadAttachmentFile, openAttachmentPreviewInNewTab } from '@/utils/download-attachment' export type CrmAttachmentRef = { ossId: string category?: string } export type CrmAttachmentFile = { ossId: string name: string url: string category?: string } /** 解析 attachmentFiles 字段(JSON 数组或逗号分隔 URL) */ export function parseCrmAttachmentField( fileStr: string | null | undefined ): CrmAttachmentRef[] { const raw = (fileStr || '').trim() if (!raw) return [] if (raw.startsWith('[')) { try { const parsed = JSON.parse(raw) if (Array.isArray(parsed)) { return parsed.map((item: unknown) => { if (typeof item === 'string') { return { ossId: item, category: 'other' } } const obj = item as Record return { ossId: String(obj.ossId || obj.url || ''), category: (obj.category as string) || 'other' } }).filter((a) => a.ossId) } } catch { // fall through } } return raw .split(',') .map((s) => s.trim()) .filter(Boolean) .map((ossId) => ({ ossId, category: 'other' })) } /** 从 ossId/url 推导展示文件名(完整 URL 与相对路径均取最后一段) */ export function crmAttachmentDisplayName(ossId: string, index = 0): string { const id = (ossId || '').trim() if (!id) return `附件${index + 1}` // 含路径分隔符时取末段(兼容 http(s) URL 与 MinIO 相对键如 crm/contract/.../uuid.pdf) if (id.includes('/')) { const seg = (id.split('/').pop() || '').split('?')[0] try { return decodeURIComponent(seg) || `附件${index + 1}` } catch { return seg || `附件${index + 1}` } } if (id.length > 12) return `附件_${id.substring(0, 8)}` return id } /** 将解析结果转为 UI 文件项(门户直接存 MinIO URL,无需 listByIds) */ export function resolveCrmAttachmentFiles( refs: CrmAttachmentRef[] ): CrmAttachmentFile[] { return refs.map((att, index) => { const ossId = att.ossId return { ossId, name: crmAttachmentDisplayName(ossId, index), url: ossId, category: att.category || 'other' } }) } /** 兼容旧 loadAttachmentInfos:批量解析并写入 cache Map */ export function fillAttachmentInfoCache( ossIds: string[], cache: Map ): void { ossIds.forEach((id, index) => { const key = (id || '').trim() if (!key || cache.has(key)) return cache.set(key, { name: crmAttachmentDisplayName(key, index), url: key }) }) } export type ContractAttachmentItem = { uid: string name: string ossId?: string url?: string localPreview?: string status: 'uploading' | 'success' | 'error' category: 'single_seal' | 'double_seal' | 'other' } /** 合同附件列表 → 保存 API 用的 JSON 字符串 */ export function serializeContractAttachments(items: ContractAttachmentItem[]): string { const payload = items .filter((file) => file.status === 'success' && file.ossId) .map((file) => ({ ossId: String(file.ossId), category: file.category || 'other' })) return payload.length ? JSON.stringify(payload) : '' } /** 合同附件列表 → uploadAttachments 接口 payload */ export function mapContractAttachmentsPayload(items: ContractAttachmentItem[]) { return items .filter((file) => file.status === 'success' && file.ossId) .map((file) => ({ ossId: String(file.ossId), category: file.category || 'other' })) } /** MinIO 相对路径 → 浏览器可访问 URL */ export function resolveCrmAttachmentUrl(fileUrl: string): string { const trimmed = String(fileUrl || '').trim() if (!trimmed) return '' if (/^https?:\/\//i.test(trimmed)) return trimmed const base = String(import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '') return base ? `${base}/${trimmed.replace(/^\//, '')}` : trimmed } export function isCrmImageFile(file: { name?: string; url?: string; ossId?: string }): boolean { const name = file.name || '' if (/\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(name)) return true const src = file.url || file.ossId || '' return /\.(jpg|jpeg|png|gif|bmp|webp)(\?|$)/i.test(src) } export function isCrmPdfFile(file: { name?: string; url?: string; ossId?: string }): boolean { const name = file.name || '' if (/\.pdf$/i.test(name)) return true const src = file.url || file.ossId || '' return /\.pdf(\?|$)/i.test(src) } export function getCrmFileExt(fileName: string): string { const ext = fileName.split('.').pop()?.toUpperCase() return ext || 'FILE' } const CRM_ATTACHMENT_CATEGORY_LABELS: Record = { single_seal: '单章附件', double_seal: '双章附件', other: '其他附件' } /** 附件分类 → 展示文案 */ export function getCrmAttachmentCategoryLabel(category?: string): string { return CRM_ATTACHMENT_CATEGORY_LABELS[category || 'other'] || CRM_ATTACHMENT_CATEGORY_LABELS.other } /** 下载 CRM 附件(fileUrl / ossId 均为 MinIO 路径或完整 URL) */ export async function downloadCrmAttachment(file: { ossId?: string url?: string name?: string fileName?: string }) { const fileUrl = String(file.url || file.ossId || '').trim() if (!fileUrl) return const fileName = String(file.fileName || file.name || '').trim() || crmAttachmentDisplayName(fileUrl) await downloadAttachmentFile({ fileUrl, fileName }, { fullFileUrl: resolveCrmAttachmentUrl }) } /** CRM 附件在新标签页预览(图片 HTML 内嵌 / PDF 预签名直开) */ export async function openCrmAttachmentPreviewInNewTab(file: { ossId?: string url?: string name?: string fileName?: string }) { const fileUrl = String(file.url || file.ossId || '').trim() if (!fileUrl) return false const fileName = String(file.fileName || file.name || '').trim() || crmAttachmentDisplayName(fileUrl) const kind = isCrmImageFile({ name: fileName, url: fileUrl, ossId: fileUrl }) ? 'image' : isCrmPdfFile({ name: fileName, url: fileUrl, ossId: fileUrl }) ? 'pdf' : 'other' return openAttachmentPreviewInNewTab( { fileUrl, fileName }, { fullFileUrl: resolveCrmAttachmentUrl, kind } ) } /** el-upload 回显:从 attachmentFiles 字符串构建 file-list */ export function buildUploadFileListFromField( fileStr: string | null | undefined ): Array> { const refs = parseCrmAttachmentField(fileStr) return refs.map((att, index) => ({ uid: `existing_${index}_${Date.now()}`, name: crmAttachmentDisplayName(att.ossId, index), url: att.ossId, ossId: att.ossId, category: att.category || 'other', status: 'success' })) } /** 从 el-upload file-list 收集 attachmentFiles JSON(供保存 API) */ export function serializeUploadAttachments( files: Array<{ status?: string; ossId?: string; url?: string; category?: string }> ): string { const items = files .filter((f) => f.status === 'success' && (f.ossId || f.url)) .map((f) => ({ ossId: String(f.ossId || f.url), category: f.category || 'other' })) return items.length ? JSON.stringify(items) : '' }