Browse Source

feat: 添加完整的Windows构建脚本 build.bat

- 支持自动检测包管理器(npm/yarn)
- 包含Node.js版本检查
- 自动安装依赖和TypeScript
- 构建生产版本并创建发布目录
- 提供详细的部署说明
- 支持命令行参数控制构建过程
feature/x1-web-request
root 4 months ago
parent
commit
1a54a55690
  1. 19
      src/X1.WebUI/.dockerignore
  2. 32
      src/X1.WebUI/Dockerfile
  3. 248
      src/X1.WebUI/build.bat
  4. 207
      src/X1.WebUI/deploy.sh
  5. 74
      src/X1.WebUI/nginx.conf
  6. 9
      src/X1.WebUI/package-lock.json
  7. 2
      src/X1.WebUI/package.json
  8. 8
      src/X1.WebUI/yarn.lock

19
src/X1.WebUI/.dockerignore

@ -0,0 +1,19 @@
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.env.development
.env.test
.nyc_output
coverage
.vscode
.idea
*.log
dist
.DS_Store
Thumbs.db
build.bat
deploy.sh

32
src/X1.WebUI/Dockerfile

@ -0,0 +1,32 @@
# 使用官方Node.js 18 Alpine镜像作为构建环境
FROM node:18-alpine AS builder
# 设置工作目录
WORKDIR /app
# 复制package.json和package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建生产版本
RUN npm run build
# 使用nginx Alpine镜像作为运行环境
FROM nginx:alpine
# 复制构建产物到nginx目录
COPY --from=builder /app/dist /usr/share/nginx/html
# 复制nginx配置文件
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]

248
src/X1.WebUI/build.bat

