mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 13:14:11 +00:00
feat(frontend): donate link, panel version label, login lang menu
- Sidebar: add heart donate link to https://donate.sanaei.dev and small panel version under 3X-UI brand - Login: swap settings-cog for translation icon, drop title, render languages as a direct list - Vite dev: inject window.X_UI_CUR_VER from config/version so dev mode matches prod - Translations: add menu.donate across all locales
This commit is contained in:
parent
f929ea4b14
commit
dcb837f4e1
18 changed files with 198 additions and 54 deletions
|
|
@ -30,14 +30,68 @@
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-text {
|
.brand-block {
|
||||||
flex: 1 1 auto;
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sider-brand-collapsed .brand-text {
|
.brand-text {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-version {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sider-brand-collapsed .brand-block {
|
||||||
|
align-items: center;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-donate {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
text-decoration: none;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 0.2s, transform 0.15s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-donate:hover,
|
||||||
|
.sidebar-donate:focus-visible {
|
||||||
|
background-color: rgba(236, 72, 153, 0.12);
|
||||||
|
color: #ec4899;
|
||||||
|
transform: scale(1.08);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-donate .anticon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-theme-cycle {
|
.sidebar-theme-cycle {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -197,6 +251,14 @@ html[data-theme='ultra-dark'] .sidebar-theme-cycle {
|
||||||
color: rgba(255, 255, 255, 0.92);
|
color: rgba(255, 255, 255, 0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.dark .sidebar-donate {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='ultra-dark'] .sidebar-donate {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
body.dark .ant-drawer .ant-drawer-content,
|
body.dark .ant-drawer .ant-drawer-content,
|
||||||
body.dark .ant-drawer .ant-drawer-body {
|
body.dark .ant-drawer .ant-drawer-body {
|
||||||
background: #252526 !important;
|
background: #252526 !important;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
ClusterOutlined,
|
ClusterOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
|
HeartOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
|
|
@ -21,6 +22,7 @@ import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
|
||||||
import './AppSidebar.css';
|
import './AppSidebar.css';
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed';
|
const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed';
|
||||||
|
const DONATE_URL = 'https://donate.sanaei.dev/';
|
||||||
|
|
||||||
interface AppSidebarProps {
|
interface AppSidebarProps {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
|
|
@ -48,6 +50,21 @@ function readCollapsed(): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DonateButton({ ariaLabel }: { ariaLabel: string }) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={DONATE_URL}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="sidebar-donate"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
title={ariaLabel}
|
||||||
|
>
|
||||||
|
<HeartOutlined />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ThemeCycleButton({ id, isDark, isUltra, onCycle, ariaLabel }: {
|
function ThemeCycleButton({ id, isDark, isUltra, onCycle, ariaLabel }: {
|
||||||
id: string;
|
id: string;
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
|
|
@ -92,6 +109,7 @@ export default function AppSidebar({ basePath = '', requestUri = '' }: AppSideba
|
||||||
|
|
||||||
const prefix = basePath.startsWith('/') ? basePath : `/${basePath || ''}`;
|
const prefix = basePath.startsWith('/') ? basePath : `/${basePath || ''}`;
|
||||||
const currentTheme: 'light' | 'dark' = isDark ? 'dark' : 'light';
|
const currentTheme: 'light' | 'dark' = isDark ? 'dark' : 'light';
|
||||||
|
const panelVersion = window.X_UI_CUR_VER || '';
|
||||||
|
|
||||||
const tabs = useMemo<{ key: string; icon: IconName; title: string }[]>(() => [
|
const tabs = useMemo<{ key: string; icon: IconName; title: string }[]>(() => [
|
||||||
{ key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') },
|
{ key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') },
|
||||||
|
|
@ -165,15 +183,23 @@ export default function AppSidebar({ basePath = '', requestUri = '' }: AppSideba
|
||||||
onCollapse={onSiderCollapse}
|
onCollapse={onSiderCollapse}
|
||||||
>
|
>
|
||||||
<div className={`sider-brand${collapsed ? ' sider-brand-collapsed' : ''}`}>
|
<div className={`sider-brand${collapsed ? ' sider-brand-collapsed' : ''}`}>
|
||||||
<span className="brand-text">{collapsed ? '3X' : '3X-UI'}</span>
|
<div className="brand-block">
|
||||||
|
<span className="brand-text">{collapsed ? '3X' : '3X-UI'}</span>
|
||||||
|
{!collapsed && panelVersion && (
|
||||||
|
<span className="brand-version">v{panelVersion}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<ThemeCycleButton
|
<div className="brand-actions">
|
||||||
id="theme-cycle"
|
<DonateButton ariaLabel={t('menu.donate') || 'Donate'} />
|
||||||
isDark={isDark}
|
<ThemeCycleButton
|
||||||
isUltra={isUltra}
|
id="theme-cycle"
|
||||||
onCycle={() => cycleTheme('theme-cycle')}
|
isDark={isDark}
|
||||||
ariaLabel={t('menu.theme')}
|
isUltra={isUltra}
|
||||||
/>
|
onCycle={() => cycleTheme('theme-cycle')}
|
||||||
|
ariaLabel={t('menu.theme')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<Menu
|
||||||
|
|
@ -208,8 +234,12 @@ export default function AppSidebar({ basePath = '', requestUri = '' }: AppSideba
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
>
|
>
|
||||||
<div className="drawer-header">
|
<div className="drawer-header">
|
||||||
<span className="drawer-brand">3X-UI</span>
|
<div className="brand-block">
|
||||||
|
<span className="drawer-brand">3X-UI</span>
|
||||||
|
{panelVersion && <span className="brand-version">v{panelVersion}</span>}
|
||||||
|
</div>
|
||||||
<div className="drawer-header-actions">
|
<div className="drawer-header-actions">
|
||||||
|
<DonateButton ariaLabel={t('menu.donate') || 'Donate'} />
|
||||||
<ThemeCycleButton
|
<ThemeCycleButton
|
||||||
id="theme-cycle-drawer"
|
id="theme-cycle-drawer"
|
||||||
isDark={isDark}
|
isDark={isDark}
|
||||||
|
|
|
||||||
|
|
@ -402,10 +402,44 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-popover {
|
.lang-list {
|
||||||
min-width: 220px;
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 160px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-select {
|
.lang-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: start;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item:hover,
|
||||||
|
.lang-item:focus-visible {
|
||||||
|
background-color: rgba(99, 102, 241, 0.12);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item.is-active {
|
||||||
|
color: var(--color-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,13 @@ import {
|
||||||
Input,
|
Input,
|
||||||
Layout,
|
Layout,
|
||||||
Popover,
|
Popover,
|
||||||
Select,
|
|
||||||
Space,
|
|
||||||
Spin,
|
Spin,
|
||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
KeyOutlined,
|
KeyOutlined,
|
||||||
LockOutlined,
|
LockOutlined,
|
||||||
SettingOutlined,
|
TranslationOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
|
@ -107,16 +105,8 @@ export default function LoginPage() {
|
||||||
return classes.join(' ');
|
return classes.join(' ');
|
||||||
}, [isDark, isUltra]);
|
}, [isDark, isUltra]);
|
||||||
|
|
||||||
const langOptions = useMemo(
|
const langList = useMemo(
|
||||||
() => LanguageManager.supportedLanguages.map((l: { value: string; name: string; icon: string }) => ({
|
() => LanguageManager.supportedLanguages as { value: string; name: string; icon: string }[],
|
||||||
value: l.value,
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
<span aria-label={l.name}>{l.icon}</span>
|
|
||||||
<span>{l.name}</span>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -154,26 +144,31 @@ export default function LoginPage() {
|
||||||
</button>
|
</button>
|
||||||
<Popover
|
<Popover
|
||||||
rootClassName={isDark ? 'dark' : 'light'}
|
rootClassName={isDark ? 'dark' : 'light'}
|
||||||
title={t('pages.settings.language')}
|
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
content={
|
content={
|
||||||
<Space orientation="vertical" size={10} className="settings-popover">
|
<ul className="lang-list">
|
||||||
<Select
|
{langList.map((l) => (
|
||||||
className="lang-select"
|
<li key={l.value}>
|
||||||
value={lang}
|
<button
|
||||||
onChange={onLangChange}
|
type="button"
|
||||||
options={langOptions}
|
className={`lang-item${lang === l.value ? ' is-active' : ''}`}
|
||||||
/>
|
onClick={() => onLangChange(l.value)}
|
||||||
</Space>
|
>
|
||||||
|
<span className="lang-item-icon" aria-hidden="true">{l.icon}</span>
|
||||||
|
<span className="lang-item-name">{l.name}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
shape="circle"
|
shape="circle"
|
||||||
size="large"
|
size="large"
|
||||||
className="toolbar-btn"
|
className="toolbar-btn"
|
||||||
aria-label={t('menu.settings')}
|
aria-label={t('pages.settings.language')}
|
||||||
icon={<SettingOutlined />}
|
icon={<TranslationOutlined />}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,17 @@ function refreshBasePath() {
|
||||||
return cachedBasePath;
|
return cachedBasePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readPanelVersion() {
|
||||||
|
try {
|
||||||
|
const versionFile = path.resolve(__dirname, '..', 'config', 'version');
|
||||||
|
return fs.readFileSync(versionFile, 'utf8').trim();
|
||||||
|
} catch (_e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// `apply: 'serve'` keeps the injection out of `vite build` — dist.go
|
// `apply: 'serve'` keeps the injection out of `vite build` — dist.go
|
||||||
// already injects webBasePath at runtime in production.
|
// already injects webBasePath and version at runtime in production.
|
||||||
function injectBasePathPlugin() {
|
function injectBasePathPlugin() {
|
||||||
return {
|
return {
|
||||||
name: 'xui-inject-base-path',
|
name: 'xui-inject-base-path',
|
||||||
|
|
@ -76,7 +85,8 @@ function injectBasePathPlugin() {
|
||||||
transformIndexHtml(html) {
|
transformIndexHtml(html) {
|
||||||
const basePath = refreshBasePath();
|
const basePath = refreshBasePath();
|
||||||
const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
const tag = `<script>window.X_UI_BASE_PATH="${escaped}";</script>`;
|
const version = readPanelVersion().replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
const tag = `<script>window.X_UI_BASE_PATH="${escaped}";window.X_UI_CUR_VER="${version}";</script>`;
|
||||||
return html.replace('</head>', `${tag}</head>`);
|
return html.replace('</head>', `${tag}</head>`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "إعدادات Xray",
|
"xray": "إعدادات Xray",
|
||||||
"apiDocs": "توثيق API",
|
"apiDocs": "توثيق API",
|
||||||
"logout": "تسجيل خروج",
|
"logout": "تسجيل خروج",
|
||||||
"link": "إدارة"
|
"link": "إدارة",
|
||||||
|
"donate": "تبرع"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Xray Configs",
|
"xray": "Xray Configs",
|
||||||
"apiDocs": "API Docs",
|
"apiDocs": "API Docs",
|
||||||
"logout": "Log Out",
|
"logout": "Log Out",
|
||||||
"link": "Manage"
|
"link": "Manage",
|
||||||
|
"donate": "Donate"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Ajustes Xray",
|
"xray": "Ajustes Xray",
|
||||||
"apiDocs": "Documentación de la API",
|
"apiDocs": "Documentación de la API",
|
||||||
"logout": "Cerrar Sesión",
|
"logout": "Cerrar Sesión",
|
||||||
"link": "Gestionar"
|
"link": "Gestionar",
|
||||||
|
"donate": "Donar"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "پیکربندی ایکسری",
|
"xray": "پیکربندی ایکسری",
|
||||||
"apiDocs": "مستندات API",
|
"apiDocs": "مستندات API",
|
||||||
"logout": "خروج",
|
"logout": "خروج",
|
||||||
"link": "مدیریت"
|
"link": "مدیریت",
|
||||||
|
"donate": "حمایت مالی"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Konfigurasi Xray",
|
"xray": "Konfigurasi Xray",
|
||||||
"apiDocs": "Dokumentasi API",
|
"apiDocs": "Dokumentasi API",
|
||||||
"logout": "Keluar",
|
"logout": "Keluar",
|
||||||
"link": "Kelola"
|
"link": "Kelola",
|
||||||
|
"donate": "Donasi"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Xray設定",
|
"xray": "Xray設定",
|
||||||
"apiDocs": "API ドキュメント",
|
"apiDocs": "API ドキュメント",
|
||||||
"logout": "ログアウト",
|
"logout": "ログアウト",
|
||||||
"link": "リンク管理"
|
"link": "リンク管理",
|
||||||
|
"donate": "寄付"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Xray Configs",
|
"xray": "Xray Configs",
|
||||||
"apiDocs": "Documentação da API",
|
"apiDocs": "Documentação da API",
|
||||||
"logout": "Sair",
|
"logout": "Sair",
|
||||||
"link": "Gerenciar"
|
"link": "Gerenciar",
|
||||||
|
"donate": "Doar"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Настройки Xray",
|
"xray": "Настройки Xray",
|
||||||
"apiDocs": "Документация API",
|
"apiDocs": "Документация API",
|
||||||
"logout": "Выход",
|
"logout": "Выход",
|
||||||
"link": "Управление"
|
"link": "Управление",
|
||||||
|
"donate": "Поддержать"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Xray Yapılandırmaları",
|
"xray": "Xray Yapılandırmaları",
|
||||||
"apiDocs": "API Belgeleri",
|
"apiDocs": "API Belgeleri",
|
||||||
"logout": "Çıkış Yap",
|
"logout": "Çıkış Yap",
|
||||||
"link": "Yönet"
|
"link": "Yönet",
|
||||||
|
"donate": "Bağış Yap"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Конфігурації Xray",
|
"xray": "Конфігурації Xray",
|
||||||
"apiDocs": "Документація API",
|
"apiDocs": "Документація API",
|
||||||
"logout": "Вийти",
|
"logout": "Вийти",
|
||||||
"link": "Керувати"
|
"link": "Керувати",
|
||||||
|
"donate": "Підтримати"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Cài đặt Xray",
|
"xray": "Cài đặt Xray",
|
||||||
"apiDocs": "Tài liệu API",
|
"apiDocs": "Tài liệu API",
|
||||||
"logout": "Đăng xuất",
|
"logout": "Đăng xuất",
|
||||||
"link": "Quản lý"
|
"link": "Quản lý",
|
||||||
|
"donate": "Quyên góp"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Xray 设置",
|
"xray": "Xray 设置",
|
||||||
"apiDocs": "API 文档",
|
"apiDocs": "API 文档",
|
||||||
"logout": "退出登录",
|
"logout": "退出登录",
|
||||||
"link": "管理"
|
"link": "管理",
|
||||||
|
"donate": "捐赠"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,8 @@
|
||||||
"xray": "Xray 設定",
|
"xray": "Xray 設定",
|
||||||
"apiDocs": "API 文件",
|
"apiDocs": "API 文件",
|
||||||
"logout": "退出登入",
|
"logout": "退出登入",
|
||||||
"link": "管理"
|
"link": "管理",
|
||||||
|
"donate": "捐贈"
|
||||||
},
|
},
|
||||||
"pages": {
|
"pages": {
|
||||||
"login": {
|
"login": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue