feat: 完成 Web2App 核心功能实现

- Electron 主进程:窗口管理、IPC、项目读写、打包引擎
- Vue 3 前端:4 个视图页面、3 个组件、Pinia 状态管理
- 生成应用模板:Express 后端 + Electron 窗口
- 支持资源导入、应用配置、跨平台打包
This commit is contained in:
gmh01
2026-07-23 18:25:35 +08:00
parent b67e27beb1
commit 2f3ec636e0
26 changed files with 8455 additions and 0 deletions

255
src/views/PackPage.vue Normal file
View 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">&larr; 返回编辑</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">&#10003;</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">&#10007;</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>