@ -0,0 +1,248 @@
@echo off
setlocal enabledelayedexpansion
chcp 65001 >nul
REM 设置错误时退出
set "EXIT_CODE=0"
REM 解析命令行参数
call :parse_arguments %*
REM 主函数
call :main
goto :end
REM 主函数
:main
call :log_info "Starting X1.WebUI build process..."
call :check_node_version
if !EXIT_CODE! neq 0 goto :end
call :detect_package_manager
if !EXIT_CODE! neq 0 goto :end
call :install_dependencies
if !EXIT_CODE! neq 0 goto :end
call :build_project
if !EXIT_CODE! neq 0 goto :end
call :create_publish_dir
call :show_publish_info
call :show_deployment_instructions
call :log_success "Build completed!"
call :log_info "Please upload the publish directory contents to server /home/hyh/x1_web/"
goto :eof
REM 检查Node.js版本
:check_node_version
call :log_info "Checking Node.js version..."
node --version >nul 2>&1
if errorlevel 1 (
call :log_error "Node.js not installed, please install Node.js 18+"
set "EXIT_CODE=1"
goto :end
)
for /f "tokens=*" %%i in ('node --version') do set "NODE_VERSION=%%i"
call :log_success "Node.js version check passed: !NODE_VERSION!"
goto :eof
REM 解析命令行参数
:parse_arguments
set "PACKAGE_MANAGER="
set "SHOW_HELP=false"
:parse_loop
if "%~1"=="" goto :parse_done
if /i "%~1"=="--npm" (
set "PACKAGE_MANAGER=npm"
call :log_info "Package manager set to npm via command line"
) else if /i "%~1"=="--yarn" (
set "PACKAGE_MANAGER=yarn"
call :log_info "Package manager set to yarn via command line"
) else if /i "%~1"=="--help" (
set "SHOW_HELP=true"
) else (
call :log_error "Unknown parameter: %~1"
call :show_help
set "EXIT_CODE=1"
goto :end
)
shift
goto :parse_loop
:parse_done
if "!SHOW_HELP!"=="true" (
call :show_help
set "EXIT_CODE=0"
goto :end
)
goto :eof
REM 显示帮助信息
:show_help
echo.
echo Usage: build.bat [options]
echo.
echo Options:
echo --npm Force using npm package manager
echo --yarn Force using yarn package manager
echo --help Show this help message
echo.
echo Examples:
echo build.bat - Auto detect package manager
echo build.bat --npm - Force using npm
echo build.bat --yarn - Force using yarn
echo build.bat --help - Show help
echo.
goto :eof
REM 检测包管理器
:detect_package_manager
call :log_info "Detecting package manager..."
if not "!PACKAGE_MANAGER!"=="" (
call :log_info "Package manager already set to: !PACKAGE_MANAGER!"
goto :eof
)
if exist "yarn.lock" (
set "PACKAGE_MANAGER=yarn"
call :log_info "Found yarn.lock, using yarn"
) else if exist "package-lock.json" (
set "PACKAGE_MANAGER=npm"
call :log_info "Found package-lock.json, using npm"
) else (
set "PACKAGE_MANAGER=npm"
call :log_info "No lock file found, defaulting to npm"
)
goto :eof
REM 安装依赖
:install_dependencies
call :log_info "Installing project dependencies..."
if not exist "node_modules" (
if "!PACKAGE_MANAGER!"=="yarn" (
call yarn install --frozen-lockfile
) else (
call npm ci
)
) else (
if "!PACKAGE_MANAGER!"=="yarn" (
call yarn install --frozen-lockfile --production
) else (
call npm ci --only=production
)
)
if errorlevel 1 (
call :log_error "Dependency installation failed"
set "EXIT_CODE=1"
goto :end
)
call :log_success "Dependencies installed successfully"
goto :eof
REM 构建项目
:build_project
call :log_info "Building production version..."
REM 直接尝试构建,如果失败则安装TypeScript
call :log_info "Attempting to build project..."
if "!PACKAGE_MANAGER!"=="yarn" (
call yarn build
) else (
call npm run build
)
if errorlevel 1 (
call :log_warning "Build failed, checking if TypeScript is needed..."
call :log_info "Installing TypeScript as dev dependency..."
if "!PACKAGE_MANAGER!"=="yarn" (
call yarn add -D typescript
) else (
call npm install --save-dev typescript
)
call :log_info "Retrying build after TypeScript installation..."
if "!PACKAGE_MANAGER!"=="yarn" (
call yarn build
) else (
call npm run build
)
if errorlevel 1 (
call :log_error "Project build failed even after TypeScript installation"
set "EXIT_CODE=1"
goto :end
)
)
call :log_success "Project build completed"
goto :eof
REM 创建发布目录
:create_publish_dir
call :log_info "Creating publish directory..."
if exist "publish" rmdir /s /q "publish"
mkdir "publish"
REM 复制构建产物到发布目录根目录
xcopy "dist\*" "publish\" /E /Y /Q
REM 复制Dockerfile和nginx配置到发布目录根目录
copy "Dockerfile" "publish\" >nul
copy "nginx.conf" "publish\" >nul
REM 复制部署脚本到发布目录根目录
copy "deploy.sh" "publish\" >nul
REM 注意:.dockerignore 文件不需要复制到发布目录,只在本地构建时使用
call :log_success "Publish directory created: publish/"
goto :eof
REM 显示文件大小
:show_publish_info
call :log_info "Publish directory info:"
for /f "tokens=1" %%i in ('dir publish /s ^| find "个文件"') do echo File count: %%i
echo.
call :log_info "Publish directory contents:"
dir publish /B
echo.
call :log_info "Publish directory structure:"
tree publish /F
goto :eof
REM 显示部署说明
:show_deployment_instructions
call :log_info "Deployment instructions:"
echo.
echo 1. Upload all contents of publish directory to server /home/hyh/x1_web/
echo 2. Ensure the following files are in /home/hyh/x1_web/ root directory:
echo - Dockerfile
echo - nginx.conf
echo - deploy.sh
echo - index.html
echo - assets/ directory
echo 3. On server run: chmod +x deploy.sh ^&^& ./deploy.sh
echo.
goto :eof
REM 日志函数(简化版本,不使用颜色)
:log_info
echo [INFO] %~1
goto :eof
:log_success
echo [SUCCESS] %~1
goto :eof
:log_warning
echo [WARNING] %~1
goto :eof
:log_error
echo [ERROR] %~1
goto :eof
:end
exit /b %EXIT_CODE%

207
src/X1.WebUI/deploy.sh

