fix(webui): 修复 Toast 提示信息过长导致 UI 溢出的问题 (#1595)

- 新增路径截断工具函数,支持 Windows/Linux 长路径处理

- 创建 toast 包装器,自动截断错误信息中的长路径

- 为 Toaster 组件添加 maxWidth 和 word-break 样式防止溢出

- 更新插件配置弹窗使用新的 toast 工具
This commit is contained in:
Qiao
2026-02-05 18:22:25 +08:00
committed by GitHub
parent 49b5496406
commit ecbac2b782
4 changed files with 204 additions and 1 deletions

View File

@@ -12,6 +12,8 @@ export const Toaster = () => {
borderRadius: '20px',
background: isDark ? '#333' : '#fff',
color: isDark ? '#fff' : '#333',
maxWidth: '400px',
wordBreak: 'break-word',
},
}}
/>

View File

@@ -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';

View File

@@ -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<Renderable, Toast>;
/**
* 包装后的 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;

View File

@@ -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) + '...';
}