Skip to content

Commit bea71c2

Browse files
committed
feat: 增加批量删除 && 修改部分ip提示文案
1 parent 9ecf932 commit bea71c2

10 files changed

Lines changed: 253 additions & 23 deletions

File tree

app/endpoints/components/add-endpoint-modal.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ export default function AddEndpointModal({
221221
labelPlacement="outside"
222222
placeholder="主服务器"
223223
maxLength={30}
224-
description="主控的显示名称"
225224
value={formData.name}
226225
onValueChange={(value) => handleInputChange('name', value)}
227226
/>
@@ -233,7 +232,6 @@ export default function AddEndpointModal({
233232
labelPlacement="outside"
234233
placeholder="http(s)://example.com:9090/api/v1"
235234
type="url"
236-
description="API 服务器的完整 URL(包含 API 前缀路径)"
237235
value={formData.url}
238236
onValueChange={(value) => handleInputChange('url', value)}
239237
className="md:col-span-1"
@@ -247,7 +245,6 @@ export default function AddEndpointModal({
247245
placeholder="输入您的 API Key"
248246
type={showApiKey ? "text" : "password"}
249247
maxLength={100}
250-
description="用于身份验证的密钥"
251248
value={formData.apiKey}
252249
onValueChange={(value) => handleInputChange('apiKey', value)}
253250
className="md:col-span-1"

app/templates/page.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ export default function TemplatesPage() {
510510
...(formData.userListenType === 'assign' ? [{
511511
label: '监听地址',
512512
key: 'userListenAddress' as keyof FormData,
513-
placeholder: '0.0.0.0',
513+
placeholder: '0.0.0.0/[2001:db8::1]',
514514
value: formData.userListenAddress
515515
}] : []),
516516
{
@@ -522,7 +522,7 @@ export default function TemplatesPage() {
522522
]
523523
},
524524
{
525-
label: '客户端',
525+
label: '中转(客户端)',
526526
type: 'relay',
527527
formFields: [
528528
{
@@ -574,7 +574,7 @@ export default function TemplatesPage() {
574574
...(formData.targetListenType === 'external' ? [{
575575
label: '目标地址',
576576
key: 'exitIp' as keyof FormData,
577-
placeholder: '192.168.1.100',
577+
placeholder: '0.0.0.0/[2001:db8::1]',
578578
value: formData.exitIp
579579
}] : []),
580580
{
@@ -608,7 +608,7 @@ export default function TemplatesPage() {
608608
...(formData.userListenType === 'assign' ? [{
609609
label: '监听地址',
610610
key: 'userListenAddress' as keyof FormData,
611-
placeholder: '0.0.0.0',
611+
placeholder: '0.0.0.0/[2001:db8::1]',
612612
value: formData.userListenAddress
613613
}] : []),
614614
{
@@ -620,7 +620,7 @@ export default function TemplatesPage() {
620620
]
621621
},
622622
{
623-
label: '客户端',
623+
label: '入口(客户端)',
624624
type: 'target',
625625
formFields: [
626626
{
@@ -654,7 +654,7 @@ export default function TemplatesPage() {
654654
]
655655
},
656656
{
657-
label: '服务端',
657+
label: '出口(服务端)',
658658
type: 'relay',
659659
formFields: [
660660
{
@@ -690,7 +690,7 @@ export default function TemplatesPage() {
690690
...(formData.targetListenType === 'external' ? [{
691691
label: '目标地址',
692692
key: 'exitIp' as keyof FormData,
693-
placeholder: '192.168.1.100',
693+
placeholder: '0.0.0.0/[2001:db8::1]',
694694
value: formData.exitIp
695695
}] : []),
696696
{
@@ -724,7 +724,7 @@ export default function TemplatesPage() {
724724
...(formData.userListenType === 'assign' ? [{
725725
label: '监听地址',
726726
key: 'userListenAddress' as keyof FormData,
727-
placeholder: '0.0.0.0',
727+
placeholder: '0.0.0.0/[2001:db8::1]',
728728
value: formData.userListenAddress
729729
}] : []),
730730
{
@@ -736,7 +736,7 @@ export default function TemplatesPage() {
736736
]
737737
},
738738
{
739-
label: '服务端',
739+
label: '入口(服务端)',
740740
type: 'relay',
741741
formFields: [
742742
{
@@ -754,7 +754,7 @@ export default function TemplatesPage() {
754754
]
755755
},
756756
{
757-
label: '客户端',
757+
label: '出口(客户端)',
758758
type: 'target',
759759
formFields: [
760760
{
@@ -806,7 +806,7 @@ export default function TemplatesPage() {
806806
...(formData.targetListenType === 'external' ? [{
807807
label: '目标地址',
808808
key: 'exitIp' as keyof FormData,
809-
placeholder: '192.168.1.100',
809+
placeholder: '0.0.0.0/[2001:db8::1]',
810810
value: formData.exitIp
811811
}] : []),
812812
{

app/tunnels/components/quick-create-tunnel-modal.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ export default function QuickCreateTunnelModal({ isOpen, onOpenChange, onSaved,
206206
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
207207
<Input
208208
label="实例名称"
209+
placeholder="xxx-tunnel"
209210
value={formData.tunnelName}
210211
onValueChange={(v)=>handleField('tunnelName',v)}
211212
/>
@@ -225,14 +226,14 @@ export default function QuickCreateTunnelModal({ isOpen, onOpenChange, onSaved,
225226

226227
{/* 隧道地址端口 */}
227228
<div className="grid grid-cols-2 gap-2">
228-
<Input label="隧道地址" value={formData.tunnelAddress} onValueChange={(v)=>handleField('tunnelAddress',v)} />
229-
<Input label="隧道端口" type="number" value={formData.tunnelPort} onValueChange={(v)=>handleField('tunnelPort',v)} />
229+
<Input label="隧道地址" value={formData.tunnelAddress} placeholder="0.0.0.0/[2001:db8::1]" onValueChange={(v)=>handleField('tunnelAddress',v)} />
230+
<Input label="隧道端口" type="number" value={formData.tunnelPort} placeholder="10101" onValueChange={(v)=>handleField('tunnelPort',v)} />
230231
</div>
231232

232233
{/* 目标地址端口 */}
233234
<div className="grid grid-cols-2 gap-2">
234-
<Input label="目标地址" value={formData.targetAddress} onValueChange={(v)=>handleField('targetAddress',v)} />
235-
<Input label="目标端口" type="number" value={formData.targetPort} onValueChange={(v)=>handleField('targetPort',v)} />
235+
<Input label="目标地址" value={formData.targetAddress} placeholder="0.0.0.0/[2001:db8::1]" onValueChange={(v)=>handleField('targetAddress',v)} />
236+
<Input label="目标端口" type="number" value={formData.targetPort} placeholder="8080" onValueChange={(v)=>handleField('targetPort',v)} />
236237
</div>
237238

238239
{/* TLS 下拉 - server */}

app/tunnels/components/toolbox.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ interface TunnelToolBoxProps {
5353
onStatusFilterChange: (status: string) => void;
5454
onEndpointFilterChange?: (endpointId: string) => void;
5555
onRefresh?: () => void;
56+
selectedCount?: number;
57+
onBulkAction?: (action: string) => void;
5658
}
5759

5860
export const TunnelToolBox: React.FC<TunnelToolBoxProps> = ({
@@ -65,6 +67,8 @@ export const TunnelToolBox: React.FC<TunnelToolBoxProps> = ({
6567
onStatusFilterChange,
6668
onEndpointFilterChange,
6769
onRefresh,
70+
selectedCount = 0,
71+
onBulkAction,
6872
}) => {
6973
const router = useRouter();
7074
const [endpoints, setEndpoints] = useState<ApiEndpoint[]>([]);
@@ -292,6 +296,34 @@ export const TunnelToolBox: React.FC<TunnelToolBoxProps> = ({
292296
</DropdownMenu>
293297
</Dropdown>
294298
</ButtonGroup>
299+
300+
{/* 批量操作按钮 */}
301+
<Dropdown placement="bottom-end">
302+
<DropdownTrigger>
303+
<Button
304+
isIconOnly
305+
variant="flat"
306+
isDisabled={selectedCount === 0 || loading}
307+
title={selectedCount === 0 ? "请选择实例" : `已选择 ${selectedCount} 个实例`}
308+
>
309+
<FontAwesomeIcon icon={faEllipsisV} />
310+
</Button>
311+
</DropdownTrigger>
312+
<DropdownMenu
313+
aria-label="批量操作"
314+
onAction={(key) => onBulkAction?.(key as string)}
315+
>
316+
<DropdownItem key="delete" startContent={<FontAwesomeIcon icon={faTrash} />} className="text-danger">
317+
批量删除
318+
</DropdownItem>
319+
{/* 预留后续批量操作 */}
320+
{/*
321+
<DropdownItem key="start" startContent={<FontAwesomeIcon icon={faPlay} />}>批量启动</DropdownItem>
322+
<DropdownItem key="stop" startContent={<FontAwesomeIcon icon={faStop} />}>批量停止</DropdownItem>
323+
<DropdownItem key="restart" startContent={<FontAwesomeIcon icon={faRotateRight} />}>批量重启</DropdownItem>
324+
*/}
325+
</DropdownMenu>
326+
</Dropdown>
295327
</Flex>
296328
</div>
297329

app/tunnels/create/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ export default function CreateTunnelPage() {
488488
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
489489
<Input
490490
label="隧道地址"
491-
placeholder="0.0.0.0"
491+
placeholder="0.0.0.0/[2001:db8::1]"
492492
value={formData.tunnelAddress}
493493
onChange={(e) => handleInputChange("tunnelAddress", e.target.value)}
494494
/>
@@ -521,7 +521,7 @@ export default function CreateTunnelPage() {
521521
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
522522
<Input
523523
label="目标地址"
524-
placeholder="0.0.0.0"
524+
placeholder="0.0.0.0/[2001:db8::1]"
525525
value={formData.targetAddress}
526526
onChange={(e) => handleInputChange("targetAddress", e.target.value)}
527527
/>

app/tunnels/page.tsx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
DropdownItem,
2525
ButtonGroup
2626
} from "@heroui/react";
27+
import { Selection } from "@react-types/shared";
2728
import React, { useCallback, useEffect, useMemo, useState } from 'react';
2829

2930
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -114,6 +115,9 @@ export default function TunnelsPage() {
114115
// 快建实例模态控制
115116
const [quickCreateOpen, setQuickCreateOpen] = useState(false);
116117

118+
// 表格多选
119+
const [selectedKeys, setSelectedKeys] = useState<Selection>(new Set<string>());
120+
117121
// 获取实例列表
118122
const fetchTunnels = async () => {
119123
try {
@@ -233,6 +237,69 @@ export default function TunnelsPage() {
233237
}
234238
};
235239

240+
// 计算已选中数量(支持全选)
241+
const getSelectedCount = () => {
242+
if (selectedKeys === "all") return filteredItems.length;
243+
if (selectedKeys instanceof Set) return selectedKeys.size;
244+
return 0;
245+
};
246+
247+
// 批量删除
248+
const handleBatchDelete = async () => {
249+
if (!selectedKeys || (selectedKeys instanceof Set && selectedKeys.size === 0)) return;
250+
251+
try {
252+
// 计算待删除的 ID 列表
253+
let ids: number[] = [];
254+
if (selectedKeys === "all") {
255+
ids = filteredItems.map((t) => Number(t.id));
256+
} else {
257+
ids = Array.from(selectedKeys as Set<string>).map((id) => Number(id));
258+
}
259+
260+
// 提示开始删除
261+
addToast({
262+
title: "批量删除中...",
263+
description: "正在删除所选实例,请稍候",
264+
color: "primary",
265+
});
266+
const response = await fetch(buildApiUrl('/api/tunnels/batch'), {
267+
method: 'DELETE',
268+
headers: {
269+
'Content-Type': 'application/json',
270+
},
271+
body: JSON.stringify({ ids }),
272+
});
273+
274+
const data = await response.json();
275+
276+
if (!response.ok) {
277+
addToast({
278+
title: '批量删除失败',
279+
description: data?.error || '批量删除失败',
280+
color: 'danger',
281+
});
282+
throw new Error(data?.error || '批量删除失败');
283+
}
284+
285+
addToast({
286+
title: '批量删除成功',
287+
description: `已删除 ${data.deleted || ids.length} 个实例`,
288+
color: 'success',
289+
});
290+
291+
// 清空选择并刷新
292+
setSelectedKeys(new Set<string>());
293+
fetchTunnels();
294+
} catch (error) {
295+
addToast({
296+
title: '批量删除失败',
297+
description: error instanceof Error ? error.message : '未知错误',
298+
color: 'danger',
299+
});
300+
}
301+
};
302+
236303
// 初始加载
237304
React.useEffect(() => {
238305
fetchTunnels();
@@ -631,6 +698,16 @@ export default function TunnelsPage() {
631698
onStatusFilterChange={onStatusFilterChange}
632699
onEndpointFilterChange={onEndpointFilterChange}
633700
onRefresh={fetchTunnels}
701+
selectedCount={getSelectedCount()}
702+
onBulkAction={(action)=>{
703+
switch(action){
704+
case 'delete':
705+
handleBatchDelete();
706+
break;
707+
default:
708+
break;
709+
}
710+
}}
634711
/>
635712
<Box className="w-full overflow-hidden">
636713
{/* 移动端:使用卡片布局 */}
@@ -784,6 +861,9 @@ export default function TunnelsPage() {
784861
<div className="hidden md:block">
785862
<Table
786863
shadow="none"
864+
selectionMode="multiple"
865+
selectedKeys={selectedKeys}
866+
onSelectionChange={setSelectedKeys}
787867
aria-label="实例实例表格"
788868
className="min-w-full"
789869
classNames={{

internal/api/routes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func (r *Router) registerRoutes() {
123123
r.router.HandleFunc("/api/tunnels", r.tunnelHandler.HandleGetTunnels).Methods("GET")
124124
r.router.HandleFunc("/api/tunnels", r.tunnelHandler.HandleCreateTunnel).Methods("POST")
125125
r.router.HandleFunc("/api/tunnels/batch", r.tunnelHandler.HandleBatchCreateTunnels).Methods("POST")
126+
r.router.HandleFunc("/api/tunnels/batch", r.tunnelHandler.HandleBatchDeleteTunnels).Methods("DELETE")
126127
r.router.HandleFunc("/api/tunnels/quick", r.tunnelHandler.HandleQuickCreateTunnel).Methods("POST")
127128
r.router.HandleFunc("/api/tunnels/template", r.tunnelHandler.HandleTemplateCreate).Methods("POST")
128129
r.router.HandleFunc("/api/tunnels", r.tunnelHandler.HandlePatchTunnels).Methods("PATCH")

0 commit comments

Comments
 (0)