From 7d07fe08b7062967d8bbb22703ebacc459e1571c Mon Sep 17 00:00:00 2001 From: sagitchu Date: Sun, 15 Mar 2026 20:53:25 +0800 Subject: [PATCH] Restore removed features from PR #322 - Add back BatchOperationFailure type and batch failure handling functions - Add back dismissNodeExpiryReminder API endpoint - Add back update channel selection UI in config page - Keep simplified version display in version-footer.tsx --- plans/039-restore-removed-features-pr322.md | 36 ++++++ vite-frontend/src/api/error-message.ts | 108 ++++++++++++++++++ vite-frontend/src/api/index.ts | 2 + vite-frontend/src/api/types.ts | 9 ++ vite-frontend/src/pages/config.tsx | 60 +++++++++- .../src/pages/dashboard/use-dashboard-data.ts | 2 + .../src/pages/forward/batch-actions.ts | 54 +++++++-- 7 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 plans/039-restore-removed-features-pr322.md diff --git a/plans/039-restore-removed-features-pr322.md b/plans/039-restore-removed-features-pr322.md new file mode 100644 index 0000000..ff336f1 --- /dev/null +++ b/plans/039-restore-removed-features-pr322.md @@ -0,0 +1,36 @@ +# 恢复 PR #322 移除的功能 + +**目标**: 修改 PR #322,保留被移除的功能,同时保留版本显示简化改动。 + +## 背景 + +PR #322 (https://github.com/Sagit-chu/flvx/pull/322) 原本移除了三个功能,用户要求**加回**这些被移除的功能: +1. 批量操作失败详情弹窗(`BatchOperationFailure` 类型及相关处理) +2. 节点到期提醒关闭功能(`dismissNodeExpiryReminder` API) +3. 更新通道选择功能(稳定版/开发版切换) + +用户要求**保留**的改动: +- 版本显示简化(移除 "v" 前缀和更新可用徽章) + +## 任务清单 + +- [x] 检出 PR #322 到本地分支 `pr-322` +- [x] 恢复 `api/types.ts` 中的 `expiryReminderDismissed` 字段 +- [x] 恢复 `api/types.ts` 中的 `BatchOperationFailure` 类型和 `failures` 字段 +- [x] 恢复 `api/error-message.ts` 中的批量操作失败处理函数 +- [x] 恢复 `api/index.ts` 中的 `dismissNodeExpiryReminder` API +- [x] 恢复 `config.tsx` 中的更新通道选择功能 +- [x] 运行 lint 验证 +- [ ] 提交并推送修改 + +## 修改的文件 + +- `vite-frontend/src/api/types.ts` - 添加 `expiryReminderDismissed` 和 `BatchOperationFailure` +- `vite-frontend/src/api/error-message.ts` - 添加批量操作失败处理函数 +- `vite-frontend/src/api/index.ts` - 添加 `dismissNodeExpiryReminder` API +- `vite-frontend/src/pages/config.tsx` - 添加更新通道选择功能 + +## 注意事项 + +- `version-footer.tsx` 保持简化版本显示(不恢复) +- `batch-action-result-modal.tsx` 未被 PR 修改,无需恢复 \ No newline at end of file diff --git a/vite-frontend/src/api/error-message.ts b/vite-frontend/src/api/error-message.ts index 11d861f..2e6e5f8 100644 --- a/vite-frontend/src/api/error-message.ts +++ b/vite-frontend/src/api/error-message.ts @@ -1,3 +1,5 @@ +import type { BatchOperationFailure } from "@/api/types"; + import axios from "axios"; interface ErrorPayload { @@ -5,6 +7,20 @@ interface ErrorPayload { message?: string; } +interface BatchFailurePayload { + id?: number; + name?: string; + reason?: string; + msg?: string; + message?: string; +} + +interface BatchResultPayload { + failures?: unknown[]; +} + +const MAX_BATCH_FAILURES_IN_TOAST = 3; + export const isUnauthorizedError = (error: unknown): boolean => { return axios.isAxiosError(error) && error.response?.status === 401; }; @@ -25,3 +41,95 @@ export const extractApiErrorMessage = ( return fallback; }; + +const normalizeBatchFailure = ( + failure: unknown, +): BatchOperationFailure | null => { + if (typeof failure === "string") { + const reason = failure.trim(); + + return reason ? { reason } : null; + } + + const payload = (failure ?? {}) as BatchFailurePayload; + const id = + typeof payload.id === "number" && Number.isFinite(payload.id) + ? payload.id + : undefined; + const name = typeof payload.name === "string" ? payload.name.trim() : ""; + const reasonSource = [payload.reason, payload.msg, payload.message].find( + (item) => typeof item === "string" && item.trim() !== "", + ); + const reason = typeof reasonSource === "string" ? reasonSource.trim() : ""; + + if (!name && !reason && id === undefined) { + return null; + } + + return { + ...(id !== undefined ? { id } : {}), + ...(name ? { name } : {}), + ...(reason ? { reason } : {}), + }; +}; + +const normalizeBatchFailureReason = ( + failure: BatchOperationFailure, +): string => { + const name = typeof failure.name === "string" ? failure.name.trim() : ""; + const reason = + typeof failure.reason === "string" ? failure.reason.trim() : ""; + + if (name && reason) { + return `${name}: ${reason}`; + } + + if (reason) { + if (typeof failure.id === "number" && Number.isFinite(failure.id)) { + return `ID ${failure.id}: ${reason}`; + } + + return reason; + } + + if (name) { + return name; + } + + if (typeof failure.id === "number" && Number.isFinite(failure.id)) { + return `ID ${failure.id} 下发失败`; + } + + return ""; +}; + +export const extractBatchFailures = ( + result: unknown, +): BatchOperationFailure[] => { + const payload = (result ?? {}) as BatchResultPayload; + + return Array.isArray(payload.failures) + ? payload.failures + .map((item) => normalizeBatchFailure(item)) + .filter((item): item is BatchOperationFailure => item !== null) + : []; +}; + +export const buildBatchFailureMessage = ( + result: unknown, + fallbackSummary: string, +): string => { + const failures = extractBatchFailures(result) + .map((item) => normalizeBatchFailureReason(item)) + .filter((item) => item !== ""); + + if (failures.length === 0) { + return fallbackSummary; + } + + const visibleFailures = failures.slice(0, MAX_BATCH_FAILURES_IN_TOAST); + const hiddenCount = failures.length - visibleFailures.length; + const hiddenSuffix = hiddenCount > 0 ? ` 等 ${failures.length} 项` : ""; + + return `${fallbackSummary}:${visibleFailures.join(";")}${hiddenSuffix}`; +}; diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index cf968ea..3eeed4c 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -78,6 +78,8 @@ export const getNodeInstallCommand = ( export const updateNodeOrder = (data: { nodes: Array<{ id: number; inx: number }>; }) => Network.post("/node/update-order", data); +export const dismissNodeExpiryReminder = (id: number) => + Network.post("/node/dismiss-expiry-reminder", { id }); export const checkNodeStatus = (nodeId?: number) => { const params = nodeId ? { nodeId } : {}; diff --git a/vite-frontend/src/api/types.ts b/vite-frontend/src/api/types.ts index f8d1043..2021ae0 100644 --- a/vite-frontend/src/api/types.ts +++ b/vite-frontend/src/api/types.ts @@ -6,6 +6,7 @@ export interface NodeApiItem { remark?: string; expiryTime?: number; renewalCycle?: "month" | "quarter" | "year" | ""; + expiryReminderDismissed?: number; syncError?: string; [key: string]: unknown; } @@ -210,6 +211,14 @@ export interface UserPackageInfoApiData { export interface BatchOperationResult { successCount: number; failCount: number; + failures?: BatchOperationFailure[]; + [key: string]: unknown; +} + +export interface BatchOperationFailure { + id?: number; + name?: string; + reason?: string; [key: string]: unknown; } diff --git a/vite-frontend/src/pages/config.tsx b/vite-frontend/src/pages/config.tsx index 617e27b..815c229 100644 --- a/vite-frontend/src/pages/config.tsx +++ b/vite-frontend/src/pages/config.tsx @@ -332,6 +332,14 @@ export default function ConfigPage() { } }; + const handleUpdateChannelChange = (channel: UpdateReleaseChannel) => { + setUpdateChannel(channel); + setUpdateReleaseChannel(channel); + toast.success( + `更新通道已切换为${channel === "stable" ? "稳定版" : "开发版"}`, + ); + }; + const handleConfigChange = (key: string, value: string) => { const newConfigs = { ...configs, [key]: value }; @@ -923,6 +931,40 @@ export default function ConfigPage() { +
+
+

+ 更新通道 +

+

+ 稳定版仅匹配纯数字版本;开发版仅匹配包含 alpha / beta / rc + 的版本。 +

+
+ + +
+