From c163756d2c4f5b9b45e5895e051967165ec9619a Mon Sep 17 00:00:00 2001 From: MatrixSeven Date: Sat, 2 Aug 2025 13:36:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SSG=E6=9E=84=E5=BB=BA|GO=E4=BA=8C?= =?UTF-8?q?=E8=BF=9B=E5=88=B6SSG=E4=BA=A7=E7=89=A9=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- Makefile | 263 +++------ build-fullstack.sh | 530 ++++++++++++++++++ chuan-next/.env.development | 1 + chuan-next/URL_ROUTING.md | 54 -- chuan-next/build-ssg-simple.sh | 87 +++ chuan-next/build-ssg.sh | 415 ++++++++++++++ chuan-next/build-static.sh | 30 + chuan-next/eslint.config.mjs | 16 + chuan-next/next.config.js | 60 +- chuan-next/next.config.static.js | 34 ++ chuan-next/package.json | 4 + chuan-next/src/app/HomePage-new.tsx | 21 +- chuan-next/src/app/api/create-room/route.ts | 30 - .../src/app/api/create-text-room/route.ts | 44 -- .../src/app/api/get-text-content/route.ts | 41 -- chuan-next/src/app/api/room-info/route.ts | 34 -- chuan-next/src/app/api/room-status/route.ts | 35 -- chuan-next/src/app/api/update-files/route.ts | 38 -- chuan-next/src/app/page.tsx | 13 +- .../src/components/TextTransfer-new.tsx | 468 ---------------- chuan-next/src/hooks/useWebSocket-new.ts | 119 ---- chuan-next/src/hooks/useWebSocket.ts | 8 +- chuan-next/src/lib/api-utils.ts | 145 +++++ chuan-next/src/lib/client-api.ts | 138 +++++ chuan-next/src/lib/config.ts | 45 +- chuan-next/src/lib/static-config.ts | 33 ++ chuan-next/tsconfig.json | 1 + cmd/main.go | 6 +- internal/web/frontend.go | 238 ++++++++ internal/web/static.go | 52 ++ internal/web/static/.gitkeep | 3 + 32 files changed, 1938 insertions(+), 1072 deletions(-) create mode 100755 build-fullstack.sh delete mode 100644 chuan-next/URL_ROUTING.md create mode 100644 chuan-next/build-ssg-simple.sh create mode 100644 chuan-next/build-ssg.sh create mode 100644 chuan-next/build-static.sh create mode 100644 chuan-next/next.config.static.js delete mode 100644 chuan-next/src/app/api/create-room/route.ts delete mode 100644 chuan-next/src/app/api/create-text-room/route.ts delete mode 100644 chuan-next/src/app/api/get-text-content/route.ts delete mode 100644 chuan-next/src/app/api/room-info/route.ts delete mode 100644 chuan-next/src/app/api/room-status/route.ts delete mode 100644 chuan-next/src/app/api/update-files/route.ts delete mode 100644 chuan-next/src/components/TextTransfer-new.tsx delete mode 100644 chuan-next/src/hooks/useWebSocket-new.ts create mode 100644 chuan-next/src/lib/api-utils.ts create mode 100644 chuan-next/src/lib/client-api.ts create mode 100644 chuan-next/src/lib/static-config.ts create mode 100644 internal/web/frontend.go create mode 100644 internal/web/static.go create mode 100644 internal/web/static/.gitkeep diff --git a/.gitignore b/.gitignore index a7a228a..f771535 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,6 @@ backup/ ./chuan-next/node_modules/ # Next.js相关 .next/ -./chuan/.next \ No newline at end of file +./chuan/.next +./internal/web/frontend/* +./file-transfer-server \ No newline at end of file diff --git a/Makefile b/Makefile index 818e529..8a74c15 100644 --- a/Makefile +++ b/Makefile @@ -1,185 +1,108 @@ -# Makefile for Chuan File Transfer System +# Makefile for File Transfer System (Full Stack) -# 默认目标 -.PHONY: help -help: - @echo "可用的命令:" - @echo " run - 运行应用程序" - @echo " build - 构建应用程序" - @echo " clean - 清理构建文件" - @echo " deps - 安装依赖" - @echo " test - 运行测试" - @echo " docker - 构建Docker镜像" +.PHONY: build clean run dev frontend backend fullstack help -# 应用程序名称 -APP_NAME=chuan -BUILD_DIR=build -MAIN_FILE=cmd/main.go - -# Go相关命令 -GO=go -GOCMD=$(GO) +# 构建参数 +GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean -GOTEST=$(GOCMD) test -GOGET=$(GOCMD) get -GOMOD=$(GOCMD) mod +BINARY_NAME=file-transfer-server +BINARY_UNIX=$(BINARY_NAME)_unix +SCRIPT_DIR=./ -# 构建标志 -LDFLAGS=-ldflags "-X main.Version=1.0.0 -X main.BuildTime=$$(date +'%Y-%m-%d %H:%M:%S')" +# 默认构建 - 完整的前后端 +build: fullstack -# 运行应用程序 -.PHONY: run -run: - @echo "启动文件传输系统..." - @mkdir -p uploads - $(GOCMD) run $(MAIN_FILE) +# 完整的前后端构建(SSG + Go嵌入) +fullstack: + @echo "🚀 开始全栈构建..." + @$(SCRIPT_DIR)build-fullstack.sh -# 构建应用程序 -.PHONY: build -build: - @echo "构建应用程序..." - @mkdir -p $(BUILD_DIR) - $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_FILE) +# 开发模式构建 +dev: + @echo "🔧 开发模式构建..." + @$(SCRIPT_DIR)build-fullstack.sh --dev --verbose -# 构建Linux版本 -.PHONY: build-linux +# 只构建前端(SSG) +frontend: + @echo "🎨 构建前端..." + @$(SCRIPT_DIR)build-fullstack.sh --frontend-only + +# 只构建后端(需要前端已构建) +backend: + @echo "⚙️ 构建后端..." + @$(SCRIPT_DIR)build-fullstack.sh --backend-only + +# 传统 Go 构建(不包含嵌入的前端) +build-go: + @echo "📦 传统 Go 构建..." + $(GOBUILD) -o $(BINARY_NAME) -v ./cmd + +# 清理所有构建文件 +clean: + @echo "🧹 清理构建文件..." + @$(SCRIPT_DIR)build-fullstack.sh --clean + $(GOCLEAN) + rm -f $(BINARY_NAME) + rm -f $(BINARY_UNIX) + +# 运行应用(先构建) +run: build + @echo "🚀 启动应用..." + ./$(BINARY_NAME) + +# 快速运行(使用现有二进制) +run-quick: + @echo "⚡ 快速启动..." + ./$(BINARY_NAME) + +# Linux 交叉编译 build-linux: - @echo "构建Linux版本..." - @mkdir -p $(BUILD_DIR) - GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-linux $(MAIN_FILE) - -# 构建Windows版本 -.PHONY: build-windows -build-windows: - @echo "构建Windows版本..." - @mkdir -p $(BUILD_DIR) - GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-windows.exe $(MAIN_FILE) - -# 构建MacOS版本 -.PHONY: build-macos -build-macos: - @echo "构建MacOS版本..." - @mkdir -p $(BUILD_DIR) - GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME)-macos $(MAIN_FILE) - -# 构建所有平台版本 -.PHONY: build-all -build-all: build-linux build-windows build-macos - @echo "所有平台构建完成" + @echo "🐧 Linux 交叉编译..." + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_UNIX) -v ./cmd # 安装依赖 -.PHONY: deps -deps: - @echo "安装Go模块依赖..." - $(GOMOD) download - $(GOMOD) tidy +install-deps: + @echo "📦 安装 Go 依赖..." + $(GOCMD) mod download + $(GOCMD) mod tidy + @echo "📦 安装前端依赖..." + cd chuan-next && yarn install -# 运行测试 -.PHONY: test -test: - @echo "运行测试..." - $(GOTEST) -v ./... - -# 清理构建文件 -.PHONY: clean -clean: - @echo "清理构建文件..." - $(GOCLEAN) - rm -rf $(BUILD_DIR) - rm -rf uploads/* - -# 格式化代码 -.PHONY: fmt -fmt: - @echo "格式化Go代码..." - $(GOCMD) fmt ./... - -# 代码检查 -.PHONY: vet -vet: - @echo "运行go vet..." +# 检查代码 +check: + @echo "🔍 代码检查..." $(GOCMD) vet ./... + $(GOCMD) fmt ./... + cd chuan-next && yarn lint -# 安全检查 -.PHONY: security -security: - @echo "运行安全检查..." - @which gosec > /dev/null || $(GOGET) github.com/securecodewarrior/gosec/v2/cmd/gosec@latest - gosec ./... +# 测试 +test: + @echo "🧪 运行测试..." + $(GOCMD) test -v ./... -# 性能测试 -.PHONY: bench -bench: - @echo "运行性能测试..." - $(GOTEST) -bench=. -benchmem ./... - -# 代码覆盖率 -.PHONY: coverage -coverage: - @echo "生成代码覆盖率报告..." - $(GOTEST) -coverprofile=coverage.out ./... - $(GOCMD) tool cover -html=coverage.out -o coverage.html - @echo "覆盖率报告已生成: coverage.html" - -# 创建Docker镜像 -.PHONY: docker -docker: - @echo "构建Docker镜像..." - docker build -t $(APP_NAME):latest . - -# 运行Docker容器 -.PHONY: docker-run -docker-run: - @echo "运行Docker容器..." - docker run -p 8080:8080 -v $(PWD)/uploads:/app/uploads $(APP_NAME):latest - -# 开发模式(热重载) -.PHONY: dev -dev: - @echo "启动开发模式(需要安装air)..." - @which air > /dev/null || $(GOGET) github.com/cosmtrek/air@latest - air - -# 安装开发工具 -.PHONY: tools -tools: - @echo "安装开发工具..." - $(GOGET) github.com/cosmtrek/air@latest - $(GOGET) github.com/securecodewarrior/gosec/v2/cmd/gosec@latest - $(GOGET) golang.org/x/tools/cmd/goimports@latest - -# 初始化项目 -.PHONY: init -init: deps tools - @echo "项目初始化完成" - @mkdir -p uploads - @mkdir -p logs - @echo "创建必要的目录" - -# 部署到生产环境 -.PHONY: deploy -deploy: build-linux - @echo "部署到生产环境..." - @echo "请手动将 $(BUILD_DIR)/$(APP_NAME)-linux 上传到服务器" - -# 查看项目状态 -.PHONY: status -status: - @echo "项目状态:" - @echo " Go版本: $$($(GOCMD) version)" - @echo " 项目路径: $$(pwd)" - @echo " 模块信息:" - @$(GOMOD) list -m all | head -10 - -# 生成API文档 -.PHONY: docs -docs: - @echo "生成API文档..." - @which swag > /dev/null || $(GOGET) github.com/swaggo/swag/cmd/swag@latest - swag init -g $(MAIN_FILE) - -# 运行所有检查 -.PHONY: check -check: fmt vet test - @echo "所有检查完成" +# 显示帮助 +help: + @echo "🛠️ 可用的构建命令:" + @echo "" + @echo "主要命令:" + @echo " make build - 完整构建(前端SSG + Go嵌入)" + @echo " make dev - 开发模式构建(包含调试信息)" + @echo " make run - 构建并运行应用" + @echo " make clean - 清理所有构建文件" + @echo "" + @echo "分离构建:" + @echo " make frontend - 只构建前端(Next.js SSG)" + @echo " make backend - 只构建后端(需要前端已构建)" + @echo " make build-go - 传统 Go 构建(不含前端)" + @echo "" + @echo "其他命令:" + @echo " make run-quick - 直接运行现有二进制" + @echo " make build-linux - Linux 交叉编译" + @echo " make install-deps- 安装所有依赖" + @echo " make check - 代码检查和格式化" + @echo " make test - 运行测试" + @echo " make help - 显示此帮助" + @echo "" + @echo "详细构建选项(直接调用脚本):" + @echo " ./build-fullstack.sh --help" diff --git a/build-fullstack.sh b/build-fullstack.sh new file mode 100755 index 0000000..4d0f1ec --- /dev/null +++ b/build-fullstack.sh @@ -0,0 +1,530 @@ +#!/bin/bash + +# ============================================================================= +# 全栈应用构建脚本 +# +# 功能: +# 1. 构建 Next.js SSG 静态文件 +# 2. 将静态文件复制到 Go 嵌入目录 +# 3. 构建 Go 二进制文件,包含嵌入的前端文件 +# 4. 生成单一可部署的二进制文件 +# +# 使用方法: +# ./build-fullstack.sh [options] +# +# 选项: +# --clean 清理所有构建文件 +# --frontend-only 只构建前端 +# --backend-only 只构建后端 +# --dev 开发模式构建 +# --verbose 显示详细输出 +# --help 显示帮助信息 +# ============================================================================= + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' + +# 配置变量 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$SCRIPT_DIR" +FRONTEND_DIR="$PROJECT_ROOT/chuan-next" +FRONTEND_OUT_DIR="$FRONTEND_DIR/out" +GO_WEB_DIR="$PROJECT_ROOT/internal/web" +FRONTEND_EMBED_DIR="$GO_WEB_DIR/frontend" +BINARY_NAME="file-transfer-server" +BINARY_PATH="$PROJECT_ROOT/$BINARY_NAME" + +# 标志变量 +CLEAN=false +FRONTEND_ONLY=false +BACKEND_ONLY=false +DEV_MODE=false +VERBOSE=false + +# 打印函数 +print_header() { + echo -e "${PURPLE}========================================${NC}" + echo -e "${PURPLE}🚀 $1${NC}" + echo -e "${PURPLE}========================================${NC}" +} + +print_step() { + echo -e "${BLUE}📋 $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_info() { + echo -e "${CYAN}ℹ️ $1${NC}" +} + +print_verbose() { + if [ "$VERBOSE" = true ]; then + echo -e "${CYAN}[VERBOSE]${NC} $1" + fi +} + +# 显示帮助 +show_help() { + cat << EOF +全栈应用构建脚本 + +此脚本将构建 Next.js 前端和 Go 后端,并将前端静态文件嵌入到 Go 二进制中。 + +使用方法: + $0 [选项] + +选项: + --clean 清理所有构建文件和缓存 + --frontend-only 只构建前端部分 + --backend-only 只构建后端部分(需要前端已构建) + --dev 开发模式构建(包含调试信息) + --verbose 显示详细构建过程 + --help 显示此帮助信息 + +示例: + $0 # 完整构建 + $0 --clean # 清理后完整构建 + $0 --frontend-only # 只构建前端 + $0 --backend-only # 只构建后端 + $0 --dev --verbose # 开发模式详细构建 + +输出: + 构建成功后会生成 '$BINARY_NAME' 可执行文件,包含完整的前后端功能。 + +EOF +} + +# 解析命令行参数 +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --clean) + CLEAN=true + shift + ;; + --frontend-only) + FRONTEND_ONLY=true + shift + ;; + --backend-only) + BACKEND_ONLY=true + shift + ;; + --dev) + DEV_MODE=true + shift + ;; + --verbose) + VERBOSE=true + shift + ;; + --help) + show_help + exit 0 + ;; + *) + print_error "未知选项: $1" + show_help + exit 1 + ;; + esac + done +} + +# 检查依赖 +check_dependencies() { + print_step "检查构建依赖..." + + local missing_deps=() + + # 检查 Node.js + if ! command -v node &> /dev/null; then + missing_deps+=("Node.js") + fi + + # 检查 yarn + if ! command -v yarn &> /dev/null; then + missing_deps+=("Yarn") + fi + + # 检查 Go + if ! command -v go &> /dev/null; then + missing_deps+=("Go") + fi + + if [ ${#missing_deps[@]} -gt 0 ]; then + print_error "缺少必要的依赖: ${missing_deps[*]}" + print_info "请安装缺少的依赖后重试" + exit 1 + fi + + print_verbose "Node.js 版本: $(node --version)" + print_verbose "Yarn 版本: $(yarn --version)" + print_verbose "Go 版本: $(go version)" + + print_success "依赖检查完成" +} + +# 清理函数 +clean_all() { + if [ "$CLEAN" = true ]; then + print_step "清理构建文件..." + + # 清理前端构建 + if [ -d "$FRONTEND_DIR/.next" ]; then + rm -rf "$FRONTEND_DIR/.next" + print_verbose "已删除 $FRONTEND_DIR/.next" + fi + + if [ -d "$FRONTEND_OUT_DIR" ]; then + rm -rf "$FRONTEND_OUT_DIR" + print_verbose "已删除 $FRONTEND_OUT_DIR" + fi + + # 清理嵌入的前端文件 + if [ -d "$FRONTEND_EMBED_DIR" ]; then + find "$FRONTEND_EMBED_DIR" -name "*.html" -o -name "*.js" -o -name "*.css" -o -name "*.json" -o -name "*.png" -o -name "*.jpg" -o -name "*.svg" -o -name "*.ico" | xargs rm -f 2>/dev/null || true + print_verbose "已清理嵌入的前端文件" + fi + + # 清理 Go 构建 + if [ -f "$BINARY_PATH" ]; then + rm -f "$BINARY_PATH" + print_verbose "已删除 $BINARY_PATH" + fi + + # 清理 Go 模块缓存(可选) + if [ "$VERBOSE" = true ]; then + go clean -modcache + fi + + print_success "清理完成" + fi +} + +# 构建前端 +build_frontend() { + if [ "$BACKEND_ONLY" = true ]; then + print_info "跳过前端构建 (--backend-only)" + return + fi + + print_step "构建 Next.js 前端..." + + # 检查前端目录 + if [ ! -d "$FRONTEND_DIR" ]; then + print_error "前端目录不存在: $FRONTEND_DIR" + exit 1 + fi + + cd "$FRONTEND_DIR" + + # 安装依赖 + print_verbose "安装前端依赖..." + if [ "$VERBOSE" = true ]; then + yarn install + else + yarn install --silent + fi + + # 执行 SSG 构建 + print_verbose "执行 SSG 构建..." + + # 临时移除 API 目录 + if [ -d "src/app/api" ]; then + mv src/app/api /tmp/next-api-backup-$(date +%s) 2>/dev/null || true + fi + + # 构建 + if [ "$VERBOSE" = true ]; then + NEXT_EXPORT=true yarn build + else + NEXT_EXPORT=true yarn build > build.log 2>&1 + if [ $? -ne 0 ]; then + print_error "前端构建失败,查看 $FRONTEND_DIR/build.log" + cat build.log + exit 1 + fi + rm -f build.log + fi + + # 恢复 API 目录 + api_backup=$(ls /tmp/next-api-backup-* 2>/dev/null | head -1) + if [ -n "$api_backup" ]; then + mv "$api_backup" src/app/api 2>/dev/null || true + fi + + cd "$PROJECT_ROOT" + + # 验证构建结果 + if [ ! -d "$FRONTEND_OUT_DIR" ] || [ ! -f "$FRONTEND_OUT_DIR/index.html" ]; then + print_error "前端构建失败:输出文件不存在" + exit 1 + fi + + print_success "前端构建完成" +} + +# 复制前端文件到嵌入目录 +copy_frontend_files() { + if [ "$BACKEND_ONLY" = true ]; then + print_info "跳过前端文件复制 (--backend-only)" + return + fi + + print_step "复制前端文件到嵌入目录..." + + # 确保嵌入目录存在 + mkdir -p "$FRONTEND_EMBED_DIR" + + # 清理现有文件(除了 .gitkeep) + find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" -delete 2>/dev/null || true + + # 复制所有文件 + if [ -d "$FRONTEND_OUT_DIR" ]; then + cp -r "$FRONTEND_OUT_DIR"/* "$FRONTEND_EMBED_DIR/" 2>/dev/null || true + + # 统计复制的文件 + file_count=$(find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" | wc -l) + total_size=$(du -sh "$FRONTEND_EMBED_DIR" 2>/dev/null | cut -f1 || echo "未知") + + print_verbose "复制了 $file_count 个文件,总大小: $total_size" + print_success "前端文件复制完成" + else + print_error "前端输出目录不存在: $FRONTEND_OUT_DIR" + exit 1 + fi +} + +# 构建后端 +build_backend() { + if [ "$FRONTEND_ONLY" = true ]; then + print_info "跳过后端构建 (--frontend-only)" + return + fi + + print_step "构建 Go 后端..." + + cd "$PROJECT_ROOT" + + # 构建参数 + local build_args=() + + if [ "$DEV_MODE" = true ]; then + build_args+=("-gcflags" "all=-N -l") # 禁用优化,启用调试 + print_verbose "开发模式构建(包含调试信息)" + else + build_args+=("-ldflags" "-s -w") # 移除调试信息和符号表 + print_verbose "生产模式构建(移除调试信息)" + fi + + build_args+=("-o" "$BINARY_NAME" "./cmd") + + # 执行构建 + print_verbose "执行 Go 构建: go build ${build_args[*]}" + + if [ "$VERBOSE" = true ]; then + go build "${build_args[@]}" + else + go build "${build_args[@]}" 2>&1 + if [ $? -ne 0 ]; then + print_error "Go 构建失败" + exit 1 + fi + fi + + # 验证构建结果 + if [ ! -f "$BINARY_PATH" ]; then + print_error "Go 构建失败:二进制文件不存在" + exit 1 + fi + + # 显示二进制文件信息 + if command -v file &> /dev/null; then + file_info=$(file "$BINARY_PATH") + print_verbose "二进制文件信息: $file_info" + fi + + binary_size=$(du -sh "$BINARY_PATH" | cut -f1) + print_verbose "二进制文件大小: $binary_size" + + print_success "后端构建完成" +} + +# 验证最终结果 +verify_build() { + print_step "验证构建结果..." + + if [ "$FRONTEND_ONLY" = true ]; then + if [ -d "$FRONTEND_OUT_DIR" ] && [ -f "$FRONTEND_OUT_DIR/index.html" ]; then + print_success "前端构建验证通过" + else + print_error "前端构建验证失败" + exit 1 + fi + return + fi + + if [ "$BACKEND_ONLY" = true ]; then + if [ -f "$BINARY_PATH" ]; then + print_success "后端构建验证通过" + else + print_error "后端构建验证失败" + exit 1 + fi + return + fi + + # 完整构建验证 + local errors=() + + if [ ! -f "$BINARY_PATH" ]; then + errors+=("二进制文件不存在") + fi + + if [ ! -d "$FRONTEND_EMBED_DIR" ]; then + errors+=("前端嵌入目录不存在") + fi + + embedded_files=$(find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" | wc -l) + if [ "$embedded_files" -eq 0 ]; then + errors+=("没有嵌入的前端文件") + fi + + if [ ${#errors[@]} -gt 0 ]; then + print_error "构建验证失败:" + for error in "${errors[@]}"; do + echo " - $error" + done + exit 1 + fi + + print_success "构建验证通过" +} + +# 显示构建摘要 +show_summary() { + print_header "构建完成" + + echo -e "${GREEN}🎉 全栈应用构建成功!${NC}" + echo "" + + if [ "$FRONTEND_ONLY" = true ]; then + print_info "📁 前端文件输出目录: $FRONTEND_OUT_DIR" + if [ -d "$FRONTEND_OUT_DIR" ]; then + file_count=$(find "$FRONTEND_OUT_DIR" -type f | wc -l) + dir_size=$(du -sh "$FRONTEND_OUT_DIR" | cut -f1) + echo " - 文件数量: $file_count" + echo " - 总大小: $dir_size" + fi + return + fi + + if [ "$BACKEND_ONLY" = true ]; then + print_info "📦 后端二进制文件: $BINARY_PATH" + if [ -f "$BINARY_PATH" ]; then + binary_size=$(du -sh "$BINARY_PATH" | cut -f1) + echo " - 文件大小: $binary_size" + fi + return + fi + + # 完整构建摘要 + print_info "📦 单一二进制文件: $BINARY_PATH" + + if [ -f "$BINARY_PATH" ]; then + binary_size=$(du -sh "$BINARY_PATH" | cut -f1) + echo " - 文件大小: $binary_size" + fi + + if [ -d "$FRONTEND_EMBED_DIR" ]; then + embedded_files=$(find "$FRONTEND_EMBED_DIR" -type f ! -name ".gitkeep" | wc -l) + echo " - 嵌入的前端文件: $embedded_files 个" + fi + + echo "" + print_info "🚀 部署说明:" + echo " 1. 只需部署单个二进制文件: $BINARY_NAME" + echo " 2. 运行命令: ./$BINARY_NAME" + echo " 3. 访问地址: http://localhost:8080" + echo "" + print_info "💡 特性:" + echo " ✅ 前端界面完全嵌入" + echo " ✅ 无需额外的静态文件服务器" + echo " ✅ 支持 SPA 路由" + echo " ✅ 自动处理 API 代理" + echo "" + + if [ "$DEV_MODE" = true ]; then + print_warning "⚠️ 这是开发模式构建,包含调试信息,不适合生产部署" + fi +} + +# 错误处理 +error_cleanup() { + print_error "构建过程中发生错误" + + # 尝试恢复 API 目录 + api_backup=$(ls /tmp/next-api-backup-* 2>/dev/null | head -1) + if [ -n "$api_backup" ] && [ -d "$FRONTEND_DIR" ]; then + mv "$api_backup" "$FRONTEND_DIR/src/app/api" 2>/dev/null || true + print_verbose "已恢复 API 目录" + fi + + exit 1 +} + +# 主函数 +main() { + print_header "全栈应用构建脚本" + + # 设置错误处理 + trap error_cleanup ERR INT TERM + + # 解析参数 + parse_args "$@" + + # 显示构建配置 + if [ "$VERBOSE" = true ]; then + print_info "构建配置:" + echo " - 清理模式: $CLEAN" + echo " - 仅前端: $FRONTEND_ONLY" + echo " - 仅后端: $BACKEND_ONLY" + echo " - 开发模式: $DEV_MODE" + echo " - 详细输出: $VERBOSE" + echo "" + fi + + # 执行构建步骤 + check_dependencies + clean_all + build_frontend + copy_frontend_files + build_backend + verify_build + show_summary +} + +# 如果脚本被直接执行 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/chuan-next/.env.development b/chuan-next/.env.development index 1a51d1f..aacbfc1 100644 --- a/chuan-next/.env.development +++ b/chuan-next/.env.development @@ -1,5 +1,6 @@ # 开发环境配置 GO_BACKEND_URL=http://localhost:8080 NEXT_PUBLIC_API_BASE_URL=http://localhost:3000 +NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws NODE_ENV=development diff --git a/chuan-next/URL_ROUTING.md b/chuan-next/URL_ROUTING.md deleted file mode 100644 index 9364beb..0000000 --- a/chuan-next/URL_ROUTING.md +++ /dev/null @@ -1,54 +0,0 @@ -# URL 路由参数说明 - -现在您可以通过URL参数直接导航到特定的功能和模式。 - -## URL 参数格式 - -``` -http://localhost:3000/?type={功能类型}&mode={操作模式} -``` - -## 支持的参数 - -### type(功能类型) -- `file` - 文件传输 -- `text` - 文字传输 -- `desktop` - 桌面共享 - -### mode(操作模式) -- `send` - 发送/共享模式 -- `receive` - 接收/观看模式 - -## 使用示例 - -### 文件传输 -- 发送文件:`/?type=file&mode=send` -- 接收文件:`/?type=file&mode=receive` - -### 文字传输 -- 发送文字:`/?type=text&mode=send` -- 接收文字:`/?type=text&mode=receive` - -### 桌面共享 -- 共享桌面:`/?type=desktop&mode=send` -- 观看桌面:`/?type=desktop&mode=receive` - -## 功能特性 - -1. **自动切换**:访问带参数的URL会自动切换到对应的功能和模式 -2. **URL同步**:用户手动切换功能和模式时,URL会自动更新 -3. **无页面刷新**:所有切换都通过客户端路由实现,无需刷新页面 -4. **向后兼容**:不带参数访问时,默认显示文件传输-发送模式 - -## 实际应用场景 - -1. **分享链接**:可以直接分享特定功能的链接给其他用户 -2. **书签管理**:用户可以为常用功能创建书签 -3. **快速访问**:通过预设链接快速跳转到特定功能 -4. **集成其他系统**:便于其他系统通过URL直接跳转到特定功能 - -## 注意事项 - -- 桌面共享功能中,`send`模式对应"共享桌面",`receive`模式对应"观看桌面" -- 如果提供了无效的参数值,系统会回退到默认设置 -- URL参数不区分大小写 diff --git a/chuan-next/build-ssg-simple.sh b/chuan-next/build-ssg-simple.sh new file mode 100644 index 0000000..09788d2 --- /dev/null +++ b/chuan-next/build-ssg-simple.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# ============================================================================= +# 简化版 SSG 构建脚本 +# 专注于 API 路由的处理和静态导出 +# ============================================================================= + +set -e + +# 颜色定义 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +# 配置 +PROJECT_ROOT="$(pwd)" +API_DIR="$PROJECT_ROOT/src/app/api" +TEMP_API_DIR="/tmp/nextjs-api-$(date +%s)" + +echo -e "${GREEN}🚀 开始 SSG 静态导出构建...${NC}" + +# 错误处理函数 +cleanup_on_error() { + echo -e "${RED}❌ 构建失败,正在恢复文件...${NC}" + if [ -d "$TEMP_API_DIR" ]; then + if [ -d "$TEMP_API_DIR/api" ]; then + mv "$TEMP_API_DIR/api" "$API_DIR" 2>/dev/null || true + echo -e "${YELLOW}📂 已恢复 API 目录${NC}" + fi + rm -rf "$TEMP_API_DIR" + fi + exit 1 +} + +# 设置错误处理 +trap cleanup_on_error ERR INT TERM + +# 步骤 1: 备份 API 路由 +if [ -d "$API_DIR" ]; then + echo -e "${YELLOW}📦 备份 API 路由...${NC}" + mkdir -p "$TEMP_API_DIR" + mv "$API_DIR" "$TEMP_API_DIR/" + echo "✅ API 路由已备份到临时目录" +else + echo -e "${YELLOW}⚠️ API 目录不存在,跳过备份${NC}" +fi + +# 步骤 2: 清理构建文件 +echo -e "${YELLOW}🧹 清理之前的构建...${NC}" +rm -rf .next out + +# 步骤 3: 执行静态构建 +echo -e "${YELLOW}🔨 执行静态导出构建...${NC}" +NEXT_EXPORT=true yarn build + +# 步骤 4: 验证构建结果 +if [ -d "out" ] && [ -f "out/index.html" ]; then + echo -e "${GREEN}✅ 静态导出构建成功!${NC}" + + # 显示构建统计 + file_count=$(find out -type f | wc -l) + dir_size=$(du -sh out | cut -f1) + echo "📊 构建统计:" + echo " - 文件数量: $file_count" + echo " - 总大小: $dir_size" +else + echo -e "${RED}❌ 构建验证失败${NC}" + cleanup_on_error +fi + +# 步骤 5: 恢复 API 路由 +if [ -d "$TEMP_API_DIR/api" ]; then + echo -e "${YELLOW}🔄 恢复 API 路由...${NC}" + mv "$TEMP_API_DIR/api" "$API_DIR" + echo "✅ API 路由已恢复" +fi + +# 步骤 6: 清理临时文件 +rm -rf "$TEMP_API_DIR" + +echo "" +echo -e "${GREEN}🎉 SSG 构建完成!${NC}" +echo -e "${GREEN}📁 静态文件位于: ./out/${NC}" +echo -e "${GREEN}🚀 部署命令: npx serve out${NC}" +echo "" +echo -e "${YELLOW}💡 提示: 静态版本会直接连接到 Go 后端 (localhost:8080)${NC}" diff --git a/chuan-next/build-ssg.sh b/chuan-next/build-ssg.sh new file mode 100644 index 0000000..88c5448 --- /dev/null +++ b/chuan-next/build-ssg.sh @@ -0,0 +1,415 @@ +#!/bin/bash + +# ============================================================================= +# Next.js SSG 静态导出构建脚本 +# +# 功能: +# 1. 自动备份和移除 API 路由 +# 2. 临时修改配置文件以支持静态导出 +# 3. 执行静态构建 +# 4. 恢复所有文件到原始状态 +# +# 使用方法: +# ./build-ssg.sh [options] +# +# 选项: +# --clean 清理之前的构建文件 +# --verbose 显示详细输出 +# --help 显示帮助信息 +# ============================================================================= + +set -e # 遇到错误立即退出 + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 配置变量 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$SCRIPT_DIR" +BACKUP_DIR="/tmp/nextjs-ssg-backup-$(date +%s)" +API_DIR="$PROJECT_ROOT/src/app/api" +BUILD_DIR="$PROJECT_ROOT/.next" +OUT_DIR="$PROJECT_ROOT/out" + +# 标志变量 +VERBOSE=false +CLEAN=false +BACKUP_CREATED=false + +# 函数:打印带颜色的消息 +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_verbose() { + if [ "$VERBOSE" = true ]; then + echo -e "${BLUE}[VERBOSE]${NC} $1" + fi +} + +# 函数:显示帮助信息 +show_help() { + cat << EOF +Next.js SSG 静态导出构建脚本 + +使用方法: + $0 [选项] + +选项: + --clean 清理之前的构建文件 (.next, out) + --verbose 显示详细输出信息 + --help 显示此帮助信息 + +示例: + $0 # 标准静态构建 + $0 --clean # 清理后构建 + $0 --verbose # 详细模式构建 + $0 --clean --verbose # 清理后详细模式构建 + +注意: + - 此脚本会临时移除 API 路由以支持静态导出 + - 构建完成后会自动恢复所有文件 + - 如果脚本被中断,请手动运行恢复函数 + +EOF +} + +# 函数:解析命令行参数 +parse_args() { + while [[ $# -gt 0 ]]; do + case $1 in + --clean) + CLEAN=true + shift + ;; + --verbose) + VERBOSE=true + shift + ;; + --help) + show_help + exit 0 + ;; + *) + print_error "未知选项: $1" + show_help + exit 1 + ;; + esac + done +} + +# 函数:检查必要的工具 +check_requirements() { + print_info "检查构建环境..." + + # 检查 Node.js + if ! command -v node &> /dev/null; then + print_error "Node.js 未安装" + exit 1 + fi + + # 检查 yarn + if ! command -v yarn &> /dev/null; then + print_error "Yarn 未安装" + exit 1 + fi + + # 检查 package.json + if [ ! -f "$PROJECT_ROOT/package.json" ]; then + print_error "package.json 不存在" + exit 1 + fi + + # 检查 next.config.js + if [ ! -f "$PROJECT_ROOT/next.config.js" ]; then + print_error "next.config.js 不存在" + exit 1 + fi + + print_verbose "Node.js 版本: $(node --version)" + print_verbose "Yarn 版本: $(yarn --version)" + print_success "环境检查通过" +} + +# 函数:清理构建文件 +clean_build() { + if [ "$CLEAN" = true ]; then + print_info "清理之前的构建文件..." + + if [ -d "$BUILD_DIR" ]; then + rm -rf "$BUILD_DIR" + print_verbose "已删除 .next 目录" + fi + + if [ -d "$OUT_DIR" ]; then + rm -rf "$OUT_DIR" + print_verbose "已删除 out 目录" + fi + + print_success "构建文件清理完成" + fi +} + +# 函数:创建备份目录 +create_backup_dir() { + print_info "创建备份目录: $BACKUP_DIR" + mkdir -p "$BACKUP_DIR" + BACKUP_CREATED=true + print_verbose "备份目录创建成功" +} + +# 函数:备份 API 路由 +backup_api_routes() { + if [ -d "$API_DIR" ]; then + print_info "备份 API 路由..." + cp -r "$API_DIR" "$BACKUP_DIR/" + print_verbose "API 路由已备份到: $BACKUP_DIR/api" + + # 移除原始 API 目录 + rm -rf "$API_DIR" + print_verbose "已移除原始 API 目录" + print_success "API 路由备份完成" + else + print_warning "API 目录不存在,跳过备份" + fi +} + +# 函数:备份并修改配置文件 +backup_and_modify_config() { + print_info "处理配置文件..." + + # 备份 next.config.js + if [ -f "$PROJECT_ROOT/next.config.js" ]; then + cp "$PROJECT_ROOT/next.config.js" "$BACKUP_DIR/next.config.js.backup" + print_verbose "已备份 next.config.js" + fi + + # 备份 package.json + if [ -f "$PROJECT_ROOT/package.json" ]; then + cp "$PROJECT_ROOT/package.json" "$BACKUP_DIR/package.json.backup" + print_verbose "已备份 package.json" + fi + + # 备份环境变量文件(如果存在) + for env_file in ".env" ".env.local" ".env.production"; do + if [ -f "$PROJECT_ROOT/$env_file" ]; then + cp "$PROJECT_ROOT/$env_file" "$BACKUP_DIR/$env_file.backup" + print_verbose "已备份 $env_file" + fi + done + + print_success "配置文件备份完成" +} + +# 函数:设置构建环境变量 +set_build_env() { + print_info "设置静态导出环境变量..." + + # 创建临时的环境变量文件 + cat > "$PROJECT_ROOT/.env.ssg" << EOF +# SSG 构建临时环境变量 +NEXT_EXPORT=true +NODE_ENV=production + +# 后端连接配置(用于静态模式) +NEXT_PUBLIC_GO_BACKEND_URL=http://localhost:8080 +NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws +NEXT_PUBLIC_API_BASE_URL=http://localhost:8080 +EOF + + print_verbose "已创建 .env.ssg 文件" + print_success "环境变量设置完成" +} + +# 函数:执行静态构建 +run_static_build() { + print_info "开始静态导出构建..." + + cd "$PROJECT_ROOT" + + # 加载静态导出环境变量 + if [ -f ".env.static" ]; then + print_info "加载静态导出环境变量..." + export $(cat .env.static | grep -v '^#' | xargs) + fi + + # 设置环境变量并执行构建 + if [ "$VERBOSE" = true ]; then + NEXT_EXPORT=true NODE_ENV=production NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 yarn build + else + NEXT_EXPORT=true NODE_ENV=production NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 yarn build > build.log 2>&1 + if [ $? -ne 0 ]; then + print_error "构建失败,查看 build.log 获取详细信息" + cat build.log + exit 1 + fi + fi + + print_success "静态构建完成" +} + +# 函数:验证构建结果 +verify_build() { + print_info "验证构建结果..." + + if [ ! -d "$OUT_DIR" ]; then + print_error "输出目录 'out' 不存在" + return 1 + fi + + if [ ! -f "$OUT_DIR/index.html" ]; then + print_error "index.html 文件未生成" + return 1 + fi + + # 计算文件数量 + file_count=$(find "$OUT_DIR" -type f | wc -l) + dir_size=$(du -sh "$OUT_DIR" | cut -f1) + + print_success "构建验证通过" + print_info "输出文件数量: $file_count" + print_info "输出目录大小: $dir_size" + + if [ "$VERBOSE" = true ]; then + print_verbose "输出目录结构:" + ls -la "$OUT_DIR" + fi +} + +# 函数:恢复所有文件 +restore_files() { + if [ "$BACKUP_CREATED" = true ] && [ -d "$BACKUP_DIR" ]; then + print_info "恢复备份文件..." + + # 恢复 API 路由 + if [ -d "$BACKUP_DIR/api" ]; then + mkdir -p "$(dirname "$API_DIR")" + cp -r "$BACKUP_DIR/api" "$API_DIR" + print_verbose "已恢复 API 路由" + fi + + # 恢复配置文件 + if [ -f "$BACKUP_DIR/next.config.js.backup" ]; then + cp "$BACKUP_DIR/next.config.js.backup" "$PROJECT_ROOT/next.config.js" + print_verbose "已恢复 next.config.js" + fi + + if [ -f "$BACKUP_DIR/package.json.backup" ]; then + cp "$BACKUP_DIR/package.json.backup" "$PROJECT_ROOT/package.json" + print_verbose "已恢复 package.json" + fi + + # 恢复环境变量文件 + for env_file in ".env" ".env.local" ".env.production"; do + if [ -f "$BACKUP_DIR/$env_file.backup" ]; then + cp "$BACKUP_DIR/$env_file.backup" "$PROJECT_ROOT/$env_file" + print_verbose "已恢复 $env_file" + fi + done + + print_success "文件恢复完成" + fi +} + +# 函数:清理临时文件 +cleanup() { + print_info "清理临时文件..." + + # 删除临时环境变量文件 + if [ -f "$PROJECT_ROOT/.env.ssg" ]; then + rm -f "$PROJECT_ROOT/.env.ssg" + print_verbose "已删除 .env.ssg" + fi + + # 删除构建日志 + if [ -f "$PROJECT_ROOT/build.log" ]; then + rm -f "$PROJECT_ROOT/build.log" + print_verbose "已删除 build.log" + fi + + # 删除备份目录 + if [ -d "$BACKUP_DIR" ]; then + rm -rf "$BACKUP_DIR" + print_verbose "已删除备份目录: $BACKUP_DIR" + fi + + print_success "临时文件清理完成" +} + +# 函数:错误处理和清理 +error_cleanup() { + print_error "构建过程中发生错误,正在恢复..." + restore_files + cleanup + exit 1 +} + +# 函数:显示构建摘要 +show_summary() { + print_success "🎉 SSG 静态导出构建完成!" + echo "" + print_info "📁 输出目录: $OUT_DIR" + print_info "🚀 部署方法:" + echo " - 将 'out' 目录上传到静态托管服务" + echo " - 或者使用: npx serve out" + echo "" + print_info "📋 构建统计:" + if [ -d "$OUT_DIR" ]; then + file_count=$(find "$OUT_DIR" -type f | wc -l) + dir_size=$(du -sh "$OUT_DIR" | cut -f1) + echo " - 文件数量: $file_count" + echo " - 总大小: $dir_size" + fi + echo "" + print_warning "⚠️ 注意: 静态版本不包含 API 路由,前端将直接连接到 Go 后端" +} + +# 主函数 +main() { + print_info "启动 Next.js SSG 静态导出构建..." + + # 设置错误处理 + trap error_cleanup ERR INT TERM + + # 解析命令行参数 + parse_args "$@" + + # 执行构建步骤 + check_requirements + clean_build + create_backup_dir + backup_api_routes + backup_and_modify_config + set_build_env + run_static_build + verify_build + + # 恢复和清理 + restore_files + cleanup + + # 显示摘要 + show_summary +} + +# 如果脚本被直接执行(而不是被 source) +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/chuan-next/build-static.sh b/chuan-next/build-static.sh new file mode 100644 index 0000000..30a3c9a --- /dev/null +++ b/chuan-next/build-static.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# 静态导出构建脚本 +# 该脚本会临时移动 API 路由到项目外部,然后进行静态导出 + +echo "开始静态导出构建..." + +# 备份 API 路由到临时目录 +if [ -d "src/app/api" ]; then + echo "备份 API 路由..." + mkdir -p /tmp/next-api-backup + mv src/app/api /tmp/next-api-backup/ +fi + +# 清理之前的构建 +rm -rf .next out + +# 设置环境变量并构建 +echo "执行静态导出..." +NEXT_EXPORT=true yarn build + +# 恢复 API 路由 +if [ -d "/tmp/next-api-backup/api" ]; then + echo "恢复 API 路由..." + mv /tmp/next-api-backup/api src/app/ + rmdir /tmp/next-api-backup +fi + +echo "静态导出构建完成!" +echo "输出目录: out/" diff --git a/chuan-next/eslint.config.mjs b/chuan-next/eslint.config.mjs index c85fb67..370e1c5 100644 --- a/chuan-next/eslint.config.mjs +++ b/chuan-next/eslint.config.mjs @@ -11,6 +11,22 @@ const compat = new FlatCompat({ const eslintConfig = [ ...compat.extends("next/core-web-vitals", "next/typescript"), + { + rules: { + // 允许 any 类型(对于快速开发) + "@typescript-eslint/no-explicit-any": "warn", + // 允许空对象类型 + "@typescript-eslint/no-empty-object-type": "warn", + // 允许未使用的变量(在开发阶段) + "@typescript-eslint/no-unused-vars": "warn", + // 允许缺少依赖的 useEffect + "react-hooks/exhaustive-deps": "warn", + // 允许缺少 alt 属性的图片 + "jsx-a11y/alt-text": "warn", + // 允许使用 img 标签 + "@next/next/no-img-element": "warn", + }, + }, ]; export default eslintConfig; diff --git a/chuan-next/next.config.js b/chuan-next/next.config.js index b578210..d599647 100644 --- a/chuan-next/next.config.js +++ b/chuan-next/next.config.js @@ -3,11 +3,15 @@ const nextConfig = { // 环境变量配置 env: { GO_BACKEND_URL: process.env.GO_BACKEND_URL, + NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL, + NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL, }, // 公共运行时配置 publicRuntimeConfig: { apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL, + backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL, wsUrl: process.env.NEXT_PUBLIC_WS_URL, }, @@ -16,25 +20,47 @@ const nextConfig = { goBackendUrl: process.env.GO_BACKEND_URL, }, - // 重写规则 - 可选,用于代理API请求 - async rewrites() { - return [ - { - source: '/api/proxy/:path*', - destination: `${process.env.GO_BACKEND_URL}/api/:path*`, - }, - ] - }, - - // 输出配置 - output: 'standalone', - - // 实验性功能 - experimental: { - serverActions: { - allowedOrigins: ['localhost:3000', 'localhost:8080'], + // 重写规则 - 仅在非静态导出模式下启用 + ...(!process.env.NEXT_EXPORT && { + async rewrites() { + return [ + { + source: '/api/proxy/:path*', + destination: `${process.env.GO_BACKEND_URL}/api/:path*`, + }, + ] }, + }), + + // 输出配置 - 根据环境变量决定输出模式 + ...(process.env.NEXT_EXPORT === 'true' ? { + output: 'export', + trailingSlash: true, + skipTrailingSlashRedirect: true, + // 静态导出时禁用不兼容的功能 + experimental: {}, + } : { + // 标准模式配置 + // output: 'standalone', // 可选:用于 Docker 部署 + experimental: { + serverActions: { + allowedOrigins: ['localhost:3000', 'localhost:8080'], + }, + }, + }), + + // 图片优化配置 + images: { + unoptimized: process.env.NEXT_EXPORT === 'true', }, + + // 实验性功能在上面配置中已处理 + + // 优化配置 + poweredByHeader: false, + + // 压缩配置 + compress: true, } module.exports = nextConfig diff --git a/chuan-next/next.config.static.js b/chuan-next/next.config.static.js new file mode 100644 index 0000000..fb7fbc8 --- /dev/null +++ b/chuan-next/next.config.static.js @@ -0,0 +1,34 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // 静态导出模式 + output: 'export', + + // 关闭服务端功能 + trailingSlash: true, + skipTrailingSlashRedirect: true, + + // 图片优化配置 + images: { + unoptimized: true, + }, + + // 环境变量配置 + env: { + GO_BACKEND_URL: process.env.GO_BACKEND_URL, + NEXT_PUBLIC_BACKEND_URL: process.env.NEXT_PUBLIC_BACKEND_URL, + NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL, + NEXT_PUBLIC_WS_URL: process.env.NEXT_PUBLIC_WS_URL, + }, + + // 公共运行时配置 + publicRuntimeConfig: { + apiBaseUrl: process.env.NEXT_PUBLIC_API_BASE_URL, + backendUrl: process.env.NEXT_PUBLIC_BACKEND_URL, + wsUrl: process.env.NEXT_PUBLIC_WS_URL, + }, + + // 禁用服务器端功能 + // 注意:在静态导出模式下,API 路由将不可用 +} + +module.exports = nextConfig diff --git a/chuan-next/package.json b/chuan-next/package.json index e8ebc58..954bbf1 100644 --- a/chuan-next/package.json +++ b/chuan-next/package.json @@ -8,6 +8,10 @@ "build": "next build", "build:dev": "NODE_ENV=development next build", "build:prod": "NODE_ENV=production next build", + "build:static": "NEXT_EXPORT=true next build", + "build:ssg": "./build-static.sh", + "export": "next export", + "export:static": "NEXT_CONFIG=next.config.static.js next build && next export", "start": "next start", "start:dev": "NODE_ENV=development next start", "start:prod": "NODE_ENV=production next start", diff --git a/chuan-next/src/app/HomePage-new.tsx b/chuan-next/src/app/HomePage-new.tsx index c46b029..1338d3d 100644 --- a/chuan-next/src/app/HomePage-new.tsx +++ b/chuan-next/src/app/HomePage-new.tsx @@ -13,6 +13,7 @@ import { useWebSocket } from '@/hooks/useWebSocket'; import { FileInfo, TransferProgress, WebSocketMessage, RoomStatus } from '@/types'; import { Upload, MessageSquare, Monitor } from 'lucide-react'; import { useToast } from '@/components/ui/toast-simple'; +import { apiPost, apiGet, debugApiConfig } from '@/lib/api-utils'; interface FileTransferData { fileId: string; @@ -98,6 +99,14 @@ export default function HomePage() { updateUrlParams(value); }, [updateUrlParams, isConnected, pickupCode, isConnecting, activeTab]); + // 监听WebSocket连接状态变化,重置连接中状态 + useEffect(() => { + if (isConnected && isConnecting) { + setIsConnecting(false); + console.log('WebSocket连接已建立,重置连接状态'); + } + }, [isConnected, isConnecting]); + // 确认切换tab const confirmTabSwitch = useCallback(() => { if (pendingTabSwitch) { @@ -430,11 +439,7 @@ export default function HomePage() { })); try { - const response = await fetch('/api/create-room', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ files: fileInfos }) - }); + const response = await apiPost('/api/create-room', { files: fileInfos }); const data = await response.json(); if (data.success) { @@ -468,21 +473,23 @@ export default function HomePage() { setIsConnecting(true); try { - const response = await fetch(`/api/room-info?code=${code}`); + const response = await apiGet(`/api/room-info?code=${code}`); const data = await response.json(); if (data.success) { setPickupCode(code); setCurrentRole('receiver'); setReceiverFiles(data.files || []); + // 开始连接WebSocket connect(code, 'receiver'); showNotification('连接成功!', 'success'); + // 注意:isConnecting状态会在WebSocket连接建立后自动重置 } else { showNotification(data.message || '取件码无效或已过期', 'error'); setIsConnecting(false); } } catch (error) { - console.error('连接失败:', error); + console.error('API调用失败:', error); showNotification('连接失败,请检查网络连接', 'error'); setIsConnecting(false); } diff --git a/chuan-next/src/app/api/create-room/route.ts b/chuan-next/src/app/api/create-room/route.ts deleted file mode 100644 index 4f7e341..0000000 --- a/chuan-next/src/app/api/create-room/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getBackendUrl } from '@/lib/config'; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - - // 使用配置管理获取后端URL - const backendUrl = getBackendUrl('/api/create-room'); - - // 转发请求到Go后端 - const response = await fetch(backendUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - const data = await response.json(); - - return NextResponse.json(data, { status: response.status }); - } catch (error) { - console.error('Error creating room:', error); - return NextResponse.json( - { success: false, message: '创建房间失败' }, - { status: 500 } - ); - } -} diff --git a/chuan-next/src/app/api/create-text-room/route.ts b/chuan-next/src/app/api/create-text-room/route.ts deleted file mode 100644 index 45ebd54..0000000 --- a/chuan-next/src/app/api/create-text-room/route.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getBackendUrl } from '@/lib/config'; - -export async function POST(req: NextRequest) { - try { - const { text } = await req.json(); - - if (!text || text.trim().length === 0) { - return NextResponse.json({ error: '文本内容不能为空' }, { status: 400 }); - } - - if (text.length > 50000) { - return NextResponse.json({ error: '文本内容过长,最大支持50,000字符' }, { status: 400 }); - } - - // 调用后端API创建文字传输房间 - const response = await fetch(getBackendUrl('/api/create-text-room'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text }), - }); - - if (!response.ok) { - throw new Error('创建文字传输房间失败'); - } - - const data = await response.json(); - - return NextResponse.json({ - success: true, - code: data.code, - message: '文字传输房间创建成功' - }); - - } catch (error) { - console.error('创建文字传输房间错误:', error); - return NextResponse.json( - { error: '服务器错误,请重试' }, - { status: 500 } - ); - } -} diff --git a/chuan-next/src/app/api/get-text-content/route.ts b/chuan-next/src/app/api/get-text-content/route.ts deleted file mode 100644 index 7ed6333..0000000 --- a/chuan-next/src/app/api/get-text-content/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(req: NextRequest) { - try { - const { code } = await req.json(); - - if (!code || code.length !== 6) { - return NextResponse.json({ error: '请输入正确的6位房间码' }, { status: 400 }); - } - - // 调用后端API获取文字内容 - const response = await fetch(`http://localhost:8080/api/get-text-content/${code}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - if (response.status === 404) { - return NextResponse.json({ error: '房间不存在或已过期' }, { status: 404 }); - } - throw new Error('获取文字内容失败'); - } - - const data = await response.json(); - - return NextResponse.json({ - success: true, - text: data.text, - message: '文字内容获取成功' - }); - - } catch (error) { - console.error('获取文字内容错误:', error); - return NextResponse.json( - { error: '服务器错误,请重试' }, - { status: 500 } - ); - } -} diff --git a/chuan-next/src/app/api/room-info/route.ts b/chuan-next/src/app/api/room-info/route.ts deleted file mode 100644 index 50d9bfc..0000000 --- a/chuan-next/src/app/api/room-info/route.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { getBackendUrl } from '@/lib/config'; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const code = searchParams.get('code'); - - if (!code) { - return NextResponse.json( - { success: false, message: '缺少取件码' }, - { status: 400 } - ); - } - - // 转发请求到Go后端 - const response = await fetch(getBackendUrl(`/api/room-info?code=${code}`), { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - const data = await response.json(); - - return NextResponse.json(data, { status: response.status }); - } catch (error) { - console.error('Error getting room info:', error); - return NextResponse.json( - { success: false, message: '获取房间信息失败' }, - { status: 500 } - ); - } -} diff --git a/chuan-next/src/app/api/room-status/route.ts b/chuan-next/src/app/api/room-status/route.ts deleted file mode 100644 index 1766642..0000000 --- a/chuan-next/src/app/api/room-status/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -const GO_BACKEND_URL = process.env.GO_BACKEND_URL || 'http://localhost:8080'; - -export async function GET(request: NextRequest) { - try { - const { searchParams } = new URL(request.url); - const code = searchParams.get('code'); - - if (!code) { - return NextResponse.json( - { success: false, message: '缺少取件码' }, - { status: 400 } - ); - } - - // 转发请求到Go后端 - const response = await fetch(`${GO_BACKEND_URL}/api/room-status?code=${code}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - const data = await response.json(); - - return NextResponse.json(data, { status: response.status }); - } catch (error) { - console.error('Error getting room status:', error); - return NextResponse.json( - { success: false, message: '获取房间状态失败' }, - { status: 500 } - ); - } -} diff --git a/chuan-next/src/app/api/update-files/route.ts b/chuan-next/src/app/api/update-files/route.ts deleted file mode 100644 index a2ee99d..0000000 --- a/chuan-next/src/app/api/update-files/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { code, files } = body; - - if (!code || !files) { - return NextResponse.json( - { success: false, message: '缺少必要参数' }, - { status: 400 } - ); - } - - // 转发请求到Go后端 - const backendUrl = process.env.NODE_ENV === 'production' - ? `https://${process.env.VERCEL_URL || 'localhost'}/api/update-files` - : 'http://localhost:8080/api/update-files'; - - const response = await fetch(backendUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ code, files }), - }); - - const data = await response.json(); - - return NextResponse.json(data); - } catch (error) { - console.error('Update files API error:', error); - return NextResponse.json( - { success: false, message: '服务器错误' }, - { status: 500 } - ); - } -} diff --git a/chuan-next/src/app/page.tsx b/chuan-next/src/app/page.tsx index d85f2af..87e108e 100644 --- a/chuan-next/src/app/page.tsx +++ b/chuan-next/src/app/page.tsx @@ -1,3 +1,14 @@ import HomePageWrapper from './HomePageWrapper'; -export default HomePageWrapper; \ No newline at end of file +// 静态生成配置 +export const metadata = { + title: 'File Transfer - 文件传输', + description: '简单快速的文件传输工具', +} + +// 启用静态生成 - 但保持客户端组件的灵活性 +export const dynamic = 'auto' + +export default function Page() { + return ; +} \ No newline at end of file diff --git a/chuan-next/src/components/TextTransfer-new.tsx b/chuan-next/src/components/TextTransfer-new.tsx deleted file mode 100644 index f58c91a..0000000 --- a/chuan-next/src/components/TextTransfer-new.tsx +++ /dev/null @@ -1,468 +0,0 @@ -"use client"; - -import React, { useState, useCallback, useEffect, useRef } from 'react'; -import { useSearchParams, useRouter } from 'next/navigation'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { MessageSquare, Copy, Send, Download, Image, Users } from 'lucide-react'; -import { useToast } from '@/components/ui/toast-simple'; - -interface TextTransferProps { - onSendText?: (text: string) => Promise; // 返回取件码 - onReceiveText?: (code: string) => Promise; // 返回文本内容 - websocket?: WebSocket | null; - isConnected?: boolean; - currentRole?: 'sender' | 'receiver'; - pickupCode?: string; -} - -export default function TextTransfer({ - onSendText, - onReceiveText, - websocket, - isConnected = false, - currentRole, - pickupCode -}: TextTransferProps) { - const searchParams = useSearchParams(); - const router = useRouter(); - const [mode, setMode] = useState<'send' | 'receive'>('send'); - const [textContent, setTextContent] = useState(''); - const [roomCode, setRoomCode] = useState(''); - const [receivedText, setReceivedText] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [isRoomCreated, setIsRoomCreated] = useState(false); - const [connectedUsers, setConnectedUsers] = useState(0); - const [images, setImages] = useState([]); - const { showToast } = useToast(); - const textareaRef = useRef(null); - const updateTimeoutRef = useRef(null); - - // 从URL参数中获取初始模式 - useEffect(() => { - const urlMode = searchParams.get('mode') as 'send' | 'receive'; - const type = searchParams.get('type'); - - if (type === 'text' && urlMode && ['send', 'receive'].includes(urlMode)) { - setMode(urlMode); - } - }, [searchParams]); - - // 监听WebSocket消息 - useEffect(() => { - if (!websocket) return; - - const handleMessage = (event: MessageEvent) => { - try { - const message = JSON.parse(event.data); - console.log('TextTransfer收到消息:', message); - - switch (message.type) { - case 'text-update': - // 实时更新文字内容 - if (message.payload?.text !== undefined) { - setReceivedText(message.payload.text); - if (currentRole === 'receiver') { - setTextContent(message.payload.text); - } - } - break; - - case 'text-send': - // 接收到发送的文字 - if (message.payload?.text) { - setReceivedText(message.payload.text); - showToast('收到新的文字内容!', 'success'); - } - break; - - case 'image-send': - // 接收到发送的图片 - if (message.payload?.imageData) { - setImages(prev => [...prev, message.payload.imageData]); - showToast('收到新的图片!', 'success'); - } - break; - - case 'room-status': - // 更新房间状态 - if (message.payload?.sender_count !== undefined && message.payload?.receiver_count !== undefined) { - setConnectedUsers(message.payload.sender_count + message.payload.receiver_count); - } - break; - } - } catch (error) { - console.error('解析WebSocket消息失败:', error); - } - }; - - websocket.addEventListener('message', handleMessage); - return () => websocket.removeEventListener('message', handleMessage); - }, [websocket, currentRole, showToast]); - - // 更新URL参数 - const updateMode = useCallback((newMode: 'send' | 'receive') => { - setMode(newMode); - const params = new URLSearchParams(searchParams.toString()); - params.set('type', 'text'); - params.set('mode', newMode); - router.push(`?${params.toString()}`, { scroll: false }); - }, [searchParams, router]); - - // 发送实时文字更新 - const sendTextUpdate = useCallback((text: string) => { - if (!websocket || !isConnected || !isRoomCreated) return; - - // 清除之前的定时器 - if (updateTimeoutRef.current) { - clearTimeout(updateTimeoutRef.current); - } - - // 设置新的定时器,防抖动 - updateTimeoutRef.current = setTimeout(() => { - websocket.send(JSON.stringify({ - type: 'text-update', - payload: { text } - })); - }, 300); // 300ms防抖 - }, [websocket, isConnected, isRoomCreated]); - - // 处理文字输入 - const handleTextChange = useCallback((e: React.ChangeEvent) => { - const newText = e.target.value; - setTextContent(newText); - - // 如果是发送方且房间已创建,发送实时更新 - if (currentRole === 'sender' && isRoomCreated) { - sendTextUpdate(newText); - } - }, [currentRole, isRoomCreated, sendTextUpdate]); - - // 创建文字传输房间 - const handleCreateRoom = useCallback(async () => { - if (!textContent.trim()) { - showToast('请输入要传输的文字内容', 'error'); - return; - } - - setIsLoading(true); - try { - if (onSendText) { - const code = await onSendText(textContent); - setRoomCode(code); - setIsRoomCreated(true); - showToast('房间创建成功!', 'success'); - } - } catch (error) { - console.error('创建房间失败:', error); - showToast('创建房间失败,请重试', 'error'); - } finally { - setIsLoading(false); - } - }, [textContent, onSendText, showToast]); - - // 加入房间 - const handleJoinRoom = useCallback(async () => { - if (!roomCode.trim() || roomCode.length !== 6) { - showToast('请输入正确的6位房间码', 'error'); - return; - } - - setIsLoading(true); - try { - if (onReceiveText) { - const text = await onReceiveText(roomCode); - setReceivedText(text); - showToast('成功加入房间!', 'success'); - } - } catch (error) { - console.error('加入房间失败:', error); - showToast('加入房间失败,请检查房间码', 'error'); - } finally { - setIsLoading(false); - } - }, [roomCode, onReceiveText, showToast]); - - // 发送文字 - const handleSendText = useCallback(() => { - if (!websocket || !isConnected || !textContent.trim()) return; - - websocket.send(JSON.stringify({ - type: 'text-send', - payload: { text: textContent } - })); - - showToast('文字已发送!', 'success'); - }, [websocket, isConnected, textContent, showToast]); - - // 处理图片粘贴 - const handlePaste = useCallback((e: React.ClipboardEvent) => { - const items = e.clipboardData?.items; - if (!items) return; - - for (let i = 0; i < items.length; i++) { - const item = items[i]; - if (item.type.indexOf('image') !== -1) { - const file = item.getAsFile(); - if (file) { - const reader = new FileReader(); - reader.onload = (event) => { - const imageData = event.target?.result as string; - setImages(prev => [...prev, imageData]); - - // 发送图片给其他用户 - if (websocket && isConnected) { - websocket.send(JSON.stringify({ - type: 'image-send', - payload: { imageData } - })); - showToast('图片已发送!', 'success'); - } - }; - reader.readAsDataURL(file); - } - } - } - }, [websocket, isConnected, showToast]); - - const copyToClipboard = useCallback(async (text: string) => { - try { - await navigator.clipboard.writeText(text); - showToast('已复制到剪贴板!', 'success'); - } catch (err) { - showToast('复制失败', 'error'); - } - }, [showToast]); - - return ( -
- {/* 模式切换 */} -
-
- - -
-
- - {mode === 'send' ? ( -
-
-
- -
-

传送文字

-

- {isRoomCreated ? '实时编辑,对方可以同步看到' : '输入要传输的文本内容'} -

- {connectedUsers > 1 && ( -
- - {connectedUsers} 人在线 -
- )} -
- -
-
-