Skip to content

Commit ecbac2b

Browse files
authored
fix(webui): 修复 Toast 提示信息过长导致 UI 溢出的问题 (#1595)
- 新增路径截断工具函数,支持 Windows/Linux 长路径处理 - 创建 toast 包装器,自动截断错误信息中的长路径 - 为 Toaster 组件添加 maxWidth 和 word-break 样式防止溢出 - 更新插件配置弹窗使用新的 toast 工具
1 parent 49b5496 commit ecbac2b

4 files changed

Lines changed: 204 additions & 1 deletion

File tree

packages/napcat-webui-frontend/src/components/toaster.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export const Toaster = () => {
1212
borderRadius: '20px',
1313
background: isDark ? '#333' : '#fff',
1414
color: isDark ? '#fff' : '#333',
15+
maxWidth: '400px',
16+
wordBreak: 'break-word',
1517
},
1618
}}
1719
/>

packages/napcat-webui-frontend/src/pages/dashboard/plugin_config_modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Input } from '@heroui/input';
44
import { Select, SelectItem } from '@heroui/select';
55
import { Switch } from '@heroui/switch';
66
import { useEffect, useState, useRef, useCallback } from 'react';
7-
import toast from 'react-hot-toast';
7+
import toast from '@/utils/toast';
88
import { EventSourcePolyfill } from 'event-source-polyfill';
99
import PluginManager, { PluginConfigSchemaItem } from '@/controllers/plugin_manager';
1010
import key from '@/const/key';
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Toast 工具模块
3+
* 包装 react-hot-toast,自动截断长路径避免溢出
4+
*/
5+
import hotToast, { ToastOptions, Renderable, ValueOrFunction, Toast } from 'react-hot-toast';
6+
import { truncateErrorMessage } from './truncate';
7+
8+
type Message = ValueOrFunction<Renderable, Toast>;
9+
10+
/**
11+
* 包装后的 toast 对象
12+
* 对 error 类型的 toast 自动应用路径截断
13+
*/
14+
const toast = {
15+
/**
16+
* 显示错误 toast,自动截断长路径
17+
*/
18+
error: (message: Message, options?: ToastOptions) => {
19+
const truncatedMessage = typeof message === 'string'
20+
? truncateErrorMessage(message)
21+
: message;
22+
return hotToast.error(truncatedMessage, options);
23+
},
24+
25+
/**
26+
* 显示成功 toast
27+
*/
28+
success: (message: Message, options?: ToastOptions) => {
29+
return hotToast.success(message, options);
30+
},
31+
32+
/**
33+
* 显示加载中 toast
34+
*/
35+
loading: (message: Message, options?: ToastOptions) => {
36+
return hotToast.loading(message, options);
37+
},
38+
39+
/**
40+
* 显示普通 toast
41+
*/
42+
custom: hotToast.custom,
43+
44+
/**
45+
* 关闭 toast
46+
*/
47+
dismiss: hotToast.dismiss,
48+
49+
/**
50+
* 移除 toast
51+
*/
52+
remove: hotToast.remove,
53+
54+
/**
55+
* Promise toast
56+
*/
57+
promise: hotToast.promise,
58+
};
59+
60+
export default toast;
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* 路径截断工具函数
3+
*
4+
* 用于解决前端提示框中长路径导致内容溢出的问题。
5+
* 当错误消息包含过长的文件路径时,会导致提示框显示异常。
6+
*
7+
* 使用场景:
8+
* - Toast 消息中包含文件路径
9+
* - 错误提示中包含配置文件路径
10+
* - 任何可能因路径过长导致 UI 溢出的场景
11+
*
12+
* 兼容性:
13+
* - Windows 路径:D:\folder\subfolder\file (使用 \ 作为分隔符)
14+
* - Linux/Unix 路径:/home/user/folder/file (使用 / 作为分隔符)
15+
*
16+
* 示例:
17+
* - Windows: D:\NapCat.Shell-1\NapCat.Shell-2\...\data → D:\NapCat.Shell-1\...\napcat-plugin-builtin\data
18+
* - Linux: /home/user/projects/napcat/plugins/data → /home/user/...\plugins/data
19+
*/
20+
21+
/**
22+
* 截断长路径,保留开头和结尾部分
23+
*
24+
* @param path - 需要截断的路径(支持 Windows 和 Linux 路径格式)
25+
* @param maxLength - 最大允许长度,默认 60 字符
26+
* @returns 截断后的路径,中间用 ... 替代
27+
*
28+
* @example
29+
* // Windows 路径
30+
* truncatePath('D:\\folder1\\folder2\\folder3\\file.txt', 30)
31+
* // 返回: 'D:\\...\\folder3\\file.txt'
32+
*
33+
* @example
34+
* // Linux 路径
35+
* truncatePath('/home/user/projects/deep/nested/file.txt', 30)
36+
* // 返回: '/home/user/.../nested/file.txt'
37+
*/
38+
export function truncatePath (path: string, maxLength: number = 60): string {
39+
if (path.length <= maxLength) {
40+
return path;
41+
}
42+
43+
// 自动检测路径分隔符,兼容 Windows (\) 和 Linux/Unix (/)
44+
const separator = path.includes('\\') ? '\\' : '/';
45+
const parts = path.split(separator);
46+
47+
if (parts.length <= 3) {
48+
// 如果路径段太少(如 D:\folder\file),直接尾部截断
49+
return path.substring(0, maxLength - 3) + '...';
50+
}
51+
52+
// 保留第一段(Windows 驱动器号如 D: 或 Linux 根目录)和最后两段(父目录+文件名)
53+
const firstPart = parts[0];
54+
const lastParts = parts.slice(-2).join(separator);
55+
56+
const truncated = `${firstPart}${separator}...${separator}${lastParts}`;
57+
58+
// 如果截断后仍然超长,回退到简单的尾部截断
59+
if (truncated.length > maxLength) {
60+
return path.substring(0, maxLength - 3) + '...';
61+
}
62+
63+
return truncated;
64+
}
65+
66+
/**
67+
* 智能截断消息文本,特别处理包含路径的错误消息
68+
*
69+
* 此函数会自动检测消息中的文件路径(Windows 和 Linux 格式)并截断过长的路径,
70+
* 以防止 UI 组件(如 Toast、Alert)因内容过长而溢出。
71+
*
72+
* @param message - 需要处理的消息文本
73+
* @param maxLength - 最终消息的最大长度,默认 100 字符
74+
* @returns 处理后的消息,路径被截断,整体长度受限
75+
*
76+
* @example
77+
* // 处理包含 Windows 路径的错误消息
78+
* truncateErrorMessage("Save failed: Error updating config: EPERM: operation not permitted, open 'D:\\very\\long\\path\\config.json'")
79+
* // 返回: "Save failed: Error updating config: EPERM: operation not permitted, open 'D:\\...\\path\\config.json'"
80+
*
81+
* @example
82+
* // 处理包含 Linux 路径的错误消息
83+
* truncateErrorMessage("Failed to read /home/user/projects/napcat/very/deep/nested/config.json")
84+
* // 返回: "Failed to read /home/user/.../nested/config.json"
85+
*/
86+
export function truncateErrorMessage (message: string, maxLength: number = 100): string {
87+
if (message.length <= maxLength) {
88+
return message;
89+
}
90+
91+
// Windows 路径正则:匹配 盘符:\路径 格式,如 D:\folder\file.txt
92+
// 排除空白字符和引号,避免匹配到路径外的内容
93+
const windowsPathRegex = /[A-Za-z]:\\[^\s'"]+/g;
94+
95+
// Linux/Unix 路径正则:匹配 /开头的多级路径,如 /home/user/file
96+
// 要求至少有两级目录,避免匹配单独的 /
97+
const unixPathRegex = /\/[^\s'"]+(?:\/[^\s'"]+)+/g;
98+
99+
let result = message;
100+
101+
// 处理 Windows 路径
102+
const windowsPaths = message.match(windowsPathRegex);
103+
if (windowsPaths) {
104+
for (const path of windowsPaths) {
105+
if (path.length > 40) {
106+
result = result.replace(path, truncatePath(path, 40));
107+
}
108+
}
109+
}
110+
111+
// 处理 Unix 路径
112+
const unixPaths = message.match(unixPathRegex);
113+
if (unixPaths) {
114+
for (const path of unixPaths) {
115+
if (path.length > 40) {
116+
result = result.replace(path, truncatePath(path, 40));
117+
}
118+
}
119+
}
120+
121+
// 如果处理路径后消息仍然超长,直接尾部截断
122+
if (result.length > maxLength) {
123+
return result.substring(0, maxLength - 3) + '...';
124+
}
125+
126+
return result;
127+
}
128+
129+
/**
130+
* 截断普通文本(简单截断,不做路径检测)
131+
*
132+
* @param text - 需要截断的文本
133+
* @param maxLength - 最大长度,默认 50 字符
134+
* @returns 截断后的文本,超长部分用 ... 替代
135+
*/
136+
export function truncateText (text: string, maxLength: number = 50): string {
137+
if (text.length <= maxLength) {
138+
return text;
139+
}
140+
return text.substring(0, maxLength - 3) + '...';
141+
}

0 commit comments

Comments
 (0)