feat: 完成 Web2App 核心功能实现
- Electron 主进程:窗口管理、IPC、项目读写、打包引擎 - Vue 3 前端:4 个视图页面、3 个组件、Pinia 状态管理 - 生成应用模板:Express 后端 + Electron 窗口 - 支持资源导入、应用配置、跨平台打包
This commit is contained in:
255
src/views/PackPage.vue
Normal file
255
src/views/PackPage.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user