From 6286bb86762c444878be91fb0e064592c24a3683 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 27 May 2026 15:06:57 +0200 Subject: [PATCH] chore(ui): polish empty states + sidebar icon + i18n page titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppSidebar: switch the inbounds icon from UserOutlined (a single person — wrong semantic) to ImportOutlined, matching the empty-state icon and reflecting the actual concept of an incoming entry point. - usePageTitle: stop hardcoding English titles; resolve them through i18n (menu.* keys are already translated), so the browser tab now follows the active language. - InboundList / NodeList: replace the bare "—" empty cell with a centered icon + t('noData') message (ImportOutlined for inbounds, ClusterOutlined for nodes), and swap opacity:0.4 for var(--ant-color-text-secondary) so the text stays readable on the light theme's tinted card background. --- frontend/src/components/AppSidebar.tsx | 8 +++---- frontend/src/hooks/usePageTitle.ts | 23 ++++++++++++--------- frontend/src/pages/inbounds/InboundList.css | 8 +++++-- frontend/src/pages/inbounds/InboundList.tsx | 13 +++++++++++- frontend/src/pages/nodes/NodeList.css | 8 +++++-- frontend/src/pages/nodes/NodeList.tsx | 14 ++++++++++++- 6 files changed, 54 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/AppSidebar.tsx b/frontend/src/components/AppSidebar.tsx index 03c11b45..49b6426c 100644 --- a/frontend/src/components/AppSidebar.tsx +++ b/frontend/src/components/AppSidebar.tsx @@ -10,6 +10,7 @@ import { CloseOutlined, DashboardOutlined, HeartOutlined, + ImportOutlined, LogoutOutlined, MenuOutlined, MoonFilled, @@ -18,7 +19,6 @@ import { SunOutlined, TeamOutlined, ToolOutlined, - UserOutlined, } from '@ant-design/icons'; import { HttpUtil } from '@/utils'; @@ -29,11 +29,11 @@ const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed'; const DONATE_URL = 'https://donate.sanaei.dev/'; const LOGOUT_KEY = '__logout__'; -type IconName = 'dashboard' | 'user' | 'team' | 'setting' | 'tool' | 'cluster' | 'logout' | 'apidocs'; +type IconName = 'dashboard' | 'inbound' | 'team' | 'setting' | 'tool' | 'cluster' | 'logout' | 'apidocs'; const iconByName: Record = { dashboard: DashboardOutlined, - user: UserOutlined, + inbound: ImportOutlined, team: TeamOutlined, setting: SettingOutlined, tool: ToolOutlined, @@ -101,7 +101,7 @@ export default function AppSidebar() { const tabs = useMemo<{ key: string; icon: IconName; title: string }[]>(() => [ { key: '/', icon: 'dashboard', title: t('menu.dashboard') }, - { key: '/inbounds', icon: 'user', title: t('menu.inbounds') }, + { key: '/inbounds', icon: 'inbound', title: t('menu.inbounds') }, { key: '/clients', icon: 'team', title: t('menu.clients') }, { key: '/nodes', icon: 'cluster', title: t('menu.nodes') }, { key: '/settings', icon: 'setting', title: t('menu.settings') }, diff --git a/frontend/src/hooks/usePageTitle.ts b/frontend/src/hooks/usePageTitle.ts index 0ff64660..4f70a286 100644 --- a/frontend/src/hooks/usePageTitle.ts +++ b/frontend/src/hooks/usePageTitle.ts @@ -1,22 +1,25 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; -const TITLES: Record = { - '/': 'Overview', - '/inbounds': 'Inbounds', - '/clients': 'Clients', - '/nodes': 'Nodes', - '/settings': 'Settings', - '/xray': 'Xray Config', - '/api-docs': 'API Docs', +const TITLE_KEYS: Record = { + '/': 'menu.dashboard', + '/inbounds': 'menu.inbounds', + '/clients': 'menu.clients', + '/nodes': 'menu.nodes', + '/settings': 'menu.settings', + '/xray': 'menu.xray', + '/api-docs': 'menu.apiDocs', }; export function usePageTitle() { const { pathname } = useLocation(); + const { t } = useTranslation(); useEffect(() => { - const title = TITLES[pathname] || '3X-UI'; + const key = TITLE_KEYS[pathname]; + const title = key ? t(key) : '3X-UI'; const host = window.location.hostname; document.title = host ? `${host} - ${title}` : title; - }, [pathname]); + }, [pathname, t]); } diff --git a/frontend/src/pages/inbounds/InboundList.css b/frontend/src/pages/inbounds/InboundList.css index 7246719e..dfd68c78 100644 --- a/frontend/src/pages/inbounds/InboundList.css +++ b/frontend/src/pages/inbounds/InboundList.css @@ -132,8 +132,12 @@ .card-empty { text-align: center; - opacity: 0.4; - padding: 20px 0; + color: var(--ant-color-text-secondary); + padding: 24px 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; } @media (max-width: 768px) { diff --git a/frontend/src/pages/inbounds/InboundList.tsx b/frontend/src/pages/inbounds/InboundList.tsx index 74a66b05..dd82f2d2 100644 --- a/frontend/src/pages/inbounds/InboundList.tsx +++ b/frontend/src/pages/inbounds/InboundList.tsx @@ -600,7 +600,10 @@ export default function InboundList({ {isMobile ? (
{sortedInbounds.length === 0 ? ( -
+
+ +
{t('noData')}
+
) : ( sortedInbounds.map((record) => (
@@ -641,6 +644,14 @@ export default function InboundList({ scroll={{ x: 1000 }} style={{ marginTop: 10 }} size="small" + locale={{ + emptyText: ( +
+ +
{t('noData')}
+
+ ), + }} onChange={(_p, _f, sorter) => { const single = Array.isArray(sorter) ? sorter[0] : sorter; const colKey = (single?.columnKey || single?.field) as SortKey | undefined; diff --git a/frontend/src/pages/nodes/NodeList.css b/frontend/src/pages/nodes/NodeList.css index baca92fb..c04d90e8 100644 --- a/frontend/src/pages/nodes/NodeList.css +++ b/frontend/src/pages/nodes/NodeList.css @@ -135,6 +135,10 @@ .card-empty { text-align: center; - opacity: 0.4; - padding: 20px 0; + color: var(--ant-color-text-secondary); + padding: 24px 12px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; } diff --git a/frontend/src/pages/nodes/NodeList.tsx b/frontend/src/pages/nodes/NodeList.tsx index 78f88963..dd23402d 100644 --- a/frontend/src/pages/nodes/NodeList.tsx +++ b/frontend/src/pages/nodes/NodeList.tsx @@ -15,6 +15,7 @@ import { import type { BadgeProps } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { + ClusterOutlined, DeleteOutlined, EditOutlined, ExclamationCircleOutlined, @@ -279,7 +280,10 @@ export default function NodeList({ <>
{dataSource.length === 0 ? ( -
+
+ +
{t('noData')}
+
) : ( dataSource.map((record) => (
@@ -435,6 +439,14 @@ export default function NodeList({ scroll={{ x: 'max-content' }} size="middle" rowKey="id" + locale={{ + emptyText: ( +
+ +
{t('noData')}
+
+ ), + }} expandable={{ expandedRowRender: (record) => , }}