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
@@ -24,6 +24,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { useTranslations } from 'next-intl';
interface ManagePageProps<T> {
title: string;
@@ -78,11 +79,12 @@ export function ManagePage<T>({
columns,
renderDetail,
emptyIcon = Layers,
emptyDescription = '暂无数据',
loadingDescription = '加载中',
emptyDescription,
loadingDescription,
getId,
headerExtra,
}: ManagePageProps<T>) {
const t = useTranslations('general.manage');
/** 悬停状态 */
const [hoveredItem, setHoveredItem] = useState<T | null>(null);
const [selectedItem, setSelectedItem] = useState<T | null>(null);
@@ -125,10 +127,10 @@ export function ManagePage<T>({
try {
setSaving(true);
await onSave(selectedItem, editData);
toast.success('保存成功');
toast.success(t('saveSuccess'));
} catch (error) {
toast.error('保存失败', {
description: error instanceof Error ? error.message : '未知错误',
toast.error(t('saveFailed'), {
description: error instanceof Error ? error.message : t('unknownError'),
});
} finally {
setSaving(false);
@@ -144,7 +146,7 @@ export function ManagePage<T>({
if (!deletingItem || !onDelete) return;
try {
await onDelete(deletingItem);
toast.success('删除成功');
toast.success(t('deleteSuccess'));
setDeletingItem(null);
// 如果删除的是当前选中的项,清除选中状态
if (selectedItem && getId(selectedItem) === getId(deletingItem)) {
@@ -152,8 +154,8 @@ export function ManagePage<T>({
setEditData({});
}
} catch (error) {
toast.error('删除失败', {
description: error instanceof Error ? error.message : '未知错误',
toast.error(t('deleteFailed'), {
description: error instanceof Error ? error.message : t('unknownError'),
});
}
};
@@ -164,7 +166,7 @@ export function ManagePage<T>({
return (
<LoadingStateWithBorder
icon={ListRestart}
description={loadingDescription}
description={loadingDescription ?? t('loading')}
/>
);
}
@@ -183,7 +185,10 @@ export function ManagePage<T>({
if (!data || data.length === 0) {
return (
<EmptyStateWithBorder icon={emptyIcon} description={emptyDescription} />
<EmptyStateWithBorder
icon={emptyIcon}
description={emptyDescription ?? t('noData')}
/>
);
}
@@ -216,7 +221,7 @@ export function ManagePage<T>({
<div className='space-y-6'>
<div>
<div className='mb-4'>
<div className='font-semibold'>配置列表</div>
<div className='font-semibold'>{t('configList')}</div>
</div>
{renderContent()}
</div>
@@ -239,18 +244,18 @@ export function ManagePage<T>({
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除?</AlertDialogTitle>
<AlertDialogTitle>{t('confirmDelete')}</AlertDialogTitle>
<AlertDialogDescription>
此操作无法撤销。这将永久删除该配置。
{t('confirmDeleteDesc')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogCancel>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
className='bg-red-600 hover:bg-red-700'
>
确认删除
{t('confirmDeleteAction')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -271,18 +276,24 @@ interface ManageDetailPanelProps {
/** 详情面板 */
export function ManageDetailPanel({
title = '配置信息',
title,
isEmpty,
emptyDescription = '请选择配置查看详情',
emptyDescription,
onSave,
saving = false,
children,
}: ManageDetailPanelProps) {
const t = useTranslations('general.manage');
const displayTitle = title ?? t('configInfo');
const displayEmptyDescription = emptyDescription ?? t('selectConfigToView');
if (isEmpty) {
return (
<div className='space-y-4'>
<div className='font-semibold mb-4'>{title}</div>
<EmptyStateWithBorder icon={Layers} description={emptyDescription} />
<div className='font-semibold mb-4'>{displayTitle}</div>
<EmptyStateWithBorder
icon={Layers}
description={displayEmptyDescription}
/>
</div>
);
}
@@ -291,7 +302,7 @@ export function ManageDetailPanel({
<div className='space-y-4 sticky top-6'>
<div>
<div className='flex items-center justify-between mb-4'>
<div className='font-semibold'>{title}</div>
<div className='font-semibold'>{displayTitle}</div>
{onSave && (
<Button
onClick={onSave}
@@ -301,10 +312,10 @@ export function ManageDetailPanel({
>
{saving ? (
<>
<Spinner /> 更新中
<Spinner /> {t('updating')}
</>
) : (
'更新'
t('update')
)}
</Button>
)}
@@ -341,6 +352,7 @@ export function ManageTable<T>({
onDelete?: (item: T) => void;
getId: (item: T) => string | number;
}) {
const t = useTranslations('general.manage');
return (
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
<ScrollArea className='w-full'>
@@ -358,7 +370,7 @@ export function ManageTable<T>({
))}
{onDelete && (
<TableHead className='whitespace-nowrap text-center w-[120px]'>
操作
{t('actions')}
</TableHead>
)}
</TableRow>
@@ -16,6 +16,7 @@ import {
} from '@/components/ui/input-otp';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { useTranslations } from 'next-intl';
interface PasswordDialogProps {
isOpen: boolean;
@@ -36,10 +37,13 @@ export function PasswordDialog({
onOpenChange,
onConfirm,
loading = false,
title = '请输入密码',
description = '为了您的账户安全,请输入6位认证密码进行验证',
title,
description,
}: PasswordDialogProps) {
const [password, setPassword] = useState('');
const t = useTranslations('general.passwordDialog');
const displayTitle = title ?? t('enterPassword');
const displayDescription = description ?? t('enterPasswordDesc');
const handleConfirm = () => {
if (password.length === 6) {
@@ -58,8 +62,8 @@ export function PasswordDialog({
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className='w-xs sm:w-md'>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
<DialogTitle>{displayTitle}</DialogTitle>
<DialogDescription>{displayDescription}</DialogDescription>
</DialogHeader>
<div className='py-2 flex justify-center'>
@@ -92,7 +96,7 @@ export function PasswordDialog({
disabled={loading}
className='h-8 text-xs'
>
取消
{t('cancel')}
</Button>
<Button
type='button'
@@ -103,10 +107,10 @@ export function PasswordDialog({
{loading ? (
<>
<Spinner className='mr-2 h-4 w-4' />
验证中...
{t('verifying')}
</>
) : (
'确认'
t('confirm')
)}
</Button>
</div>