diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 51857e6e..3e6de6e5 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -9,7 +9,7 @@ import { ClusterOutlined, LogoutOutlined, CloseOutlined, - MenuFoldOutlined, + MenuOutlined, } from '@ant-design/icons-vue'; import { currentTheme } from '@/composables/useTheme.js'; @@ -58,11 +58,23 @@ const tabs = computed(() => [ { key: `${prefix}logout`, icon: 'logout', title: t('logout') }, ]); +// Logout sits in its own pinned-to-bottom block on the drawer; the +// remaining items are the navigation proper. The full-height sider on +// desktop still uses `tabs` as-is so the desktop look is unchanged. +const navTabs = computed(() => tabs.value.filter((tab) => tab.icon !== 'logout')); +const utilTabs = computed(() => tabs.value.filter((tab) => tab.icon === 'logout')); + const activeTab = ref([props.requestUri]); const drawerOpen = ref(false); const collapsed = ref(JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || 'false')); +// Drawer width is capped against the viewport — AD-Vue's default 378px +// overflows on narrow phones (e.g. 360px portrait), leaving the page +// hidden behind the mask. `min()` keeps it sane on both phones and +// tablets while never exceeding 320px on larger displays. +const drawerWidth = 'min(82vw, 320px)'; + function openLink(key) { if (key.startsWith('http')) { window.open(key); @@ -91,6 +103,9 @@ function closeDrawer() { @@ -123,21 +154,96 @@ function closeDrawer() { height: 100%; } +/* `.sider-brand` and `.drawer-brand` share the same light-theme colour + * but differ in layout — the sider one is centered with its own + * top-of-sidebar padding + border, the drawer one sits inside a flex + * header next to the close button. Dark/ultra colour overrides live + * in the non-scoped block at the bottom (theme classes attach to + * body / html). */ +.sider-brand, +.drawer-brand { + font-weight: 600; + font-size: 18px; + letter-spacing: 0.5px; + color: rgba(0, 0, 0, 0.88); +} + +.sider-brand { + text-align: center; + padding: 16px 12px; + border-bottom: 1px solid rgba(128, 128, 128, 0.15); + user-select: none; +} + +.sider-brand-collapsed { + font-size: 16px; + padding: 16px 4px; + letter-spacing: 0; +} + .drawer-handle { position: fixed; - top: 16px; - left: 16px; + top: 12px; + left: 12px; z-index: 1100; background: rgba(0, 0, 0, 0.55); color: #fff; border: none; - width: 36px; - height: 36px; + width: 40px; + height: 40px; border-radius: 50%; cursor: pointer; display: none; align-items: center; justify-content: center; + font-size: 18px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); +} + +.drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid rgba(128, 128, 128, 0.15); +} + +.drawer-close { + background: transparent; + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 16px; + color: rgba(0, 0, 0, 0.65); +} + +.drawer-close:hover, +.drawer-close:focus-visible { + background: rgba(128, 128, 128, 0.18); +} + +.drawer-menu :deep(.ant-menu-item) { + height: 48px; + line-height: 48px; + margin: 0; + border-radius: 0; +} + +.drawer-menu :deep(.ant-menu-item .anticon) { + font-size: 16px; +} + +/* Push the utility (Logout) block to the bottom of the flex-column + * drawer body and separate it from the nav block with a hairline. The + * border colour is theme-neutral so it reads on both light and dark. */ +.drawer-utility { + margin-top: auto; + border-top: 1px solid rgba(128, 128, 128, 0.15); } @media (max-width: 768px) { @@ -161,3 +267,48 @@ function closeDrawer() { } } + + diff --git a/frontend/src/components/DateTimePicker.vue b/frontend/src/components/DateTimePicker.vue index a9ff91ec..750d2594 100644 --- a/frontend/src/components/DateTimePicker.vue +++ b/frontend/src/components/DateTimePicker.vue @@ -235,8 +235,8 @@ function onAntChange(next) { /* ===== Dark (navy) ======================================================= */ body.dark .persian-datepicker-input { - background: #142340; - border-color: #1f3358; + background: #252526; + border-color: #3c3c3c; color: rgba(255, 255, 255, 0.88); } @@ -251,14 +251,14 @@ body.dark .persian-datepicker-input:focus { body.dark .vpd-main .vpd-icon-btn { background: rgba(255, 255, 255, 0.04) !important; - border: 1px solid #1f3358 !important; + border: 1px solid #3c3c3c !important; border-right: none !important; border-radius: 6px 0 0 6px !important; color: rgba(255, 255, 255, 0.75) !important; } body.dark .vpd-wrapper .vpd-content { - background: #1a2c4d; + background: #2d2d30; color: rgba(255, 255, 255, 0.88); box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.32), 0 3px 6px -4px rgba(0, 0, 0, 0.48), @@ -266,7 +266,7 @@ body.dark .vpd-wrapper .vpd-content { } body.dark .vpd-wrapper .vpd-body { - background: #1a2c4d; + background: #2d2d30; color: rgba(255, 255, 255, 0.88); } @@ -315,7 +315,7 @@ body.dark .vpd-wrapper .vpd-actions button:hover { body.dark .vpd-wrapper .vpd-addon-list, body.dark .vpd-wrapper .vpd-addon-list-content { - background: #1a2c4d; + background: #2d2d30; color: rgba(255, 255, 255, 0.88); } diff --git a/frontend/src/composables/useTheme.js b/frontend/src/composables/useTheme.js index bac910fd..59d3e009 100644 --- a/frontend/src/composables/useTheme.js +++ b/frontend/src/composables/useTheme.js @@ -27,14 +27,15 @@ export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light')); // AD-Vue 4 theme config consumed by every page's . // Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla -// blue primary. Dark uses a navy palette across page/cards/modals so -// the sidebar blends with the rest of the surface; ultra-dark stays -// neutral black on top of darkAlgorithm. +// blue primary. Dark uses a neutral grey palette modelled on VS Code's +// Dark+ chrome (`#1e1e1e` editor, `#252526` sidebar, `#2d2d30` panel), +// so the panel reads as a familiar modern IDE rather than the older +// navy shade. Ultra-dark stays pure-black on darkAlgorithm. const DARK_TOKENS = { - colorBgBase: '#0a1426', - colorBgLayout: '#0a1426', - colorBgContainer: '#142340', - colorBgElevated: '#1a2c4d', + colorBgBase: '#1e1e1e', + colorBgLayout: '#1e1e1e', + colorBgContainer: '#252526', + colorBgElevated: '#2d2d30', }; const ULTRA_DARK_TOKENS = { colorBgBase: '#000', @@ -47,13 +48,12 @@ const ULTRA_DARK_TOKENS = { // + trigger backgrounds and `#001529` / `#000c17` as the dark Menu item // backgrounds (see node_modules/ant-design-vue/es/{layout,menu}/style/ // index.js). Override at the component-token level so the sider blends -// with darkAlgorithm's neutral surfaces. -// Dark theme uses a refined navy for the sidebar — distinct from the -// neutral ultra-dark and warmer than AD-Vue's stock #001529. +// with darkAlgorithm's neutral surfaces. Sider/trigger use the same +// `#252526` / `#333333` tones VS Code does for its activity bar. const DARK_LAYOUT_TOKENS = { - colorBgHeader: '#0d1d33', - colorBgTrigger: '#15294a', - colorBgBody: '#000', + colorBgHeader: '#252526', + colorBgTrigger: '#333333', + colorBgBody: '#1e1e1e', }; const ULTRA_DARK_LAYOUT_TOKENS = { colorBgHeader: '#0a0a0a', @@ -61,9 +61,9 @@ const ULTRA_DARK_LAYOUT_TOKENS = { colorBgBody: '#000', }; const DARK_MENU_TOKENS = { - colorItemBg: '#0d1d33', - colorSubItemBg: '#08142a', - menuSubMenuBg: '#0d1d33', + colorItemBg: '#252526', + colorSubItemBg: '#1e1e1e', + menuSubMenuBg: '#252526', }; const ULTRA_DARK_MENU_TOKENS = { colorItemBg: '#0a0a0a', diff --git a/frontend/src/pages/inbounds/InboundsPage.vue b/frontend/src/pages/inbounds/InboundsPage.vue index 084f8135..a46ed98d 100644 --- a/frontend/src/pages/inbounds/InboundsPage.vue +++ b/frontend/src/pages/inbounds/InboundsPage.vue @@ -650,8 +650,8 @@ function onRowAction({ key, dbInbound }) { } .inbounds-page.is-dark { - --bg-page: #0a1222; - --bg-card: #151f31; + --bg-page: #1e1e1e; + --bg-card: #252526; } .inbounds-page.is-dark.is-ultra { diff --git a/frontend/src/pages/index/IndexPage.vue b/frontend/src/pages/index/IndexPage.vue index 1c9e28b5..292e4156 100644 --- a/frontend/src/pages/index/IndexPage.vue +++ b/frontend/src/pages/index/IndexPage.vue @@ -336,8 +336,8 @@ async function openConfig() { } .index-page.is-dark { - --bg-page: #0a1222; - --bg-card: #151f31; + --bg-page: #1e1e1e; + --bg-card: #252526; } .index-page.is-dark.is-ultra { diff --git a/frontend/src/pages/login/LoginPage.vue b/frontend/src/pages/login/LoginPage.vue index 4954cf1f..a106fba2 100644 --- a/frontend/src/pages/login/LoginPage.vue +++ b/frontend/src/pages/login/LoginPage.vue @@ -13,6 +13,19 @@ import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue'; const { t } = useI18n(); +const fetched = ref(false); +const submitting = ref(false); +const twoFactorEnable = ref(false); +const version = computed(() => window.X_UI_CUR_VER || ''); + +const user = reactive({ + username: '', + password: '', + twoFactorCode: '', +}); + +const basePath = window.X_UI_BASE_PATH || ''; + const headlineWords = computed(() => [t('pages.login.hello'), t('pages.login.title')]); const HEADLINE_INTERVAL_MS = 2000; const headlineIndex = ref(0); @@ -28,23 +41,9 @@ onBeforeUnmount(() => { if (headlineTimer != null) window.clearInterval(headlineTimer); }); -const fetched = ref(false); -const submitting = ref(false); -const twoFactorEnable = ref(false); - -const user = reactive({ - username: '', - password: '', - twoFactorCode: '', -}); - -const basePath = window.X_UI_BASE_PATH || ''; - onMounted(async () => { const msg = await HttpUtil.post('/getTwoFactorEnable'); - if (msg.success) { - twoFactorEnable.value = !!msg.obj; - } + if (msg.success) twoFactorEnable.value = !!msg.obj; fetched.value = true; }); @@ -52,9 +51,7 @@ async function login() { submitting.value = true; try { const msg = await HttpUtil.post('/login', user); - if (msg.success) { - window.location.href = basePath + 'panel/'; - } + if (msg.success) window.location.href = basePath + 'panel/'; } finally { submitting.value = false; } @@ -70,102 +67,85 @@ function onLangChange(next) {