Restore BatchActionResultModal usage in tunnel.tsx

This commit is contained in:
sagitchu
2026-03-15 22:12:33 +08:00
parent 23d2060742
commit f2ee939006
2 changed files with 114 additions and 15 deletions
+3 -1
View File
@@ -23,7 +23,8 @@ PR #322 (https://github.com/Sagit-chu/flvx/pull/322) 原本移除了三个功能
- [x] 恢复 `use-dashboard-data.ts` 中的 `expiryReminderDismissed` 过滤逻辑 - [x] 恢复 `use-dashboard-data.ts` 中的 `expiryReminderDismissed` 过滤逻辑
- [x] 恢复 `batch-actions.ts` 中的 `BatchOperationFailure` 相关处理 - [x] 恢复 `batch-actions.ts` 中的 `BatchOperationFailure` 相关处理
- [x] 恢复 `forward.tsx` 中的 `BatchActionResultModal` 使用 - [x] 恢复 `forward.tsx` 中的 `BatchActionResultModal` 使用
- [x] 提交并推送修改 - [x] 恢复 `tunnel.tsx` 中的 `BatchActionResultModal` 使用
- [ ] 提交并推送修改
## 修改的文件 ## 修改的文件
@@ -34,6 +35,7 @@ PR #322 (https://github.com/Sagit-chu/flvx/pull/322) 原本移除了三个功能
- `vite-frontend/src/pages/dashboard/use-dashboard-data.ts` - 恢复 `expiryReminderDismissed` 过滤逻辑 - `vite-frontend/src/pages/dashboard/use-dashboard-data.ts` - 恢复 `expiryReminderDismissed` 过滤逻辑
- `vite-frontend/src/pages/forward/batch-actions.ts` - 恢复批量操作失败处理 - `vite-frontend/src/pages/forward/batch-actions.ts` - 恢复批量操作失败处理
- `vite-frontend/src/pages/forward.tsx` - 恢复 `BatchActionResultModal` 组件使用 - `vite-frontend/src/pages/forward.tsx` - 恢复 `BatchActionResultModal` 组件使用
- `vite-frontend/src/pages/tunnel.tsx` - 恢复 `BatchActionResultModal` 组件使用
## 注意事项 ## 注意事项
+110 -13
View File
@@ -1,3 +1,5 @@
import type { BatchOperationFailure } from "@/api/types";
import { useState, useEffect, useMemo, useRef, useCallback } from "react"; import { useState, useEffect, useMemo, useRef, useCallback } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import {
@@ -20,6 +22,7 @@ import { CSS } from "@dnd-kit/utilities";
import { SearchBar } from "@/components/search-bar"; import { SearchBar } from "@/components/search-bar";
import { AnimatedPage } from "@/components/animated-page"; import { AnimatedPage } from "@/components/animated-page";
import { BatchActionResultModal } from "@/components/batch-action-result-modal";
import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card";
import { Button } from "@/shadcn-bridge/heroui/button"; import { Button } from "@/shadcn-bridge/heroui/button";
import { Input, Textarea } from "@/shadcn-bridge/heroui/input"; import { Input, Textarea } from "@/shadcn-bridge/heroui/input";
@@ -63,7 +66,11 @@ import {
} from "@/pages/tunnel/form"; } from "@/pages/tunnel/form";
import { useLocalStorageState } from "@/hooks/use-local-storage-state"; import { useLocalStorageState } from "@/hooks/use-local-storage-state";
import { loadStoredOrder, saveOrder } from "@/utils/order-storage"; import { loadStoredOrder, saveOrder } from "@/utils/order-storage";
import { extractApiErrorMessage } from "@/api/error-message"; import {
buildBatchFailureMessage,
extractBatchFailures,
extractApiErrorMessage,
} from "@/api/error-message";
interface ChainTunnel { interface ChainTunnel {
nodeId: number; nodeId: number;
@@ -122,6 +129,20 @@ interface BatchProgressState {
percent: number; percent: number;
} }
interface BatchResultModalState {
failures: BatchOperationFailure[];
open: boolean;
summary: string;
title: string;
}
const EMPTY_BATCH_RESULT_MODAL_STATE: BatchResultModalState = {
failures: [],
open: false,
summary: "",
title: "",
};
const TUNNEL_ORDER_KEY = "tunnel-order"; const TUNNEL_ORDER_KEY = "tunnel-order";
const mapTunnelApiItems = (items: any[]): Tunnel[] => { const mapTunnelApiItems = (items: any[]): Tunnel[] => {
@@ -232,6 +253,8 @@ export default function TunnelPage() {
label: "", label: "",
percent: 0, percent: 0,
}); });
const [batchResultModal, setBatchResultModal] =
useState<BatchResultModalState>(EMPTY_BATCH_RESULT_MODAL_STATE);
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -842,6 +865,18 @@ export default function TunnelPage() {
setSelectedIds(new Set()); setSelectedIds(new Set());
}; };
const openBatchResultModal = useCallback(
(title: string, summary: string, failures: BatchOperationFailure[]) => {
setBatchResultModal({
failures,
open: true,
summary,
title,
});
},
[],
);
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
if (selectedIds.size === 0) return; if (selectedIds.size === 0) return;
setBatchLoading(true); setBatchLoading(true);
@@ -874,9 +909,19 @@ export default function TunnelPage() {
return next; return next;
}); });
} else { } else {
toast.error( const failures = extractBatchFailures(result);
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
); if (failures.length > 0) {
openBatchResultModal(
"批量删除结果",
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
failures,
);
} else {
toast.error(
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
);
}
setBatchProgress({ setBatchProgress({
active: true, active: true,
label: `部分完成:成功 ${result.successCount} 项,正在刷新列表...`, label: `部分完成:成功 ${result.successCount} 项,正在刷新列表...`,
@@ -915,9 +960,22 @@ export default function TunnelPage() {
if (result.failCount === 0) { if (result.failCount === 0) {
toast.success(`成功重新下发 ${result.successCount} 项`); toast.success(`成功重新下发 ${result.successCount} 项`);
} else { } else {
toast.error( const failures = extractBatchFailures(result);
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
); if (failures.length > 0) {
openBatchResultModal(
"批量下发结果",
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
failures,
);
} else {
toast.error(
buildBatchFailureMessage(
result,
`成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
),
);
}
} }
setSelectedIds(new Set()); setSelectedIds(new Set());
setSelectMode(false); setSelectMode(false);
@@ -1204,7 +1262,21 @@ export default function TunnelPage() {
</Chip> </Chip>
</div> </div>
</div> </div>
<div className="cursor-grab active:cursor-grabbing p-1 -mr-1 text-default-400 hover:text-default-600 transition-colors touch-manipulation flex-shrink-0" {...listeners} style={{ touchAction: "none" }} title="拖拽排序"><svg aria-hidden="true" className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" /></svg></div> <div
className="cursor-grab active:cursor-grabbing p-1 -mr-1 text-default-400 hover:text-default-600 transition-colors touch-manipulation flex-shrink-0"
{...listeners}
style={{ touchAction: "none" }}
title="拖拽排序"
>
<svg
aria-hidden="true"
className="w-4 h-4"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M7 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 2zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 7 14zm6-8a2 2 0 1 1-.001-4.001A2 2 0 0 1 13 6zm0 2a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 8zm0 6a2 2 0 1 1 .001 4.001A2 2 0 0 1 13 14z" />
</svg>
</div>
</div> </div>
</CardHeader> </CardHeader>
@@ -1343,7 +1415,6 @@ export default function TunnelPage() {
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className="flex gap-1.5 mt-3"> <div className="flex gap-1.5 mt-3">
@@ -1442,8 +1513,11 @@ export default function TunnelPage() {
)} )}
{/* 新增/编辑模态框 */} {/* 新增/编辑模态框 */}
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }} <Modal
backdrop="blur" backdrop="blur"
classNames={{
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
}}
isOpen={modalOpen} isOpen={modalOpen}
placement="center" placement="center"
scrollBehavior="outside" scrollBehavior="outside"
@@ -2376,8 +2450,11 @@ export default function TunnelPage() {
</Modal> </Modal>
{/* 删除确认模态框 */} {/* 删除确认模态框 */}
<Modal classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }} <Modal
backdrop="blur" backdrop="blur"
classNames={{
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
}}
isOpen={deleteModalOpen} isOpen={deleteModalOpen}
placement="center" placement="center"
scrollBehavior="outside" scrollBehavior="outside"
@@ -2419,7 +2496,9 @@ export default function TunnelPage() {
{/* 诊断结果模态框 */} {/* 诊断结果模态框 */}
<Modal <Modal
backdrop="blur" backdrop="blur"
classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }} classNames={{
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
}}
isOpen={diagnosisModalOpen} isOpen={diagnosisModalOpen}
placement="center" placement="center"
scrollBehavior="inside" scrollBehavior="inside"
@@ -3000,7 +3079,11 @@ export default function TunnelPage() {
</ModalContent> </ModalContent>
</Modal> </Modal>
<Modal backdrop="blur" classNames={{ base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden" }} <Modal
backdrop="blur"
classNames={{
base: "!w-[calc(100%-32px)] !mx-auto sm:!w-full rounded-2xl overflow-hidden",
}}
isOpen={batchDeleteModalOpen} isOpen={batchDeleteModalOpen}
onOpenChange={setBatchDeleteModalOpen} onOpenChange={setBatchDeleteModalOpen}
> >
@@ -3030,6 +3113,20 @@ export default function TunnelPage() {
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
<BatchActionResultModal
failures={batchResultModal.failures}
isOpen={batchResultModal.open}
summary={batchResultModal.summary}
title={batchResultModal.title}
onOpenChange={(open) => {
if (open) {
setBatchResultModal((prev) => ({ ...prev, open: true }));
return;
}
setBatchResultModal(EMPTY_BATCH_RESULT_MODAL_STATE);
}}
/>
</AnimatedPage> </AnimatedPage>
); );
} }