- Electron 主进程:窗口管理、IPC、项目读写、打包引擎 - Vue 3 前端:4 个视图页面、3 个组件、Pinia 状态管理 - 生成应用模板:Express 后端 + Electron 窗口 - 支持资源导入、应用配置、跨平台打包
256 lines
5.8 KiB
Vue
256 lines
5.8 KiB
Vue
<template>
|
||
<div class="pack-page">
|
||
<header class="pack-header">
|
||
<div class="header-left">
|
||
<button class="btn btn-secondary btn-sm" @click="goBack">← 返回编辑</button>
|
||
<h2>打包应用</h2>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="pack-body">
|
||
<div class="pack-config card">
|
||
<h3>打包配置</h3>
|
||
|
||
<div class="info-row">
|
||
<span class="label">应用名称</span>
|
||
<span>{{ store.config.name }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">版本号</span>
|
||
<span>{{ store.config.version }}</span>
|
||
</div>
|
||
<div class="info-row">
|
||
<span class="label">资源文件数</span>
|
||
<span>{{ fileCount }} 个文件</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="label">输出路径</label>
|
||
<div class="output-selector">
|
||
<input type="text" :value="outputPath || '默认位置'" readonly />
|
||
<button class="btn btn-sm btn-secondary" @click="selectOutputPath">选择</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-row">
|
||
<span class="label">目标平台</span>
|
||
<span>{{ platformLabel }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!packStore.isPacking && !packStore.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 class="result-icon">✓</div>
|
||
<h3>打包完成!</h3>
|
||
<p>安装包已生成</p>
|
||
<p v-if="packStore.result.outputPath" class="output-path">{{ packStore.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 class="result-icon error">✗</div>
|
||
<h3>打包失败</h3>
|
||
<p>{{ packStore.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>
|
||
</template>
|
||
|
||
<script setup>
|
||
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 fileCount = computed(() => countFiles(store.files))
|
||
|
||
const platformLabel = computed(() => {
|
||
const p = process.platform
|
||
if (p === 'win32') return 'Windows (.exe)'
|
||
if (p === 'darwin') return 'macOS (.dmg)'
|
||
if (p === 'linux') return 'Linux (.AppImage)'
|
||
return p
|
||
})
|
||
|
||
async function selectOutputPath() {
|
||
const ext = process.platform === 'win32' ? 'exe' : process.platform === 'darwin' ? 'dmg' : 'AppImage'
|
||
const path = await window.api.dialog.saveFile({
|
||
defaultPath: `${store.config.name}-Setup.${ext}`,
|
||
filters: [{ name: '安装包', extensions: [ext] }]
|
||
})
|
||
if (path) outputPath.value = path
|
||
}
|
||
|
||
function startPack() {
|
||
packStore.startPacking(store.projectPath, outputPath.value)
|
||
}
|
||
|
||
function goBack() {
|
||
packStore.reset()
|
||
router.push('/editor')
|
||
}
|
||
|
||
function countFiles(files) {
|
||
let count = 0
|
||
for (const f of files) {
|
||
if (f.type === 'file') count++
|
||
if (f.children) count += countFiles(f.children)
|
||
}
|
||
return count
|
||
}
|
||
|
||
onBeforeUnmount(() => {
|
||
if (!packStore.result) {
|
||
packStore.reset()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.pack-page {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.pack-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 20px;
|
||
background: var(--surface);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-left h2 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.pack-body {
|
||
flex: 1;
|
||
padding: 24px;
|
||
max-width: 600px;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.pack-config {
|
||
padding: 20px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.pack-config h3 {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.output-selector {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.output-selector input {
|
||
flex: 1;
|
||
}
|
||
|
||
.pack-actions {
|
||
text-align: center;
|
||
}
|
||
|
||
.btn-start {
|
||
padding: 12px 40px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.pack-hint {
|
||
margin-top: 8px;
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.result-success, .result-error {
|
||
padding: 30px;
|
||
text-align: center;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.result-icon {
|
||
font-size: 48px;
|
||
color: var(--success);
|
||
}
|
||
|
||
.result-icon.error {
|
||
color: var(--danger);
|
||
}
|
||
|
||
.result-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.output-path {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
word-break: break-all;
|
||
max-width: 100%;
|
||
}
|
||
</style>
|