293 lines
8.3 KiB
JavaScript
293 lines
8.3 KiB
JavaScript
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(/<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] : ''
|
||
}
|