@ -0,0 +1,207 @@
#!/bin/bash
# 设置错误时退出
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 默认配置
IMAGE_NAME="x1-webui"
CONTAINER_NAME="x1-webui-container"
PORT="9905"
REMOVE_MODE=false
# 解析命令行参数
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-p|--port)
PORT="$2"
shift 2
;;
-i|--image)
IMAGE_NAME="$2"
shift 2
;;
-c|--container)
CONTAINER_NAME="$2"
shift 2
;;
-r|--remove)
REMOVE_MODE=true
shift
;;
-h|--help)
echo "用法: $0 [选项]"
echo "选项:"
echo " -p, --port PORT 指定端口 (默认: 9905)"
echo " -i, --image NAME 指定镜像名称 (默认: x1-webui)"
echo " -c, --container NAME 指定容器名称 (默认: x1-webui-container)"
echo " -r, --remove 删除旧容器和镜像"
echo " -h, --help 显示帮助信息"
exit 0
;;
*)
log_error "未知选项: $1"
exit 1
;;
esac
done
}
# 检查Docker
check_docker() {
log_info "检查Docker安装状态..."
if ! command -v docker &> /dev/null; then
log_error "Docker未安装,请先安装Docker"
exit 1
fi
if ! docker info &> /dev/null; then
log_error "Docker服务未运行,请启动Docker服务"
exit 1
fi
log_success "Docker检查通过"
}
# 检查必要文件
check_files() {
log_info "检查必要文件..."
local required_files=("Dockerfile" "nginx.conf" "index.html")
for file in "${required_files[@]}"; do
if [ ! -f "$file" ]; then
log_error "缺少必要文件: $file"
exit 1
fi
done
log_success "文件检查通过"
}
# 停止并删除旧容器
stop_old_container() {
log_info "停止旧容器..."
if docker ps -a --format "table {{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
docker stop "$CONTAINER_NAME" || true
docker rm "$CONTAINER_NAME" || true
log_success "旧容器已停止并删除"
else
log_info "没有找到旧容器"
fi
}
# 删除旧镜像
remove_old_image() {
if [ "$REMOVE_MODE" = true ]; then
log_info "删除旧镜像..."
if docker images --format "table {{.Repository}}" | grep -q "^${IMAGE_NAME}$"; then
docker rmi "$IMAGE_NAME" || true
log_success "旧镜像已删除"
else
log_info "没有找到旧镜像"
fi
fi
}
# 构建Docker镜像
build_image() {
log_info "构建Docker镜像: $IMAGE_NAME"
docker build -t "$IMAGE_NAME" .
log_success "镜像构建完成"
}
# 运行容器
run_container() {
log_info "启动容器: $CONTAINER_NAME"
docker run -d \
--name "$CONTAINER_NAME" \
-p "$PORT:80" \
--restart unless-stopped \
"$IMAGE_NAME"
log_success "容器启动完成"
}
# 健康检查
health_check() {
log_info "等待应用启动..."
local max_attempts=30
local attempt=1
while [ $attempt -le $max_attempts ]; do
if curl -f -s "http://localhost:$PORT/health" > /dev/null 2>&1; then
log_success "应用启动成功!"
return 0
fi
log_info "等待应用启动... ($attempt/$max_attempts)"
sleep 2
((attempt++))
done
log_warning "应用启动超时,但容器可能仍在运行"
return 1
}
# 显示部署信息
show_deployment_info() {
log_success "部署完成!"
echo ""
echo "部署信息:"
echo " 镜像名称: $IMAGE_NAME"
echo " 容器名称: $CONTAINER_NAME"
echo " 访问端口: $PORT"
echo " 访问地址: http://localhost:$PORT"
echo ""
log_info "容器状态:"
docker ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo ""
log_info "容器日志:"
docker logs --tail 10 "$CONTAINER_NAME"
}
# 主函数
main() {
log_info "开始部署X1.WebUI..."
parse_arguments "$@"
check_docker
check_files
stop_old_container
remove_old_image
build_image
run_container
health_check
show_deployment_info
log_success "部署完成!"
}
# 执行主函数
main "$@"

74
src/X1.WebUI/nginx.conf

@ -0,0 +1,74 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log;
# 基本设置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 处理React Router的客户端路由
location / {
try_files $uri $uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# API代理到后端9905端口
location /api/ {
proxy_pass http://host.docker.internal:9905;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

9
src/X1.WebUI/package-lock.json

@ -69,7 +69,7 @@
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"typescript": "^5.9.2",
"vite": "^5.0.8"
}
},
@ -6502,11 +6502,10 @@
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"version": "5.9.2",
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

2
src/X1.WebUI/package.json

@ -71,7 +71,7 @@
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"typescript": "^5.2.2",
"typescript": "^5.9.2",
"vite": "^5.0.8"
}
}

8
src/X1.WebUI/yarn.lock

@ -3419,10 +3419,10 @@ type-fest@^0.20.2:
resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
typescript@^5.2.2, typescript@>=4.2.0:
version "5.8.3"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
typescript@^5.9.2, typescript@>=4.2.0:
version "5.9.2"
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.9.2.tgz"
integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
undici-types@~6.21.0:
version "6.21.0"

Loading…
Cancel
Save