wyxd
This commit is contained in:
292
utils/wyxd-parser.js
Normal file
292
utils/wyxd-parser.js
Normal file
@@ -0,0 +1,292 @@
|
||||
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] : ''
|
||||
}
|
||||
Reference in New Issue
Block a user