Add Auth for WebPage and APIs

This commit is contained in:
Raymond Zhou 2025-03-09 12:19:58 +08:00
parent e6b87f89ec
commit 5e6d0e0ee6
27 changed files with 15384 additions and 1720 deletions

View file

@ -1,2 +0,0 @@
NEXT_PUBLIC_WS_URL=ws://localhost:3001
NEXT_PUBLIC_API_URL=http://localhost:3001/api

62
ui/app/auth/page.tsx Normal file
View file

@ -0,0 +1,62 @@
'use client';
import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation';
export default function AuthPage() {
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ password }),
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('authToken', data.token);
router.push('/');
} else {
setError('密码错误');
}
} catch (err) {
setError('验证失败,请重试');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-light-primary dark:bg-dark-primary">
<div className="w-full max-w-md p-8 space-y-6 bg-light-secondary dark:bg-dark-secondary rounded-lg shadow-lg">
<h1 className="text-2xl font-bold text-center">访</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-3 border rounded-md bg-light-primary dark:bg-dark-primary"
placeholder="输入密码"
required
/>
</div>
{error && (
<p className="text-red-500 text-sm text-center">{error}</p>
)}
<button
type="submit"
className="w-full p-3 text-white bg-blue-600 rounded-md hover:bg-blue-700 transition-colors"
>
</button>
</form>
</div>
</div>
);
}

View file

