From 534e1ba153d1062f30f6b673fa0baaccc6f8e962 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 9 Jun 2025 19:06:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8F=90=E7=A4=BA=E6=A1=86?= =?UTF-8?q?=E7=9A=84=E5=BC=B9=E5=87=BA=E4=BD=8D=E7=BD=AE=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/axios-client.ts | 10 +- app/api/files/files-upload.ts | 5 +- app/components/reviews/ReviewPointsList.tsx | 84 +++++- app/components/ui/Tooltip.tsx | 71 ++++- app/config/api-config.ts | 104 ++++++++ app/root.tsx | 2 +- docs/deployment-config.md | 119 +++++++++ docs/docker-deployment.md | 272 ++++++++++++++++++++ 8 files changed, 635 insertions(+), 32 deletions(-) create mode 100644 app/config/api-config.ts create mode 100644 docs/deployment-config.md create mode 100644 docs/docker-deployment.md diff --git a/app/api/axios-client.ts b/app/api/axios-client.ts index 26b2b03..4022d6e 100644 --- a/app/api/axios-client.ts +++ b/app/api/axios-client.ts @@ -1,5 +1,6 @@ import axios, { AxiosRequestConfig, AxiosResponse, isAxiosError } from 'axios'; import { mockData, type MockApiResponse } from './mock'; +import { API_BASE_URL, DOCUMENT_URL } from '../config/api-config'; /** * API响应类型 @@ -13,14 +14,15 @@ export type ApiResponse = { export type QueryParams = Record; -// 获取 API 基础 URL +// 获取 API 基础 URL (从配置文件导入) // const API_BASE_URL = 'http://172.16.0.58:8008'; -const API_BASE_URL = 'http://nas.7bm.co:3000'; +// const API_BASE_URL = 'http://nas.7bm.co:3000'; // const API_BASE_URL = 'http://172.18.0.100:3000'; // const API_BASE_URL = 'http://172.16.0.119:9000/admin'; -// 文档URL前缀 -export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/'; +// 文档URL前缀 (从配置文件导入) +// export const DOCUMENT_URL = 'http://nas.7bm.co:9000/docauditai/'; +export { DOCUMENT_URL }; // 是否使用模拟数据(开发环境使用) const USE_MOCK_DATA = false; // 设置为true使用模拟数据,避免API连接问题 diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index e02b70a..3fb2b6c 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -1,5 +1,6 @@ import { postgrestGet, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; +import { UPLOAD_URL } from '../../config/api-config'; // import { API_BASE_URL } from '../client'; /** @@ -153,13 +154,13 @@ export async function uploadDocumentToServer( formData.append('upload_info', JSON.stringify(uploadInfo)); // console.log('【调试】FormData准备完成:', JSON.stringify(uploadInfo)); - // console.log('【调试】准备发送请求到服务器:', 'http://172.16.0.58:8008/admin/documents/upload'); + // console.log('【调试】准备发送请求到服务器:', UPLOAD_URL); // 发送请求 // const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, { try { // console.log('【调试】开始fetch请求...'); - const response = await fetch('http://172.16.0.58:8008/admin/documents/upload', { + const response = await fetch(UPLOAD_URL, { // const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', { // const response = await fetch('http://172.16.0.119:8000/admin/documents/upload', { method: 'POST', diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 626affa..2017e5b 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -147,7 +147,8 @@ interface ReviewPointsListProps { let activeTooltip = { show: false, // 控制提示框是否显示 content: null as React.ReactNode, // 提示框内容(React节点) - position: { top: 0, left: 0 } // 提示框在屏幕上的位置 + position: { top: 0, left: 0 }, // 提示框在屏幕上的位置 + ready: false // 新增:控制是否已准备好显示 }; /** @@ -186,7 +187,10 @@ function TooltipPortal() { style={{ top: `${tooltip.position.top}px`, left: `${tooltip.position.left}px`, - transform: 'translate(-100%, -50%)' // 调整位置,使提示框在指针左侧居中显示 + transform: 'translate(-100%, -50%)', // 调整位置,使提示框在指针左侧居中显示 + opacity: tooltip.ready ? 1 : 0, // 根据ready状态控制透明度 + visibility: tooltip.ready ? 'visible' : 'hidden', // 使用visibility确保在位置计算时元素存在但不可见 + transition: 'opacity 0.15s ease-out' // 添加平滑过渡效果 }} > {tooltip.content} @@ -203,22 +207,66 @@ function TooltipPortal() { * @param position 显示位置坐标 */ function showTooltip(content: React.ReactNode, position: { top: number; left: number }): void { - // 更新全局状态对象 + // 先设置内容和位置,但不立即显示 activeTooltip = { show: true, content, - position + position, + ready: false // 初始设为未准备好 }; - // 触发自定义事件,通知TooltipPortal组件更新状态 + + // 触发事件,让TooltipPortal渲染tooltip(但不可见) window.dispatchEvent(new Event('tooltip-update')); + + // 使用RAF确保tooltip已渲染到DOM后再计算最终位置 + requestAnimationFrame(() => { + // 查找刚创建的tooltip元素 + const tooltipElement = document.querySelector('.fixed.bg-white.shadow-lg.rounded-md') as HTMLElement; + + if (tooltipElement) { + // 获取tooltip的实际尺寸 + const tooltipRect = tooltipElement.getBoundingClientRect(); + + // 重新计算位置,确保tooltip不会超出视口 + let adjustedTop = position.top; + let adjustedLeft = position.left; + + // 检查是否超出右边界 + if (adjustedLeft - tooltipRect.width < 0) { + adjustedLeft = tooltipRect.width + 10; // 留一些边距 + } + + // 检查是否超出上边界 + if (adjustedTop - tooltipRect.height / 2 < 0) { + adjustedTop = tooltipRect.height / 2 + 10; + } + + // 检查是否超出下边界 + if (adjustedTop + tooltipRect.height / 2 > window.innerHeight) { + adjustedTop = window.innerHeight - tooltipRect.height / 2 - 10; + } + + // 更新位置并设为准备好显示 + activeTooltip.position = { top: adjustedTop, left: adjustedLeft }; + activeTooltip.ready = true; + + // 再次触发事件更新显示状态 + window.dispatchEvent(new Event('tooltip-update')); + } else { + // 如果找不到tooltip元素,直接显示 + activeTooltip.ready = true; + window.dispatchEvent(new Event('tooltip-update')); + } + }); } /** * 隐藏提示框的辅助函数 */ function hideTooltip(): void { - // 设置为不显示状态 + // 设置为不显示状态并重置ready状态 activeTooltip.show = false; + activeTooltip.ready = false; // 触发自定义事件,通知TooltipPortal组件更新状态 window.dispatchEvent(new Event('tooltip-update')); } @@ -232,6 +280,7 @@ function hideTooltip(): void { */ const ReactTableTooltip = ({ content }: { content: string }) => { const [showTooltip, setShowTooltip] = useState(false); + const [renderedContent, setRenderedContent] = useState(null); const textRef = useRef(null); const isTableLike = content.includes('\t') && content.includes('\n'); @@ -245,7 +294,14 @@ const ReactTableTooltip = ({ content }: { content: string }) => { } }; - setTimeout(checkTextOverflow, 0); + // 预渲染内容并缓存 + if (isTableLike) { + setRenderedContent(renderReactTable(content)); + } else { + setRenderedContent(content); + } + + requestAnimationFrame(checkTextOverflow); window.addEventListener('resize', checkTextOverflow); return () => { window.removeEventListener('resize', checkTextOverflow); @@ -310,14 +366,14 @@ const ReactTableTooltip = ({ content }: { content: string }) => {
{showTooltip ? (
@@ -1561,8 +1617,8 @@ export function ReviewPointsList({ fieldElements.push(
- -

{messageContent}

+ +

{messageContent}

); @@ -1980,10 +2036,10 @@ export function ReviewPointsList({ {/* 建议内容显示区域 */} {reviewPoint.suggestion && ( -
-
+
+
-

{reviewPoint.suggestion}

+

{reviewPoint.suggestion}

)} diff --git a/app/components/ui/Tooltip.tsx b/app/components/ui/Tooltip.tsx index 6644120..a54202e 100644 --- a/app/components/ui/Tooltip.tsx +++ b/app/components/ui/Tooltip.tsx @@ -60,6 +60,9 @@ export function Tooltip({ // 获取实际显示状态 const isVisible = isControlled ? controlledVisible : visible; + // 添加一个状态来控制实际渲染,解决位置跳跃问题 + const [readyToShow, setReadyToShow] = useState(false); + // 引用DOM元素 const triggerRef = useRef(null); const tooltipRef = useRef(null); @@ -73,11 +76,17 @@ export function Tooltip({ if (!isControlled) { setVisible(newVisible); } - onVisibleChange?.(newVisible); - // 当显示状态变为false时,重置actualPlacement为初始placement + // 当显示状态变为false时,立即隐藏 if (!newVisible) { + setReadyToShow(false); + onVisibleChange?.(newVisible); + // 当显示状态变为false时,重置actualPlacement为初始placement setActualPlacement(placement); + } else { + // 当显示状态变为true时,先延迟设置readyToShow + // 这样允许位置预计算完成后再显示tooltip + onVisibleChange?.(newVisible); } }; @@ -97,7 +106,6 @@ export function Tooltip({ // 获取触发元素位置 const triggerRect = triggerRef.current.getBoundingClientRect(); - const tooltipRect = tooltipRef.current.getBoundingClientRect(); // 设置最大宽度 tooltipRef.current.style.maxWidth = typeof maxWidth === 'number' @@ -115,6 +123,16 @@ export function Tooltip({ tooltipRef.current.style.overflow = 'visible'; } + // 强制重新计算布局,确保maxHeight生效后再获取尺寸 + tooltipRef.current.style.visibility = 'hidden'; + tooltipRef.current.style.display = 'block'; + + // 触发重排,确保maxHeight限制已应用 + void tooltipRef.current.offsetHeight; + + // 获取应用maxHeight后的实际tooltip尺寸 + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + // 计算各个方向的位置 let top = 0, left = 0; let arrowTop = 0, arrowLeft = 0; @@ -311,6 +329,17 @@ export function Tooltip({ break; } } + + // 恢复可见性(位置计算完成后) + tooltipRef.current.style.visibility = 'visible'; + + // 如果位置已更新且还没有显示,现在可以显示了 + if (!readyToShow) { + // 使用requestAnimationFrame确保DOM更新后再显示 + requestAnimationFrame(() => { + setReadyToShow(true); + }); + } }; // 当组件首次挂载或placement改变时,重置actualPlacement @@ -318,12 +347,27 @@ export function Tooltip({ setActualPlacement(placement); }, [placement]); - // 计算提示框位置 + // 当isVisible状态变化时,处理初始化和清理工作 useEffect(() => { - if (!isVisible) return; + if (!isVisible) { + // 当tooltip隐藏时,重置readyToShow状态 + setReadyToShow(false); + return; + } - // 初始化位置 - updateTooltipPosition(); + // 创建一个隐藏的tooltip用于预计算位置 + // 使用一个隐藏的div来渲染tooltip,并获取其尺寸 + const preCalculatePosition = () => { + // 初始时先不显示,让updateTooltipPosition完成位置计算 + setReadyToShow(false); + + // 初始化位置计算 - 添加短暂延迟确保内容已渲染 + setTimeout(() => { + updateTooltipPosition(); + }, 10); + }; + + preCalculatePosition(); // 添加滚动和调整大小事件监听器 window.addEventListener('scroll', updateTooltipPosition, true); @@ -338,7 +382,7 @@ export function Tooltip({ window.removeEventListener('resize', updateTooltipPosition); clearInterval(intervalId); }; - }, [isVisible, placement, maxWidth, showArrow, fixedPlacement, actualPlacement]); + }, [isVisible, placement, maxWidth, showArrow, fixedPlacement]); // 处理点击外部关闭 useEffect(() => { @@ -389,18 +433,23 @@ export function Tooltip({ `tooltip-${actualPlacement}`, // 使用actualPlacement而不是placement `tooltip-${theme}`, rich ? 'tooltip-rich' : '', - isVisible ? 'tooltip-visible' : '', + readyToShow ? 'tooltip-visible' : 'tooltip-hidden', // 使用readyToShow控制可见性 fixedPlacement ? 'tooltip-fixed-placement' : '', className ].filter(Boolean).join(' '); - // 使用Portal渲染提示框 + // 使用Portal渲染提示框,但根据readyToShow来控制可见性样式 const tooltipPortal = isVisible && typeof document !== 'undefined' ? createPortal(
{renderTooltipContent()} {showArrow &&
} diff --git a/app/config/api-config.ts b/app/config/api-config.ts new file mode 100644 index 0000000..85e4e7e --- /dev/null +++ b/app/config/api-config.ts @@ -0,0 +1,104 @@ +/** + * API配置文件 + * 统一管理所有API地址,方便部署时修改 + * 支持环境变量覆盖配置 + */ + +// 环境配置类型 +interface ApiConfig { + // 主API基础URL + baseUrl: string; + // 文档服务URL + documentUrl: string; + // 文档上传API URL + uploadUrl: string; + // PostgREST URL (如果使用) + postgrestUrl?: string; +} + +// 不同环境的默认配置 +const configs: Record = { + // 开发环境 + development: { + baseUrl: 'http://nas.7bm.co:3000', + documentUrl: 'http://nas.7bm.co:9000/docauditai/', + uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload', + }, + + // 测试环境 + testing: { + baseUrl: 'http://172.18.0.100:3000', + documentUrl: 'http://nas.7bm.co:9000/docauditai/', + uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload', + }, + + // 生产环境 + production: { + baseUrl: 'http://nas.7bm.co:3000', + documentUrl: 'http://nas.7bm.co:9000/docauditai/', + uploadUrl: 'http://172.16.0.58:8008/admin/documents/upload', + }, + + // 备用配置 (可以根据需要添加更多环境) + staging: { + baseUrl: 'http://172.16.0.119:9000/admin', + documentUrl: 'http://nas.7bm.co:9000/docauditai/', + uploadUrl: 'http://172.16.0.119:8000/admin/documents/upload', + } +}; + +// 获取当前环境,默认为development +const getCurrentEnvironment = (): string => { + // 优先使用环境变量,然后使用 NODE_ENV + return process.env.NEXT_PUBLIC_API_ENV || process.env.NODE_ENV || 'development'; +}; + +// 从环境变量获取配置,如果环境变量不存在则使用默认配置 +const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => { + return { + baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || defaultConfig.baseUrl, + documentUrl: process.env.NEXT_PUBLIC_DOCUMENT_URL || defaultConfig.documentUrl, + uploadUrl: process.env.NEXT_PUBLIC_UPLOAD_URL || defaultConfig.uploadUrl, + postgrestUrl: process.env.NEXT_PUBLIC_POSTGREST_URL || defaultConfig.postgrestUrl, + }; +}; + +// 获取当前配置 +const getCurrentConfig = (): ApiConfig => { + const env = getCurrentEnvironment(); + const defaultConfig = configs[env] || configs.development; + + // 如果是浏览器环境,尝试从环境变量覆盖配置 + if (typeof window !== 'undefined' || process.env.NEXT_PUBLIC_API_BASE_URL) { + return getConfigFromEnv(defaultConfig); + } + + return defaultConfig; +}; + +// 导出当前环境的配置 +export const apiConfig = getCurrentConfig(); + +// 导出具体的配置项,方便使用 +export const { + baseUrl: API_BASE_URL, + documentUrl: DOCUMENT_URL, + uploadUrl: UPLOAD_URL, + postgrestUrl: POSTGREST_URL +} = apiConfig; + +// 导出所有配置,供调试使用 +export { configs }; + +// 工具函数:设置环境(主要用于测试) +export const setEnvironment = (env: string): ApiConfig => { + return configs[env] || configs.development; +}; + +// 调试信息(仅在开发环境显示) +// if (process.env.NODE_ENV === 'development') { + console.log('📦 API配置信息:', { + environment: getCurrentEnvironment(), + config: apiConfig + }); +// } \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 77d47d8..7d97f66 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -197,7 +197,7 @@ export function links() { { rel: "stylesheet", href: messageModalStyles }, { rel: "stylesheet", href: toastStyles }, // 添加 Antd 样式 - { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" }, + // { rel: "stylesheet", href: "https://cdn.jsdelivr.net/npm/antd@5/dist/reset.css" }, { rel: "icon", type: "image/svg+xml", href: "/logo.svg" }, // { rel: "preconnect", href: "https://fonts.googleapis.com" }, // { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, diff --git a/docs/deployment-config.md b/docs/deployment-config.md new file mode 100644 index 0000000..ba6013b --- /dev/null +++ b/docs/deployment-config.md @@ -0,0 +1,119 @@ +# 部署配置说明 + +## API 地址配置 + +本项目已将所有 API 地址统一配置在 `app/config/api-config.ts` 文件中,支持多种配置方式: + +### 1. 默认配置 + +在 `api-config.ts` 中定义了不同环境的默认配置: + +- **development** (开发环境) +- **testing** (测试环境) +- **production** (生产环境) +- **staging** (预发布环境) + +### 2. 环境变量配置 (推荐) + +可以通过环境变量覆盖默认配置,支持以下环境变量: + +```bash +# 指定环境类型 +NEXT_PUBLIC_API_ENV=production + +# API基础URL +NEXT_PUBLIC_API_BASE_URL=http://your-api-server.com:3000 + +# 文档服务URL +NEXT_PUBLIC_DOCUMENT_URL=http://your-document-server.com:9000/docauditai/ + +# 文档上传API URL +NEXT_PUBLIC_UPLOAD_URL=http://your-upload-server.com:8008/admin/documents/upload + +# PostgREST URL (可选) +NEXT_PUBLIC_POSTGREST_URL=http://your-postgrest-server.com:3000 +``` + +### 3. 部署方式 + +#### 方式一:环境变量文件 (推荐) + +1. 在项目根目录创建 `.env.local` 文件: + +```bash +# .env.local +NODE_ENV=production +NEXT_PUBLIC_API_BASE_URL=http://production-api.company.com:3000 +NEXT_PUBLIC_DOCUMENT_URL=http://production-docs.company.com:9000/docauditai/ +NEXT_PUBLIC_UPLOAD_URL=http://production-upload.company.com:8008/admin/documents/upload +``` + +2. 构建和启动应用: + +```bash +npm run build +npm start +``` + +#### 方式二:直接修改配置文件 + +直接编辑 `app/config/api-config.ts` 中对应环境的配置: + +```typescript +// 修改生产环境配置 +production: { + baseUrl: 'http://your-production-api.com:3000', + documentUrl: 'http://your-production-docs.com:9000/docauditai/', + uploadUrl: 'http://your-production-upload.com:8008/admin/documents/upload', +} +``` + +#### 方式三:Docker 环境变量 + +在 Docker 部署时传入环境变量: + +```bash +docker run -d \ + -e NODE_ENV=production \ + -e NEXT_PUBLIC_API_BASE_URL=http://api.company.com:3000 \ + -e NEXT_PUBLIC_DOCUMENT_URL=http://docs.company.com:9000/docauditai/ \ + -e NEXT_PUBLIC_UPLOAD_URL=http://upload.company.com:8008/admin/documents/upload \ + your-app-image +``` + +### 4. 配置项说明 + +| 配置项 | 用途 | 示例 | +|--------|------|------| +| `API_BASE_URL` | 主要API服务地址 | `http://nas.7bm.co:3000` | +| `DOCUMENT_URL` | 文档下载服务地址 | `http://nas.7bm.co:9000/docauditai/` | +| `UPLOAD_URL` | 文档上传API地址 | `http://172.16.0.58:8008/admin/documents/upload` | + +### 5. 文件影响范围 + +以下文件已更新使用统一配置: + +- `app/api/axios-client.ts` - 使用 `API_BASE_URL` 和 `DOCUMENT_URL` +- `app/api/files/files-upload.ts` - 使用 `UPLOAD_URL` + +### 6. 验证配置 + +在开发环境下,控制台会输出当前使用的配置信息,便于调试: + +``` +📦 API配置信息: { + environment: "development", + config: { + baseUrl: "http://nas.7bm.co:3000", + documentUrl: "http://nas.7bm.co:9000/docauditai/", + uploadUrl: "http://172.16.0.58:8008/admin/documents/upload" + } +} +``` + +### 7. 注意事项 + +1. 环境变量必须以 `NEXT_PUBLIC_` 开头才能在客户端使用 +2. 修改环境变量后需要重新构建应用 +3. URL 地址末尾的斜杠要保持一致 +4. 确保所有服务地址在目标环境中可访问 \ No newline at end of file diff --git a/docs/docker-deployment.md b/docs/docker-deployment.md new file mode 100644 index 0000000..c260b13 --- /dev/null +++ b/docs/docker-deployment.md @@ -0,0 +1,272 @@ +# Docker 部署指南 + +## 环境变量工作原理 + +### 在 Remix 项目中如何工作 + +1. **构建时环境变量**: 在 Docker 构建时,环境变量会被嵌入到构建产物中 +2. **运行时环境变量**: 通过 `dotenv` 包在应用启动时加载环境变量 +3. **客户端环境变量**: 以 `NEXT_PUBLIC_` 开头的变量可以在客户端访问 + +### API 配置文件的识别机制 + +我们的 `app/config/api-config.ts` 配置文件通过以下方式识别环境变量: + +```typescript +// 从环境变量获取配置,如果环境变量不存在则使用默认配置 +const getConfigFromEnv = (defaultConfig: ApiConfig): ApiConfig => { + return { + baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || defaultConfig.baseUrl, + documentUrl: process.env.NEXT_PUBLIC_DOCUMENT_URL || defaultConfig.documentUrl, + uploadUrl: process.env.NEXT_PUBLIC_UPLOAD_URL || defaultConfig.uploadUrl, + postgrestUrl: process.env.NEXT_PUBLIC_POSTGREST_URL || defaultConfig.postgrestUrl, + }; +}; +``` + +### 文件使用配置的映射关系 + +1. **app/api/axios-client.ts** 使用: + - `API_BASE_URL` (来自 `NEXT_PUBLIC_API_BASE_URL`) + - `DOCUMENT_URL` (来自 `NEXT_PUBLIC_DOCUMENT_URL`) + +2. **app/api/files/files-upload.ts** 使用: + - `UPLOAD_URL` (来自 `NEXT_PUBLIC_UPLOAD_URL`) + +## Docker 部署方式 + +### 方式一:直接使用 docker run 命令 + +```bash +# 构建镜像 +docker build -t docreview-app . + +# 运行容器,通过 -e 参数传入环境变量 +docker run -d \ + --name docreview \ + -p 3000:3000 \ + -e NODE_ENV=production \ + -e NEXT_PUBLIC_API_BASE_URL=http://your-api-server.com:3000 \ + -e NEXT_PUBLIC_DOCUMENT_URL=http://your-docs-server.com:9000/docauditai/ \ + -e NEXT_PUBLIC_UPLOAD_URL=http://your-upload-server.com:8008/admin/documents/upload \ + docreview-app +``` + +### 方式二:使用 docker-compose + +1. **使用内联环境变量**: + +```yaml +# docker-compose.yml +version: '3.8' +services: + docreview-app: + build: . + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - NEXT_PUBLIC_API_BASE_URL=http://api.company.com:3000 + - NEXT_PUBLIC_DOCUMENT_URL=http://docs.company.com:9000/docauditai/ + - NEXT_PUBLIC_UPLOAD_URL=http://upload.company.com:8008/admin/documents/upload +``` + +2. **使用环境变量文件**: + +创建 `.env.production` 文件: +```bash +NODE_ENV=production +NEXT_PUBLIC_API_BASE_URL=http://api.company.com:3000 +NEXT_PUBLIC_DOCUMENT_URL=http://docs.company.com:9000/docauditai/ +NEXT_PUBLIC_UPLOAD_URL=http://upload.company.com:8008/admin/documents/upload +``` + +然后在 docker-compose.yml 中引用: +```yaml +services: + docreview-app: + build: . + ports: + - "3000:3000" + env_file: + - .env.production +``` + +### 方式三:Kubernetes 部署 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: docreview-app +spec: + replicas: 2 + selector: + matchLabels: + app: docreview-app + template: + metadata: + labels: + app: docreview-app + spec: + containers: + - name: docreview-app + image: docreview-app:latest + ports: + - containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + - name: NEXT_PUBLIC_API_BASE_URL + value: "http://api.company.com:3000" + - name: NEXT_PUBLIC_DOCUMENT_URL + value: "http://docs.company.com:9000/docauditai/" + - name: NEXT_PUBLIC_UPLOAD_URL + value: "http://upload.company.com:8008/admin/documents/upload" +``` + +## 环境变量验证 + +### 在容器内检查环境变量 + +```bash +# 进入运行中的容器 +docker exec -it docreview /bin/sh + +# 查看环境变量 +env | grep NEXT_PUBLIC + +# 或者查看具体变量 +echo $NEXT_PUBLIC_API_BASE_URL +echo $NEXT_PUBLIC_DOCUMENT_URL +echo $NEXT_PUBLIC_UPLOAD_URL +``` + +### 应用内验证 + +在开发模式下,配置文件会输出当前使用的配置: +``` +📦 API配置信息: { + environment: "production", + config: { + baseUrl: "http://api.company.com:3000", + documentUrl: "http://docs.company.com:9000/docauditai/", + uploadUrl: "http://upload.company.com:8008/admin/documents/upload" + } +} +``` + +## 构建时的环境变量注入 + +### 方式一:Docker 构建时传入 + +```bash +docker build \ + --build-arg NEXT_PUBLIC_API_BASE_URL=http://api.company.com:3000 \ + --build-arg NEXT_PUBLIC_DOCUMENT_URL=http://docs.company.com:9000/docauditai/ \ + --build-arg NEXT_PUBLIC_UPLOAD_URL=http://upload.company.com:8008/admin/documents/upload \ + -t docreview-app . +``` + +然后在 Dockerfile 中添加: +```dockerfile +# 在构建阶段添加 +ARG NEXT_PUBLIC_API_BASE_URL +ARG NEXT_PUBLIC_DOCUMENT_URL +ARG NEXT_PUBLIC_UPLOAD_URL + +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL +ENV NEXT_PUBLIC_DOCUMENT_URL=$NEXT_PUBLIC_DOCUMENT_URL +ENV NEXT_PUBLIC_UPLOAD_URL=$NEXT_PUBLIC_UPLOAD_URL +``` + +## 多环境配置示例 + +### 开发环境 +```bash +# .env.development +NODE_ENV=development +NEXT_PUBLIC_API_BASE_URL=http://localhost:8008 +NEXT_PUBLIC_DOCUMENT_URL=http://localhost:9000/docauditai/ +NEXT_PUBLIC_UPLOAD_URL=http://localhost:8008/admin/documents/upload +``` + +### 测试环境 +```bash +# .env.testing +NODE_ENV=testing +NEXT_PUBLIC_API_BASE_URL=http://test-api.company.com:3000 +NEXT_PUBLIC_DOCUMENT_URL=http://test-docs.company.com:9000/docauditai/ +NEXT_PUBLIC_UPLOAD_URL=http://test-upload.company.com:8008/admin/documents/upload +``` + +### 生产环境 +```bash +# .env.production +NODE_ENV=production +NEXT_PUBLIC_API_BASE_URL=http://api.company.com:3000 +NEXT_PUBLIC_DOCUMENT_URL=http://docs.company.com:9000/docauditai/ +NEXT_PUBLIC_UPLOAD_URL=http://upload.company.com:8008/admin/documents/upload +``` + +## 部署脚本示例 + +创建 `deploy.sh` 脚本: + +```bash +#!/bin/bash + +# 部署脚本 +ENV=${1:-production} + +echo "部署环境: $ENV" + +# 构建镜像 +docker build -t docreview-app:$ENV . + +# 停止旧容器 +docker stop docreview-$ENV 2>/dev/null || true +docker rm docreview-$ENV 2>/dev/null || true + +# 根据环境启动容器 +case $ENV in + "development") + docker run -d \ + --name docreview-$ENV \ + -p 3001:3000 \ + --env-file .env.development \ + docreview-app:$ENV + ;; + "testing") + docker run -d \ + --name docreview-$ENV \ + -p 3002:3000 \ + --env-file .env.testing \ + docreview-app:$ENV + ;; + "production") + docker run -d \ + --name docreview-$ENV \ + -p 3000:3000 \ + --env-file .env.production \ + docreview-app:$ENV + ;; +esac + +echo "部署完成,容器名: docreview-$ENV" +``` + +使用方法: +```bash +chmod +x deploy.sh +./deploy.sh production # 部署生产环境 +./deploy.sh testing # 部署测试环境 +``` + +## 注意事项 + +1. **环境变量优先级**:Docker 环境变量 > 环境变量文件 > 默认配置 +2. **客户端变量**:必须以 `NEXT_PUBLIC_` 开头才能在客户端使用 +3. **构建时注入**:客户端环境变量在构建时被注入,运行时修改不会生效 +4. **安全性**:不要在环境变量中存储敏感信息,如API密钥等 +5. **网络访问**:确保容器能够访问配置的API地址 \ No newline at end of file