This commit is contained in:
asc24
2026-07-24 16:13:19 +08:00
parent dc4f6b2132
commit c3fce0ee70
180 changed files with 50264 additions and 1407 deletions

292
utils/wyxd-parser.js Normal file
View 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] : ''
}