mirror of
https://git.oceanpay.cc/danial/kami_apple_exchage.git
synced 2025-12-18 22:29:09 +00:00
feat: 新增SPA构建脚本和配置更新
- 新增 build-spa.bat 和 build-spa.sh 脚本,简化SPA应用构建流程 - 更新 next.config.js,调整输出配置为静态导出,并添加尾部斜杠和未优化图像设置 - 修改 package.json,添加构建和服务命令 - 更新 CORS_SOLUTION.md,简化代理规则 - 删除不再使用的健康检查和代理API - 优化 ESLint 配置,添加警告规则 - 更新 Dockerfile,使用 Node 24 版本 - 新增公共 HTML 模板和404页面组件 - 清理无用的 package-lock.json 文件
This commit is contained in:
@@ -101,14 +101,9 @@ pnpm dev
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/proxy/:path*',
|
||||
source: ':path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_TARGET_API_URL}/:path*`,
|
||||
},
|
||||
// 添加更多代理规则
|
||||
{
|
||||
source: '/api/v2/:path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_V2_API_URL}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:18-alpine AS base
|
||||
FROM node:24-alpine AS base
|
||||
|
||||
# 安装依赖阶段
|
||||
FROM base AS deps
|
||||
|
||||
41
frontend/build-spa.bat
Normal file
41
frontend/build-spa.bat
Normal file
@@ -0,0 +1,41 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo 🚀 开始构建SPA应用...
|
||||
|
||||
REM 清理之前的构建
|
||||
echo 📁 清理之前的构建文件...
|
||||
if exist .next rmdir /s /q .next
|
||||
if exist out rmdir /s /q out
|
||||
|
||||
REM 安装依赖
|
||||
echo 📦 安装依赖...
|
||||
call npm install
|
||||
|
||||
REM 构建应用
|
||||
echo 🔨 构建SPA应用...
|
||||
call npm run build
|
||||
|
||||
REM 检查构建结果
|
||||
if exist out (
|
||||
echo ✅ 构建成功!
|
||||
echo 📊 构建统计:
|
||||
for /f %%i in ('dir /s /b out ^| find /c /v ""') do echo - 总文件数: %%i
|
||||
for /f %%i in ('dir out /-c ^| find "个文件"') do echo - 总大小: %%i
|
||||
echo.
|
||||
echo 📁 构建输出目录: out/
|
||||
echo 🌐 可以通过以下方式测试:
|
||||
echo - 使用 npx serve out
|
||||
echo - 或使用任何静态文件服务器
|
||||
) else (
|
||||
echo ❌ 构建失败!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 🎯 SPA配置已应用:
|
||||
echo - 静态导出 (output: 'export')
|
||||
echo - 尾部斜杠 (trailingSlash: true)
|
||||
echo - 未优化图像 (unoptimized: true)
|
||||
echo - 路由回退处理
|
||||
|
||||
pause
|
||||
39
frontend/build-spa.sh
Normal file
39
frontend/build-spa.sh
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🚀 开始构建SPA应用..."
|
||||
|
||||
# 清理之前的构建
|
||||
echo "📁 清理之前的构建文件..."
|
||||
rm -rf .next
|
||||
rm -rf out
|
||||
|
||||
# 安装依赖
|
||||
echo "📦 安装依赖..."
|
||||
npm install
|
||||
|
||||
# 构建应用
|
||||
echo "🔨 构建SPA应用..."
|
||||
npm run build
|
||||
|
||||
# 检查构建结果
|
||||
if [ -d "out" ]; then
|
||||
echo "✅ 构建成功!"
|
||||
echo "📊 构建统计:"
|
||||
echo " - 总文件数: $(find out -type f | wc -l)"
|
||||
echo " - 总大小: $(du -sh out | cut -f1)"
|
||||
echo ""
|
||||
echo "📁 构建输出目录: out/"
|
||||
echo "🌐 可以通过以下方式测试:"
|
||||
echo " - 使用 npx serve out"
|
||||
echo " - 或使用任何静态文件服务器"
|
||||
else
|
||||
echo "❌ 构建失败!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🎯 SPA配置已应用:"
|
||||
echo " - 静态导出 (output: 'export')"
|
||||
echo " - 尾部斜杠 (trailingSlash: true)"
|
||||
echo " - 未优化图像 (unoptimized: true)"
|
||||
echo " - 路由回退处理"
|
||||
@@ -25,6 +25,9 @@ export default [
|
||||
},
|
||||
rules: {
|
||||
...typescriptPlugin.configs.recommended.rules,
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-empty-object-type": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// 输出独立可部署的应用
|
||||
output: 'standalone',
|
||||
output: 'export',
|
||||
|
||||
// 启用严格模式
|
||||
reactStrictMode: true,
|
||||
|
||||
// SPA配置 - 确保所有路由都指向index.html
|
||||
trailingSlash: true,
|
||||
|
||||
// 图像优化配置
|
||||
images: {
|
||||
domains: ['localhost'],
|
||||
formats: ['image/avif', 'image/webp'],
|
||||
unoptimized: true, // 静态导出需要禁用图像优化
|
||||
},
|
||||
|
||||
// 环境变量
|
||||
@@ -22,52 +26,6 @@ const nextConfig = {
|
||||
|
||||
// 压缩配置
|
||||
compress: true,
|
||||
|
||||
// 代理配置 - 绕过CORS限制
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/api/proxy/:path*',
|
||||
destination: `${process.env.NEXT_PUBLIC_TARGET_API_URL || 'http://localhost:3001'}/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// 跨域资源共享配置
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff',
|
||||
},
|
||||
{
|
||||
key: 'X-XSS-Protection',
|
||||
value: '1; mode=block',
|
||||
},
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'SAMEORIGIN',
|
||||
},
|
||||
// 添加CORS头
|
||||
{
|
||||
key: 'Access-Control-Allow-Origin',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Methods',
|
||||
value: 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
},
|
||||
{
|
||||
key: 'Access-Control-Allow-Headers',
|
||||
value: 'Content-Type, Authorization, X-Requested-With',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
8601
frontend/package-lock.json
generated
8601
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,9 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:spa": "next build && echo 'SPA构建完成,输出目录: out/'",
|
||||
"start": "next start",
|
||||
"serve": "npx serve out -p 3000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -13,6 +15,8 @@
|
||||
"@headlessui/react": "^2.2.6",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
@@ -39,6 +43,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
||||
9
frontend/pnpm-lock.yaml
generated
9
frontend/pnpm-lock.yaml
generated
@@ -20,6 +20,12 @@ importers:
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: ^1.1.14
|
||||
version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.14
|
||||
version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -93,6 +99,9 @@ importers:
|
||||
specifier: ^3.22.4
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.19.9
|
||||
|
||||
71
frontend/public/index.html
Normal file
71
frontend/public/index.html
Normal file
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="爬虫监控前端应用" />
|
||||
<title>爬虫监控系统</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
|
||||
<div id="root">
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在加载应用...</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// SPA路由回退处理
|
||||
if (window.location.pathname !== '/' && !window.location.pathname.includes('.')) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,17 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* 健康检查API
|
||||
* 用于K8s和其他监控系统检查服务是否正常运行
|
||||
*/
|
||||
export async function GET() {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0',
|
||||
environment: process.env.NEXT_PUBLIC_ENV || 'development',
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const TARGET_API_URL = process.env.NEXT_PUBLIC_TARGET_API_URL || 'http://localhost:3001';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
) {
|
||||
return handleProxyRequest(request, params.path, 'GET');
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
) {
|
||||
return handleProxyRequest(request, params.path, 'POST');
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
) {
|
||||
return handleProxyRequest(request, params.path, 'PUT');
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
) {
|
||||
return handleProxyRequest(request, params.path, 'DELETE');
|
||||
}
|
||||
|
||||
export async function OPTIONS(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
) {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleProxyRequest(
|
||||
request: NextRequest,
|
||||
pathSegments: string[],
|
||||
method: string
|
||||
) {
|
||||
try {
|
||||
const path = pathSegments.join('/');
|
||||
const url = new URL(request.url);
|
||||
const targetUrl = `${TARGET_API_URL}/${path}${url.search}`;
|
||||
|
||||
// 准备请求头
|
||||
const headers = new Headers();
|
||||
request.headers.forEach((value, key) => {
|
||||
// 跳过一些可能导致问题的头
|
||||
if (!['host', 'origin', 'referer'].includes(key.toLowerCase())) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加必要的头
|
||||
headers.set('Content-Type', 'application/json');
|
||||
|
||||
// 准备请求体
|
||||
let body: string | undefined;
|
||||
if (method !== 'GET' && method !== 'DELETE') {
|
||||
body = await request.text();
|
||||
}
|
||||
|
||||
// 发送请求到目标API
|
||||
const response = await fetch(targetUrl, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
// 获取响应数据
|
||||
const responseData = await response.text();
|
||||
let jsonData;
|
||||
try {
|
||||
jsonData = JSON.parse(responseData);
|
||||
} catch {
|
||||
jsonData = responseData;
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
return NextResponse.json(jsonData, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('代理请求失败:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '代理请求失败', message: error instanceof Error ? error.message : '未知错误' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
23
frontend/src/app/not-found.tsx
Normal file
23
frontend/src/app/not-found.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function NotFound() {
|
||||
useEffect(() => {
|
||||
// 在SPA中,所有未找到的路由都应该重定向到首页
|
||||
// 使用window.location而不是useRouter,避免SSR问题
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold mb-4">页面未找到</h1>
|
||||
<p className="text-gray-600 mb-4">正在重定向到首页...</p>
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import { Label } from "@/components/ui/label";
|
||||
import { Settings } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { RefreshIntervalSelector } from "@/components/dashboard/refresh-interval-selector";
|
||||
import { ApiConfigDisplay } from "@/components/dashboard/api-config-display";
|
||||
import { ThemeToggle } from "@/components/layout/theme-toggle";
|
||||
import { ThemeTransition } from "@/components/layout/theme-transition";
|
||||
import { NotificationBell } from "@/components/dashboard/notification-bell";
|
||||
@@ -133,7 +132,7 @@ function HomeContentInner() {
|
||||
{/* 右列 - 线程监控和表单 */}
|
||||
<div className="space-y-6">
|
||||
{/* API配置 */}
|
||||
<ApiConfigDisplay refreshEnabled={isRefreshing} refreshInterval={refreshInterval} />
|
||||
{/* <ApiConfigDisplay refreshEnabled={isRefreshing} refreshInterval={refreshInterval} /> */}
|
||||
|
||||
{/* 线程监控 */}
|
||||
<ThreadMonitor refreshInterval={refreshInterval} refreshEnabled={isRefreshing} />
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CrawlerThread } from "@/lib/api/crawler-service";
|
||||
import { crawlerService, CrawlerThread } from "@/lib/api/crawler-service";
|
||||
import { AlertCircle, Clock, CheckCircle, Pause, Play } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
// 导入 CrawlerService
|
||||
import { CrawlerService as CrawlerServiceClass } from "@/lib/api/crawler-service";
|
||||
import { Timer } from "@/components/animate-ui/icons/timer";
|
||||
|
||||
interface ThreadMonitorProps {
|
||||
@@ -30,7 +27,6 @@ export function ThreadMonitor({ refreshInterval = 5000, refreshEnabled = true }:
|
||||
},
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const crawlerService = new CrawlerServiceClass(); // 使用正确的类名
|
||||
|
||||
const fetchThreads = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CardKeyRequest, crawlerService, CrawlerService } from "@/lib/api/crawler-service";
|
||||
import { CardKeyRequest, crawlerService } from "@/lib/api/crawler-service";
|
||||
import {
|
||||
Clock,
|
||||
User,
|
||||
|
||||
@@ -144,7 +144,7 @@ export class CrawlerService {
|
||||
try {
|
||||
const response = await this.apiService.get('/api/thread_status');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
} catch (_) {
|
||||
// 返回模拟数据
|
||||
return {
|
||||
max_threads: 0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
|
||||
interface ThemeContextType {
|
||||
isTransitioning: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"target": "ES2023",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
Reference in New Issue
Block a user