mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-02 23:06:36 +08:00
fix(frontend): replace browser confirm dialogs with AlertDialog
统一删除、撤销与未保存离开等确认操作为 shadcn AlertDialog,避免 window.confirm/alert 打断界面风格;同步更新 WAF 编辑器相关单测与 changelog。
This commit is contained in:
@@ -37,6 +37,16 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
@@ -50,6 +60,8 @@ import { UserService } from '@/lib/services/user';
|
||||
import { useAuth } from '@/components/providers/auth-provider';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type ConfirmTarget = { id: number; name: string };
|
||||
|
||||
export function AccessTokenMain() {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -60,6 +72,12 @@ export function AccessTokenMain() {
|
||||
const [copiedId, setCopiedId] = React.useState<number | null>(null);
|
||||
const [newCreatedToken, setNewCreatedToken] =
|
||||
React.useState<CreateTokenResponse | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = React.useState<ConfirmTarget | null>(
|
||||
null,
|
||||
);
|
||||
const [rotateTarget, setRotateTarget] = React.useState<ConfirmTarget | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// 获取 Token 列表
|
||||
const accessTokensQuery = useQuery({
|
||||
@@ -91,6 +109,7 @@ export function AccessTokenMain() {
|
||||
const deleteTokenMutation = useMutation({
|
||||
mutationFn: (id: number) => UserService.deleteAccessToken(id),
|
||||
onSuccess: () => {
|
||||
setDeleteTarget(null);
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['user', 'access-tokens'],
|
||||
});
|
||||
@@ -105,6 +124,7 @@ export function AccessTokenMain() {
|
||||
const rotateTokenMutation = useMutation({
|
||||
mutationFn: (id: number) => UserService.rotateAccessToken(id),
|
||||
onSuccess: (data) => {
|
||||
setRotateTarget(null);
|
||||
setNewCreatedToken(data);
|
||||
setViewDialogOpen(true);
|
||||
void queryClient.invalidateQueries({
|
||||
@@ -129,26 +149,6 @@ export function AccessTokenMain() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteToken = (id: number, name: string) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`确定要删除并撤销令牌「${name}」吗?删除后此令牌将立即失效且不可恢复。`,
|
||||
)
|
||||
) {
|
||||
deleteTokenMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRotateToken = (id: number, name: string) => {
|
||||
if (
|
||||
window.confirm(
|
||||
`确定要轮换令牌「${name}」的密钥吗?轮换后系统将生成全新密钥,原令牌密钥将立即失效。`,
|
||||
)
|
||||
) {
|
||||
rotateTokenMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyText = async (text: string, id: number) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
@@ -289,22 +289,8 @@ export function AccessTokenMain() {
|
||||
size='sm'
|
||||
className='text-xs border-dashed text-muted-foreground hover:text-primary hover:bg-primary/5 rounded-lg h-8 px-2.5'
|
||||
onClick={() =>
|
||||
handleCopyText(token.masked_token, token.id)
|
||||
setRotateTarget({ id: token.id, name: token.name })
|
||||
}
|
||||
>
|
||||
{copiedId === token.id ? (
|
||||
<Check className='size-3.5 mr-1 text-emerald-500' />
|
||||
) : (
|
||||
<Copy className='size-3.5 mr-1' />
|
||||
)}
|
||||
复制
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='text-xs border-dashed text-muted-foreground hover:text-primary hover:bg-primary/5 rounded-lg h-8 px-2.5'
|
||||
onClick={() => handleRotateToken(token.id, token.name)}
|
||||
disabled={rotateTokenMutation.isPending}
|
||||
>
|
||||
<RefreshCw
|
||||
@@ -317,7 +303,9 @@ export function AccessTokenMain() {
|
||||
variant='outline'
|
||||
size='sm'
|
||||
className='text-xs border-dashed text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 hover:border-rose-500/20 rounded-lg h-8 px-2.5'
|
||||
onClick={() => handleDeleteToken(token.id, token.name)}
|
||||
onClick={() =>
|
||||
setDeleteTarget({ id: token.id, name: token.name })
|
||||
}
|
||||
disabled={deleteTokenMutation.isPending}
|
||||
>
|
||||
<Trash2 className='size-3.5 mr-1' />
|
||||
@@ -491,6 +479,62 @@ export function AccessTokenMain() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认撤销访问令牌</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要删除并撤销令牌「{deleteTarget?.name}
|
||||
」吗?删除后此令牌将立即失效且不可恢复。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteTokenMutation.isPending}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteTokenMutation.isPending}
|
||||
onClick={() =>
|
||||
deleteTarget && deleteTokenMutation.mutate(deleteTarget.id)
|
||||
}
|
||||
>
|
||||
{deleteTokenMutation.isPending ? '撤销中...' : '确认撤销'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(rotateTarget)}
|
||||
onOpenChange={(open) => !open && setRotateTarget(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认轮换访问令牌</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定要轮换令牌「{rotateTarget?.name}
|
||||
」的密钥吗?轮换后系统将生成全新密钥,原令牌密钥将立即失效。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={rotateTokenMutation.isPending}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={rotateTokenMutation.isPending}
|
||||
onClick={() =>
|
||||
rotateTarget && rotateTokenMutation.mutate(rotateTarget.id)
|
||||
}
|
||||
>
|
||||
{rotateTokenMutation.isPending ? '轮换中...' : '确认轮换'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,16 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { AuthSourceModal } from '@/components/common/settings/auth-source-modal';
|
||||
import services from '@/lib/services';
|
||||
import type { AuthSource, SystemConfig } from '@/lib/services/admin';
|
||||
@@ -94,6 +104,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [authSourceModalOpen, setAuthSourceModalOpen] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState<AuthSource | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<AuthSource | null>(null);
|
||||
|
||||
const [capCount, setCapCount] = useState('');
|
||||
const [capDifficulty, setCapDifficulty] = useState('');
|
||||
@@ -232,6 +243,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
await services.adminAuthSource.deleteAuthSource(sourceId);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setDeleteTarget(null);
|
||||
await queryClient.invalidateQueries({ queryKey: ['auth', 'sources'] });
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['auth', 'public-sources'],
|
||||
@@ -486,15 +498,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
size='icon'
|
||||
className='size-8 text-muted-foreground hover:text-rose-500 hover:bg-rose-500/10 rounded-lg transition-colors'
|
||||
disabled={deleteSourceMutation.isPending}
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm(
|
||||
`确定删除认证源「${source.display_name || source.name}」吗?`,
|
||||
)
|
||||
) {
|
||||
deleteSourceMutation.mutate(source.id);
|
||||
}
|
||||
}}
|
||||
onClick={() => setDeleteTarget(source)}
|
||||
>
|
||||
<Trash2 className='size-4' />
|
||||
</Button>
|
||||
@@ -702,6 +706,35 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
||||
await authSourcesQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除认证源</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定删除认证源「
|
||||
{deleteTarget?.display_name || deleteTarget?.name}
|
||||
」吗?删除后无法恢复。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteSourceMutation.isPending}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteSourceMutation.isPending}
|
||||
onClick={() =>
|
||||
deleteTarget && deleteSourceMutation.mutate(deleteTarget.id)
|
||||
}
|
||||
>
|
||||
{deleteSourceMutation.isPending ? '删除中...' : '确认删除'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,16 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import services from '@/lib/services';
|
||||
import type { Template } from '@/lib/services/admin/types';
|
||||
import { toast } from 'sonner';
|
||||
@@ -33,6 +43,7 @@ export function TemplatesManager() {
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(
|
||||
null,
|
||||
);
|
||||
const [deleteTarget, setDeleteTarget] = useState<Template | null>(null);
|
||||
|
||||
// Form states
|
||||
const [key, setKey] = useState('');
|
||||
@@ -93,6 +104,7 @@ export function TemplatesManager() {
|
||||
await services.adminTemplate.deleteTemplate(key);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setDeleteTarget(null);
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin', 'templates'] });
|
||||
toast.success('通知模板已删除');
|
||||
},
|
||||
@@ -223,13 +235,7 @@ export function TemplatesManager() {
|
||||
disabled={
|
||||
tmpl.is_system || deleteTemplateMutation.isPending
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm(`确定删除模板「${tmpl.name}」吗?`)
|
||||
) {
|
||||
deleteTemplateMutation.mutate(tmpl.key);
|
||||
}
|
||||
}}
|
||||
onClick={() => setDeleteTarget(tmpl)}
|
||||
>
|
||||
<Trash2 className='size-4' />
|
||||
</Button>
|
||||
@@ -398,6 +404,33 @@ export function TemplatesManager() {
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除模板</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确定删除模板「{deleteTarget?.name}」吗?删除后无法恢复。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteTemplateMutation.isPending}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={deleteTemplateMutation.isPending}
|
||||
onClick={() =>
|
||||
deleteTarget && deleteTemplateMutation.mutate(deleteTarget.key)
|
||||
}
|
||||
>
|
||||
{deleteTemplateMutation.isPending ? '删除中...' : '确认删除'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user