导出优化

This commit is contained in:
tianmo committed 2026-09-05 18:45:43 +08:00
1 parent 72d8daa283
commit 79ce3165c1
17 files changed
+1559 -1291

No files matched your search

+61 -37
View File
@@ -18,6 +18,11 @@ export interface ConnectorImage {
dataBase64: string
}
export interface SpineConnectorProgress {
phase: 'images' | 'upload' | 'processing' | 'download'
progress: number | null
}
interface ConnectorErrorBody {
code?: string
message?: string
@@ -74,21 +79,26 @@ async function sourceToImage(source: string, folder: string, name: string): Prom
}
/** 收集 Spine JSON 中附件会引用的粒子与拖尾图片。 */
export async function collectSpineExportImages(systems: ParticleSystem[]) {
export async function collectSpineExportImages(systems: ParticleSystem[], onProgress?: (completed: number, total: number) => void) {
const requests: Array<Promise<ConnectorImage | null>> = []
let completed = 0
const track = (request: Promise<ConnectorImage | null>) => request.finally(() => {
completed++
onProgress?.(completed, requests.length)
})
for (const system of systems.filter((item) => item.visible !== false)) {
for (const resource of system.config.imageResources) {
if (resource.imageMode === 'sequence' && resource.sequenceFrames.length) {
for (const frame of resource.sequenceFrames) {
requests.push(sourceToImage(frame.previewUrl, resource.textureFolder, frame.name || resource.textureName))
requests.push(track(sourceToImage(frame.previewUrl, resource.textureFolder, frame.name || resource.textureName)))
}
} else {
requests.push(sourceToImage(resource.previewUrl, resource.textureFolder, resource.textureName))
requests.push(track(sourceToImage(resource.previewUrl, resource.textureFolder, resource.textureName)))
}
}
if (system.config.trail) {
for (const resource of system.config.trailResources) {
requests.push(sourceToImage(resource.previewUrl, '', resource.textureName))
requests.push(track(sourceToImage(resource.previewUrl, '', resource.textureName)))
}
}
}
@@ -113,39 +123,53 @@ export async function convertWithSpineConnector(input: {
spineVersion: SpineExportVersion
skeletonJson: string
systems: ParticleSystem[]
}) {
const images = await collectSpineExportImages(input.systems)
let response: Response
try {
response = await fetch(`${CONNECTOR_URL}/v1/convert`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
schemaVersion: 1,
projectName: input.projectName,
spineVersion: input.spineVersion,
skeletonJson: input.skeletonJson,
images,
}),
})
} catch {
throw new SpineConnectorError('未检测到本地 Spine 连接器,请先启动连接器', 'CONNECTOR_OFFLINE')
}
if (!response.ok) {
const body = await response.json().catch(() => ({} as ConnectorErrorBody)) as ConnectorErrorBody
throw new SpineConnectorError(body.message || `Spine 连接器返回错误:HTTP ${response.status}`, body.code, body.pairUrl)
}
const encodedFileName = response.headers.get('X-Spine-File-Name')
let fileName = `${cleanPart(input.projectName, 'SpineParticle')}.zip`
if (encodedFileName) {
try { fileName = decodeURIComponent(encodedFileName) } catch { /* 保留安全的默认文件名 */ }
}
return {
blob: await response.blob(),
fileName,
spineEditorVersion: response.headers.get('X-Spine-Editor-Version') || input.spineVersion,
imageCount: images.length,
}
}, onProgress?: (progress: SpineConnectorProgress) => void) {
const images = await collectSpineExportImages(input.systems, (completed, total) => {
onProgress?.({ phase: 'images', progress: total ? completed / total : 1 })
})
const body = JSON.stringify({
schemaVersion: 1,
projectName: input.projectName,
spineVersion: input.spineVersion,
skeletonJson: input.skeletonJson,
images,
})
return new Promise<{ blob: Blob; fileName: string; spineEditorVersion: string; imageCount: number }>((resolve, reject) => {
const request = new XMLHttpRequest()
request.open('POST', `${CONNECTOR_URL}/v1/convert`)
request.responseType = 'blob'
request.setRequestHeader('Content-Type', 'application/json')
request.upload.onprogress = (event) => {
onProgress?.({ phase: 'upload', progress: event.lengthComputable && event.total > 0 ? event.loaded / event.total : null })
}
request.upload.onloadend = () => onProgress?.({ phase: 'processing', progress: null })
request.onprogress = (event) => {
onProgress?.({ phase: 'download', progress: event.lengthComputable && event.total > 0 ? event.loaded / event.total : null })
}
request.onerror = () => reject(new SpineConnectorError('未检测到本地 Spine 连接器,请先启动连接器', 'CONNECTOR_OFFLINE'))
request.onload = async () => {
const responseBlob = request.response as Blob
if (request.status < 200 || request.status >= 300) {
let errorBody: ConnectorErrorBody = {}
try { errorBody = JSON.parse(await responseBlob.text()) as ConnectorErrorBody } catch { /* 使用 HTTP 回退信息 */ }
reject(new SpineConnectorError(errorBody.message || `Spine 连接器返回错误:HTTP ${request.status}`, errorBody.code, errorBody.pairUrl))
return
}
const encodedFileName = request.getResponseHeader('X-Spine-File-Name')
let fileName = `${cleanPart(input.projectName, 'SpineParticle')}.zip`
if (encodedFileName) {
try { fileName = decodeURIComponent(encodedFileName) } catch { /* 保留安全的默认文件名 */ }
}
resolve({
blob: responseBlob,
fileName,
spineEditorVersion: request.getResponseHeader('X-Spine-Editor-Version') || input.spineVersion,
imageCount: images.length,
})
}
request.send(body)
})
}
export function downloadSpineProject(blob: Blob, fileName: string) {