This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
@@ -1,9 +1,9 @@
"use client"
'use client';
import {useEffect, useMemo, useState} from "react"
import {useQuery} from "@tanstack/react-query"
import { useEffect, useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {Button} from "@/components/ui/button"
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@@ -11,17 +11,17 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {ProxyRouteService} from "@/lib/services/openflare"
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { ProxyRouteService } from '@/lib/services/openflare';
type UptimeKumaSiteSelectModalProps = {
open: boolean
selectedSites: string[]
onOpenChange: (open: boolean) => void
onSave: (sites: string[]) => void
}
open: boolean;
selectedSites: string[];
onOpenChange: (open: boolean) => void;
onSave: (sites: string[]) => void;
};
export function UptimeKumaSiteSelectModal({
open,
@@ -29,62 +29,66 @@ export function UptimeKumaSiteSelectModal({
onOpenChange,
onSave,
}: UptimeKumaSiteSelectModalProps) {
const [searchTerm, setSearchTerm] = useState("")
const [tempSelected, setTempSelected] = useState<Set<string>>(new Set())
const [searchTerm, setSearchTerm] = useState('');
const [tempSelected, setTempSelected] = useState<Set<string>>(new Set());
const routesQuery = useQuery({
queryKey: ["openflare", "proxy-routes"],
queryKey: ['openflare', 'proxy-routes'],
queryFn: () => ProxyRouteService.list(),
enabled: open,
})
});
useEffect(() => {
if (!open) return
setTempSelected(new Set(selectedSites.map((site) => site.trim()).filter(Boolean)))
setSearchTerm("")
}, [open, selectedSites])
if (!open) return;
setTempSelected(
new Set(selectedSites.map((site) => site.trim()).filter(Boolean)),
);
setSearchTerm('');
}, [open, selectedSites]);
const filteredRoutes = useMemo(() => {
const routes = routesQuery.data ?? []
const keyword = searchTerm.trim().toLowerCase()
if (!keyword) return routes
const routes = routesQuery.data ?? [];
const keyword = searchTerm.trim().toLowerCase();
if (!keyword) return routes;
return routes.filter((route) => {
const domains = (route.zone_domains ?? []).map((item) => item.domain).join(' ')
const domains = (route.zone_domains ?? [])
.map((item) => item.domain)
.join(' ');
return (
route.site_name.toLowerCase().includes(keyword) ||
domains.toLowerCase().includes(keyword)
)
})
}, [routesQuery.data, searchTerm])
);
});
}, [routesQuery.data, searchTerm]);
const toggleSite = (siteName: string) => {
setTempSelected((previous) => {
const next = new Set(previous)
if (next.has(siteName)) next.delete(siteName)
else next.add(siteName)
return next
})
}
const next = new Set(previous);
if (next.has(siteName)) next.delete(siteName);
else next.add(siteName);
return next;
});
};
const handleSelectAll = () => {
setTempSelected((previous) => {
const next = new Set(previous)
filteredRoutes.forEach((route) => next.add(route.site_name))
return next
})
}
const next = new Set(previous);
filteredRoutes.forEach((route) => next.add(route.site_name));
return next;
});
};
const handleDeselectAll = () => {
setTempSelected((previous) => {
const next = new Set(previous)
filteredRoutes.forEach((route) => next.delete(route.site_name))
return next
})
}
const next = new Set(previous);
filteredRoutes.forEach((route) => next.delete(route.site_name));
return next;
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogContent className='max-w-2xl'>
<DialogHeader>
<DialogTitle>选择监控站点</DialogTitle>
<DialogDescription>
@@ -92,88 +96,111 @@ export function UptimeKumaSiteSelectModal({
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-1.5">
<div className='space-y-4'>
<div className='space-y-1.5'>
<Label>搜索站点</Label>
<Input
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
placeholder="按名称或域名搜索..."
placeholder='按名称或域名搜索...'
/>
</div>
<div className="flex flex-wrap gap-2">
<Button type="button" variant="outline" size="sm" onClick={handleSelectAll}>
<div className='flex flex-wrap gap-2'>
<Button
type='button'
variant='outline'
size='sm'
onClick={handleSelectAll}
>
全选过滤项
</Button>
<Button type="button" variant="outline" size="sm" onClick={handleDeselectAll}>
<Button
type='button'
variant='outline'
size='sm'
onClick={handleDeselectAll}
>
清空过滤项
</Button>
</div>
<div className="max-h-72 overflow-y-auto rounded-lg border">
<div className='max-h-72 overflow-y-auto rounded-lg border'>
{routesQuery.isLoading ? (
<p className="p-4 text-sm text-muted-foreground">加载站点列表...</p>
<p className='p-4 text-sm text-muted-foreground'>
加载站点列表...
</p>
) : routesQuery.isError ? (
<p className="p-4 text-sm text-destructive">
<p className='p-4 text-sm text-destructive'>
{routesQuery.error instanceof Error
? routesQuery.error.message
: "加载站点失败"}
: '加载站点失败'}
</p>
) : filteredRoutes.length === 0 ? (
<p className="p-4 text-sm text-muted-foreground">无匹配的站点</p>
<p className='p-4 text-sm text-muted-foreground'>无匹配的站点</p>
) : (
<table className="w-full text-left text-sm">
<thead className="sticky top-0 bg-muted/60 text-xs uppercase text-muted-foreground">
<table className='w-full text-left text-sm'>
<thead className='sticky top-0 bg-muted/60 text-xs uppercase text-muted-foreground'>
<tr>
<th className="w-12 px-3 py-2">选择</th>
<th className="px-3 py-2">站点名称</th>
<th className="px-3 py-2">主域名</th>
<th className='w-12 px-3 py-2'>选择</th>
<th className='px-3 py-2'>站点名称</th>
<th className='px-3 py-2'>主域名</th>
</tr>
</thead>
<tbody className="divide-y">
<tbody className='divide-y'>
{filteredRoutes.map((route) => {
const checked = tempSelected.has(route.site_name)
const checked = tempSelected.has(route.site_name);
return (
<tr
key={route.id}
className="cursor-pointer hover:bg-muted/30"
className='cursor-pointer hover:bg-muted/30'
onClick={() => toggleSite(route.site_name)}
>
<td className="px-3 py-2" onClick={(event) => event.stopPropagation()}>
<td
className='px-3 py-2'
onClick={(event) => event.stopPropagation()}
>
<input
type="checkbox"
type='checkbox'
checked={checked}
onChange={() => toggleSite(route.site_name)}
className="size-4 rounded border-input"
className='size-4 rounded border-input'
/>
</td>
<td className="px-3 py-2 font-medium">{route.site_name}</td>
<td className="px-3 py-2 text-muted-foreground">
{(route.zone_domains ?? []).map((item) => item.domain).join(', ') || '—'}
<td className='px-3 py-2 font-medium'>
{route.site_name}
</td>
<td className='px-3 py-2 text-muted-foreground'>
{(route.zone_domains ?? [])
.map((item) => item.domain)
.join(', ') || '—'}
</td>
</tr>
)
);
})}
</tbody>
</table>
)}
</div>
<p className="text-right text-xs text-muted-foreground">
<p className='text-right text-xs text-muted-foreground'>
已选择 {tempSelected.size} 个监控站点
</p>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
<Button
type='button'
variant='outline'
onClick={() => onOpenChange(false)}
>
取消
</Button>
<Button
type="button"
type='button'
onClick={() => {
onSave(Array.from(tempSelected))
onOpenChange(false)
onSave(Array.from(tempSelected));
onOpenChange(false);
}}
>
保存选择
@@ -181,5 +208,5 @@ export function UptimeKumaSiteSelectModal({
</DialogFooter>
</DialogContent>
</Dialog>
)
}
);
}