From ecbac2b7825966d0f3972793ceeee84b100b1b38 Mon Sep 17 00:00:00 2001 From: Qiao Date: Thu, 5 Feb 2026 18:22:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E4=BF=AE=E5=A4=8D=20Toast=20?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E4=BF=A1=E6=81=AF=E8=BF=87=E9=95=BF=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=20UI=20=E6=BA=A2=E5=87=BA=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20(#1595)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增路径截断工具函数,支持 Windows/Linux 长路径处理 - 创建 toast 包装器,自动截断错误信息中的长路径 - 为 Toaster 组件添加 maxWidth 和 word-break 样式防止溢出 - 更新插件配置弹窗使用新的 toast 工具 --- .../src/components/toaster.tsx | 2 + .../pages/dashboard/plugin_config_modal.tsx | 2 +- .../napcat-webui-frontend/src/utils/toast.ts | 60 ++++++++ .../src/utils/truncate.ts | 141 ++++++++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 packages/napcat-webui-frontend/src/utils/toast.ts create mode 100644 packages/napcat-webui-frontend/src/utils/truncate.ts diff --git a/packages/napcat-webui-frontend/src/components/toaster.tsx b/packages/napcat-webui-frontend/src/components/toaster.tsx index c8b71687..e4a6a1e0 100644 --- a/packages/napcat-webui-frontend/src/components/toaster.tsx +++ b/packages/napcat-webui-frontend/src/components/toaster.tsx @@ -12,6 +12,8 @@ export const Toaster = () => { borderRadius: '20px', background: isDark ? '#333' : '#fff', color: isDark ? '#fff' : '#333', + maxWidth: '400px', + wordBreak: 'break-word', }, }} /> diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/plugin_config_modal.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/plugin_config_modal.tsx index 2c30a371..180e987e 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/plugin_config_modal.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/plugin_config_modal.tsx @@ -4,7 +4,7 @@ import { Input } from '@heroui/input'; import { Select, SelectItem } from '@heroui/select'; import { Switch } from '@heroui/switch'; import { useEffect, useState, useRef, useCallback } from 'react'; -import toast from 'react-hot-toast'; +import toast from '@/utils/toast'; import { EventSourcePolyfill } from 'event-source-polyfill'; import PluginManager, { PluginConfigSchemaItem } from '@/controllers/plugin_manager'; import key from '@/const/key'; diff --git a/packages/napcat-webui-frontend/src/utils/toast.ts b/packages/napcat-webui-frontend/src/utils/toast.ts new file mode 100644 index 00000000..0eb8b7b5 --- /dev/null +++ b/packages/napcat-webui-frontend/src/utils/toast.ts @@ -0,0 +1,60 @@ +/** + * Toast 工具模块 + * 包装 react-hot-toast,自动截断长路径避免溢出 + */ +import hotToast, { ToastOptions, Renderable, ValueOrFunction, Toast } from 'react-hot-toast'; +import { truncateErrorMessage } from './truncate'; + +type Message = ValueOrFunction; + +/** + * 包装后的 toast 对象 + * 对 error 类型的 toast 自动应用路径截断 + */ +const toast = { + /** + * 显示错误 toast,自动截断长路径 + */ + error: (message: Message, options?: ToastOptions) => { + const truncatedMessage = typeof message === 'string' + ? truncateErrorMessage(message) + : message; + return hotToast.error(truncatedMessage, options); + }, + + /** + * 显示成功 toast + */ + success: (message: Message, options?: ToastOptions) => { + return hotToast.success(message, options); + }, + + /** + * 显示加载中 toast + */ + loading: (message: Message, options?: ToastOptions) => { + return hotToast.loading(message, options); + }, + + /** + * 显示普通 toast + */ + custom: hotToast.custom, + + /** + * 关闭 toast + */ + dismiss: hotToast.dismiss, + + /** + * 移除 toast + */ + remove: hotToast.remove, + + /** + * Promise toast + */ + promise: hotToast.promise, +}; + +export default toast; diff --git a/packages/napcat-webui-frontend/src/utils/truncate.ts b/packages/napcat-webui-frontend/src/utils/truncate.ts new file mode 100644 index 00000000..438ad876 --- /dev/null +++ b/packages/napcat-webui-frontend/src/utils/truncate.ts @@ -0,0 +1,141 @@ +/** + * 路径截断工具函数 + * + * 用于解决前端提示框中长路径导致内容溢出的问题。 + * 当错误消息包含过长的文件路径时,会导致提示框显示异常。 + * + * 使用场景: + * - Toast 消息中包含文件路径 + * - 错误提示中包含配置文件路径 + * - 任何可能因路径过长导致 UI 溢出的场景 + * + * 兼容性: + * - Windows 路径:D:\folder\subfolder\file (使用 \ 作为分隔符) + * - Linux/Unix 路径:/home/user/folder/file (使用 / 作为分隔符) + * + * 示例: + * - Windows: D:\NapCat.Shell-1\NapCat.Shell-2\...\data → D:\NapCat.Shell-1\...\napcat-plugin-builtin\data + * - Linux: /home/user/projects/napcat/plugins/data → /home/user/...\plugins/data + */ + +/** + * 截断长路径,保留开头和结尾部分 + * + * @param path - 需要截断的路径(支持 Windows 和 Linux 路径格式) + * @param maxLength - 最大允许长度,默认 60 字符 + * @returns 截断后的路径,中间用 ... 替代 + * + * @example + * // Windows 路径 + * truncatePath('D:\\folder1\\folder2\\folder3\\file.txt', 30) + * // 返回: 'D:\\...\\folder3\\file.txt' + * + * @example + * // Linux 路径 + * truncatePath('/home/user/projects/deep/nested/file.txt', 30) + * // 返回: '/home/user/.../nested/file.txt' + */ +export function truncatePath (path: string, maxLength: number = 60): string { + if (path.length <= maxLength) { + return path; + } + + // 自动检测路径分隔符,兼容 Windows (\) 和 Linux/Unix (/) + const separator = path.includes('\\') ? '\\' : '/'; + const parts = path.split(separator); + + if (parts.length <= 3) { + // 如果路径段太少(如 D:\folder\file),直接尾部截断 + return path.substring(0, maxLength - 3) + '...'; + } + + // 保留第一段(Windows 驱动器号如 D: 或 Linux 根目录)和最后两段(父目录+文件名) + const firstPart = parts[0]; + const lastParts = parts.slice(-2).join(separator); + + const truncated = `${firstPart}${separator}...${separator}${lastParts}`; + + // 如果截断后仍然超长,回退到简单的尾部截断 + if (truncated.length > maxLength) { + return path.substring(0, maxLength - 3) + '...'; + } + + return truncated; +} + +/** + * 智能截断消息文本,特别处理包含路径的错误消息 + * + * 此函数会自动检测消息中的文件路径(Windows 和 Linux 格式)并截断过长的路径, + * 以防止 UI 组件(如 Toast、Alert)因内容过长而溢出。 + * + * @param message - 需要处理的消息文本 + * @param maxLength - 最终消息的最大长度,默认 100 字符 + * @returns 处理后的消息,路径被截断,整体长度受限 + * + * @example + * // 处理包含 Windows 路径的错误消息 + * truncateErrorMessage("Save failed: Error updating config: EPERM: operation not permitted, open 'D:\\very\\long\\path\\config.json'") + * // 返回: "Save failed: Error updating config: EPERM: operation not permitted, open 'D:\\...\\path\\config.json'" + * + * @example + * // 处理包含 Linux 路径的错误消息 + * truncateErrorMessage("Failed to read /home/user/projects/napcat/very/deep/nested/config.json") + * // 返回: "Failed to read /home/user/.../nested/config.json" + */ +export function truncateErrorMessage (message: string, maxLength: number = 100): string { + if (message.length <= maxLength) { + return message; + } + + // Windows 路径正则:匹配 盘符:\路径 格式,如 D:\folder\file.txt + // 排除空白字符和引号,避免匹配到路径外的内容 + const windowsPathRegex = /[A-Za-z]:\\[^\s'"]+/g; + + // Linux/Unix 路径正则:匹配 /开头的多级路径,如 /home/user/file + // 要求至少有两级目录,避免匹配单独的 / + const unixPathRegex = /\/[^\s'"]+(?:\/[^\s'"]+)+/g; + + let result = message; + + // 处理 Windows 路径 + const windowsPaths = message.match(windowsPathRegex); + if (windowsPaths) { + for (const path of windowsPaths) { + if (path.length > 40) { + result = result.replace(path, truncatePath(path, 40)); + } + } + } + + // 处理 Unix 路径 + const unixPaths = message.match(unixPathRegex); + if (unixPaths) { + for (const path of unixPaths) { + if (path.length > 40) { + result = result.replace(path, truncatePath(path, 40)); + } + } + } + + // 如果处理路径后消息仍然超长,直接尾部截断 + if (result.length > maxLength) { + return result.substring(0, maxLength - 3) + '...'; + } + + return result; +} + +/** + * 截断普通文本(简单截断,不做路径检测) + * + * @param text - 需要截断的文本 + * @param maxLength - 最大长度,默认 50 字符 + * @returns 截断后的文本,超长部分用 ... 替代 + */ +export function truncateText (text: string, maxLength: number = 50): string { + if (text.length <= maxLength) { + return text; + } + return text.substring(0, maxLength - 3) + '...'; +}