@ -4,6 +4,7 @@ import { Search } from 'lucide-react';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { toast } from 'sonner';
import { getApiUrl, get } from '@/lib/api';
interface Discover {
title: string;
@ -19,25 +20,11 @@ const Page = () => {
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/discover`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message);
}
data.blogs = data.blogs.filter((blog: Discover) => blog.thumbnail);
setDiscover(data.blogs);
} catch (err: any) {
console.error('Error fetching data:', err.message);
toast.error('Error fetching data');
const data = await get<{ blogs: Discover[] }>(getApiUrl('/discover'));
setDiscover(data.blogs.filter(blog => blog.thumbnail));
} catch (error) {
console.error('Unable to fetch discovers:', error);
toast.error('Unable to fetch discovers');
} finally {
setLoading(false);
}
@ -88,9 +75,17 @@ const Page = () => {
<img
className="object-cover w-full aspect-video"
src={
new URL(item.thumbnail).origin +
new URL(item.thumbnail).pathname +
`?id=${new URL(item.thumbnail).searchParams.get('id')}`
(() => {
try {
if (!item.thumbnail) return '/placeholder.jpg';
const url = new URL(item.thumbnail);
return url.origin + url.pathname +
(url.searchParams.get('id') ?
`?id=${url.searchParams.get('id')}` : '');
} catch (e) {
return item.thumbnail || '/placeholder.jpg';
}
})()
}
alt={item.title}
/>

View file

@ -5,6 +5,7 @@ import { cn } from '@/lib/utils';
import Sidebar from '@/components/Sidebar';
import { Toaster } from 'sonner';
import ThemeProvider from '@/components/theme/Provider';
import AuthCheck from '@/components/AuthCheck';
const montserrat = Montserrat({
weight: ['300', '400', '500', '700'],
@ -28,7 +29,9 @@ export default function RootLayout({
<html className="h-full" lang="en" suppressHydrationWarning>
<body className={cn('h-full', montserrat.className)}>
<ThemeProvider>
<Sidebar>{children}</Sidebar>
<AuthCheck>
<Sidebar>{children}</Sidebar>
</AuthCheck>
<Toaster
toastOptions={{
unstyled: true,

View file

@ -5,6 +5,8 @@ import { cn, formatTimeDifference } from '@/lib/utils';
import { BookOpenText, ClockIcon, Delete, ScanEye } from 'lucide-react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { toast } from 'sonner';
import { getApiUrl, get } from '@/lib/api';
export interface Chat {
id: string;
@ -21,17 +23,15 @@ const Page = () => {
const fetchChats = async () => {
setLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/chats`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json();
setChats(data.chats);
setLoading(false);
try {
const data = await get<{ chats: Chat[] }>(getApiUrl('/chats'));
setChats(data.chats);
} catch (error) {
console.error('获取聊天记录失败:', error);
toast.error('获取聊天记录失败');
} finally {
setLoading(false);
}
};
fetchChats();

View file

@ -7,13 +7,15 @@ import { Switch } from '@headlessui/react';
import ThemeSwitcher from '@/components/theme/Switcher';
import { ImagesIcon, VideoIcon } from 'lucide-react';
import Link from 'next/link';
import { toast } from 'sonner';
import { getApiUrl, get, post } from '@/lib/api';
interface SettingsType {
chatModelProviders: {
[key: string]: [Record<string, any>];
[key: string]: Record<string, any>[];
};
embeddingModelProviders: {
[key: string]: [Record<string, any>];
[key: string]: Record<string, any>[];
};
openaiApiKey: string;
groqApiKey: string;
@ -116,63 +118,62 @@ const Page = () => {
useEffect(() => {
const fetchConfig = async () => {
setIsLoading(true);
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
headers: {
'Content-Type': 'application/json',
},
});
try {
const data = await get<SettingsType>(getApiUrl('/config'));
setConfig(data);
const data = (await res.json()) as SettingsType;
setConfig(data);
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
const embeddingModelProvidersKeys = Object.keys(
data.embeddingModelProviders || {},
);
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
const embeddingModelProvidersKeys = Object.keys(
data.embeddingModelProviders || {},
);
const defaultChatModelProvider =
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
const defaultEmbeddingModelProvider =
embeddingModelProvidersKeys.length > 0
? embeddingModelProvidersKeys[0]
: '';
const defaultChatModelProvider =
chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : '';
const defaultEmbeddingModelProvider =
embeddingModelProvidersKeys.length > 0
? embeddingModelProvidersKeys[0]
: '';
const chatModelProvider =
localStorage.getItem('chatModelProvider') ||
defaultChatModelProvider ||
'';
const chatModel =
localStorage.getItem('chatModel') ||
(data.chatModelProviders &&
data.chatModelProviders[chatModelProvider]?.length > 0
? data.chatModelProviders[chatModelProvider][0].name
: undefined) ||
'';
const embeddingModelProvider =
localStorage.getItem('embeddingModelProvider') ||
defaultEmbeddingModelProvider ||
'';
const embeddingModel =
localStorage.getItem('embeddingModel') ||
(data.embeddingModelProviders &&
data.embeddingModelProviders[embeddingModelProvider]?.[0].name) ||
'';
const chatModelProvider =
localStorage.getItem('chatModelProvider') ||
defaultChatModelProvider ||
'';
const chatModel =
localStorage.getItem('chatModel') ||
(data.chatModelProviders &&
data.chatModelProviders[chatModelProvider]?.length > 0
? data.chatModelProviders[chatModelProvider][0].name
: undefined) ||
'';
const embeddingModelProvider =
localStorage.getItem('embeddingModelProvider') ||
defaultEmbeddingModelProvider ||
'';
const embeddingModel =
localStorage.getItem('embeddingModel') ||
(data.embeddingModelProviders &&
data.embeddingModelProviders[embeddingModelProvider]?.[0].name) ||
'';
setSelectedChatModelProvider(chatModelProvider);
setSelectedChatModel(chatModel);
setSelectedEmbeddingModelProvider(embeddingModelProvider);
setSelectedEmbeddingModel(embeddingModel);
setChatModels(data.chatModelProviders || {});
setEmbeddingModels(data.embeddingModelProviders || {});
setSelectedChatModelProvider(chatModelProvider);
setSelectedChatModel(chatModel);
setSelectedEmbeddingModelProvider(embeddingModelProvider);
setSelectedEmbeddingModel(embeddingModel);
setChatModels(data.chatModelProviders || {});
setEmbeddingModels(data.embeddingModelProviders || {});
setAutomaticImageSearch(
localStorage.getItem('autoImageSearch') === 'true',
);
setAutomaticVideoSearch(
localStorage.getItem('autoVideoSearch') === 'true',
);
setIsLoading(false);
setAutomaticImageSearch(
localStorage.getItem('autoImageSearch') === 'true',
);
setAutomaticVideoSearch(
localStorage.getItem('autoVideoSearch') === 'true',
);
} catch (error) {
console.error('获取配置失败:', error);
toast.error('获取配置失败');
} finally {
setIsLoading(false);
}
};
fetchConfig();
@ -187,39 +188,15 @@ const Page = () => {
[key]: value,
} as SettingsType;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/config`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConfig),
},
);
if (!response.ok) {
throw new Error('Failed to update config');
}
await post(getApiUrl('/config'), updatedConfig);
setConfig(updatedConfig);
if (
key.toLowerCase().includes('api') ||
key.toLowerCase().includes('url')
) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) {
throw new Error('Failed to fetch updated config');
}
const data = await res.json();
const data = await get<SettingsType>(getApiUrl('/config'));
setChatModels(data.chatModelProviders || {});
setEmbeddingModels(data.embeddingModelProviders || {});
@ -332,13 +309,11 @@ const Page = () => {
} else if (key === 'embeddingModel') {
localStorage.setItem('embeddingModel', value);
}
} catch (err) {
console.error('Failed to save:', err);
setConfig((prev) => ({ ...prev! }));
} catch (error) {
console.error('保存配置失败:', error);
toast.error('保存配置失败');
} finally {
setTimeout(() => {
setSavingStates((prev) => ({ ...prev, [key]: false }));
}, 500);
setSavingStates((prev) => ({ ...prev, [key]: false }));
}
};

View file

@ -0,0 +1,65 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
export default function AuthCheck({ children }: { children: React.ReactNode }) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
const router = useRouter();
const pathname = usePathname();
useEffect(() => {
// 检查用户是否已经通过认证
const authToken = localStorage.getItem('authToken');
// 如果当前路径是认证页面,不需要重定向
if (pathname === '/auth') {
setIsAuthenticated(true);
return;
}
// 如果未认证且不在认证页面,重定向到认证页面
if (!authToken) {
router.push('/auth');
setIsAuthenticated(false);
} else {
// 验证令牌有效性
checkTokenValidity(authToken);
}
}, [pathname, router]);
// 验证令牌有效性
const checkTokenValidity = async (token: string) => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/verify-token`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
setIsAuthenticated(true);
} else {
// 令牌无效,清除存储并重定向到登录页
localStorage.removeItem('authToken');
router.push('/auth');
setIsAuthenticated(false);
}
} catch (error) {
console.error('验证令牌时出错:', error);
setIsAuthenticated(true); // 网络错误时暂时允许访问
}
};
// 如果认证状态尚未确定,显示加载状态
if (isAuthenticated === null) {
return (
<div className="min-h-screen flex items-center justify-center bg-light-primary dark:bg-dark-primary">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}
// 如果在认证页面或已认证,显示子组件
return <>{children}</>;
}

