mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
prettier
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import {useState} from "react"
|
||||
import {toast} from "sonner"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Spinner} from "@/components/ui/spinner"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {Layers, ListRestart, LucideIcon, Trash2} from "lucide-react"
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { ErrorInline } from '@/components/layout/error';
|
||||
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||
import { LoadingStateWithBorder } from '@/components/layout/loading';
|
||||
import { Layers, ListRestart, LucideIcon, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -15,49 +15,54 @@ import {
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import {TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
|
||||
import {ScrollArea, ScrollBar} from "@/components/ui/scroll-area"
|
||||
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||
|
||||
interface ManagePageProps<T> {
|
||||
title: string
|
||||
icon?: LucideIcon
|
||||
data: T[]
|
||||
loading: boolean
|
||||
error: Error | null
|
||||
onReload: () => void
|
||||
title: string;
|
||||
icon?: LucideIcon;
|
||||
data: T[];
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
onReload: () => void;
|
||||
|
||||
/** 获取初始编辑数据 */
|
||||
getInitialEditData: (item: T) => Partial<T>
|
||||
onSave: (item: T, editData: Partial<T>) => Promise<void>
|
||||
onDelete?: (item: T) => Promise<void>
|
||||
getInitialEditData: (item: T) => Partial<T>;
|
||||
onSave: (item: T, editData: Partial<T>) => Promise<void>;
|
||||
onDelete?: (item: T) => Promise<void>;
|
||||
|
||||
/** 渲染表格 (Config-Driven) */
|
||||
columns: {
|
||||
header: string
|
||||
cell: (item: T) => React.ReactNode
|
||||
width?: string
|
||||
align?: 'left' | 'center' | 'right'
|
||||
className?: string
|
||||
}[]
|
||||
header: string;
|
||||
cell: (item: T) => React.ReactNode;
|
||||
width?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
}[];
|
||||
|
||||
/** 渲染详情 */
|
||||
renderDetail: (props: {
|
||||
selected: T | null
|
||||
hovered: T | null
|
||||
editData: Partial<T>
|
||||
onEditDataChange: (field: keyof T, value: T[keyof T]) => void
|
||||
onSave: () => void
|
||||
saving: boolean
|
||||
}) => React.ReactNode
|
||||
selected: T | null;
|
||||
hovered: T | null;
|
||||
editData: Partial<T>;
|
||||
onEditDataChange: (field: keyof T, value: T[keyof T]) => void;
|
||||
onSave: () => void;
|
||||
saving: boolean;
|
||||
}) => React.ReactNode;
|
||||
|
||||
/** 空状态图标 */
|
||||
emptyIcon?: LucideIcon
|
||||
emptyDescription?: string
|
||||
loadingDescription?: string
|
||||
getId: (item: T) => string | number
|
||||
headerExtra?: React.ReactNode
|
||||
emptyIcon?: LucideIcon;
|
||||
emptyDescription?: string;
|
||||
loadingDescription?: string;
|
||||
getId: (item: T) => string | number;
|
||||
headerExtra?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ManagePage<T>({
|
||||
@@ -73,85 +78,85 @@ export function ManagePage<T>({
|
||||
columns,
|
||||
renderDetail,
|
||||
emptyIcon = Layers,
|
||||
emptyDescription = "暂无数据",
|
||||
loadingDescription = "加载中",
|
||||
emptyDescription = '暂无数据',
|
||||
loadingDescription = '加载中',
|
||||
getId,
|
||||
headerExtra
|
||||
headerExtra,
|
||||
}: ManagePageProps<T>) {
|
||||
/** 悬停状态 */
|
||||
const [hoveredItem, setHoveredItem] = useState<T | null>(null)
|
||||
const [selectedItem, setSelectedItem] = useState<T | null>(null)
|
||||
const [editData, setEditData] = useState<Partial<T>>({})
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [deletingItem, setDeletingItem] = useState<T | null>(null)
|
||||
const [hoveredItem, setHoveredItem] = useState<T | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<T | null>(null);
|
||||
const [editData, setEditData] = useState<Partial<T>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingItem, setDeletingItem] = useState<T | null>(null);
|
||||
|
||||
/** 悬停处理 */
|
||||
const handleHover = (item: T | null) => {
|
||||
setHoveredItem(item)
|
||||
}
|
||||
setHoveredItem(item);
|
||||
};
|
||||
|
||||
/** 选择处理 */
|
||||
const handleSelect = (item: T) => {
|
||||
const itemId = getId(item)
|
||||
const selectedId = selectedItem ? getId(selectedItem) : null
|
||||
const itemId = getId(item);
|
||||
const selectedId = selectedItem ? getId(selectedItem) : null;
|
||||
|
||||
if (itemId === selectedId) {
|
||||
setSelectedItem(null)
|
||||
setEditData({})
|
||||
setSelectedItem(null);
|
||||
setEditData({});
|
||||
} else {
|
||||
setSelectedItem(item)
|
||||
setEditData(getInitialEditData(item))
|
||||
setSelectedItem(item);
|
||||
setEditData(getInitialEditData(item));
|
||||
}
|
||||
setHoveredItem(null)
|
||||
}
|
||||
setHoveredItem(null);
|
||||
};
|
||||
|
||||
/** 编辑数据处理 */
|
||||
const handleEditDataChange = (field: keyof T, value: T[keyof T]) => {
|
||||
setEditData(prev => ({
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}))
|
||||
}
|
||||
[field]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
/** 保存处理 */
|
||||
const handleSave = async () => {
|
||||
if (!selectedItem) return
|
||||
if (!selectedItem) return;
|
||||
|
||||
try {
|
||||
setSaving(true)
|
||||
await onSave(selectedItem, editData)
|
||||
toast.success('保存成功')
|
||||
setSaving(true);
|
||||
await onSave(selectedItem, editData);
|
||||
toast.success('保存成功');
|
||||
} catch (error) {
|
||||
toast.error('保存失败', {
|
||||
description: error instanceof Error ? error.message : '未知错误'
|
||||
})
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
} finally {
|
||||
setSaving(false)
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 删除处理 */
|
||||
const handleDeleteClick = (item: T) => {
|
||||
setDeletingItem(item)
|
||||
}
|
||||
setDeletingItem(item);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
if (!deletingItem || !onDelete) return
|
||||
if (!deletingItem || !onDelete) return;
|
||||
try {
|
||||
await onDelete(deletingItem)
|
||||
toast.success('删除成功')
|
||||
setDeletingItem(null)
|
||||
await onDelete(deletingItem);
|
||||
toast.success('删除成功');
|
||||
setDeletingItem(null);
|
||||
// 如果删除的是当前选中的项,清除选中状态
|
||||
if (selectedItem && getId(selectedItem) === getId(deletingItem)) {
|
||||
setSelectedItem(null)
|
||||
setEditData({})
|
||||
setSelectedItem(null);
|
||||
setEditData({});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('删除失败', {
|
||||
description: error instanceof Error ? error.message : '未知错误'
|
||||
})
|
||||
description: error instanceof Error ? error.message : '未知错误',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 渲染内容 */
|
||||
const renderContent = () => {
|
||||
@@ -161,28 +166,25 @@ export function ManagePage<T>({
|
||||
icon={ListRestart}
|
||||
description={loadingDescription}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-8 border border-dashed rounded-lg">
|
||||
<div className='p-8 border border-dashed rounded-lg'>
|
||||
<ErrorInline
|
||||
error={error}
|
||||
onRetry={onReload}
|
||||
className="justify-center"
|
||||
className='justify-center'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<EmptyStateWithBorder
|
||||
icon={emptyIcon}
|
||||
description={emptyDescription}
|
||||
/>
|
||||
)
|
||||
<EmptyStateWithBorder icon={emptyIcon} description={emptyDescription} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -196,27 +198,25 @@ export function ManagePage<T>({
|
||||
onDelete={onDelete ? handleDeleteClick : undefined}
|
||||
getId={getId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="flex pb-2 mb-6 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{Icon && <Icon className="size-5 text-primary" />}
|
||||
<div className='py-6'>
|
||||
<div className='flex pb-2 mb-6 items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{Icon && <Icon className='size-5 text-primary' />}
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{headerExtra}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className='space-y-6'>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<div className="font-semibold">配置列表</div>
|
||||
<div className='mb-4'>
|
||||
<div className='font-semibold'>配置列表</div>
|
||||
</div>
|
||||
{renderContent()}
|
||||
</div>
|
||||
@@ -228,12 +228,15 @@ export function ManagePage<T>({
|
||||
editData,
|
||||
onEditDataChange: handleEditDataChange,
|
||||
onSave: handleSave,
|
||||
saving
|
||||
saving,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={!!deletingItem} onOpenChange={(open) => !open && setDeletingItem(null)}>
|
||||
<AlertDialog
|
||||
open={!!deletingItem}
|
||||
onOpenChange={(open) => !open && setDeletingItem(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除?</AlertDialogTitle>
|
||||
@@ -243,67 +246,73 @@ export function ManagePage<T>({
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete} className="bg-red-600 hover:bg-red-700">
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className='bg-red-600 hover:bg-red-700'
|
||||
>
|
||||
确认删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** 详情面板 props */
|
||||
interface ManageDetailPanelProps {
|
||||
title?: string
|
||||
isEmpty: boolean
|
||||
emptyDescription?: string
|
||||
onSave?: () => void
|
||||
saving?: boolean
|
||||
children: React.ReactNode
|
||||
title?: string;
|
||||
isEmpty: boolean;
|
||||
emptyDescription?: string;
|
||||
onSave?: () => void;
|
||||
saving?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** 详情面板 */
|
||||
export function ManageDetailPanel({
|
||||
title = "配置信息",
|
||||
title = '配置信息',
|
||||
isEmpty,
|
||||
emptyDescription = "请选择配置查看详情",
|
||||
emptyDescription = '请选择配置查看详情',
|
||||
onSave,
|
||||
saving = false,
|
||||
children
|
||||
children,
|
||||
}: ManageDetailPanelProps) {
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="font-semibold mb-4">{title}</div>
|
||||
<EmptyStateWithBorder
|
||||
icon={Layers}
|
||||
description={emptyDescription}
|
||||
/>
|
||||
<div className='space-y-4'>
|
||||
<div className='font-semibold mb-4'>{title}</div>
|
||||
<EmptyStateWithBorder icon={Layers} description={emptyDescription} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sticky top-6">
|
||||
<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='flex items-center justify-between mb-4'>
|
||||
<div className='font-semibold'>{title}</div>
|
||||
{onSave && (
|
||||
<Button
|
||||
onClick={onSave}
|
||||
disabled={saving}
|
||||
size="sm"
|
||||
className="px-3 h-7 text-xs"
|
||||
size='sm'
|
||||
className='px-3 h-7 text-xs'
|
||||
>
|
||||
{saving ? (<><Spinner /> 更新中</>) : '更新'}
|
||||
{saving ? (
|
||||
<>
|
||||
<Spinner /> 更新中
|
||||
</>
|
||||
) : (
|
||||
'更新'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** 表格面板 */
|
||||
@@ -315,56 +324,61 @@ export function ManageTable<T>({
|
||||
onSelect,
|
||||
onHover,
|
||||
onDelete,
|
||||
getId
|
||||
getId,
|
||||
}: {
|
||||
data: T[]
|
||||
data: T[];
|
||||
columns: {
|
||||
header: string
|
||||
cell: (item: T) => React.ReactNode
|
||||
width?: string
|
||||
align?: 'left' | 'center' | 'right'
|
||||
className?: string
|
||||
}[]
|
||||
selected: T | null
|
||||
hovered: T | null
|
||||
onSelect: (item: T) => void
|
||||
onHover: (item: T | null) => void
|
||||
onDelete?: (item: T) => void
|
||||
getId: (item: T) => string | number
|
||||
header: string;
|
||||
cell: (item: T) => React.ReactNode;
|
||||
width?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
}[];
|
||||
selected: T | null;
|
||||
hovered: T | null;
|
||||
onSelect: (item: T) => void;
|
||||
onHover: (item: T | null) => void;
|
||||
onDelete?: (item: T) => void;
|
||||
getId: (item: T) => string | number;
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-dashed shadow-none rounded-lg overflow-hidden">
|
||||
<ScrollArea className="w-full">
|
||||
<div className="relative w-full">
|
||||
<table className="w-full caption-bottom text-sm">
|
||||
<div className='border border-dashed shadow-none rounded-lg overflow-hidden'>
|
||||
<ScrollArea className='w-full'>
|
||||
<div className='relative w-full'>
|
||||
<table className='w-full caption-bottom text-sm'>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b border-dashed">
|
||||
<TableRow className='border-b border-dashed'>
|
||||
{columns.map((col, index) => (
|
||||
<TableHead
|
||||
key={index}
|
||||
className={`whitespace-nowrap ${ col.width ? `w-[${ col.width }]` : '' } ${ col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : '' } ${ col.className || '' }`}
|
||||
className={`whitespace-nowrap ${col.width ? `w-[${col.width}]` : ''} ${col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : ''} ${col.className || ''}`}
|
||||
>
|
||||
{col.header}
|
||||
</TableHead>
|
||||
))}
|
||||
{onDelete && <TableHead className="whitespace-nowrap text-center w-[120px]">操作</TableHead>}
|
||||
{onDelete && (
|
||||
<TableHead className='whitespace-nowrap text-center w-[120px]'>
|
||||
操作
|
||||
</TableHead>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="animate-in fade-in duration-200">
|
||||
<TableBody className='animate-in fade-in duration-200'>
|
||||
{data.map((item) => {
|
||||
const id = getId(item)
|
||||
const isSelected = selected && getId(selected) === id
|
||||
const isHovered = hovered && getId(hovered) === id
|
||||
const id = getId(item);
|
||||
const isSelected = selected && getId(selected) === id;
|
||||
const isHovered = hovered && getId(hovered) === id;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={id}
|
||||
className={`border-b border-dashed cursor-pointer transition-colors ${ isSelected
|
||||
? 'bg-primary/5 hover:bg-primary/10'
|
||||
: isHovered
|
||||
? 'bg-gray-100 hover:bg-muted/50'
|
||||
: 'hover:bg-gray-100'
|
||||
}`}
|
||||
className={`border-b border-dashed cursor-pointer transition-colors ${
|
||||
isSelected
|
||||
? 'bg-primary/5 hover:bg-primary/10'
|
||||
: isHovered
|
||||
? 'bg-gray-100 hover:bg-muted/50'
|
||||
: 'hover:bg-gray-100'
|
||||
}`}
|
||||
onMouseEnter={() => onHover(item)}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
onClick={() => onSelect(item)}
|
||||
@@ -372,34 +386,34 @@ export function ManageTable<T>({
|
||||
{columns.map((col, index) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
className={`text-xs py-1 ${ col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : '' } ${ col.className || '' }`}
|
||||
className={`text-xs py-1 ${col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : ''} ${col.className || ''}`}
|
||||
>
|
||||
{col.cell(item)}
|
||||
</TableCell>
|
||||
))}
|
||||
{onDelete && (
|
||||
<TableCell className="text-xs py-1 text-center">
|
||||
<TableCell className='text-xs py-1 text-center'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-red-600 hover:bg-red-50"
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-6 w-6 text-muted-foreground hover:text-red-600 hover:bg-red-50'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(item)
|
||||
e.stopPropagation();
|
||||
onDelete(item);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<Trash2 className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</table>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
<ScrollBar orientation='horizontal' />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import * as React from "react"
|
||||
import { useState } from "react"
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from "@/components/ui/input-otp"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Spinner } from "@/components/ui/spinner"
|
||||
} from '@/components/ui/input-otp';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
interface PasswordDialogProps {
|
||||
isOpen: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onConfirm: (password: string) => void
|
||||
loading?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: (password: string) => void;
|
||||
loading?: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码输入对话框
|
||||
*
|
||||
*
|
||||
* 用于敏感操作前的二次确认
|
||||
*/
|
||||
export function PasswordDialog({
|
||||
@@ -36,35 +36,33 @@ export function PasswordDialog({
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
loading = false,
|
||||
title = "请输入密码",
|
||||
description = "为了您的账户安全,请输入6位认证密码进行验证"
|
||||
title = '请输入密码',
|
||||
description = '为了您的账户安全,请输入6位认证密码进行验证',
|
||||
}: PasswordDialogProps) {
|
||||
const [password, setPassword] = useState("")
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (password.length === 6) {
|
||||
onConfirm(password)
|
||||
onConfirm(password);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* 当对话框关闭时重置密码*/
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setPassword("")
|
||||
setPassword('');
|
||||
}
|
||||
}, [isOpen])
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-xs sm:w-md">
|
||||
<DialogContent className='w-xs sm:w-md'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-2 flex justify-center">
|
||||
<div className='py-2 flex justify-center'>
|
||||
<InputOTP
|
||||
maxLength={6}
|
||||
value={password}
|
||||
@@ -72,47 +70,47 @@ export function PasswordDialog({
|
||||
disabled={loading}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && password.length === 6 && !loading) {
|
||||
handleConfirm()
|
||||
handleConfirm();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<InputOTPGroup className="gap-2">
|
||||
<InputOTPSlot index={0} className="w-10 h-10 text-base" />
|
||||
<InputOTPSlot index={1} className="w-10 h-10 text-base" />
|
||||
<InputOTPSlot index={2} className="w-10 h-10 text-base" />
|
||||
<InputOTPSlot index={3} className="w-10 h-10 text-base" />
|
||||
<InputOTPSlot index={4} className="w-10 h-10 text-base" />
|
||||
<InputOTPSlot index={5} className="w-10 h-10 text-base" />
|
||||
<InputOTPGroup className='gap-2'>
|
||||
<InputOTPSlot index={0} className='w-10 h-10 text-base' />
|
||||
<InputOTPSlot index={1} className='w-10 h-10 text-base' />
|
||||
<InputOTPSlot index={2} className='w-10 h-10 text-base' />
|
||||
<InputOTPSlot index={3} className='w-10 h-10 text-base' />
|
||||
<InputOTPSlot index={4} className='w-10 h-10 text-base' />
|
||||
<InputOTPSlot index={5} className='w-10 h-10 text-base' />
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<div className='flex justify-end gap-2 pt-2'>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant='ghost'
|
||||
onClick={() => onOpenChange(false)}
|
||||
disabled={loading}
|
||||
className="h-8 text-xs"
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="bg-primary hover:bg-primary/90 h-8 text-xs"
|
||||
type='button'
|
||||
className='bg-primary hover:bg-primary/90 h-8 text-xs'
|
||||
onClick={handleConfirm}
|
||||
disabled={password.length !== 6 || loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Spinner className="mr-2 h-4 w-4" />
|
||||
<Spinner className='mr-2 h-4 w-4' />
|
||||
验证中...
|
||||
</>
|
||||
) : (
|
||||
"确认"
|
||||
'确认'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user