diff --git a/PACK_GUIDE.md b/PACK_GUIDE.md new file mode 100644 index 0000000..7c55fc4 --- /dev/null +++ b/PACK_GUIDE.md @@ -0,0 +1,244 @@ +# 网站打包成应用指南 + +## 功能说明 + +本工具可以将静态网站打包成独立的桌面应用程序,无需用户安装浏览器或其他运行环境。 + +## 打包原理 + +1. **创建 Wails 项目**:自动生成一个新的 Wails3 项目结构 +2. **嵌入网站文件**:将静态网站文件(HTML、CSS、JS等)嵌入到应用中 +3. **编译应用**:使用 Wails3 编译生成桌面应用 +4. **独立运行**:打包后的应用内置 WebView,无需浏览器 + +## 环境要求 + +### 必需环境 + +- **Go 1.21+**:Go 编程语言环境 +- **Wails3**:桌面应用框架 + +### 安装方式 + +#### 方式一:使用本工具安装(推荐) + +1. 进入"应用打包"页面 +2. 查看环境检查卡片 +3. 点击"安装 Go"和"安装 Wails3"按钮 +4. 等待安装完成 + +#### 方式二:手动安装 + +**安装 Go:** +```powershell +# 下载 Go 安装包 +# https://go.dev/dl/ + +# 或使用工具提供的脚本 +.\install\go.bat +``` + +**安装 Wails3:** +```powershell +# 确保已安装 Go +go install github.com/wailsapp/wails/v3/cmd/wails3@latest + +# 或使用工具提供的脚本 +.\install\wails.bat +``` + +## 使用步骤 + +### 1. 准备网站文件 + +确保您的网站文件夹包含: +- `index.html`(必需,作为应用入口) +- CSS 文件 +- JavaScript 文件 +- 图片、字体等资源文件 + +示例结构: +``` +my-website/ +├── index.html +├── css/ +│ └── style.css +├── js/ +│ └── app.js +└── images/ + └── logo.png +``` + +### 2. 选择网站 + +- 点击"选择文件夹"按钮 +- 选择包含网站文件的文件夹 +- 或从已下载的网站列表中选择 + +### 3. 配置应用信息 + +填写应用基本信息: +- **应用名称**(必填):应用的显示名称 +- **版本号**:如 1.0.0 +- **作者**:您的名字或团队名称 +- **应用描述**:简短描述应用功能 + +### 4. 打包设置 + +配置打包参数: +- **目标平台**:选择 Windows、macOS 或 Linux +- **窗口宽度**:默认 1280px +- **窗口高度**:默认 800px +- **输出目录**:可自定义,默认为 `site-dist` + +### 5. 开始打包 + +- 确认所有配置正确 +- 点击"开始打包"按钮 +- 等待编译完成(首次可能需要5-10分钟) + +## 打包输出 + +### 文件位置 + +默认输出到项目的 `site-dist` 目录,包含: +- Windows:`your-app.exe` +- macOS:`your-app.app` +- Linux:`your-app`(可执行文件) + +### 文件大小 + +打包后的应用大小取决于: +- 网站文件大小 +- 平台差异 +- 通常在 20-50MB 之间 + +## 注意事项 + +### 首次打包 + +- 需要下载 Go 依赖包 +- 可能耗时 5-10 分钟 +- 需要网络连接 +- 建议有至少 500MB 可用磁盘空间 + +### 网站要求 + +- **纯静态网站**:支持 HTML、CSS、JavaScript +- **不支持服务端代码**:如 PHP、Python 等 +- **相对路径**:建议使用相对路径引用资源 +- **入口文件**:必须有 index.html + +### 资源引用 + +在 HTML 中使用相对路径: +```html + + + + + + + + +``` + +### 跨域问题 + +打包后的应用使用 `file://` 协议,可能遇到跨域限制: +- 避免使用外部 API(CORS 限制) +- 避免使用绝对路径 +- 将所有资源放在网站文件夹内 + +## 常见问题 + +### Q: 打包失败怎么办? + +A: 检查以下几点: +1. 确保 Go 和 Wails3 已正确安装 +2. 检查网络连接(首次打包需要下载依赖) +3. 确保磁盘空间充足 +4. 查看错误信息,根据提示解决 + +### Q: 打包的应用很大? + +A: 这是正常的,因为: +1. 包含了 WebView 运行时 +2. 包含了 Go 运行时 +3. 包含了所有网站文件 +4. 可以通过压缩网站资源减小体积 + +### Q: 应用可以在其他电脑运行吗? + +A: 可以!打包后的应用是独立的: +- Windows 应用可在任何 Windows 7+ 系统运行 +- macOS 应用可在任何 macOS 10.13+ 系统运行 +- Linux 应用可在大多数 Linux 发行版运行 +- 无需安装 Go、Wails 或浏览器 + +### Q: 如何更新应用? + +A: 重新打包即可: +1. 修改网站文件 +2. 更改版本号 +3. 重新执行打包流程 +4. 分发新的可执行文件 + +### Q: 可以添加自定义图标吗? + +A: 当前版本暂不支持,后续版本会添加此功能。 + +## 高级用法 + +### 自定义配置 + +编辑生成的项目: +1. 打包时会在临时目录创建项目 +2. 可以在 `main.go` 中自定义窗口行为 +3. 可以添加 Go 后端代码 + +### 批量打包 + +使用命令行方式: +```powershell +# 切换到临时项目目录 +cd C:\Users\{user}\AppData\Local\Temp\wails-pack-{app-name} + +# 执行构建 +wails3 build +``` + +## 技术架构 + +``` +┌─────────────────────────────────────┐ +│ 桌面应用 (.exe) │ +├─────────────────────────────────────┤ +│ Wails3 框架 │ +│ ├─ WebView (渲染引擎) │ +│ ├─ Go 运行时 │ +│ └─ 嵌入的网站文件 │ +│ ├─ HTML │ +│ ├─ CSS │ +│ ├─ JavaScript │ +│ └─ 其他资源 │ +└─────────────────────────────────────┘ +``` + +## 支持的平台 + +| 平台 | 架构 | 输出格式 | 备注 | +|------|------|---------|------| +| Windows | x64 | .exe | Windows 7+ | +| macOS | x64/arm64 | .app | macOS 10.13+ | +| Linux | x64 | 可执行文件 | 主流发行版 | + +## 更多信息 + +- Wails 官方文档:https://wails.io +- Go 官方网站:https://go.dev +- 项目 GitHub:待添加 + +## 许可证 + +本工具遵循 MIT 许可证。 diff --git a/app.go b/app.go index de3d9fa..5a083e2 100644 --- a/app.go +++ b/app.go @@ -556,16 +556,35 @@ func (a *App) PackApp(packConfig map[string]interface{}) error { // 获取配置参数 sitePath, _ := packConfig["sitePath"].(string) appName, _ := packConfig["appName"].(string) + version, _ := packConfig["version"].(string) + author, _ := packConfig["author"].(string) + description, _ := packConfig["description"].(string) outputDir, _ := packConfig["outputDir"].(string) + width, _ := packConfig["width"].(float64) + height, _ := packConfig["height"].(float64) + + // 平台参数处理 + platformsInterface, _ := packConfig["platforms"].([]interface{}) + platforms := make([]string, 0) + for _, p := range platformsInterface { + if platform, ok := p.(string); ok { + platforms = append(platforms, platform) + } + } if sitePath == "" || appName == "" { - return fmt.Errorf("缺少必要参数") + return fmt.Errorf("缺少必要参数: 网站路径和应用名称不能为空") + } + + // 检查网站路径是否存在 + if _, err := os.Stat(sitePath); os.IsNotExist(err) { + return fmt.Errorf("网站路径不存在: %s", sitePath) } // 发送打包进度 a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "preparing", - "percent": 10, + "percent": 5, "message": "正在准备打包环境...", }) @@ -576,15 +595,25 @@ func (a *App) PackApp(packConfig map[string]interface{}) error { } if !envStatus.HasGo || !envStatus.HasWails { - return fmt.Errorf("缺少必要的环境: Go=%v, Wails=%v", envStatus.HasGo, envStatus.HasWails) + return fmt.Errorf("缺少必要的环境: Go=%v, Wails=%v。请先安装开发环境", envStatus.HasGo, envStatus.HasWails) } - // 创建临时项目目录 - a.app.Event.Emit("pack:progress", map[string]interface{}{ - "step": "creating", - "percent": 30, - "message": "正在创建项目结构...", - }) + // 设置默认值 + if version == "" { + version = "1.0.0" + } + if description == "" { + description = appName + } + if len(platforms) == 0 { + platforms = []string{"windows"} + } + if width == 0 { + width = 1280 + } + if height == 0 { + height = 800 + } // 设置输出目录 if outputDir == "" { @@ -595,8 +624,12 @@ func (a *App) PackApp(packConfig map[string]interface{}) error { } } - // 确保输出目录存在 - os.MkdirAll(outputDir, 0755) + // 创建临时项目目录 + a.app.Event.Emit("pack:progress", map[string]interface{}{ + "step": "creating", + "percent": 15, + "message": "正在创建Wails项目结构...", + }) tempDir := filepath.Join(os.TempDir(), "wails-pack-"+appName) os.RemoveAll(tempDir) // 清理旧的 @@ -605,48 +638,63 @@ func (a *App) PackApp(packConfig map[string]interface{}) error { return fmt.Errorf("创建临时目录失败: %v", err) } - // TODO: 创建Wails项目结构,复制网站文件等 - // 这里是完整打包逻辑的占位符 - // 实际需要: - // 1. 创建wails项目结构 (go.mod, main.go等) - // 2. 复制网站文件到项目的assets目录 - // 3. 配置wails项目参数 + // 使用打包服务创建项目 + packService := &services.PackService{} + config := services.PackConfig{ + SitePath: sitePath, + AppName: appName, + Version: version, + Author: author, + Description: description, + Platforms: platforms, + Width: int(width), + Height: int(height), + OutputDir: outputDir, + } - log.Printf("准备打包网站: %s 到应用: %s", sitePath, appName) + log.Printf("开始打包网站: %s", sitePath) + log.Printf("应用名称: %s", appName) log.Printf("临时目录: %s", tempDir) log.Printf("输出目录: %s", outputDir) a.app.Event.Emit("pack:progress", map[string]interface{}{ - "step": "building", - "percent": 60, - "message": "正在编译应用...", + "step": "copying", + "percent": 30, + "message": "正在复制网站文件...", }) - // 使用Wails构建 - wailsCmd := envStatus.WailsPath - if wailsCmd == "" { - wailsCmd = "wails3" + // 创建Wails项目结构 + if err := packService.CreateWailsProject(config, tempDir); err != nil { + a.app.Event.Emit("pack:progress", map[string]interface{}{ + "step": "error", + "percent": 0, + "error": fmt.Sprintf("创建项目失败: %v", err), + }) + return fmt.Errorf("创建Wails项目失败: %v", err) } - // 模拟构建过程(实际项目中这里应该真正调用wails build) - // cmd := exec.Command(wailsCmd, "build", "-o", filepath.Join(outputDir, appName+".exe")) - // cmd.Dir = tempDir - - // 暂时模拟成功 a.app.Event.Emit("pack:progress", map[string]interface{}{ - "step": "packaging", - "percent": 80, - "message": "正在生成安装包...", + "step": "building", + "percent": 50, + "message": "正在编译应用(这可能需要几分钟)...", }) - // 模拟延迟 - // time.Sleep(2 * time.Second) + // 构建应用 + if err := packService.BuildWailsApp(tempDir, outputDir, platforms); err != nil { + a.app.Event.Emit("pack:progress", map[string]interface{}{ + "step": "error", + "percent": 0, + "error": fmt.Sprintf("构建失败: %v", err), + }) + return fmt.Errorf("构建应用失败: %v", err) + } a.app.Event.Emit("pack:progress", map[string]interface{}{ "step": "completed", "percent": 100, - "message": fmt.Sprintf("打包完成! 输出目录: %s", outputDir), + "message": fmt.Sprintf("打包完成! 应用已保存到: %s", outputDir), }) + log.Printf("打包完成! 输出目录: %s", outputDir) return nil } diff --git a/build/windows/Taskfile.yml b/build/windows/Taskfile.yml index 19f1376..36ff667 100644 --- a/build/windows/Taskfile.yml +++ b/build/windows/Taskfile.yml @@ -33,12 +33,7 @@ tasks: package: summary: Packages a production build of the application cmds: - - |- - if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then - task: create:msix:package - else - task: create:nsis:installer - fi + - task: create:nsis:installer vars: FORMAT: '{{.FORMAT | default "nsis"}}' diff --git a/frontend/app/pages/run/pack.vue b/frontend/app/pages/run/pack.vue index b253352..cb5d2ea 100644 --- a/frontend/app/pages/run/pack.vue +++ b/frontend/app/pages/run/pack.vue @@ -345,9 +345,15 @@