View file

@ -12,6 +12,7 @@ import { getSuggestions } from '@/lib/actions';
import { Settings } from 'lucide-react';
import Link from 'next/link';
import NextError from 'next/error';
import { getApiUrl, get } from '@/lib/api';
export type Message = {
messageId: string;
@ -71,20 +72,10 @@ const useSocket = (
localStorage.setItem('autoVideoSearch', 'false');
}
const providers = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/models`,
{
headers: {
'Content-Type': 'application/json',
},
},
).then(async (res) => {
if (!res.ok)
throw new Error(
`Failed to fetch models: ${res.status} ${res.statusText}`,
);
return res.json();
});
const providers = await get<{
chatModelProviders: Record<string, Record<string, any>>,
embeddingModelProviders: Record<string, Record<string, any>>
}>(getApiUrl('/models'));
if (
!chatModel ||
@ -201,6 +192,12 @@ const useSocket = (
searchParams.append('embeddingModel', embeddingModel!);
searchParams.append('embeddingModelProvider', embeddingModelProvider);
// 添加认证令牌
const authToken = localStorage.getItem('authToken');
if (authToken) {
searchParams.append('token', authToken);
}
wsURL.search = searchParams.toString();
@ -315,55 +312,45 @@ const loadMessages = async (
setFiles: (files: File[]) => void,
setFileIds: (fileIds: string[]) => void,
) => {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
},
);
try {
const data = await get<any>(getApiUrl(`/chats/${chatId}`));
const messages = data.messages.map((msg: any) => {
return {
...msg,
...JSON.parse(msg.metadata),
};
}) as Message[];
if (res.status === 404) {
setNotFound(true);
setMessages(messages);
const history = messages.map((msg) => {
return [msg.role, msg.content];
}) as [string, string][];
console.debug(new Date(), 'app:messages_loaded');
document.title = messages[0].content;
const files = data.chat.files.map((file: any) => {
return {
fileName: file.name,
fileExtension: file.name.split('.').pop(),
fileId: file.fileId,
};
});
setFiles(files);
setFileIds(files.map((file: File) => file.fileId));
setChatHistory(history);
setFocusMode(data.chat.focusMode);
setIsMessagesLoaded(true);
return;
} catch (error) {
console.debug(new Date(), 'ws:error', error);
setIsMessagesLoaded(true);
setNotFound(true);
}
const data = await res.json();
const messages = data.messages.map((msg: any) => {
return {
...msg,
...JSON.parse(msg.metadata),
};
}) as Message[];
setMessages(messages);
const history = messages.map((msg) => {
return [msg.role, msg.content];
}) as [string, string][];
console.debug(new Date(), 'app:messages_loaded');
document.title = messages[0].content;
const files = data.chat.files.map((file: any) => {
return {
fileName: file.name,
fileExtension: file.name.split('.').pop(),
fileId: file.fileId,
};
});
setFiles(files);
setFileIds(files.map((file: File) => file.fileId));
setChatHistory(history);
setFocusMode(data.chat.focusMode);
setIsMessagesLoaded(true);
};
const ChatWindow = ({ id }: { id?: string }) => {

View file

@ -1,4 +1,4 @@
import { Trash } from 'lucide-react';
import { TrashIcon } from 'lucide-react';
import {
Description,
Dialog,
@ -11,6 +11,8 @@ import {
import { Fragment, useState } from 'react';
import { toast } from 'sonner';
import { Chat } from '@/app/library/page';
import { useRouter } from 'next/navigation';
import { getApiUrl, del } from '@/lib/api';
const DeleteChat = ({
chatId,
@ -25,36 +27,27 @@ const DeleteChat = ({
}) => {
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleDelete = async () => {
setLoading(true);
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/chats/${chatId}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
},
);
if (res.status != 200) {
throw new Error('Failed to delete chat');
}
const newChats = chats.filter((chat) => chat.id !== chatId);
setChats(newChats);
await del(getApiUrl(`/chats/${chatId}`));
// 从列表中移除聊天
setChats(chats.filter((chat) => chat.id !== chatId));
if (redirect) {
window.location.href = '/';
router.push('/');
}
} catch (err: any) {
toast.error(err.message);
toast.success('聊天已删除');
} catch (error) {
console.error('删除聊天失败:', error);
toast.error('删除聊天失败');
} finally {
setConfirmationDialogOpen(false);
setLoading(false);
setConfirmationDialogOpen(false);
}
};
@ -66,7 +59,7 @@ const DeleteChat = ({
}}
className="bg-transparent text-red-400 hover:scale-105 transition duration-200"
>
<Trash size={17} />
<TrashIcon size={17} />
</button>
<Transition appear show={confirmationDialogOpen} as={Fragment}>
<Dialog

View file

@ -41,8 +41,18 @@ const Attach = ({
data.append('embedding_model_provider', embeddingModelProvider!);
data.append('embedding_model', embeddingModel!);
// 获取认证令牌
const authToken = localStorage.getItem('authToken');
// 创建headers对象添加认证令牌
const headers: HeadersInit = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/uploads`, {
method: 'POST',
headers,
body: data,
});

