mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-09 09:06:36 +08:00
fix(frontend): keep browser API on same-origin rewrites in dev
Document that NEXT_PUBLIC_WAVELET_BACKEND_URL must stay unset for local next dev so axios hits /api/* and Next rewrites proxy to the backend.
This commit is contained in:
@@ -3,6 +3,7 @@ import { FileTextIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
/**
|
||||
* 空状态展示组件属性
|
||||
@@ -55,6 +56,9 @@ export function EmptyState({
|
||||
className,
|
||||
iconSize = 'md',
|
||||
}: EmptyStateProps) {
|
||||
const t = useTranslations('layout.empty');
|
||||
const displayTitle = title ?? t('noData');
|
||||
const displayDescription = description ?? t('noContent');
|
||||
const iconSizes = { sm: 'size-10', md: 'size-12', lg: 'size-16' };
|
||||
const iconInnerSizes = { sm: 'size-5', md: 'size-6', lg: 'size-8' };
|
||||
|
||||
@@ -76,10 +80,14 @@ export function EmptyState({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{title && <h3 className='text-base font-medium mb-1'>{title}</h3>}
|
||||
{displayTitle && (
|
||||
<h3 className='text-base font-medium mb-1'>{displayTitle}</h3>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
<p className='text-sm text-muted-foreground max-w-md'>{description}</p>
|
||||
{displayDescription && (
|
||||
<p className='text-sm text-muted-foreground max-w-md'>
|
||||
{displayDescription}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{onAction && actionText && (
|
||||
@@ -128,10 +136,12 @@ export function EmptyStateWithBorder(props: EmptyStateProps) {
|
||||
* @returns {React.ReactNode} 简化版空状态组件
|
||||
*/
|
||||
export function EmptyInline({
|
||||
message = '暂无数据',
|
||||
message,
|
||||
icon: Icon = FileTextIcon,
|
||||
className,
|
||||
}: Pick<EmptyStateProps, 'icon' | 'className'> & { message?: string }) {
|
||||
const t = useTranslations('layout.empty');
|
||||
const displayMessage = message ?? t('noData');
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -140,7 +150,7 @@ export function EmptyInline({
|
||||
)}
|
||||
>
|
||||
<Icon className='size-4' />
|
||||
<span>{message}</span>
|
||||
<span>{displayMessage}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AlertCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
/**
|
||||
* 错误展示组件属性
|
||||
@@ -48,16 +49,19 @@ export interface ErrorDisplayProps {
|
||||
* @returns {React.ReactNode} 错误展示组件
|
||||
*/
|
||||
export function ErrorDisplay({
|
||||
title = '加载失败',
|
||||
title,
|
||||
message,
|
||||
error,
|
||||
onRetry,
|
||||
retryText = '重试',
|
||||
retryText,
|
||||
icon: Icon = AlertCircle,
|
||||
className,
|
||||
showStack = false,
|
||||
}: ErrorDisplayProps) {
|
||||
const errorMessage = message || error?.message || '发生未知错误,请稍后重试';
|
||||
const t = useTranslations('layout.error');
|
||||
const displayTitle = title ?? t('loadFailed');
|
||||
const displayRetryText = retryText ?? t('retry');
|
||||
const errorMessage = message || error?.message || t('unknownError');
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -70,7 +74,7 @@ export function ErrorDisplay({
|
||||
<Icon className='size-6 text-red-600 dark:text-red-400' />
|
||||
</div>
|
||||
|
||||
<h3 className='text-lg font-semibold mb-2'>{title}</h3>
|
||||
<h3 className='text-lg font-semibold mb-2'>{displayTitle}</h3>
|
||||
|
||||
<p className='text-sm text-muted-foreground max-w-md mb-4'>
|
||||
{errorMessage}
|
||||
@@ -78,14 +82,14 @@ export function ErrorDisplay({
|
||||
|
||||
{onRetry && (
|
||||
<Button onClick={onRetry} variant='outline' className='mt-2'>
|
||||
{retryText}
|
||||
{displayRetryText}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showStack && error?.stack && process.env.NODE_ENV === 'development' && (
|
||||
<details className='mt-6 text-left max-w-2xl w-full'>
|
||||
<summary className='cursor-pointer text-xs text-muted-foreground hover:text-foreground'>
|
||||
查看详细错误信息
|
||||
{t('viewDetails')}
|
||||
</summary>
|
||||
<pre className='mt-2 p-4 bg-muted rounded-lg text-xs overflow-auto'>
|
||||
{error.stack}
|
||||
@@ -115,7 +119,8 @@ export function ErrorInline({
|
||||
onRetry,
|
||||
className,
|
||||
}: Omit<ErrorDisplayProps, 'title' | 'icon' | 'showStack'>) {
|
||||
const errorMessage = message || error?.message || '发生未知错误';
|
||||
const t = useTranslations('layout.error');
|
||||
const errorMessage = message || error?.message || t('unknownErrorShort');
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -133,7 +138,7 @@ export function ErrorInline({
|
||||
size='sm'
|
||||
className='h-6 px-4 text-xs'
|
||||
>
|
||||
重试
|
||||
{t('retry')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { motion } from 'motion/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { FileTextIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
/**
|
||||
* 加载页面组件
|
||||
@@ -88,12 +89,15 @@ interface LoadingStateProps extends React.ComponentProps<'div'> {
|
||||
* 用于统一显示加载中的状态
|
||||
*/
|
||||
export function LoadingState({
|
||||
title = '加载中',
|
||||
description = '正在获取活动数据...',
|
||||
title,
|
||||
description,
|
||||
icon: Icon = FileTextIcon,
|
||||
className,
|
||||
iconSize = 'md',
|
||||
}: LoadingStateProps) {
|
||||
const t = useTranslations('layout.loading');
|
||||
const displayTitle = title ?? t('loading');
|
||||
const displayDescription = description ?? t('loadingDesc');
|
||||
const iconSizes = { sm: 'size-8', md: 'size-10', lg: 'size-14' };
|
||||
const iconInnerSizes = { sm: 'size-4', md: 'size-5', lg: 'size-7' };
|
||||
|
||||
@@ -115,13 +119,15 @@ export function LoadingState({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{title && (
|
||||
<h3 className='text-sm font-medium mb-1 animate-pulse'>{title}</h3>
|
||||
{displayTitle && (
|
||||
<h3 className='text-sm font-medium mb-1 animate-pulse'>
|
||||
{displayTitle}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
{displayDescription && (
|
||||
<p className='text-xs text-muted-foreground max-w-md animate-pulse'>
|
||||
{description}
|
||||
{displayDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Kbd, KbdGroup } from '@/components/ui/kbd';
|
||||
import { type SearchItem, searchItems } from '@/lib/utils/search-data';
|
||||
import { FileText, Home, Settings, Shield } from 'lucide-react';
|
||||
import { useUser } from '@/contexts/user-context';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface SearchDialogProps {
|
||||
open: boolean;
|
||||
@@ -28,34 +29,43 @@ const categoryIcons = {
|
||||
};
|
||||
|
||||
const categoryLabels = {
|
||||
page: '页面',
|
||||
feature: '功能',
|
||||
setting: '设置',
|
||||
admin: '管理',
|
||||
page: 'categoryPage',
|
||||
feature: 'categoryFeature',
|
||||
setting: 'categorySetting',
|
||||
admin: 'categoryAdmin',
|
||||
};
|
||||
|
||||
const getTips = (metaKey: string) => [
|
||||
const getTips = (metaKey: string, t: (key: string) => string) => [
|
||||
<>
|
||||
<span className='text-muted-foreground/80 lowercase'>Tips: 还可以使用</span>
|
||||
<span className='text-muted-foreground/80 lowercase'>
|
||||
{t('tipPrefix')}{' '}
|
||||
</span>
|
||||
<Kbd className='mx-1'>/</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'>来打开此界面</span>
|
||||
</>,
|
||||
<>
|
||||
<span className='text-muted-foreground/80 lowercase'>Tips: 使用</span>
|
||||
<Kbd className='mx-1'>↑</Kbd>
|
||||
<Kbd className='mx-1'>↓</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'>来切换选中项</span>
|
||||
</>,
|
||||
<>
|
||||
<span className='text-muted-foreground/80 lowercase'>Tips: 按住</span>
|
||||
<Kbd className='mx-1'>{metaKey}</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'>+</span>
|
||||
<Kbd className='mx-1'>↵</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'>在新标签页打开</span>
|
||||
<span className='text-muted-foreground/80 lowercase'> {t('tipSlash')}</span>
|
||||
</>,
|
||||
<>
|
||||
<span className='text-muted-foreground/80 lowercase'>
|
||||
你知道吗:搜索功能还在持续升级中
|
||||
{t('tipPrefix')}{' '}
|
||||
</span>
|
||||
<Kbd className='mx-1'>↑</Kbd>
|
||||
<Kbd className='mx-1'>↓</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'> {t('tipArrow')}</span>
|
||||
</>,
|
||||
<>
|
||||
<span className='text-muted-foreground/80 lowercase'>
|
||||
{t('tipPrefix')}{' '}
|
||||
</span>
|
||||
<Kbd className='mx-1'>{metaKey}</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'>+</span>
|
||||
<Kbd className='mx-1'>↵</Kbd>
|
||||
<span className='text-muted-foreground/80 lowercase'>
|
||||
{' '}
|
||||
{t('tipNewTab')}
|
||||
</span>
|
||||
</>,
|
||||
<>
|
||||
<span className='text-muted-foreground/80 lowercase'>
|
||||
{t('tipDidYouKnow')}
|
||||
</span>
|
||||
</>,
|
||||
];
|
||||
@@ -63,6 +73,7 @@ const getTips = (metaKey: string) => [
|
||||
export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
||||
const router = useRouter();
|
||||
const { user } = useUser();
|
||||
const t = useTranslations('layout.search');
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentTip, setCurrentTip] = useState<React.ReactNode>(null);
|
||||
const [results, setResults] = useState<SearchItem[]>([]);
|
||||
@@ -99,11 +110,11 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const tips = getTips(metaKey);
|
||||
const tips = getTips(metaKey, t);
|
||||
const randomTip = tips[Math.floor(Math.random() * tips.length)];
|
||||
setCurrentTip(randomTip);
|
||||
}
|
||||
}, [open, metaKey]);
|
||||
}, [open, metaKey, t]);
|
||||
|
||||
useEffect(() => {
|
||||
const items = searchItems(search, user?.is_admin);
|
||||
@@ -170,18 +181,20 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
||||
className='max-w-[calc(100%-1.5rem)] sm:max-w-[500px] md:max-w-[540px]'
|
||||
>
|
||||
<CommandInput
|
||||
placeholder='搜索页面和功能...'
|
||||
placeholder={t('searchPlaceholder')}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>没有找到相关内容,换个词试试?</CommandEmpty>
|
||||
<CommandEmpty>{t('noResults')}</CommandEmpty>
|
||||
{Object.entries(groupedResults).map(([category, items]) => {
|
||||
const Icon = categoryIcons[category as keyof typeof categoryIcons];
|
||||
return (
|
||||
<CommandGroup
|
||||
key={category}
|
||||
heading={categoryLabels[category as keyof typeof categoryLabels]}
|
||||
heading={t(
|
||||
categoryLabels[category as keyof typeof categoryLabels],
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<CommandItem
|
||||
@@ -217,11 +230,11 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
||||
</div>
|
||||
</div>
|
||||
<span className='ml-3 shrink-0 rounded-md bg-muted px-1.5 py-0.5 text-[10px] leading-none font-medium text-muted-foreground'>
|
||||
{
|
||||
{t(
|
||||
categoryLabels[
|
||||
item.category as keyof typeof categoryLabels
|
||||
]
|
||||
}
|
||||
],
|
||||
)}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
@@ -235,11 +248,11 @@ export function SearchDialog({ open, onOpenChange }: SearchDialogProps) {
|
||||
{isCtrlPressed && <Kbd>{metaKey}</Kbd>}
|
||||
<Kbd>↵</Kbd>
|
||||
</KbdGroup>
|
||||
<span>{isCtrlPressed ? '在新标签页打开' : '打开'}</span>
|
||||
<span>{isCtrlPressed ? t('openInNewTab') : t('open')}</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Kbd>Esc</Kbd>
|
||||
<span>关闭搜索界面</span>
|
||||
<span>{t('closeSearch')}</span>
|
||||
</div>
|
||||
<div className='ml-auto flex items-center gap-1'>{currentTip}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user