feat: add deploy script
This commit is contained in:
145
deploy.sh
Executable file
145
deploy.sh
Executable file
@@ -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
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { translations, Language, TranslationKey } from '../lib/translations';
|
import { translations, Language, TranslationKey } from '../lib/translations';
|
||||||
|
import { detectLanguageByIP } from '../lib/ipLocation';
|
||||||
|
|
||||||
interface LanguageContextType {
|
interface LanguageContextType {
|
||||||
language: Language;
|
language: Language;
|
||||||
@@ -10,12 +11,36 @@ interface LanguageContextType {
|
|||||||
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
// 初始化语言:优先使用 localStorage,否则使用浏览器语言作为临时值
|
||||||
const [language, setLanguageState] = useState<Language>(() => {
|
const [language, setLanguageState] = useState<Language>(() => {
|
||||||
const saved = localStorage.getItem('language');
|
const saved = localStorage.getItem('language');
|
||||||
if (saved === 'en' || saved === 'zh') return saved;
|
if (saved === 'en' || saved === 'zh') return saved;
|
||||||
|
// 临时使用浏览器语言,后续会被IP检测覆盖(如果没有保存的语言)
|
||||||
return navigator.language.startsWith('zh') ? 'zh' : 'en';
|
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) => {
|
const setLanguage = (lang: Language) => {
|
||||||
setLanguageState(lang);
|
setLanguageState(lang);
|
||||||
localStorage.setItem('language', lang);
|
localStorage.setItem('language', lang);
|
||||||
|
|||||||
78
src/lib/ipLocation.ts
Normal file
78
src/lib/ipLocation.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* IP 地理位置检测工具
|
||||||
|
* 用于根据用户IP地址判断语言偏好
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface IPLocationResponse {
|
||||||
|
country_code?: string;
|
||||||
|
country?: string;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据IP地址检测用户所在国家/地区
|
||||||
|
* 使用免费的 ipapi.co 服务(无需API key)
|
||||||
|
*
|
||||||
|
* @returns Promise<string | null> 返回国家代码(如 'CN', 'US'),失败返回 null
|
||||||
|
*/
|
||||||
|
export async function detectCountryByIP(): Promise<string | null> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -8,8 +8,13 @@ export default defineConfig({
|
|||||||
exclude: ['lucide-react'],
|
exclude: ['lucide-react'],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
// 确保生成带哈希的文件名(默认已启用)
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
|
// 确保文件名包含哈希,便于缓存管理
|
||||||
|
entryFileNames: 'assets/[name]-[hash].js',
|
||||||
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
// React 核心
|
// React 核心
|
||||||
'vendor-react': ['react', 'react-dom'],
|
'vendor-react': ['react', 'react-dom'],
|
||||||
|
|||||||
Reference in New Issue
Block a user