导出优化
This commit is contained in:
1 parent
72d8daa283
commit
79ce3165c1
17 files changed
+1559
-1291
No files matched your search
@@ -1002,6 +1002,7 @@ import ColorGradientEditor from './ColorGradientEditor.vue'
|
||||
import ImageResourceCard from './ImageResourceCard.vue'
|
||||
import SpineObjectPanel from '../spine/SpineObjectPanel.vue'
|
||||
import { downloadEditorConfig, loadEditorConfigFile, loadEditorConfigText } from '../editor/editorConfig'
|
||||
import { beginTransfer, readResponseTextWithProgress } from '../editor/transferProgress'
|
||||
import { defaultSpineExportSettings, ensureSpineExportSettings } from '../export/spineExportSettings'
|
||||
import { buildSpineJson, downloadSpineJson } from '../export/spineJsonExporter'
|
||||
import { validateSpineJson } from '../export/spineJsonValidator'
|
||||
@@ -1112,15 +1113,24 @@ async function loadSelectedPreset() {
|
||||
presetLoading.value = true
|
||||
configError.value = false
|
||||
configMessage.value = '正在加载预设…'
|
||||
const transfer = beginTransfer('加载预设', `正在下载 ${file}…`)
|
||||
try {
|
||||
await transfer.paint()
|
||||
const response = await fetch(`/Preset/${encodeURIComponent(file)}`, { cache: 'no-store' })
|
||||
if (!response.ok) throw new Error(`无法读取预设:${file}`)
|
||||
await loadEditorConfigText(store, await response.text())
|
||||
const text = await readResponseTextWithProgress(response, (progress) => {
|
||||
transfer.update(progress == null ? null : progress * 55, `正在下载 ${file}…`)
|
||||
})
|
||||
transfer.update(58, '正在解析预设配置…')
|
||||
transfer.update(70, '正在解析并恢复预设资源…')
|
||||
await loadEditorConfigText(store, text)
|
||||
configMessage.value = `已加载预设:${file.replace(/\.json$/i, '')}`
|
||||
await transfer.finish('预设加载完成')
|
||||
} catch (error) {
|
||||
configError.value = true
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
configMessage.value = `${message},请检查预设文件命名`
|
||||
await transfer.fail(message)
|
||||
} finally {
|
||||
presetLoading.value = false
|
||||
selectedPreset.value = ''
|
||||
@@ -1146,7 +1156,7 @@ function nextPaint() {
|
||||
return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||
}
|
||||
|
||||
async function prepareExportFrames() {
|
||||
async function prepareExportFrames(onProgress?: (progress: number) => void) {
|
||||
store.timeline.playing = false
|
||||
store.timeline.frame = 0
|
||||
store.timeline.recorded = false
|
||||
@@ -1158,8 +1168,10 @@ async function prepareExportFrames() {
|
||||
const deadline = performance.now() + Math.max(8000, expectedMs * 2.5 + 3000)
|
||||
while (!store.timeline.recorded) {
|
||||
if (performance.now() > deadline) throw new Error('重新计算动画超时,请确认页面仍在运行后重试')
|
||||
onProgress?.(Math.min(0.98, store.timeline.frame / Math.max(1, store.timeline.totalFrames - 1)))
|
||||
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||
}
|
||||
onProgress?.(1)
|
||||
}
|
||||
|
||||
async function exportSpineJson() {
|
||||
@@ -1192,8 +1204,11 @@ async function exportSpineProject() {
|
||||
exportError.value = false
|
||||
connectorPairUrl.value = ''
|
||||
exportMessage.value = '正在重新计算粒子动画…'
|
||||
const transfer = beginTransfer('导出 Spine 包', '正在重新计算粒子动画…')
|
||||
try {
|
||||
await prepareExportFrames()
|
||||
await transfer.paint()
|
||||
await prepareExportFrames((progress) => transfer.update(progress * 18, '正在重新计算粒子动画…'))
|
||||
transfer.update(20, '正在生成 Spine JSON…')
|
||||
const result = buildSpineJson({ systems: store.systems as unknown as ParticleSystem[], timeline: store.timeline, settings: exportSettings.value })
|
||||
validateSpineJson(result.text, exportSettings.value.spineVersion)
|
||||
exportMessage.value = '正在整理图片并调用本机 Spine…'
|
||||
@@ -1203,15 +1218,23 @@ async function exportSpineProject() {
|
||||
spineVersion: exportSettings.value.spineVersion,
|
||||
skeletonJson: result.text,
|
||||
systems: store.systems as unknown as ParticleSystem[],
|
||||
}, ({ phase, progress }) => {
|
||||
if (phase === 'images') transfer.update(progress == null ? null : 22 + progress * 18, '正在整理工程图片…')
|
||||
else if (phase === 'upload') transfer.update(progress == null ? null : 40 + progress * 30, '正在上传数据到 Spine 连接器…')
|
||||
else if (phase === 'processing') transfer.update(null, 'Spine 正在转换工程,请稍候…')
|
||||
else transfer.update(progress == null ? null : 82 + progress * 16, '正在下载生成的 Spine 工程包…')
|
||||
})
|
||||
transfer.update(99, '正在交给浏览器下载…')
|
||||
downloadSpineProject(converted.blob, converted.fileName)
|
||||
exportMessage.value = `已生成 ${converted.fileName}:包含 .spine 工程和 ${converted.imageCount} 张图片,Spine ${converted.spineEditorVersion}`
|
||||
await refreshConnectorHealth()
|
||||
await transfer.finish('Spine 工程包已开始下载')
|
||||
} catch (error) {
|
||||
exportError.value = true
|
||||
if (error instanceof SpineConnectorError && error.pairUrl) connectorPairUrl.value = error.pairUrl
|
||||
exportMessage.value = error instanceof Error ? error.message : String(error)
|
||||
await refreshConnectorHealth()
|
||||
await transfer.fail(exportMessage.value)
|
||||
} finally {
|
||||
exportBusy.value = false
|
||||
exportTarget.value = null
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<Transition name="transfer-fade">
|
||||
<div v-if="transferProgress.visible" class="transfer-overlay" role="status" aria-live="polite">
|
||||
<div class="transfer-dialog" :class="{ failed: transferProgress.failed }">
|
||||
<div class="transfer-heading">
|
||||
<span>{{ transferProgress.title }}</span>
|
||||
<strong v-if="!transferProgress.indeterminate">{{ Math.round(transferProgress.progress) }}%</strong>
|
||||
</div>
|
||||
<div class="transfer-track" :class="{ indeterminate: transferProgress.indeterminate }">
|
||||
<div class="transfer-bar" :style="transferProgress.indeterminate ? undefined : { width: `${transferProgress.progress}%` }"></div>
|
||||
</div>
|
||||
<div class="transfer-detail">{{ transferProgress.detail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { transferProgress } from '../editor/transferProgress'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transfer-overlay { position: fixed; z-index: 2000; inset: 0; display: grid; place-items: center; pointer-events: none; background: rgba(5, 8, 15, 0.26); backdrop-filter: blur(1px); }
|
||||
.transfer-dialog { width: min(420px, calc(100vw - 40px)); padding: 18px 20px 16px; border: 1px solid #414b66; border-radius: 10px; box-shadow: 0 18px 55px rgba(0, 0, 0, 0.46); background: #171d2b; color: #e7ebf5; }
|
||||
.transfer-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 12px; font-size: 14px; font-weight: 700; }
|
||||
.transfer-heading strong { color: #a8a6ff; font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.transfer-track { height: 9px; overflow: hidden; border: 1px solid #35415a; border-radius: 999px; background: #0d1422; }
|
||||
.transfer-bar { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #4d75e8, #7774ff); transition: width .16s ease; }
|
||||
.transfer-track.indeterminate .transfer-bar { width: 42%; animation: transfer-slide 1s ease-in-out infinite; }
|
||||
.transfer-detail { min-height: 17px; margin-top: 9px; color: #8895ae; font-size: 11px; line-height: 1.5; }
|
||||
.transfer-dialog.failed { border-color: #754351; }
|
||||
.transfer-dialog.failed .transfer-heading strong, .transfer-dialog.failed .transfer-detail { color: #e99aa9; }
|
||||
.transfer-dialog.failed .transfer-bar { background: #c85d70; }
|
||||
.transfer-fade-enter-active, .transfer-fade-leave-active { transition: opacity .16s ease; }
|
||||
.transfer-fade-enter-from, .transfer-fade-leave-to { opacity: 0; }
|
||||
@keyframes transfer-slide { from { transform: translateX(-110%); } to { transform: translateX(250%); } }
|
||||
</style>
|
||||
|
||||
Reference in new issue
Block a user