View file

@ -39,8 +39,18 @@ const AttachSmall = ({
data.append('embedding_model_provider', embeddingModelProvider!);
data.append('embedding_model', embeddingModel!);
// 获取认证令牌
const authToken = localStorage.getItem('authToken');
// 创建headers对象添加认证令牌
const headers: HeadersInit = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/uploads`, {
method: 'POST',
headers,
body: data,
});

View file

@ -1,9 +1,11 @@
/* eslint-disable @next/next/no-img-element */
import { ImagesIcon, PlusIcon } from 'lucide-react';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import Lightbox from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
import { getApiUrl, post } from '@/lib/api';
import { toast } from 'sonner';
type Image = {
url: string;
@ -37,34 +39,25 @@ const SearchImages = ({
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/images`,
const data = await post<{ images: Image[] }>(
getApiUrl('/images'),
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
query,
chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIKey,
customOpenAIBaseURL,
}),
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
}
);
const data = await res.json();
const images = data.images ?? [];
setImages(images);
setImages(data.images);
setSlides(
images.map((image: Image) => {
data.images.map((image: Image) => {
return {
src: image.img_src,
};

View file

@ -4,6 +4,7 @@ import { useRef, useState } from 'react';
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
import 'yet-another-react-lightbox/styles.css';
import { Message } from './ChatWindow';
import { getApiUrl, post } from '@/lib/api';
type Video = {
url: string;
@ -52,40 +53,31 @@ const Searchvideos = ({
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
const data = await post<{ videos: Video[] }>(
getApiUrl('/videos'),
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
query,
chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIKey,
customOpenAIBaseURL,
}),
},
body: JSON.stringify({
query: query,
chatHistory: chatHistory,
chatModel: {
provider: chatModelProvider,
model: chatModel,
...(chatModelProvider === 'custom_openai' && {
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
},
}),
},
}
);
const data = await res.json();
const videos = data.videos ?? [];
setVideos(videos);
setVideos(data.videos || []);
setSlides(
videos.map((video: Video) => {
data.videos.map((video: Video) => {
return {
type: 'video-slide',
iframe_src: video.iframe_src,
src: video.img_src,
};
}),
})
);
setLoading(false);
}}

View file

@ -1,4 +1,5 @@
import { Message } from '@/components/ChatWindow';
import { getApiUrl, post } from './api';
export const getSuggestions = async (chatHisory: Message[]) => {
const chatModel = localStorage.getItem('chatModel');
@ -7,12 +8,9 @@ export const getSuggestions = async (chatHisory: Message[]) => {
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/suggestions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
const data = await post<{ suggestions: string[] }>(
getApiUrl('/suggestions'),
{
chatHistory: chatHisory,
chatModel: {
provider: chatModelProvider,
@ -22,10 +20,8 @@ export const getSuggestions = async (chatHisory: Message[]) => {
customOpenAIBaseURL,
}),
},
}),
});
const data = (await res.json()) as { suggestions: string[] };
}
);
return data.suggestions;
};

