mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-04 17:16:37 +08:00
refactor(frontend): extract reusable hooks and page helper modules
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
export interface ForwardAddressItem {
|
||||
id: number;
|
||||
address: string;
|
||||
copying: boolean;
|
||||
}
|
||||
|
||||
export type ForwardAddressAction =
|
||||
| { type: "none" }
|
||||
| { type: "copy"; text: string; label: string }
|
||||
| { type: "modal"; title: string; items: ForwardAddressItem[] };
|
||||
|
||||
const splitAddressEntries = (value: string): string[] => {
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item);
|
||||
};
|
||||
|
||||
const formatAddressWithPort = (ip: string, port: number): string => {
|
||||
if (ip.includes(":") && !ip.startsWith("[")) {
|
||||
return `[${ip}]:${port}`;
|
||||
}
|
||||
|
||||
return `${ip}:${port}`;
|
||||
};
|
||||
|
||||
export const formatInAddress = (ipString: string, port: number): string => {
|
||||
if (!ipString) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const items = splitAddressEntries(ipString);
|
||||
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const hasPort = /:\d+$/.test(items[0]);
|
||||
|
||||
if (hasPort) {
|
||||
if (items.length === 1) {
|
||||
return items[0];
|
||||
}
|
||||
|
||||
return `${items[0]} (+${items.length - 1}个)`;
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (items.length === 1) {
|
||||
return formatAddressWithPort(items[0], port);
|
||||
}
|
||||
|
||||
return `${formatAddressWithPort(items[0], port)} (+${items.length - 1}个)`;
|
||||
};
|
||||
|
||||
export const formatRemoteAddress = (addressString: string): string => {
|
||||
if (!addressString) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const addresses = splitAddressEntries(addressString);
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (addresses.length === 1) {
|
||||
return addresses[0];
|
||||
}
|
||||
|
||||
return `${addresses[0]} (+${addresses.length - 1})`;
|
||||
};
|
||||
|
||||
export const hasMultipleAddresses = (addressString: string): boolean => {
|
||||
if (!addressString) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return splitAddressEntries(addressString).length > 1;
|
||||
};
|
||||
|
||||
export const resolveForwardAddressAction = (
|
||||
addressString: string,
|
||||
port: number | null,
|
||||
title: string,
|
||||
): ForwardAddressAction => {
|
||||
if (!addressString) {
|
||||
return { type: "none" };
|
||||
}
|
||||
|
||||
let addresses: string[];
|
||||
|
||||
if (port !== null) {
|
||||
const items = splitAddressEntries(addressString);
|
||||
|
||||
if (items.length <= 1) {
|
||||
return {
|
||||
type: "copy",
|
||||
text: formatInAddress(addressString, port),
|
||||
label: title,
|
||||
};
|
||||
}
|
||||
|
||||
const hasPort = /:\d+$/.test(items[0]);
|
||||
|
||||
addresses = hasPort
|
||||
? items
|
||||
: items.map((ip) => formatAddressWithPort(ip, port));
|
||||
} else {
|
||||
addresses = splitAddressEntries(addressString);
|
||||
|
||||
if (addresses.length <= 1) {
|
||||
return {
|
||||
type: "copy",
|
||||
text: addressString,
|
||||
label: title,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "modal",
|
||||
title: `${title} (${addresses.length}个)`,
|
||||
items: addresses.map((address, index) => ({
|
||||
id: index,
|
||||
address,
|
||||
copying: false,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { BatchOperationResult } from "@/api/types";
|
||||
|
||||
import {
|
||||
batchChangeTunnel,
|
||||
batchDeleteForwards,
|
||||
batchPauseForwards,
|
||||
batchRedeployForwards,
|
||||
batchResumeForwards,
|
||||
} from "@/api";
|
||||
import { extractApiErrorMessage } from "@/api/error-message";
|
||||
|
||||
export interface ForwardBatchActionOutcome {
|
||||
toastVariant: "success" | "error";
|
||||
toastMessage: string;
|
||||
shouldRefresh: boolean;
|
||||
closeDeleteModal?: boolean;
|
||||
closeChangeTunnelModal?: boolean;
|
||||
resetTargetTunnel?: boolean;
|
||||
}
|
||||
|
||||
const normalizeBatchResult = (value: unknown): BatchOperationResult => {
|
||||
const raw = (value ?? {}) as Partial<BatchOperationResult>;
|
||||
|
||||
return {
|
||||
successCount: Number(raw.successCount ?? 0),
|
||||
failCount: Number(raw.failCount ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
const buildBatchToast = (
|
||||
result: BatchOperationResult,
|
||||
successText: string,
|
||||
): Pick<ForwardBatchActionOutcome, "toastVariant" | "toastMessage"> => {
|
||||
if (result.failCount === 0) {
|
||||
return {
|
||||
toastVariant: "success",
|
||||
toastMessage: successText,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: `成功 ${result.successCount} 项,失败 ${result.failCount} 项`,
|
||||
};
|
||||
};
|
||||
|
||||
export const executeForwardBatchDelete = async (
|
||||
ids: number[],
|
||||
): Promise<ForwardBatchActionOutcome> => {
|
||||
try {
|
||||
const response = await batchDeleteForwards(ids);
|
||||
|
||||
if (response.code !== 0) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: response.msg || "删除失败",
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
|
||||
const summary = normalizeBatchResult(response.data);
|
||||
|
||||
return {
|
||||
...buildBatchToast(summary, `成功删除 ${summary.successCount} 项`),
|
||||
shouldRefresh: true,
|
||||
closeDeleteModal: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: extractApiErrorMessage(error, "删除失败"),
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const executeForwardBatchToggleService = async (
|
||||
ids: number[],
|
||||
enable: boolean,
|
||||
): Promise<ForwardBatchActionOutcome> => {
|
||||
const fallback = enable ? "启用失败" : "停用失败";
|
||||
|
||||
try {
|
||||
const response = enable
|
||||
? await batchResumeForwards(ids)
|
||||
: await batchPauseForwards(ids);
|
||||
|
||||
if (response.code !== 0) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: response.msg || fallback,
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
|
||||
const summary = normalizeBatchResult(response.data);
|
||||
|
||||
return {
|
||||
...buildBatchToast(
|
||||
summary,
|
||||
enable
|
||||
? `成功启用 ${summary.successCount} 项`
|
||||
: `成功停用 ${summary.successCount} 项`,
|
||||
),
|
||||
shouldRefresh: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: extractApiErrorMessage(error, fallback),
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const executeForwardBatchRedeploy = async (
|
||||
ids: number[],
|
||||
): Promise<ForwardBatchActionOutcome> => {
|
||||
try {
|
||||
const response = await batchRedeployForwards(ids);
|
||||
|
||||
if (response.code !== 0) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: response.msg || "下发失败",
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
|
||||
const summary = normalizeBatchResult(response.data);
|
||||
|
||||
return {
|
||||
...buildBatchToast(summary, `成功重新下发 ${summary.successCount} 项`),
|
||||
shouldRefresh: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: extractApiErrorMessage(error, "下发失败"),
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const executeForwardBatchChangeTunnel = async (
|
||||
ids: number[],
|
||||
targetTunnelId: number,
|
||||
): Promise<ForwardBatchActionOutcome> => {
|
||||
try {
|
||||
const response = await batchChangeTunnel({
|
||||
forwardIds: ids,
|
||||
targetTunnelId,
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: response.msg || "隧道失败",
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
|
||||
const summary = normalizeBatchResult(response.data);
|
||||
|
||||
return {
|
||||
...buildBatchToast(summary, `成功换隧道 ${summary.successCount} 项`),
|
||||
shouldRefresh: true,
|
||||
closeChangeTunnelModal: true,
|
||||
resetTargetTunnel: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
toastVariant: "error",
|
||||
toastMessage: extractApiErrorMessage(error, "隧道失败"),
|
||||
shouldRefresh: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
export interface ForwardDiagnosisEntry {
|
||||
success: boolean;
|
||||
description: string;
|
||||
nodeName: string;
|
||||
nodeId: string;
|
||||
targetIp: string;
|
||||
targetPort?: number;
|
||||
message?: string;
|
||||
averageTime?: number;
|
||||
packetLoss?: number;
|
||||
fromChainType?: number;
|
||||
fromInx?: number;
|
||||
toChainType?: number;
|
||||
toInx?: number;
|
||||
}
|
||||
|
||||
export interface ForwardDiagnosisResult {
|
||||
forwardName: string;
|
||||
timestamp: number;
|
||||
results: ForwardDiagnosisEntry[];
|
||||
}
|
||||
|
||||
export interface ForwardDiagnosisFallbackInput {
|
||||
forwardName: string;
|
||||
remoteAddr: string;
|
||||
description: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const pickPrimaryTargetIp = (remoteAddr: string): string => {
|
||||
return remoteAddr.split(",")[0] || "-";
|
||||
};
|
||||
|
||||
export const buildForwardDiagnosisFallbackResult = ({
|
||||
forwardName,
|
||||
remoteAddr,
|
||||
description,
|
||||
message,
|
||||
}: ForwardDiagnosisFallbackInput): ForwardDiagnosisResult => {
|
||||
return {
|
||||
forwardName,
|
||||
timestamp: Date.now(),
|
||||
results: [
|
||||
{
|
||||
success: false,
|
||||
description,
|
||||
nodeName: "-",
|
||||
nodeId: "-",
|
||||
targetIp: pickPrimaryTargetIp(remoteAddr),
|
||||
message,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const getForwardDiagnosisQualityDisplay = (
|
||||
averageTime?: number,
|
||||
packetLoss?: number,
|
||||
): {
|
||||
text: string;
|
||||
color: "success" | "primary" | "warning" | "danger";
|
||||
} | null => {
|
||||
if (averageTime === undefined || packetLoss === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (averageTime < 30 && packetLoss === 0) {
|
||||
return { text: "🚀 优秀", color: "success" };
|
||||
}
|
||||
|
||||
if (averageTime < 50 && packetLoss === 0) {
|
||||
return { text: "✨ 很好", color: "success" };
|
||||
}
|
||||
|
||||
if (averageTime < 100 && packetLoss < 1) {
|
||||
return { text: "👍 良好", color: "primary" };
|
||||
}
|
||||
|
||||
if (averageTime < 150 && packetLoss < 2) {
|
||||
return { text: "😐 一般", color: "warning" };
|
||||
}
|
||||
|
||||
if (averageTime < 200 && packetLoss < 5) {
|
||||
return { text: "😟 较差", color: "warning" };
|
||||
}
|
||||
|
||||
return { text: "😵 很差", color: "danger" };
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import { loadStoredOrder } from "@/utils/order-storage";
|
||||
|
||||
export interface ForwardOrderItem {
|
||||
id: number;
|
||||
userId?: number;
|
||||
inx?: number;
|
||||
}
|
||||
|
||||
export const FORWARD_ORDER_KEY = "forward-order";
|
||||
|
||||
export const getUserScopedForwards = <T extends ForwardOrderItem>(
|
||||
forwards: T[],
|
||||
currentUserId: number | null,
|
||||
): T[] => {
|
||||
if (currentUserId === null) {
|
||||
return forwards;
|
||||
}
|
||||
|
||||
return forwards.filter((item) => item.userId === currentUserId);
|
||||
};
|
||||
|
||||
export const buildForwardOrder = <T extends ForwardOrderItem>(
|
||||
forwards: T[],
|
||||
currentUserId: number | null,
|
||||
): { order: number[]; fromDatabase: boolean } => {
|
||||
const userForwards = getUserScopedForwards(forwards, currentUserId);
|
||||
|
||||
const hasDbOrdering = userForwards.some(
|
||||
(item) => item.inx !== undefined && item.inx !== 0,
|
||||
);
|
||||
|
||||
if (hasDbOrdering) {
|
||||
const dbOrder = [...userForwards]
|
||||
.sort((a, b) => (a.inx ?? 0) - (b.inx ?? 0))
|
||||
.map((item) => item.id);
|
||||
|
||||
return { order: dbOrder, fromDatabase: true };
|
||||
}
|
||||
|
||||
return {
|
||||
order: loadStoredOrder(
|
||||
FORWARD_ORDER_KEY,
|
||||
userForwards.map((item) => item.id),
|
||||
),
|
||||
fromDatabase: false,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user