From 7c5409a6c763570e94311509d1aba2fab370f836 Mon Sep 17 00:00:00 2001 From: liuyuanchuang Date: Mon, 26 Jan 2026 07:10:58 +0800 Subject: [PATCH] feat: add deploy script --- deploy.sh | 145 +++++++++++++++++++++++++++++++ src/contexts/LanguageContext.tsx | 25 ++++++ src/lib/ipLocation.ts | 78 +++++++++++++++++ vite.config.ts | 5 ++ 4 files changed, 253 insertions(+) create mode 100755 deploy.sh create mode 100644 src/lib/ipLocation.ts diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..5e1aa73 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,145 @@ +#!/bin/bash + +# Document AI Frontend 部署脚本 +# 功能:构建项目并部署到 ecs 服务器 + +set -e # 遇到错误立即退出 + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 服务器配置 +ECS_HOST="ecs" +DEPLOY_PATH="/texpixel" + +# 打印带颜色的消息 +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" +} + +# 检查命令是否存在 +check_command() { + if ! command -v $1 &> /dev/null; then + print_error "$1 命令未找到,请先安装" + exit 1 + fi +} + +# 部署到服务器 +deploy_to_server() { + local server=$1 + print_info "开始部署到 ${server}..." + + # 上传构建产物 + print_info "上传 dist 目录到 ${server}..." + if scp -r dist ${server}:~ > /dev/null 2>&1; then + print_success "文件上传成功" + else + print_error "文件上传失败" + return 1 + fi + + # SSH 执行部署操作 + print_info "在 ${server} 上执行部署操作..." + ssh ${server} << EOF + set -e + cd ${DEPLOY_PATH} + + # 备份旧版本 + if [ -d "dist" ]; then + echo "备份旧版本..." + rm -rf dist_bak/ + mv dist dist_bak + fi + + # 移动新版本 + if [ -d ~/dist ]; then + mv ~/dist . + echo "部署完成!" + else + echo "错误:找不到 ~/dist 目录" + exit 1 + fi + + # 重新加载 nginx(如果配置了) + if command -v nginx &> /dev/null; then + echo "重新加载 nginx..." + nginx -t && nginx -s reload || echo "警告:nginx 重新加载失败,请手动检查" + fi +EOF + + if [ $? -eq 0 ]; then + print_success "${server} 部署成功!" + else + print_error "${server} 部署失败!" + return 1 + fi +} + +# 主函数 +main() { + print_info "==========================================" + print_info "Document AI Frontend 部署脚本" + print_info "==========================================" + echo "" + + # 检查必要的命令 + print_info "检查环境..." + check_command "npm" + check_command "scp" + check_command "ssh" + + # 步骤1: 构建项目 + print_info "步骤 1/2: 构建项目..." + if npm run build; then + print_success "构建完成!" + else + print_error "构建失败!" + exit 1 + fi + echo "" + + # 检查 dist 目录是否存在 + if [ ! -d "dist" ]; then + print_error "dist 目录不存在,构建可能失败" + exit 1 + fi + + # 步骤2: 部署到 ecs + print_info "步骤 2/2: 部署到 ecs..." + if deploy_to_server ${ECS_HOST}; then + print_success "ecs 部署完成" + else + print_error "ecs 部署失败" + exit 1 + fi + echo "" + + # 完成 + print_info "清理临时文件..." + # 可以选择是否删除本地的 dist 目录 + # rm -rf dist + + print_success "==========================================" + print_success "部署完成!" + print_success "==========================================" +} + +# 运行主函数 +main diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index 5aed3ff..5ab8fa0 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useState, useEffect } from 'react'; import { translations, Language, TranslationKey } from '../lib/translations'; +import { detectLanguageByIP } from '../lib/ipLocation'; interface LanguageContextType { language: Language; @@ -10,12 +11,36 @@ interface LanguageContextType { const LanguageContext = createContext(undefined); export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + // 初始化语言:优先使用 localStorage,否则使用浏览器语言作为临时值 const [language, setLanguageState] = useState(() => { const saved = localStorage.getItem('language'); if (saved === 'en' || saved === 'zh') return saved; + // 临时使用浏览器语言,后续会被IP检测覆盖(如果没有保存的语言) return navigator.language.startsWith('zh') ? 'zh' : 'en'; }); + // 检测IP地理位置并设置语言(仅在首次加载且没有保存的语言时) + useEffect(() => { + const saved = localStorage.getItem('language'); + + // 如果用户已经手动选择过语言,则不进行IP检测 + if (saved === 'en' || saved === 'zh') { + return; + } + + // 异步检测IP并设置语言 + detectLanguageByIP() + .then((detectedLang) => { + setLanguageState(detectedLang); + // 注意:这里不保存到 localStorage,让用户首次访问时使用IP检测的结果 + // 如果用户手动切换语言,才会保存到 localStorage + }) + .catch((error) => { + // IP检测失败时,保持使用浏览器语言检测的结果 + console.warn('Failed to detect language by IP:', error); + }); + }, []); // 仅在组件挂载时执行一次 + const setLanguage = (lang: Language) => { setLanguageState(lang); localStorage.setItem('language', lang); diff --git a/src/lib/ipLocation.ts b/src/lib/ipLocation.ts new file mode 100644 index 0000000..6d98704 --- /dev/null +++ b/src/lib/ipLocation.ts @@ -0,0 +1,78 @@ +/** + * IP 地理位置检测工具 + * 用于根据用户IP地址判断语言偏好 + */ + +interface IPLocationResponse { + country_code?: string; + country?: string; + error?: boolean; +} + +/** + * 根据IP地址检测用户所在国家/地区 + * 使用免费的 ipapi.co 服务(无需API key) + * + * @returns Promise 返回国家代码(如 'CN', 'US'),失败返回 null + */ +export async function detectCountryByIP(): Promise { + try { + // 使用 ipapi.co 免费服务(无需API key,有速率限制但足够使用) + const response = await fetch('https://ipapi.co/json/', { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + console.warn('IP location detection failed:', response.status); + return null; + } + + const data: IPLocationResponse = await response.json(); + + if (data.error || !data.country_code) { + return null; + } + + return data.country_code.toUpperCase(); + } catch (error) { + // 静默失败,不影响用户体验 + console.warn('IP location detection error:', error); + return null; + } +} + +/** + * 根据国家代码判断应该使用的语言 + * + * @param countryCode 国家代码(如 'CN', 'TW', 'HK', 'SG' 等) + * @returns 'zh' | 'en' 推荐的语言 + */ +export function getLanguageByCountry(countryCode: string | null): 'zh' | 'en' { + if (!countryCode) { + return 'en'; + } + + // 中文地区列表 + const chineseRegions = [ + 'CN', // 中国大陆 + 'TW', // 台湾 + 'HK', // 香港 + 'MO', // 澳门 + 'SG', // 新加坡(主要使用中文) + ]; + + return chineseRegions.includes(countryCode) ? 'zh' : 'en'; +} + +/** + * 检测用户IP并返回推荐语言 + * + * @returns Promise<'zh' | 'en'> 推荐的语言 + */ +export async function detectLanguageByIP(): Promise<'zh' | 'en'> { + const countryCode = await detectCountryByIP(); + return getLanguageByCountry(countryCode); +} diff --git a/vite.config.ts b/vite.config.ts index 6bb01e5..0cc17e1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,8 +8,13 @@ export default defineConfig({ exclude: ['lucide-react'], }, build: { + // 确保生成带哈希的文件名(默认已启用) rollupOptions: { output: { + // 确保文件名包含哈希,便于缓存管理 + entryFileNames: 'assets/[name]-[hash].js', + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash].[ext]', manualChunks: { // React 核心 'vendor-react': ['react', 'react-dom'],