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>
|
</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 class="btn btn-primary btn-start" @click="startPack" :disabled="!store.hasProject">
|
||||||
开始打包
|
开始打包
|
||||||
</button>
|
</button>
|
||||||
<p class="pack-hint">打包过程可能需要几分钟,请耐心等待</p>
|
<p class="pack-hint">打包过程可能需要几分钟,请耐心等待</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PackProgress
|
<div v-if="result && result.success" class="result-success card">
|
||||||
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 class="result-icon">✓</div>
|
<div class="result-icon">✓</div>
|
||||||
<h3>打包完成!</h3>
|
<h3>打包完成!</h3>
|
||||||
<p>安装包已生成</p>
|
<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">
|
<div class="result-actions">
|
||||||
<button class="btn btn-secondary" @click="goBack">返回编辑</button>
|
<button class="btn btn-secondary" @click="goBack">返回编辑</button>
|
||||||
<button class="btn btn-primary" @click="startPack">重新打包</button>
|
<button class="btn btn-primary" @click="startPack">重新打包</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<div class="result-icon error">✗</div>
|
||||||
<h3>打包失败</h3>
|
<h3>打包失败</h3>
|
||||||
<p>{{ packStore.result.error }}</p>
|
<p>{{ result.error }}</p>
|
||||||
<div class="result-actions">
|
<div class="result-actions">
|
||||||
<button class="btn btn-secondary" @click="goBack">返回编辑</button>
|
<button class="btn btn-secondary" @click="goBack">返回编辑</button>
|
||||||
<button class="btn btn-primary" @click="startPack">重试</button>
|
<button class="btn btn-primary" @click="startPack">重试</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -80,18 +93,22 @@
|
|||||||
import { ref, computed, onBeforeUnmount } from 'vue'
|
import { ref, computed, onBeforeUnmount } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useProjectStore } from '../stores/project'
|
import { useProjectStore } from '../stores/project'
|
||||||
import { usePackagingStore } from '../stores/packaging'
|
|
||||||
import PackProgress from '../components/PackProgress.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useProjectStore()
|
const store = useProjectStore()
|
||||||
const packStore = usePackagingStore()
|
|
||||||
const outputPath = ref(null)
|
|
||||||
|
|
||||||
if (!store.hasProject) {
|
if (!store.hasProject) {
|
||||||
router.replace('/')
|
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 fileCount = computed(() => countFiles(store.files))
|
||||||
|
|
||||||
const platformLabel = computed(() => {
|
const platformLabel = computed(() => {
|
||||||
@@ -102,6 +119,74 @@ const platformLabel = computed(() => {
|
|||||||
return p
|
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() {
|
async function selectOutputPath() {
|
||||||
const ext = process.platform === 'win32' ? 'exe' : process.platform === 'darwin' ? 'dmg' : 'AppImage'
|
const ext = process.platform === 'win32' ? 'exe' : process.platform === 'darwin' ? 'dmg' : 'AppImage'
|
||||||
const path = await window.api.dialog.saveFile({
|
const path = await window.api.dialog.saveFile({
|
||||||
@@ -111,12 +196,10 @@ async function selectOutputPath() {
|
|||||||
if (path) outputPath.value = path
|
if (path) outputPath.value = path
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPack() {
|
|
||||||
packStore.startPacking(store.projectPath, outputPath.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
packStore.reset()
|
showOverlay.value = false
|
||||||
|
isPacking.value = false
|
||||||
|
result.value = null
|
||||||
router.push('/editor')
|
router.push('/editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,8 +213,8 @@ function countFiles(files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (!packStore.result) {
|
if (isPacking.value) {
|
||||||
packStore.reset()
|
window.api.pack.cancel()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -252,4 +335,96 @@ onBeforeUnmount(() => {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
max-width: 100%;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user