更新 env变量使用收口到env.ts,constants暂时移除

This commit is contained in:
moux1024 2025-09-29 14:18:01 +08:00
parent 0b63661ae0
commit fa524af5b5
18 changed files with 268 additions and 87 deletions

View File

@ -4,7 +4,7 @@
### 目录结构与职责 ### 目录结构与职责
- `constants.ts`:基础配置(`BASE_URL``NEXT_PUBLIC_BASE_URL` 注入) - 基础配置从 `@/lib/env` 导入 `baseUrl`
- `request.ts`Axios 实例与拦截器、通用 `get/post/put/del``stream`SSE风格下载进度`downloadStream``streamJsonPost` - `request.ts`Axios 实例与拦截器、通用 `get/post/put/del``stream`SSE风格下载进度`downloadStream``streamJsonPost`
- `errorHandle.ts`:错误码映射与统一提示、特殊码处理(如 401 跳转登录、402 不弹提示) - `errorHandle.ts`:错误码映射与统一提示、特殊码处理(如 401 跳转登录、402 不弹提示)
- `common.ts`:通用类型与与上传相关的工具(获取七牛 Token、上传 - `common.ts`:通用类型与与上传相关的工具(获取七牛 Token、上传
@ -16,7 +16,7 @@
1. 使用 `request.ts` 提供的 `get/post/put/del` 包装函数发起请求,返回后端响应体(已通过响应拦截器做业务码检查)。 1. 使用 `request.ts` 提供的 `get/post/put/del` 包装函数发起请求,返回后端响应体(已通过响应拦截器做业务码检查)。
2. 业务成功码:`code === 0``code === 202`(长任务/排队等需要前端自行处理状态)。若非成功码,拦截器会调用 `errorHandle``Promise.reject` 2. 业务成功码:`code === 0``code === 202`(长任务/排队等需要前端自行处理状态)。若非成功码,拦截器会调用 `errorHandle``Promise.reject`
3. 认证:前端从 `localStorage.token` 注入 `Authorization: Bearer <token>`,请确保登录流程写入 `token` 3. 认证:前端从 `localStorage.token` 注入 `Authorization: Bearer <token>`,请确保登录流程写入 `token`
4. 基础地址:通过环境变量 `NEXT_PUBLIC_BASE_URL` 注入,构建前需设置 4. 基础地址:`@/lib/env``baseUrl` 获取,统一管理环境变量
### 错误处理约定 ### 错误处理约定
@ -90,7 +90,7 @@ await downloadStream('/download/file', 'result.mp4');
#### 浏览器前端React/Next.js CSR #### 浏览器前端React/Next.js CSR
- 直接使用 `get/post/put/del`;确保登录后将 `token` 写入 `localStorage` - 直接使用 `get/post/put/del`;确保登录后将 `token` 写入 `localStorage`
- 环境变量:在 `.env.local` 配置 `NEXT_PUBLIC_BASE_URL` - 环境变量:在 `.env.local` 配置 `NEXT_PUBLIC_BASE_URL`,通过 `@/lib/env` 统一管理
- 错误提示:由 `errorHandle` 统一处理402 会展示积分不足通知 - 错误提示:由 `errorHandle` 统一处理402 会展示积分不足通知
#### Next.js Route Handler服务端 API #### Next.js Route Handler服务端 API
@ -101,7 +101,7 @@ await downloadStream('/download/file', 'result.mp4');
#### Next.js Server Components/SSR #### Next.js Server Components/SSR
- 服务端不具备 `localStorage`,如需鉴权请改为从 Cookie/Headers 传递 token并在转发时设置 `Authorization` - 服务端不具备 `localStorage`,如需鉴权请改为从 Cookie/Headers 传递 token并在转发时设置 `Authorization`
- 服务器端可直接使用 `fetch(BASE_URL + path, { headers })` - 服务器端可直接使用 `fetch(baseUrl + path, { headers })`
#### Node/ServerlessVercel/Cloudflare #### Node/ServerlessVercel/Cloudflare

View File

@ -1,5 +1,5 @@
// Common API 相关接口 // Common API 相关接口
import { BASE_URL } from './constants' import { baseUrl } from '@/lib/env';
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
code: number code: number
@ -44,7 +44,7 @@ export const getUploadToken = async (timeoutMs: number = 10000): Promise<{ token
}, timeoutMs) }, timeoutMs)
try { try {
const response = await fetch(`${BASE_URL}/common/get-upload-token`, { const response = await fetch(`${baseUrl}/common/get-upload-token`, {
method: "GET", method: "GET",
headers: { headers: {
Accept: "application/json", Accept: "application/json",

View File

@ -1,4 +0,0 @@
export const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
// export const BASE_URL = 'https://77.smartvideo.py.qikongjian.com'
// export const BASE_URL ='http://192.168.120.5:8000'
//

View File

@ -1,5 +1,5 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios'; import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig, AxiosHeaders } from 'axios';
import { BASE_URL } from './constants' import { baseUrl } from '@/lib/env';
import { errorHandle } from './errorHandle'; import { errorHandle } from './errorHandle';
/** /**
@ -20,7 +20,7 @@ const handleRequestError = (error: any, defaultMessage: string = '请求失败')
}; };
// 创建 axios 实例 // 创建 axios 实例
const request: AxiosInstance = axios.create({ const request: AxiosInstance = axios.create({
baseURL: BASE_URL, // 设置基础URL baseURL: baseUrl, // 设置基础URL
timeout: 300000, // 请求超时时间 timeout: 300000, // 请求超时时间
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -102,7 +102,7 @@ export async function streamJsonPost<T = any>(
) { ) {
try { try {
const token = localStorage?.getItem('token') || ''; const token = localStorage?.getItem('token') || '';
const response = await fetch(`${BASE_URL}${url}`, { const response = await fetch(`${baseUrl}${url}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,4 +1,4 @@
import { BASE_URL } from "./constants"; import { baseUrl } from '@/lib/env';
import { post } from './request'; import { post } from './request';
// 获取路演配置数据 // 获取路演配置数据
@ -8,7 +8,7 @@ export const fetchRoadshowConfigs = async () => {
try { try {
console.log('开始请求接口数据...'); console.log('开始请求接口数据...');
const response = await fetch(BASE_URL + '/serversetting/roadshow-configs', { const response = await fetch(baseUrl + '/serversetting/roadshow-configs', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,7 +1,6 @@
import { post, streamJsonPost } from "./request"; import { post, streamJsonPost } from "./request";
import { ProjectTypeEnum } from "@/app/model/enums"; import { ProjectTypeEnum } from "@/app/model/enums";
import { ApiResponse } from "@/api/common"; import { ApiResponse } from "@/api/common";
import { BASE_URL } from "./constants";
import { import {
AITextEntity, AITextEntity,
RoleEntity, RoleEntity,

View File

@ -5,6 +5,7 @@ import { Providers } from '@/components/providers';
import { ConfigProvider, theme } from 'antd'; import { ConfigProvider, theme } from 'antd';
import CallbackModal from '@/components/common/CallbackModal'; import CallbackModal from '@/components/common/CallbackModal';
import { useAppStartupAnalytics } from '@/hooks/useAppStartupAnalytics'; import { useAppStartupAnalytics } from '@/hooks/useAppStartupAnalytics';
import { gaEnabled, gaMeasurementId } from '@/lib/env';
// 创建上下文来传递弹窗控制方法 // 创建上下文来传递弹窗控制方法
const CallbackModalContext = createContext<{ const CallbackModalContext = createContext<{
@ -53,16 +54,16 @@ export default function RootLayout({
<link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" /> <link rel="icon" type="image/x-icon" sizes="32x32" href="/favicon.ico?v=1" />
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" /> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=1" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" /> <link rel="apple-touch-icon" sizes="180x180" href="/favicon.ico?v=1" />
{process.env.NEXT_PUBLIC_GA_ENABLED === 'true' && ( {gaEnabled && (
<> <>
<script async src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}`}></script> <script async src={`https://www.googletagmanager.com/gtag/js?id=${gaMeasurementId}`}></script>
<script <script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
window.dataLayer = window.dataLayer || []; window.dataLayer = window.dataLayer || [];
function gtag(){window.dataLayer.push(arguments);} function gtag(){window.dataLayer.push(arguments);}
gtag('js', new Date()); gtag('js', new Date());
gtag('config', '${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}', { gtag('config', '${gaMeasurementId}', {
page_title: document.title, page_title: document.title,
page_location: window.location.href, page_location: window.location.href,
send_page_view: true send_page_view: true

View File

@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { CheckCircle, XCircle, Loader2, AlertTriangle } from "lucide-react"; import { CheckCircle, XCircle, Loader2, AlertTriangle } from "lucide-react";
import type { OAuthCallbackParams } from "@/app/types/google-oauth"; import type { OAuthCallbackParams } from "@/app/types/google-oauth";
import { baseUrl } from '@/lib/env';
// 根据后端实际返回格式定义响应类型 // 根据后端实际返回格式定义响应类型
interface GoogleOAuthResponse { interface GoogleOAuthResponse {
@ -98,8 +99,7 @@ export default function OAuthCallback() {
console.log('最终使用的邀请码:', finalInviteCode); console.log('最终使用的邀请码:', finalInviteCode);
// 根据 jiekou.md 文档调用统一的 Python OAuth 接口 // 根据 jiekou.md 文档调用统一的 Python OAuth 接口
// 使用 NEXT_PUBLIC_BASE_URL 配置,默认为 https://77.smartvideo.py.qikongjian.com // 使用统一配置中的 baseUrl
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || 'https://77.smartvideo.py.qikongjian.com';
console.log('🔧 调用 Python OAuth 接口:', baseUrl); console.log('🔧 调用 Python OAuth 接口:', baseUrl);
const response = await fetch(`${baseUrl}/api/oauth/google`, { const response = await fetch(`${baseUrl}/api/oauth/google`, {

View File

@ -8,6 +8,7 @@
"use client"; "use client";
import React, { useState, useCallback, useEffect, useRef } from 'react'; import React, { useState, useCallback, useEffect, useRef } from 'react';
import { cutUrl } from '@/lib/env';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
Zap, Zap,
@ -95,7 +96,6 @@ export const AIEditingIframe = React.forwardRef<AIEditingIframeHandle, AIEditing
const iframeRef = useRef<HTMLIFrameElement>(null); const iframeRef = useRef<HTMLIFrameElement>(null);
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null); const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const cutUrl = process.env.NEXT_PUBLIC_CUT_URL || 'https://cut.movieflow.ai';
console.log('cutUrl', cutUrl); console.log('cutUrl', cutUrl);
// 构建智能剪辑URL // 构建智能剪辑URL

View File

@ -8,6 +8,7 @@ import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit'; import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector'; import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
import { useDeviceType } from '@/hooks/useDeviceType'; import { useDeviceType } from '@/hooks/useDeviceType';
import { cutUrlTo, errorConfig } from '@/lib/env';
interface UseWorkflowDataProps { interface UseWorkflowDataProps {
onEditPlanGenerated?: () => void; onEditPlanGenerated?: () => void;
@ -40,7 +41,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
const { isMobile, isTablet, isDesktop } = useDeviceType(); const { isMobile, isTablet, isDesktop } = useDeviceType();
const cutUrl = process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai'; const cutUrl = cutUrlTo;
console.log('cutUrl', cutUrl); console.log('cutUrl', cutUrl);
useEffect(() => { useEffect(() => {
@ -77,7 +78,6 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
} }
}); });
let loadingText: any = useRef(LOADING_TEXT_MAP.getInfo); let loadingText: any = useRef(LOADING_TEXT_MAP.getInfo);
const errorConfig = Number(process.env.NEXT_PUBLIC_ERROR_CONFIG);
// 更新 taskObject 的类型 // 更新 taskObject 的类型

View File

@ -2,6 +2,8 @@
* *
*/ */
import { getVideoEditApiConfig as getEnvVideoEditConfig, isDevelopment } from '@/lib/env';
export interface VideoEditApiConfig { export interface VideoEditApiConfig {
/** 是否使用Mock API */ /** 是否使用Mock API */
useMockApi: boolean; useMockApi: boolean;
@ -26,32 +28,23 @@ export const defaultVideoEditApiConfig: VideoEditApiConfig = {
remoteApiBase: '/video-edit', remoteApiBase: '/video-edit',
localApiBase: '/api/video-edit', localApiBase: '/api/video-edit',
timeout: 10000, timeout: 10000,
enableDebugLog: process.env.NODE_ENV === 'development' enableDebugLog: isDevelopment
}; };
/** /**
* API配置 * API配置
*/ */
export function getVideoEditApiConfig(): VideoEditApiConfig { export function getVideoEditApiConfig(): VideoEditApiConfig {
// 可以从环境变量或其他配置源读 // 从统一环境配置获
const config = { ...defaultVideoEditApiConfig }; const envConfig = getEnvVideoEditConfig();
// 环境变量覆盖 return {
if (process.env.NEXT_PUBLIC_VIDEO_EDIT_USE_MOCK === 'true') { ...defaultVideoEditApiConfig,
config.useMockApi = true; useMockApi: envConfig.useMockApi,
config.useLocalApi = false; useLocalApi: !envConfig.useRemoteApi,
} remoteApiBase: envConfig.remoteApiBase,
enableDebugLog: envConfig.enableDebugLog,
if (process.env.NEXT_PUBLIC_VIDEO_EDIT_USE_REMOTE === 'true') { };
config.useLocalApi = false;
config.useMockApi = false;
}
if (process.env.NEXT_PUBLIC_VIDEO_EDIT_REMOTE_BASE) {
config.remoteApiBase = process.env.NEXT_PUBLIC_VIDEO_EDIT_REMOTE_BASE;
}
return config;
} }
/** /**
@ -155,7 +148,7 @@ export const errorHandlingConfig = {
*/ */
export const performanceConfig = { export const performanceConfig = {
/** 是否启用性能监控 */ /** 是否启用性能监控 */
enabled: process.env.NODE_ENV === 'development', enabled: isDevelopment,
/** 慢请求阈值(毫秒) */ /** 慢请求阈值(毫秒) */
slowRequestThreshold: 2000, slowRequestThreshold: 2000,
/** 是否记录所有请求 */ /** 是否记录所有请求 */
@ -193,7 +186,7 @@ export const uiConfig = {
/** 描述最大长度 */ /** 描述最大长度 */
maxDescriptionLength: 500, maxDescriptionLength: 500,
/** 是否显示调试信息 */ /** 是否显示调试信息 */
showDebugInfo: process.env.NODE_ENV === 'development', showDebugInfo: isDevelopment,
/** 动画配置 */ /** 动画配置 */
animations: { animations: {
enabled: true, enabled: true,

View File

@ -1,4 +1,5 @@
// src/components/VantaHaloBackground.jsx // src/components/VantaHaloBackground.jsx
// 未使用
'use client'; 'use client';
import React, { useRef, useEffect, memo } from 'react' import React, { useRef, useEffect, memo } from 'react'

View File

@ -8,11 +8,9 @@ import type {
OAuthState OAuthState
} from '@/app/types/google-oauth'; } from '@/app/types/google-oauth';
import { setUserProperties } from '@/utils/analytics'; import { setUserProperties } from '@/utils/analytics';
import { javaUrl, baseUrl, googleClientId, getGoogleRedirectUri } from '@/lib/env';
// API配置 // API配置 - 直接使用导入的配置变量
//const JAVA_BASE_URL = 'http://192.168.120.36:8080';
const JAVA_BASE_URL = process.env.NEXT_PUBLIC_JAVA_URL || 'https://77.app.java.auth.qikongjian.com';
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL
// Token存储键 // Token存储键
const TOKEN_KEY = 'token'; const TOKEN_KEY = 'token';
@ -43,7 +41,7 @@ type RegisterUserResponse = {
*/ */
export const loginUser = async (email: string, password: string) => { export const loginUser = async (email: string, password: string) => {
try { try {
const response = await fetch(`${JAVA_BASE_URL}/api/user/login`, { const response = await fetch(`${javaUrl}/api/user/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -205,9 +203,6 @@ export const authFetch = async (url: string, options: RequestInit = {}) => {
// Google OAuth相关函数 // Google OAuth相关函数
// Google Client ID - 从环境变量获取
const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com';
/** /**
* Google GSI SDK * Google GSI SDK
*/ */
@ -268,10 +263,8 @@ export const signInWithGoogle = async (inviteCode?: string): Promise<void> => {
} }
} }
// 从环境变量获取配置 // 从统一配置获取配置
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com'; const redirectUri = getGoogleRedirectUri();
const javaBaseUrl = process.env.NEXT_PUBLIC_JAVA_URL || 'https://auth.test.movieflow.ai';
const redirectUri = process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI || `${javaBaseUrl}/api/auth/google/callback`;
// 生成随机nonce用于安全验证 // 生成随机nonce用于安全验证
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32))) const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)))
@ -286,12 +279,9 @@ export const signInWithGoogle = async (inviteCode?: string): Promise<void> => {
}; };
console.log('使用的配置:', { console.log('使用的配置:', {
clientId, clientId: googleClientId,
javaBaseUrl, javaBaseUrl: javaUrl,
redirectUri, redirectUri
envClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
envJavaUrl: process.env.NEXT_PUBLIC_JAVA_URL,
envRedirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI
}); });
// 详细的调试日志 // 详细的调试日志
@ -300,14 +290,14 @@ export const signInWithGoogle = async (inviteCode?: string): Promise<void> => {
console.log(' - 当前协议:', window.location.protocol); console.log(' - 当前协议:', window.location.protocol);
console.log(' - 当前端口:', window.location.port); console.log(' - 当前端口:', window.location.port);
console.log(' - 完整 origin:', window.location.origin); console.log(' - 完整 origin:', window.location.origin);
console.log(' - 环境变量 NEXT_PUBLIC_GOOGLE_REDIRECT_URI:', process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI); console.log(' - 环境变量 NEXT_PUBLIC_GOOGLE_REDIRECT_URI:', redirectUri);
console.log(' - 最终使用的 redirect_uri:', redirectUri); console.log(' - 最终使用的 redirect_uri:', redirectUri);
console.log(' - Google Client ID:', GOOGLE_CLIENT_ID); console.log(' - Google Client ID:', googleClientId);
// 构建Google OAuth2授权URL // 构建Google OAuth2授权URL
const authParams = new URLSearchParams({ const authParams = new URLSearchParams({
access_type: 'online', access_type: 'online',
client_id: clientId, client_id: googleClientId,
nonce: nonce, nonce: nonce,
redirect_uri: redirectUri, redirect_uri: redirectUri,
response_type: 'code', // 使用授权码模式 response_type: 'code', // 使用授权码模式
@ -362,7 +352,7 @@ export const loginWithGoogleToken = async (idToken: string, action: 'login' | 'r
inviteCode inviteCode
}; };
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/login`, { const response = await fetch(`${javaUrl}/api/auth/google/login`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -417,7 +407,7 @@ export const bindGoogleAccount = async (bindToken: string, idToken?: string) =>
confirm: true confirm: true
}; };
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/bind`, { const response = await fetch(`${javaUrl}/api/auth/google/bind`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -453,7 +443,7 @@ export const getGoogleBindStatus = async () => {
throw new Error('User not authenticated'); throw new Error('User not authenticated');
} }
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/status`, { const response = await fetch(`${javaUrl}/api/auth/google/status`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -529,7 +519,7 @@ export const getUserProfile = async (): Promise<any> => {
throw new Error('No token available'); throw new Error('No token available');
} }
const response = await fetch(`${BASE_URL}/auth/profile`, { const response = await fetch(`${baseUrl}/auth/profile`, {
method: 'GET', method: 'GET',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -638,7 +628,7 @@ export const registerUser = async ({
inviteCode?: string; inviteCode?: string;
}): Promise<any> => { }): Promise<any> => {
try { try {
const response = await fetch(`${BASE_URL}/api/user/register`, { const response = await fetch(`${baseUrl}/api/user/register`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -676,7 +666,7 @@ export const registerUserWithInvite = async ({
invite_code?: string; invite_code?: string;
}): Promise<RegisterUserResponse> => { }): Promise<RegisterUserResponse> => {
try { try {
const response = await fetch(`${BASE_URL}/api/user_fission/register_with_invite`, { const response = await fetch(`${baseUrl}/api/user_fission/register_with_invite`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -707,7 +697,7 @@ export const registerUserWithInvite = async ({
*/ */
export const sendVerificationLink = async (email: string) => { export const sendVerificationLink = async (email: string) => {
try { try {
const response = await fetch(`${JAVA_BASE_URL}/api/user/sendVerificationLink?email=${email}`); const response = await fetch(`${javaUrl}/api/user/sendVerificationLink?email=${email}`);
const data = await response.json(); const data = await response.json();
if(!data.success){ if(!data.success){
throw new Error(data.message||data.msg) throw new Error(data.message||data.msg)

202
lib/env.ts Normal file
View File

@ -0,0 +1,202 @@
/**
*
*
*/
/**
*
*/
export interface EnvConfig {
// 基础配置
nodeEnv: string;
isDevelopment: boolean;
isProduction: boolean;
// API 基础 URL 配置
baseUrl: string;
javaUrl: string;
cutUrl: string;
cutUrlTo: string;
// Google OAuth 配置
googleClientId: string;
googleRedirectUri: string;
// Google Analytics 配置
gaEnabled: boolean;
gaMeasurementId: string;
// 视频编辑配置
videoEditUseMock: boolean;
videoEditUseRemote: boolean;
videoEditRemoteBase: string;
// 其他配置
errorConfig: number;
}
/**
*
*/
export const getEnvConfig = (): EnvConfig => {
const nodeEnv = process.env.NODE_ENV || 'development';
return {
// 基础配置
nodeEnv,
isDevelopment: nodeEnv === 'development',
isProduction: nodeEnv === 'production',
// API 基础 URL 配置
baseUrl: process.env.NEXT_PUBLIC_BASE_URL || 'https://77.smartvideo.py.qikongjian.com',
javaUrl: process.env.NEXT_PUBLIC_JAVA_URL || 'https://77.app.java.auth.qikongjian.com',
cutUrl: process.env.NEXT_PUBLIC_CUT_URL || 'https://smartcut.api.movieflow.ai',
cutUrlTo: process.env.NEXT_PUBLIC_CUT_URL_TO || 'https://smartcut.api.movieflow.ai',
// Google OAuth 配置
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || '847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com',
googleRedirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI || '',
// Google Analytics 配置
gaEnabled: process.env.NEXT_PUBLIC_GA_ENABLED === 'true',
gaMeasurementId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-4BDXV6TWF4',
// 视频编辑配置
videoEditUseMock: process.env.NEXT_PUBLIC_VIDEO_EDIT_USE_MOCK === 'true',
videoEditUseRemote: process.env.NEXT_PUBLIC_VIDEO_EDIT_USE_REMOTE === 'true',
videoEditRemoteBase: process.env.NEXT_PUBLIC_VIDEO_EDIT_REMOTE_BASE || '/video-edit',
// 其他配置
errorConfig: Number(process.env.NEXT_PUBLIC_ERROR_CONFIG) || 0,
};
};
/**
*
*/
export const env = getEnvConfig();
/**
*
*/
export const {
// 基础配置
nodeEnv,
isDevelopment,
isProduction,
// API 基础 URL 配置
baseUrl,
javaUrl,
cutUrl,
cutUrlTo,
// Google OAuth 配置
googleClientId,
googleRedirectUri,
// Google Analytics 配置
gaEnabled,
gaMeasurementId,
// 视频编辑配置
videoEditUseMock,
videoEditUseRemote,
videoEditRemoteBase,
// 其他配置
errorConfig,
} = env;
/**
* Google OAuth URI
*/
export const getGoogleRedirectUri = (): string => {
if (googleRedirectUri) {
return googleRedirectUri;
}
return `${javaUrl}/api/auth/google/callback`;
};
/**
* Google Analytics
*/
export const isGAAvailable = (): boolean => {
return typeof window !== 'undefined' &&
typeof window.gtag === 'function' &&
gaEnabled;
};
/**
* API
*/
export const getVideoEditApiConfig = () => {
return {
useMockApi: videoEditUseMock,
useRemoteApi: videoEditUseRemote,
remoteApiBase: videoEditRemoteBase,
localApiBase: '/api/video-edit',
enableDebugLog: isDevelopment,
};
};
/**
*
*/
export const validateEnvConfig = (): { isValid: boolean; errors: string[] } => {
const errors: string[] = [];
// 验证必需的配置
if (!baseUrl) {
errors.push('NEXT_PUBLIC_BASE_URL is required');
}
if (!javaUrl) {
errors.push('NEXT_PUBLIC_JAVA_URL is required');
}
if (!googleClientId) {
errors.push('NEXT_PUBLIC_GOOGLE_CLIENT_ID is required');
}
// 验证 URL 格式
try {
new URL(baseUrl);
} catch {
errors.push('NEXT_PUBLIC_BASE_URL must be a valid URL');
}
try {
new URL(javaUrl);
} catch {
errors.push('NEXT_PUBLIC_JAVA_URL must be a valid URL');
}
return {
isValid: errors.length === 0,
errors,
};
};
/**
*
*/
export const logEnvConfig = (): void => {
if (isDevelopment) {
console.log('🔧 环境变量配置:', {
nodeEnv,
baseUrl,
javaUrl,
cutUrl,
cutUrlTo,
googleClientId,
googleRedirectUri: getGoogleRedirectUri(),
gaEnabled,
gaMeasurementId,
videoEditUseMock,
videoEditUseRemote,
videoEditRemoteBase,
errorConfig,
});
}
};

View File

@ -2,6 +2,8 @@
* *
*/ */
import { baseUrl } from '@/lib/env';
// 注意:这里不使用 @/api/request 中的 post 函数,因为它会将请求发送到远程服务器 // 注意:这里不使用 @/api/request 中的 post 函数,因为它会将请求发送到远程服务器
// 我们需要直接调用本地的 Next.js API 路由 // 我们需要直接调用本地的 Next.js API 路由
@ -10,8 +12,7 @@
*/ */
const localPost = async <T>(url: string, data: any): Promise<T> => { const localPost = async <T>(url: string, data: any): Promise<T> => {
try { try {
// 使用环境变量中的 BASE_URL生产要求使用 NEXT_PUBLIC_BASE_URL // 使用统一配置中的 BASE_URL
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || '';
const isAbsolute = /^https?:\/\//i.test(url); const isAbsolute = /^https?:\/\//i.test(url);
const normalizedBase = baseUrl.replace(/\/$/, ''); const normalizedBase = baseUrl.replace(/\/$/, '');
const normalizedPath = url.startsWith('/') ? url : `/${url}`; const normalizedPath = url.startsWith('/') ? url : `/${url}`;

View File

@ -3,6 +3,8 @@
* 访 * 访
*/ */
import { isGAAvailable as checkGAAvailable, gaMeasurementId } from '@/lib/env';
// 扩展全局Window接口 // 扩展全局Window接口
declare global { declare global {
interface Window { interface Window {
@ -74,16 +76,14 @@ const normalizeEventParams = (
* GA是否可用 * GA是否可用
*/ */
export const isGAAvailable = (): boolean => { export const isGAAvailable = (): boolean => {
return typeof window !== 'undefined' && return checkGAAvailable();
typeof window.gtag === 'function' &&
process.env.NEXT_PUBLIC_GA_ENABLED === 'true';
}; };
/** /**
* GA测量ID * GA测量ID
*/ */
export const getGAMeasurementId = (): string => { export const getGAMeasurementId = (): string => {
return process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || 'G-4BDXV6TWF4'; return gaMeasurementId;
}; };
/** /**
@ -113,7 +113,7 @@ export const trackEvent = (
window.gtag('event', eventName, eventParams); window.gtag('event', eventName, eventParams);
// 开发环境下打印日志 // 开发环境下打印日志
if (process.env.NODE_ENV === 'development') { if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
console.log('GA Event:', eventName, eventParams); console.log('GA Event:', eventName, eventParams);
} }
} catch (error) { } catch (error) {
@ -150,7 +150,7 @@ export const trackPageView = (
window.gtag('config', getGAMeasurementId(), pageParams); window.gtag('config', getGAMeasurementId(), pageParams);
// 开发环境下打印日志 // 开发环境下打印日志
if (process.env.NODE_ENV === 'development') { if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
console.log('GA Page View:', pagePath, pageParams); console.log('GA Page View:', pagePath, pageParams);
} }
} catch (error) { } catch (error) {

View File

@ -3,10 +3,7 @@
* Next.js * Next.js
*/ */
/** import { isDevelopment } from '@/lib/env';
*
*/
export const isDevelopment = process.env.NODE_ENV === 'development';
/** /**
* *

View File

@ -1,6 +1,7 @@
import { notification } from 'antd'; import { notification } from 'antd';
import { downloadVideo } from './tools'; import { downloadVideo } from './tools';
import { getGenerateEditPlan } from '@/api/video_flow'; import { getGenerateEditPlan } from '@/api/video_flow';
import { cutUrl } from '@/lib/env';
/** /**
* - * -
@ -115,7 +116,7 @@ export class VideoExportService {
this.config = { this.config = {
maxRetries: config.maxRetries || 3, maxRetries: config.maxRetries || 3,
pollInterval: config.pollInterval || 5000, // 5秒轮询 pollInterval: config.pollInterval || 5000, // 5秒轮询
apiBaseUrl: process.env.NEXT_PUBLIC_CUT_URL || 'https://smartcut.api.movieflow.ai' apiBaseUrl: cutUrl
}; };
} }
@ -857,7 +858,7 @@ export class VideoExportService {
export const videoExportService = new VideoExportService({ export const videoExportService = new VideoExportService({
maxRetries: 3, maxRetries: 3,
pollInterval: 5000, // 5秒轮询间隔 pollInterval: 5000, // 5秒轮询间隔
// apiBaseUrl 使用环境变量 NEXT_PUBLIC_CUT_URL,在构造函数中处理 // apiBaseUrl 使用统一配置中的 cutUrl,在构造函数中处理
}); });
/** /**