96
ui/lib/api.ts Normal file
View file

@ -0,0 +1,96 @@
/**
* API请求工具函数
*/
// 基本API请求函数
export async function fetchWithToken(
url: string,
options: RequestInit = {}
): Promise<Response> {
// 从本地存储获取令牌
const token = localStorage.getItem('authToken');
// 准备请求头
const headers = {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...options.headers,
};
// 发送请求
return fetch(url, {
...options,
headers,
});
}
// GET请求
export async function get<T>(url: string, options: RequestInit = {}): Promise<T> {
const response = await fetchWithToken(url, {
method: 'GET',
...options,
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
return response.json();
}
// POST请求
export async function post<T>(
url: string,
data: any,
options: RequestInit = {}
): Promise<T> {
const response = await fetchWithToken(url, {
method: 'POST',
body: JSON.stringify(data),
...options,
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
return response.json();
}
// PUT请求
export async function put<T>(
url: string,
data: any,
options: RequestInit = {}
): Promise<T> {
const response = await fetchWithToken(url, {
method: 'PUT',
body: JSON.stringify(data),
...options,
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
return response.json();
}
// DELETE请求
export async function del<T>(url: string, options: RequestInit = {}): Promise<T> {
const response = await fetchWithToken(url, {
method: 'DELETE',
...options,
});
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
return response.json();
}
// 获取完整API URL
export function getApiUrl(path: string): string {
return `${process.env.NEXT_PUBLIC_API_URL}${path.startsWith('/') ? path : `/${path}`}`;
}

6961
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff