Files
phone-app------test1-/utils/wyxd-parser.js

363 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,<base64data>"
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(/<body[^>]*>([\s\S]*?)<\/body>/i)
const bodyHtml = bodyMatch ? bodyMatch[1] : fullHtml
const pageRegex = /<page\s[^>]*\/?>/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(/<head[^>]*>([\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 =
`<div class="wyxd-page-num" style="text-align:center;margin-top:30px;padding:10px 0;">
<span style="display:inline-block;background:rgba(59,130,246,0.08);color:#3b82f6;font-size:12px;
font-weight:700;padding:4px 16px;border-radius:100px;letter-spacing:1px;">
${slide.num}</span></div>`
processedHtml += pageNumHtml
}
}
return {
...slide,
html: `<div style="width:100%;background:#fff;box-sizing:border-box;min-height:100%;">
<style>${baseStyle}</style>
${processedHtml}
</div>`,
headStyle: headContent
}
})
}
const extractBaseStyle = (html) => {
const styleMatch = html.match(/<style[^>]*>([\s\S]*?)<\/style>/i)
return styleMatch ? styleMatch[1] : ''
}