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:
ryan
2026-07-24 16:37:24 +08:00
parent a4588b05e0
commit 9f15f42b47
75 changed files with 5971 additions and 2450 deletions
+15 -5
View File
@@ -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>
);
}
+13 -8
View File
@@ -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>
+12 -6
View File
@@ -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>
+44 -31
View File
@@ -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>