fix(ui): use utf-8 encoding for inline SVG icons to support non-latin characters (#7892)

* fix(ui): use utf-8 encoding for inline SVG icons to support non-latin characters

* chore(test-item): re-add data-sortable-handle to TestItem box

* docs: update changelog
This commit is contained in:
oomeow
2026-09-09 12:00:10 +00:00
committed by GitHub
parent 380d03cb97
commit b06a966a4b
5 changed files with 38 additions and 60 deletions
+20 -18
View File
@@ -2,24 +2,26 @@
### 🐞 修复问题
修复 macOS 多显示器切换时托盘速率样式不同步
修复 WebSocket 在 onConnected 初始化完成前被保存的问题,避免初始化失败后留下无监听且无法重试的无效连接。
修复 Mihomo API 变更后 inline proxy-provider 被错误过滤的问题,解决节点显示 Unknown 及延迟测试失败的问题
修复 macOS 开启「访问系统范围设置需要管理员密码」时无法设置系统代理的问题
修复从未开启过系统代理的用户,每次内核启动都收到报错的问题
修复受限机器上「安装服务 → 自动切换到服务模式」整条流程无法走通的问题
修复 Windows 卸载后保留 TUN 配置、重新安装时因系统服务缺失而无法启动内核的问题,启动时会在当前会话回退到 Sidecar,并保留原 TUN 偏好
修复系统代理开关切换失败后,界面停留在错误状态的问题
修复托盘与热键切换系统代理失败时没有任何提示的问题
修复代理守卫反复失败后无限重试且不告知用户的问题
修复错误提示只显示最外层信息、丢失具体原因的问题
修复过长的错误提示撑满界面的问题(右键复制仍为完整内容)
修复部分错误提示显示为 [object Object] 的问题
修复 i18n 清理脚本因导入方式错误而无法运行的问题
修复 Linux 桌面环境下主题切换时应用外观不跟随的问题
修复 Gemini 测试正确性
修复 macOS/Linux 用户模式下旧版服务遗留的 cache.db 属主为 root 且当前用户不可写,导致无法记住上次选择节点的问题
修复代理组节点 `PASS-RULE` 被过滤丢失的问题
- 修复 macOS 多显示器切换时托盘速率样式不同步
- 修复 WebSocket 在 onConnected 初始化完成前被保存的问题,避免初始化失败后留下无监听且无法重试的无效连接。
- 修复 Mihomo API 变更后 inline proxy-provider 被错误过滤的问题,解决节点显示 Unknown 及延迟测试失败的问题
- 修复 macOS 开启「访问系统范围设置需要管理员密码」时无法设置系统代理的问题
- 修复从未开启过系统代理的用户,每次内核启动都收到报错的问题
- 修复受限机器上「安装服务 → 自动切换到服务模式」整条流程无法走通的问题
- 修复 Windows 卸载后保留 TUN 配置、重新安装时因系统服务缺失而无法启动内核的问题,启动时会在当前会话回退到 Sidecar,并保留原 TUN 偏好
- 修复系统代理开关切换失败后,界面停留在错误状态的问题
- 修复托盘与热键切换系统代理失败时没有任何提示的问题
- 修复代理守卫反复失败后无限重试且不告知用户的问题
- 修复错误提示只显示最外层信息、丢失具体原因的问题
- 修复过长的错误提示撑满界面的问题(右键复制仍为完整内容)
- 修复部分错误提示显示为 [object Object] 的问题
- 修复 i18n 清理脚本因导入方式错误而无法运行的问题
- 修复 Linux 桌面环境下主题切换时应用外观不跟随的问题
- 修复 Gemini 测试正确性
- 修复 macOS/Linux 用户模式下旧版服务遗留的 cache.db 属主为 root 且当前用户不可写,导致无法记住上次选择节点的问题
- 修复代理组节点 `PASS-RULE` 被过滤丢失的问题
- 修复 Windows 主窗口打开时关机被「此程序正在阻止关机」卡住、无法退出的问题
- 修复代理组与首页测试列表的内联 SVG 图标含中文等非 Latin-1 字符时无法显示、甚至导致页面报错的问题,改用 UTF-8 URL 编码渲染图标
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
+7 -16
View File
@@ -43,35 +43,26 @@ export const GroupItem = (props: Props) => {
>
{group?.icon?.trim().startsWith('http') && (
<img
alt="group icon"
alt={group.name}
src={iconCachePath === '' ? group.icon : iconCachePath}
width="32px"
style={{
marginRight: '12px',
borderRadius: '6px',
}}
style={{ marginRight: '12px', borderRadius: '6px' }}
/>
)}
{group?.icon?.trim().startsWith('data') && (
<img
alt="group icon"
alt={group.name}
src={group.icon}
width="32px"
style={{
marginRight: '12px',
borderRadius: '6px',
}}
style={{ marginRight: '12px', borderRadius: '6px' }}
/>
)}
{group?.icon?.trim().startsWith('<svg') && (
<img
alt="group icon"
src={`data:image/svg+xml;base64,${btoa(group.icon ?? '')}`}
alt={group.name}
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(group.icon)}`}
width="32px"
style={{
marginRight: '12px',
borderRadius: '6px',
}}
style={{ marginRight: '12px', borderRadius: '6px' }}
/>
)}
<ListItemText
+4 -4
View File
@@ -112,7 +112,7 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
{enable_group_icon && group.icon?.trim().startsWith('http') && (
<img
src={iconCachePath === '' ? group.icon : iconCachePath}
alt="group icon"
alt={group.name}
width="32px"
style={{ marginRight: '12px', borderRadius: '6px' }}
/>
@@ -120,15 +120,15 @@ export const ProxyRender = memo(function ProxyRender(props: RenderProps) {
{enable_group_icon && group.icon?.trim().startsWith('data') && (
<img
src={group.icon}
alt="group icon"
alt={group.name}
width="32px"
style={{ marginRight: '12px', borderRadius: '6px' }}
/>
)}
{enable_group_icon && group.icon?.trim().startsWith('<svg') && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
alt="group icon"
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(group.icon)}`}
alt={group.name}
width="32px"
/>
)}
+4 -2
View File
@@ -72,16 +72,18 @@ export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{icon.trim().startsWith('http') && (
<img
alt={name}
src={iconCachePath === '' ? icon : iconCachePath}
height="40px"
/>
)}
{icon.trim().startsWith('data') && (
<img src={icon} height="40px" />
<img alt={name} src={icon} height="40px" />
)}
{icon.trim().startsWith('<svg') && (
<img
src={`data:image/svg+xml;base64,${btoa(icon)}`}
alt={name}
src={`data:image/svg+xml;charset=utf-8,${encodeURIComponent(icon)}`}
height="40px"
/>
)}
+3 -20
View File
@@ -75,34 +75,17 @@ export const TestViewer = forwardRef<TestViewerRef, Props>(
throw new Error(t('tests.modals.test.errors.urlRequired'))
}
let newList
let uid
if (form.icon && form.icon.startsWith('<svg')) {
// 移除 icon 中的注释
if (form.icon) {
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, '')
}
const doc = new DOMParser().parseFromString(
form.icon,
'image/svg+xml',
)
if (doc.querySelector('parsererror')) {
throw new Error(t('tests.modals.test.errors.invalidSvg'))
}
}
if (openType === 'new') {
uid = nanoid()
const uid = nanoid()
const item = { ...form, uid }
newList = [...testList, item]
const newList = [...testList, item]
await patchVerge({ test_list: newList })
onChange(uid)
} else {
if (!form.uid) {
throw new Error(t('tests.modals.test.errors.uidMissing'))
}
uid = form.uid
const uid = form.uid
await patchTestList(uid, form)
onChange(uid, form)