import JSZip from 'jszip' const BASE_URL = 'https://cloud.yuxindazhineng.com' const CACHE_PREFIX = 'wyxd_' // ===== 缓存管理 ===== const cacheKey = (workspaceId, filePath) => `${CACHE_PREFIX}${workspaceId}_${filePath}` export const isWyxdCached = (workspaceId, filePath) => { try { const cached = uni.getStorageSync(cacheKey(workspaceId, filePath)) return !!(cached && cached.slides && cached.slides.length > 0) } catch (_) { return false } } export const getCachedWyxd = (workspaceId, filePath) => { try { const cached = uni.getStorageSync(cacheKey(workspaceId, filePath)) if (cached && cached.slides && cached.slides.length > 0) return cached } catch (_) {} return null } export const setCachedWyxd = (workspaceId, filePath, data) => { try { const toStore = { ...data, cachedAt: Date.now() } uni.setStorageSync(cacheKey(workspaceId, filePath), toStore) } catch (_) {} } export const clearCachedWyxd = (workspaceId, filePath) => { try { uni.removeStorageSync(cacheKey(workspaceId, filePath)) } catch (_) {} } // ===== 下载 ===== const getDownloadUrl = (token, workspaceId, filePath) => { console.log("执行getDownloadUrl"); return new Promise((resolve, reject) => { const cleanPath = filePath.startsWith('./') ? filePath.slice(2) : filePath uni.request({ url: `${BASE_URL}/cloud_api/phone/workspace/downloadFile`, method: 'POST', header: { 'Content-Type': 'application/json' }, data: { "access_token": token, "workspaces_id": workspaceId, "file_path": cleanPath }, success: (res) => { if (res.statusCode === 200) { const respond = res.data const arr = respond.success ? (respond.message || respond.data) : respond const fileInfo = Array.isArray(arr) ? arr[0] : arr console.log("fileInfo:",fileInfo); if (fileInfo && fileInfo.url) { console.log("fileInfo.url",fileInfo.url); resolve(fileInfo.url) } else { console.log("获取下载地址失败"); reject(respond?.error || '获取下载地址失败') } } else { reject(`请求失败:${res.statusCode}`) } }, fail: (err) => reject(err) }) }) } export const downloadWyxdAsZip = (token, workspaceId, filePath, onProgress) => { return new Promise(async (resolve, reject) => { try { console.log("执行downloadWyxdAsZip"); const downloadUrl = await getDownloadUrl(token, workspaceId, filePath) console.log("downloadWyxdAsZip-downloadUrl",downloadUrl); const downloadTask = uni.downloadFile({ url: downloadUrl, success: (res) => { if (res.statusCode === 200) { console.log("downloadWyxdAsZip-tempFilePath",res.tempFilePath); resolve(res.tempFilePath) } else { reject(`下载失败:${res.statusCode}`) } }, fail: (err) => reject(err) }) if (onProgress && downloadTask) { downloadTask.onProgressUpdate((res) => { onProgress(res.progress) }) } } catch (err) { reject(err) } }) } // ===== 解压 ===== // 手动 UTF-8 解码,兼容不支持 TextDecoder 的旧版本 WebView const utf8Decode = (uint8arr) => { let result = '' let i = 0 while (i < uint8arr.length) { const b0 = uint8arr[i] if (b0 < 128) { result += String.fromCharCode(b0) i++ } else if (b0 < 224) { result += String.fromCharCode(((b0 & 31) << 6) | (uint8arr[i + 1] & 63)) i += 2 } else if (b0 < 240) { result += String.fromCharCode(((b0 & 15) << 12) | ((uint8arr[i + 1] & 63) << 6) | (uint8arr[i + 2] & 63)) i += 3 } else { result += String.fromCharCode(((b0 & 7) << 18) | ((uint8arr[i + 1] & 63) << 12) | ((uint8arr[i + 2] & 63) << 6) | (uint8arr[i + 3] & 63)) i += 4 } } return result } const readFileAsArrayBuffer = (filePath) => { return new Promise((resolve, reject) => { // App 环境检测:plus 对象仅在 App 运行时存在 if (typeof plus !== 'undefined') { console.log('[readFileAsArrayBuffer] App环境,filePath:', filePath) console.log('[readFileAsArrayBuffer] typeof plus.io:', typeof plus.io) console.log('[readFileAsArrayBuffer] typeof plus.io.resolveLocalFileSystemURL:', typeof plus.io?.resolveLocalFileSystemURL) console.log('[readFileAsArrayBuffer] typeof plus.io.FileReader:', typeof plus.io?.FileReader) // 方式1:尝试 plus.io.resolveLocalFileSystemURL if (plus.io && typeof plus.io.resolveLocalFileSystemURL === 'function') { plus.io.resolveLocalFileSystemURL(filePath, (entry) => { console.log('[readFileAsArrayBuffer] entry.isFile:', entry.isFile) console.log('[readFileAsArrayBuffer] entry.isDirectory:', entry.isDirectory) console.log('[readFileAsArrayBuffer] entry.fullPath:', entry.fullPath) console.log('[readFileAsArrayBuffer] typeof entry.file:', typeof entry.file) if (typeof entry.file !== 'function') { reject(new Error('entry 没有 file 方法,isFile=' + entry.isFile + ' isDirectory=' + entry.isDirectory)) return } try { entry.file((file) => { console.log('[readFileAsArrayBuffer] entry.file 成功,file.size:', file.size) const reader = new plus.io.FileReader() reader.onloadend = (e) => { const dataUrl = e.target.result console.log('[readFileAsArrayBuffer] readAsDataURL 完成,dataUrl 长度:', dataUrl?.length) try { // dataUrl 格式: "data:;base64," const base64 = dataUrl.substring(dataUrl.indexOf(',') + 1) const binary = atob(base64) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) console.log('[readFileAsArrayBuffer] 成功转为 Uint8Array,长度:', bytes.length) resolve(bytes) } catch (err) { console.error('[readFileAsArrayBuffer] base64 解码失败', err) reject(new Error('文件解码失败:' + err.message)) } } reader.onerror = (e) => { console.error('[readFileAsArrayBuffer] plus.io 读取失败', e) reject(new Error('文件读取失败')) } reader.readAsDataURL(file) }, (err) => { console.error('[readFileAsArrayBuffer] 获取File对象失败', err) reject(new Error('获取文件失败:' + (err.message || ''))) }) } catch (e) { console.error('[readFileAsArrayBuffer] entry.file 调用同步异常:', e) reject(new Error('entry.file 调用失败:' + (e.message || e))) } }, (err) => { console.error('[readFileAsArrayBuffer] 解析文件路径失败', err) reject(new Error('解析文件路径失败:' + (err.message || ''))) }) return } // 方式2:降级 - 尝试 uni.getFileSystemManager if (typeof uni !== 'undefined' && typeof uni.getFileSystemManager === 'function') { console.log('[readFileAsArrayBuffer] 降级使用 uni.getFileSystemManager') const fs = uni.getFileSystemManager() fs.readFile({ filePath: filePath, success: (res) => { const base64 = res.data || '' const binary = atob(base64) const bytes = new Uint8Array(binary.length) for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) resolve(bytes) }, fail: (err) => reject(new Error('无法读取文件:' + (err.errMsg || ''))) }) return } reject(new Error('App 环境下无可用的文件读取 API')) return } // H5 环境:fetch blob URL fetch(filePath) .then(res => { if (!res.ok) throw new Error('文件请求失败:' + res.status) return res.arrayBuffer() }) .then(buf => resolve(new Uint8Array(buf))) .catch(err => reject(new Error('H5 读取文件失败:' + (err.message || err)))) }) } export const extractWyxd = async (zipFilePath) => { const data = await readFileAsArrayBuffer(zipFilePath) console.log("data",data); const zip = await JSZip.loadAsync(data) const result = { header: '', html: '', images: {} } for (const [filename, zipEntry] of Object.entries(zip.files)) { if (zipEntry.dir) continue const pureName = filename.replace(/^.*[/\\]/, '') const content = await zipEntry.async('uint8array') if (pureName === 'header.txt') { result.header = utf8Decode(content) } else if (pureName.endsWith('.html') || pureName.endsWith('.htm')) { result.html = utf8Decode(content) } else if (/\.(png|jpg|jpeg|gif|svg|webp|bmp)$/i.test(pureName)) { result.images[filename.replace(/^\.[/\\]?/, '')] = arrayBufferToBase64(content, pureName) } } return result } const arrayBufferToBase64 = (buffer, filename) => { let binary = '' const bytes = new Uint8Array(buffer) for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]) } const ext = (filename || '').split('.').pop().toLowerCase() const mimeMap = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp', bmp: 'image/bmp' } const mime = mimeMap[ext] || 'image/png' return `data:${mime};base64,${btoa(binary)}` } // ===== 解析 HTML 为幻灯片 ===== export const parseSlides = (fullHtml, headerHtml, imageMap) => { const bodyMatch = fullHtml.match(/]*>([\s\S]*?)<\/body>/i) const bodyHtml = bodyMatch ? bodyMatch[1] : fullHtml const pageRegex = /]*\/?>/gi const parts = bodyHtml.split(pageRegex) if (parts.length === 0) return [] const pageTags = bodyHtml.match(pageRegex) || [] const slides = [] // 第一段(封面之前可能没有 page 标签) parts.forEach((part, idx) => { const html = part.trim() if (!html) return // 获取对应的 page 标签参数 let pageAttrs = { order: idx, num: 0 } if (idx < pageTags.length) { const tagStr = pageTags[idx] const orderMatch = tagStr.match(/order\s*=\s*["'](\d+)["']/i) const numMatch = tagStr.match(/num\s*=\s*["'](\d+)["']/i) if (orderMatch) pageAttrs.order = parseInt(orderMatch[1]) if (numMatch) pageAttrs.num = parseInt(numMatch[1]) } const slide = { order: pageAttrs.order, num: pageAttrs.num, type: 'content', html: html } if (/class\s*=\s*["'][^"']*\bwyxd-cover\b/i.test(html)) { slide.type = 'cover' } else if (/class\s*=\s*["'][^"']*\bwyxd-toc\b/i.test(html)) { slide.type = 'toc' } slides.push(slide) }) return buildSlideHtml(slides, headerHtml, imageMap, fullHtml) } const getHeadContent = (html) => { const match = String(html).match(/]*>([\s\S]*?)<\/head>/i) return match ? match[1] : '' } const buildSlideHtml = (slides, headerHtml, imageMap, fullHtml) => { const headContent = getHeadContent(fullHtml) const baseStyle = extractBaseStyle(fullHtml) return slides.map((slide) => { let processedHtml = slide.html // 替换图片 src processedHtml = processedHtml.replace(/src\s*=\s*["']([^"']+)["']/gi, (match, src) => { // 尝试多种路径匹配 let base64 = imageMap[src] || imageMap[src.replace(/^.*?images\//i, 'images/')] if (base64) { return `src="${base64}"` } return match }) // 正文页:注入页眉 + 页码 if (slide.type === 'content') { // 页眉 if (headerHtml) { processedHtml = headerHtml + processedHtml } // 页码 if (slide.num > 0) { const pageNumHtml = `
${slide.num}
` processedHtml += pageNumHtml } } return { ...slide, html: `
${processedHtml}
`, headStyle: headContent } }) } const extractBaseStyle = (html) => { const styleMatch = html.match(/]*>([\s\S]*?)<\/style>/i) return styleMatch ? styleMatch[1] : '' }