mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
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.
This commit is contained in:
parent
2bba1d21d2
commit
6286bb8676
6 changed files with 54 additions and 20 deletions
|
|
@ -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') },
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />,
|
||||
}}
|
||||
|
|
|
|||
Loading…
Reference in a new issue