Compare commits
2 Commits
afdfce6502
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e143eebee7 | ||
|
|
5e890108a1 |
@@ -1,211 +0,0 @@
|
||||
<template>
|
||||
<div class="pack-progress card">
|
||||
<div class="progress-header">
|
||||
<div class="spinner" :class="{ active: !result }">
|
||||
<div class="spinner-ring"></div>
|
||||
</div>
|
||||
<h3>打包进度</h3>
|
||||
</div>
|
||||
|
||||
<div class="progress-bar-container">
|
||||
<div
|
||||
class="progress-bar"
|
||||
:class="{ indeterminate: isIndeterminate }"
|
||||
:style="barStyle"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div v-if="progress.stage" class="progress-info">
|
||||
<span class="progress-label">{{ stageLabel }}</span>
|
||||
<span class="progress-percent">{{ displayPercent }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="progress.message" class="progress-message">
|
||||
<pre>{{ progress.message }}</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="!result" class="progress-actions">
|
||||
<button class="btn btn-danger btn-sm" @click="emit('cancel')">
|
||||
取消打包
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="result && result.error" class="progress-error">
|
||||
<p>错误详情:</p>
|
||||
<pre>{{ result.error }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
progress: {
|
||||
type: Object,
|
||||
default: () => ({ stage: '', message: '', percent: 0 })
|
||||
},
|
||||
result: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['cancel'])
|
||||
|
||||
const stageLabel = computed(() => {
|
||||
const labels = {
|
||||
'starting': '准备开始',
|
||||
'prepare': '准备中',
|
||||
'copy-files': '复制文件',
|
||||
'configure': '配置应用',
|
||||
'install': '安装依赖',
|
||||
'download': '下载中',
|
||||
'build': '打包中',
|
||||
'sign': '签名中',
|
||||
'complete': '打包完成',
|
||||
'error': '出错了'
|
||||
}
|
||||
return labels[props.progress.stage] || props.progress.stage
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
return ['install', 'download'].includes(props.progress.stage)
|
||||
})
|
||||
|
||||
const barStyle = computed(() => {
|
||||
if (isIndeterminate.value) return {}
|
||||
return { width: Math.round(props.progress.percent) + '%' }
|
||||
})
|
||||
|
||||
const displayPercent = computed(() => {
|
||||
if (isIndeterminate.value) return '进行中...'
|
||||
return Math.round(props.progress.percent) + '%'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pack-progress {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.progress-header h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner.active .spinner-ring {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2.5px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
.progress-bar.indeterminate {
|
||||
width: 30%;
|
||||
animation: indeterminate 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes indeterminate {
|
||||
0% { transform: translateX(-100%); }
|
||||
50% { transform: translateX(200%); }
|
||||
100% { transform: translateX(400%); }
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.progress-percent {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.progress-message pre {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.progress-actions {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.progress-error {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.progress-error p {
|
||||
font-size: 13px;
|
||||
color: var(--danger);
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-error pre {
|
||||
font-size: 12px;
|
||||
color: #991b1b;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const usePackagingStore = defineStore('packaging', {
|
||||
state: () => ({
|
||||
isPacking: false,
|
||||
progress: {
|
||||
stage: '',
|
||||
message: '',
|
||||
percent: 0
|
||||
},
|
||||
result: null
|
||||
}),
|
||||
|
||||
actions: {
|
||||
startPacking(projectPath, outputPath) {
|
||||
this.isPacking = true
|
||||
this.progress = { stage: 'starting', message: '准备开始...', percent: 0 }
|
||||
this.result = null
|
||||
|
||||
const cleanup = window.api.pack.onProgress((progress) => {
|
||||
this.progress = progress
|
||||
})
|
||||
|
||||
window.api.pack.start(projectPath, outputPath).then((result) => {
|
||||
this.isPacking = false
|
||||
this.result = result
|
||||
cleanup()
|
||||
}).catch((err) => {
|
||||
this.isPacking = false
|
||||
this.result = { success: false, error: err.message }
|
||||
cleanup()
|
||||
})
|
||||
},
|
||||
|
||||
cancelPacking() {
|
||||
window.api.pack.cancel()
|
||||
this.isPacking = false
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.isPacking = false
|
||||
this.progress = { stage: '', message: '', percent: 0 }
|
||||
this.result = null
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -38,41 +38,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!packStore.isPacking && !packStore.result" class="pack-actions">
|
||||
<div v-if="!isPacking && !result" class="pack-actions">
|
||||
<button class="btn btn-primary btn-start" @click="startPack" :disabled="!store.hasProject">
|
||||
开始打包
|
||||
</button>
|
||||
<p class="pack-hint">打包过程可能需要几分钟,请耐心等待</p>
|
||||
</div>
|
||||
|
||||
<PackProgress
|
||||
v-if="packStore.isPacking || packStore.result"
|
||||
:progress="packStore.progress"
|
||||
:result="packStore.result"
|
||||
@cancel="packStore.cancelPacking()"
|
||||
/>
|
||||
|
||||
<div v-if="packStore.result && packStore.result.success" class="result-success card">
|
||||
<div v-if="result && result.success" class="result-success card">
|
||||
<div class="result-icon">✓</div>
|
||||
<h3>打包完成!</h3>
|
||||
<p>安装包已生成</p>
|
||||
<p v-if="packStore.result.outputPath" class="output-path">{{ packStore.result.outputPath }}</p>
|
||||
<p v-if="result.outputPath" class="output-path">{{ result.outputPath }}</p>
|
||||
<div class="result-actions">
|
||||
<button class="btn btn-secondary" @click="goBack">返回编辑</button>
|
||||
<button class="btn btn-primary" @click="startPack">重新打包</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="packStore.result && !packStore.result.success" class="result-error card">
|
||||
<div v-if="result && !result.success" class="result-error card">
|
||||
<div class="result-icon error">✗</div>
|
||||
<h3>打包失败</h3>
|
||||
<p>{{ packStore.result.error }}</p>
|
||||
<p>{{ result.error }}</p>
|
||||
<div class="result-actions">
|
||||
<button class="btn btn-secondary" @click="goBack">返回编辑</button>
|
||||
<button class="btn btn-primary" @click="startPack">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showOverlay" class="pack-overlay">
|
||||
<div class="overlay-content">
|
||||
<div class="overlay-spinner">
|
||||
<div class="spinner-ring"></div>
|
||||
</div>
|
||||
<div class="overlay-stage">{{ stageLabel }}</div>
|
||||
<div class="overlay-bar">
|
||||
<div
|
||||
class="overlay-bar-fill"
|
||||
:class="{ pulse: isIndeterminate }"
|
||||
:style="barStyle"
|
||||
></div>
|
||||
</div>
|
||||
<div v-if="progressMsg" class="overlay-msg">{{ progressMsg }}</div>
|
||||
<button v-if="isPacking" class="btn btn-danger btn-sm overlay-cancel" @click="cancelPack">
|
||||
取消打包
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -80,18 +93,22 @@
|
||||
import { ref, computed, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useProjectStore } from '../stores/project'
|
||||
import { usePackagingStore } from '../stores/packaging'
|
||||
import PackProgress from '../components/PackProgress.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useProjectStore()
|
||||
const packStore = usePackagingStore()
|
||||
const outputPath = ref(null)
|
||||
|
||||
if (!store.hasProject) {
|
||||
router.replace('/')
|
||||
}
|
||||
|
||||
const outputPath = ref(null)
|
||||
const isPacking = ref(false)
|
||||
const result = ref(null)
|
||||
const progressStage = ref('')
|
||||
const progressMsg = ref('')
|
||||
const progressPct = ref(0)
|
||||
const showOverlay = ref(false)
|
||||
|
||||
const fileCount = computed(() => countFiles(store.files))
|
||||
|
||||
const platformLabel = computed(() => {
|
||||
@@ -102,6 +119,74 @@ const platformLabel = computed(() => {
|
||||
return p
|
||||
})
|
||||
|
||||
const stageLabel = computed(() => {
|
||||
const labels = {
|
||||
'starting': '准备开始…',
|
||||
'prepare': '准备中…',
|
||||
'copy-files': '复制文件…',
|
||||
'configure': '配置应用…',
|
||||
'install': '安装依赖…',
|
||||
'download': '下载中…',
|
||||
'build': '打包中…',
|
||||
'sign': '签名中…',
|
||||
'complete': '打包完成',
|
||||
'error': '出错了'
|
||||
}
|
||||
return labels[progressStage.value] || progressStage.value || '准备开始…'
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
return ['starting', 'install', 'download'].includes(progressStage.value)
|
||||
})
|
||||
|
||||
const barStyle = computed(() => {
|
||||
if (isIndeterminate.value) return {}
|
||||
return { width: Math.round(progressPct.value) + '%' }
|
||||
})
|
||||
|
||||
async function startPack() {
|
||||
isPacking.value = true
|
||||
result.value = null
|
||||
progressStage.value = 'starting'
|
||||
progressMsg.value = '正在准备打包环境…'
|
||||
progressPct.value = 0
|
||||
showOverlay.value = true
|
||||
|
||||
const cleanup = window.api.pack.onProgress((p) => {
|
||||
progressStage.value = p.stage
|
||||
progressMsg.value = p.message
|
||||
if (p.percent != null) progressPct.value = p.percent
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await window.api.pack.start(store.projectPath, outputPath.value)
|
||||
isPacking.value = false
|
||||
result.value = res
|
||||
if (res.error) {
|
||||
progressStage.value = 'error'
|
||||
progressMsg.value = res.error
|
||||
} else {
|
||||
progressStage.value = 'complete'
|
||||
progressMsg.value = '打包完成!'
|
||||
progressPct.value = 100
|
||||
}
|
||||
} catch (err) {
|
||||
isPacking.value = false
|
||||
result.value = { success: false, error: err.message }
|
||||
progressStage.value = 'error'
|
||||
progressMsg.value = err.message
|
||||
}
|
||||
cleanup?.()
|
||||
}
|
||||
|
||||
function cancelPack() {
|
||||
window.api.pack.cancel()
|
||||
isPacking.value = false
|
||||
showOverlay.value = false
|
||||
progressStage.value = ''
|
||||
progressMsg.value = ''
|
||||
}
|
||||
|
||||
async function selectOutputPath() {
|
||||
const ext = process.platform === 'win32' ? 'exe' : process.platform === 'darwin' ? 'dmg' : 'AppImage'
|
||||
const path = await window.api.dialog.saveFile({
|
||||
@@ -111,12 +196,10 @@ async function selectOutputPath() {
|
||||
if (path) outputPath.value = path
|
||||
}
|
||||
|
||||
function startPack() {
|
||||
packStore.startPacking(store.projectPath, outputPath.value)
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
packStore.reset()
|
||||
showOverlay.value = false
|
||||
isPacking.value = false
|
||||
result.value = null
|
||||
router.push('/editor')
|
||||
}
|
||||
|
||||
@@ -130,8 +213,8 @@ function countFiles(files) {
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!packStore.result) {
|
||||
packStore.reset()
|
||||
if (isPacking.value) {
|
||||
window.api.pack.cancel()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -252,4 +335,96 @@ onBeforeUnmount(() => {
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pack-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 32px 40px;
|
||||
min-width: 380px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.overlay-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner-ring {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.overlay-stage {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.overlay-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overlay-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary), #818cf8);
|
||||
border-radius: 3px;
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
.overlay-bar-fill.pulse {
|
||||
width: 28%;
|
||||
animation: slide 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% { transform: translateX(-100%); }
|
||||
50% { transform: translateX(200%); }
|
||||
100% { transform: translateX(400%); }
|
||||
}
|
||||
|
||||
.overlay-msg {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
word-break: break-all;
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.overlay-cancel {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user