Przeglądaj źródła

chore(ui): polish empty states + sidebar icon + i18n page titles

- 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.
MHSanaei 7 godzin temu
rodzic
commit
6286bb8676

+ 4 - 4
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<IconName, ComponentType> = {
   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') },

+ 13 - 10
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<string, string> = {
-  '/': 'Overview',
-  '/inbounds': 'Inbounds',
-  '/clients': 'Clients',
-  '/nodes': 'Nodes',
-  '/settings': 'Settings',
-  '/xray': 'Xray Config',
-  '/api-docs': 'API Docs',
+const TITLE_KEYS: Record<string, string> = {
+  '/': '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]);
 }

+ 6 - 2
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) {

+ 12 - 1
frontend/src/pages/inbounds/InboundList.tsx

@@ -600,7 +600,10 @@ export default function InboundList({
         {isMobile ? (
           <div className="inbound-cards">
             {sortedInbounds.length === 0 ? (
-              <div className="card-empty">—</div>
+              <div className="card-empty">
+                <ImportOutlined style={{ fontSize: 28, opacity: 0.5 }} />
+                <div>{t('noData')}</div>
+              </div>
             ) : (
               sortedInbounds.map((record) => (
                 <div key={record.id} className="inbound-card">
@@ -641,6 +644,14 @@ export default function InboundList({
             scroll={{ x: 1000 }}
             style={{ marginTop: 10 }}
             size="small"
+            locale={{
+              emptyText: (
+                <div className="card-empty">
+                  <ImportOutlined style={{ fontSize: 32, marginBottom: 8 }} />
+                  <div>{t('noData')}</div>
+                </div>
+              ),
+            }}
             onChange={(_p, _f, sorter) => {
               const single = Array.isArray(sorter) ? sorter[0] : sorter;
               const colKey = (single?.columnKey || single?.field) as SortKey | undefined;

+ 6 - 2
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;
 }

+ 13 - 1
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({
         <>
           <div className="node-cards">
             {dataSource.length === 0 ? (
-              <div className="card-empty">—</div>
+              <div className="card-empty">
+                <ClusterOutlined style={{ fontSize: 28, opacity: 0.5 }} />
+                <div>{t('noData')}</div>
+              </div>
             ) : (
               dataSource.map((record) => (
                 <div key={record.id} className="node-card">
@@ -435,6 +439,14 @@ export default function NodeList({
           scroll={{ x: 'max-content' }}
           size="middle"
           rowKey="id"
+          locale={{
+            emptyText: (
+              <div className="card-empty">
+                <ClusterOutlined style={{ fontSize: 32, marginBottom: 8 }} />
+                <div>{t('noData')}</div>
+              </div>
+            ),
+          }}
           expandable={{
             expandedRowRender: (record) => <NodeHistoryPanel node={record} />,
           }}