import JSZip from 'jszip' import * as FileSystem from 'expo-file-system' import * as Sharing from 'expo-sharing' import { getDatabase } from '../../db/database' import type { SummaryRow } from './useSummaryData' const BOM = '\uFEFF' // 主 CSV 表头(含照片目录列) const HEADERS = ['楼层', '标记点名', '物品名', '类别', '数量', '单价', '评估参考价格', '备注', '照片数', '照片目录'] // 照片索引 CSV 表头 const INDEX_HEADERS = ['物品ID', '楼层', '标记点', '物品名', '照片文件路径'] function escapeCsv(value: string | number): string { const str = String(value) if (str.includes(',') || str.includes('"') || str.includes('\n')) { return `"${str.replace(/"/g, '""')}"` } return str } function sanitizeDirName(name: string): string { return name.replace(/[\\/:*?"<>|]/g, '_').slice(0, 20) } function formatDate(d: Date): string { const pad = (n: number) => String(n).padStart(2, '0') return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}` } async function getPhotoPathsByItem(itemId: number): Promise { const db = getDatabase() const rows = await db.getAllAsync<{ filePath: string }>( `SELECT file_path as filePath FROM photos WHERE item_id = ? ORDER BY sort_order ASC`, [itemId], ) return rows.map((r) => r.filePath) } // 仅导出 CSV 数据(不含照片) export async function exportDataOnly(rows: SummaryRow[]): Promise { const canShare = await Sharing.isAvailableAsync() if (!canShare) throw new Error('当前设备不支持分享功能') const DATA_HEADERS = ['楼层', '标记点名', '物品名', '类别', '数量', '单价', '评估参考价格', '备注', '照片数'] const csvLines = [ DATA_HEADERS.join(','), ...rows.map((r) => [ `${r.floor}F`, r.markerLabel || '—', r.name, r.category, r.quantity, r.estimatedValue > 0 ? r.estimatedValue : '', r.referencePrice > 0 ? r.referencePrice : '', r.remark, r.photoCount, ] .map(escapeCsv) .join(','), ), ] const csvContent = BOM + csvLines.join('\r\n') const csvPath = `${FileSystem.cacheDirectory}不动产摸底_${formatDate(new Date())}.csv` await FileSystem.writeAsStringAsync(csvPath, csvContent, { encoding: FileSystem.EncodingType.UTF8, }) await Sharing.shareAsync(csvPath, { mimeType: 'text/csv', dialogTitle: '导出摸底数据(仅数据)', }) } export async function exportWithPhotos(rows: SummaryRow[]): Promise { const canShare = await Sharing.isAvailableAsync() if (!canShare) throw new Error('当前设备不支持分享功能') const zip = new JSZip() const photosFolder = zip.folder('photos')! // 照片索引行(每张照片一行) const indexLines: string[] = [INDEX_HEADERS.join(',')] // 为每个物品生成照片子目录名,并打包照片 const photoDirMap = new Map() // itemId → 子目录路径(相对 ZIP 根) for (const row of rows) { if (row.photoCount === 0) continue const paths = await getPhotoPathsByItem(row.itemId) if (paths.length === 0) continue const dirName = `${row.floor}F_${sanitizeDirName(row.markerLabel)}_${sanitizeDirName(row.name)}(${row.itemId})` const itemFolder = photosFolder.folder(dirName)! const relativeDirPath = `photos/${dirName}/` photoDirMap.set(row.itemId, relativeDirPath) for (let i = 0; i < paths.length; i++) { const filePath = paths[i] const photoFileName = `${i + 1}.jpg` const photoRelativePath = `${relativeDirPath}${photoFileName}` try { const info = await FileSystem.getInfoAsync(filePath) if (!info.exists) continue const base64 = await FileSystem.readAsStringAsync(filePath, { encoding: FileSystem.EncodingType.Base64, }) itemFolder.file(photoFileName, base64, { base64: true }) } catch { // 单张照片读取失败时跳过,不中断整体导出 continue } // 写入索引行 indexLines.push( [row.itemId, `${row.floor}F`, row.markerLabel, row.name, photoRelativePath] .map(escapeCsv) .join(','), ) } } // 生成照片索引文件 if (indexLines.length > 1) { photosFolder.file('index.csv', BOM + indexLines.join('\r\n')) } // 生成主 CSV(含照片目录列) const csvLines = [ HEADERS.join(','), ...rows.map((r) => [ `${r.floor}F`, r.markerLabel || '—', r.name, r.category, r.quantity, r.estimatedValue > 0 ? r.estimatedValue : '', r.referencePrice > 0 ? r.referencePrice : '', r.remark, r.photoCount, photoDirMap.get(r.itemId) ?? '', ] .map(escapeCsv) .join(','), ), ] zip.file('摸底数据.csv', BOM + csvLines.join('\r\n')) // 生成 ZIP 并分享 const zipBase64 = await zip.generateAsync({ type: 'base64' }) const zipPath = `${FileSystem.cacheDirectory}不动产摸底_${formatDate(new Date())}.zip` await FileSystem.writeAsStringAsync(zipPath, zipBase64, { encoding: FileSystem.EncodingType.Base64, }) await Sharing.shareAsync(zipPath, { mimeType: 'application/zip', dialogTitle: '导出不动产摸底数据', }) }