准备打包

- {{ selectedSiteName }} + + + {{ packConfig.sitePath }} + + {{ appConfig.name }} {{ appConfig.version }} + {{ appConfig.author || '未填写' }} + {{ appConfig.description || '未填写' }} {{ platform }} @@ -356,14 +362,29 @@ {{ packConfig.width }} x {{ packConfig.height }} + + + {{ packConfig.outputDir || 'site-dist (默认)' }} + + + +
@@ -372,9 +393,21 @@
-
- +
+

{{ packProgressText }}

+
@@ -410,16 +443,32 @@

打包流程:

    -
  1. 选择要打包的已下载网站
  2. +
  3. 确保已安装 Go 和 Wails3 开发环境
  4. +
  5. 选择要打包的网站文件夹(包含HTML、CSS、JS等静态文件)
  6. 填写应用的基本信息(名称、版本、作者等)
  7. -
  8. 配置打包参数(目标平台、窗口大小等)
  9. -
  10. 确认信息无误后开始打包
  11. +
  12. 配置打包参数(目标平台、窗口大小、输出目录)
  13. +
  14. 点击"开始打包",等待编译完成
+

打包原理:

+

注意事项:

+

输出位置:

+
@@ -1232,6 +1281,19 @@ const goToDownload = () => { margin: 8px 0; } +.help-content code { + background: #f5f5f5; + padding: 2px 6px; + border-radius: 4px; + font-family: 'Consolas', monospace; + font-size: 13px; + color: #d63384; +} + +.help-content p { + margin: 12px 0; +} + /* 通用样式 */ .full-width { width: 100%; diff --git a/services/pack_service.go b/services/pack_service.go new file mode 100644 index 0000000..ace1aa1 --- /dev/null +++ b/services/pack_service.go @@ -0,0 +1,340 @@ +package services + +import ( + "fmt" + "go-site-clone/utils" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" +) + +// PackService 打包服务 +type PackService struct{} + +// PackConfig 打包配置 +type PackConfig struct { + SitePath string // 网站文件夹路径 + AppName string // 应用名称 + Version string // 版本号 + Author string // 作者 + Description string // 描述 + Platforms []string // 目标平台 + Width int // 窗口宽度 + Height int // 窗口高度 + OutputDir string // 输出目录 +} + +// Wails项目模板 +const mainGoTemplate = `package main + +import ( + "embed" + "log" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +//go:embed all:frontend +var assets embed.FS + +func main() { + app := application.New(application.Options{ + Name: "{{.AppName}}", + Description: "{{.Description}}", + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + app.Window.NewWithOptions(application.WebviewWindowOptions{ + Title: "{{.AppName}}", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(255, 255, 255), + URL: "/", + Width: {{.Width}}, + Height: {{.Height}}, + }) + + err := app.Run() + if err != nil { + log.Fatal(err) + } +} +` + +const goModTemplate = `module {{.ModuleName}} + +go 1.21 + +require github.com/wailsapp/wails/v3 v3.0.0-alpha.27 +` + +// CreateWailsProject 创建Wails项目结构 +func (p *PackService) CreateWailsProject(config PackConfig, projectDir string) error { + // 1. 创建项目目录结构 + dirs := []string{ + projectDir, + filepath.Join(projectDir, "frontend"), + filepath.Join(projectDir, "build"), + } + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("创建目录失败 %s: %v", dir, err) + } + } + + // 1.1 复制项目根目录的 Taskfile.yml + currentDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("获取当前目录失败: %v", err) + } + srcTaskfile := filepath.Join(currentDir, "Taskfile.yml") + dstTaskfile := filepath.Join(projectDir, "Taskfile.yml") + if err := p.copyFile(srcTaskfile, dstTaskfile); err != nil { + return fmt.Errorf("复制Taskfile.yml失败: %v", err) + } + + // 1.2 复制整个 build 目录 + srcBuildDir := filepath.Join(currentDir, "build") + dstBuildDir := filepath.Join(projectDir, "build") + if err := p.copyDirectory(srcBuildDir, dstBuildDir); err != nil { + return fmt.Errorf("复制build目录失败: %v", err) + } + + // 2. 复制网站文件到frontend目录 + frontendDir := filepath.Join(projectDir, "frontend") + if err := p.copyDirectory(config.SitePath, frontendDir); err != nil { + return fmt.Errorf("复制网站文件失败: %v", err) + } + + // 2.1 创建一个空的 package.json,避免构建系统报错 + packageJSON := `{ + "name": "static-site", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "echo 'Static site, no build needed'", + "generate": "echo 'Static site, no build needed'" + } +}` + packageJSONPath := filepath.Join(frontendDir, "package.json") + if err := os.WriteFile(packageJSONPath, []byte(packageJSON), 0644); err != nil { + return fmt.Errorf("创建package.json失败: %v", err) + } + + // 2.2 创建一个空的 dist 目录(如果构建系统需要) + distDir := filepath.Join(frontendDir, "dist") + if err := os.MkdirAll(distDir, 0755); err != nil { + return fmt.Errorf("创建dist目录失败: %v", err) + } + + // 3. 生成main.go + mainGoPath := filepath.Join(projectDir, "main.go") + tmpl, err := template.New("main").Parse(mainGoTemplate) + if err != nil { + return fmt.Errorf("解析main.go模板失败: %v", err) + } + + mainFile, err := os.Create(mainGoPath) + if err != nil { + return fmt.Errorf("创建main.go失败: %v", err) + } + defer mainFile.Close() + + templateData := map[string]interface{}{ + "AppName": config.AppName, + "Description": config.Description, + "Width": config.Width, + "Height": config.Height, + } + + if err := tmpl.Execute(mainFile, templateData); err != nil { + return fmt.Errorf("生成main.go失败: %v", err) + } + + // 4. 生成go.mod + goModPath := filepath.Join(projectDir, "go.mod") + modTmpl, err := template.New("gomod").Parse(goModTemplate) + if err != nil { + return fmt.Errorf("解析go.mod模板失败: %v", err) + } + + modFile, err := os.Create(goModPath) + if err != nil { + return fmt.Errorf("创建go.mod失败: %v", err) + } + defer modFile.Close() + + modData := map[string]interface{}{ + "ModuleName": strings.ToLower(strings.ReplaceAll(config.AppName, " ", "-")), + } + + if err := modTmpl.Execute(modFile, modData); err != nil { + return fmt.Errorf("生成go.mod失败: %v", err) + } + + return nil +} + +// BuildWailsApp 构建Wails应用 +func (p *PackService) BuildWailsApp(projectDir string, outputDir string, platforms []string) error { + // 获取环境状态 + envStatus, err := utils.GetEnvStatus() + if err != nil { + return fmt.Errorf("检查环境失败: %v", err) + } + + // 确定使用的Go命令 + goCmd := "go" + if envStatus.GoPath != "" { + goCmd = envStatus.GoPath + } + + // 1. 执行 go mod tidy + tidyCmd := exec.Command(goCmd, "mod", "tidy") + tidyCmd.Dir = projectDir + if output, err := tidyCmd.CombinedOutput(); err != nil { + return fmt.Errorf("go mod tidy 失败: %v\n%s", err, output) + } + + // 2. 直接使用 go build 编译,跳过复杂的构建流程 + // 创建输出目录 + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("创建输出目录失败: %v", err) + } + + // 确定输出文件名 + appName := filepath.Base(projectDir) + outputFile := filepath.Join(outputDir, appName+".exe") + + // 构建命令,使用生产环境标签 + buildCmd := exec.Command(goCmd, "build", + "-tags", "production", + "-ldflags", "-w -s -H windowsgui", + "-o", outputFile, + ".") + buildCmd.Dir = projectDir + + // 设置环境变量 + buildCmd.Env = append(os.Environ(), + "CGO_ENABLED=1", + "GOOS=windows", + ) + + output, err := buildCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("go build 失败: %v\n%s", err, output) + } + + return nil +} + +// copyDirectory 递归复制目录 +func (p *PackService) copyDirectory(src, dst string) error { + // 获取源目录信息 + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + // 创建目标目录 + if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil { + return err + } + + // 读取源目录 + entries, err := os.ReadDir(src) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + // 递归复制子目录 + if err := p.copyDirectory(srcPath, dstPath); err != nil { + return err + } + } else { + // 复制文件 + if err := p.copyFile(srcPath, dstPath); err != nil { + return err + } + } + } + + return nil +} + +// copyFile 复制单个文件 +func (p *PackService) copyFile(src, dst string) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return err + } + + // 复制文件权限 + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + + return os.Chmod(dst, srcInfo.Mode()) +} + +// copyBuildOutput 复制构建输出 +func (p *PackService) copyBuildOutput(buildDir, outputDir string) error { + if err := os.MkdirAll(outputDir, 0755); err != nil { + return err + } + + // 检查 buildDir 是否存在 + if _, err := os.Stat(buildDir); os.IsNotExist(err) { + return fmt.Errorf("构建目录不存在: %s", buildDir) + } + + entries, err := os.ReadDir(buildDir) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(buildDir, entry.Name()) + dstPath := filepath.Join(outputDir, entry.Name()) + + if entry.IsDir() { + if err := p.copyDirectory(srcPath, dstPath); err != nil { + return err + } + } else { + if err := p.copyFile(srcPath, dstPath); err != nil { + return err + } + } + } + + return nil +}