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) => { 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 if (fileInfo && fileInfo.url) { resolve(fileInfo.url) } else { 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 { const downloadUrl = await getDownloadUrl(token, workspaceId, filePath) const downloadTask = uni.downloadFile({ url: downloadUrl, success: (res) => { if (res.statusCode === 200) { 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 环境:uni.getFileSystemManager const fs = uni.getFileSystemManager if (typeof fs === 'function') { fs().readFile({ filePath: filePath, encoding: 'base64', 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 || err.message || ''))) }) 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) 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 = /