mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-13 09:36:05 +00:00
feat(frontend): refresh dark theme + redesign login page
- Swap navy dark palette for VS Code Dark+ neutrals (#1e1e1e/#252526/ #2d2d30) across theme tokens, page backgrounds and DateTimePicker - Add brand header to the mobile drawer and desktop sider, and recolor the drawer body so it reads as one panel with the menu - Redesign login page with a centered card, cycling Hello/Welcome headline and per-theme animated gradient-blob backgrounds
This commit is contained in:
parent
f1760b0a28
commit
c1efc48694
10 changed files with 490 additions and 290 deletions
|
|
@ -9,7 +9,7 @@ import {
|
||||||
ClusterOutlined,
|
ClusterOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
CloseOutlined,
|
CloseOutlined,
|
||||||
MenuFoldOutlined,
|
MenuOutlined,
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
import { currentTheme } from '@/composables/useTheme.js';
|
import { currentTheme } from '@/composables/useTheme.js';
|
||||||
|
|
@ -58,11 +58,23 @@ const tabs = computed(() => [
|
||||||
{ key: `${prefix}logout`, icon: 'logout', title: t('logout') },
|
{ 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 activeTab = ref([props.requestUri]);
|
||||||
|
|
||||||
const drawerOpen = ref(false);
|
const drawerOpen = ref(false);
|
||||||
const collapsed = ref(JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || '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) {
|
function openLink(key) {
|
||||||
if (key.startsWith('http')) {
|
if (key.startsWith('http')) {
|
||||||
window.open(key);
|
window.open(key);
|
||||||
|
|
@ -91,6 +103,9 @@ function closeDrawer() {
|
||||||
<template>
|
<template>
|
||||||
<div class="ant-sidebar">
|
<div class="ant-sidebar">
|
||||||
<a-layout-sider :theme="currentTheme" collapsible :collapsed="collapsed" breakpoint="md" @collapse="onCollapse">
|
<a-layout-sider :theme="currentTheme" collapsible :collapsed="collapsed" breakpoint="md" @collapse="onCollapse">
|
||||||
|
<div class="sider-brand" :class="{ 'sider-brand-collapsed': collapsed }">
|
||||||
|
{{ collapsed ? '3X' : '3X-UI' }}
|
||||||
|
</div>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<a-menu :theme="currentTheme" mode="inline" :selected-keys="activeTab" @click="({ key }) => openLink(key)">
|
<a-menu :theme="currentTheme" mode="inline" :selected-keys="activeTab" @click="({ key }) => openLink(key)">
|
||||||
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
||||||
|
|
@ -101,19 +116,35 @@ function closeDrawer() {
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
|
|
||||||
<a-drawer placement="left" :closable="false" :open="drawerOpen" :wrap-class-name="currentTheme"
|
<a-drawer placement="left" :closable="false" :open="drawerOpen" :wrap-class-name="currentTheme"
|
||||||
:wrap-style="{ padding: 0 }" :style="{ height: '100%' }" @close="closeDrawer">
|
:wrap-style="{ padding: 0 }" :width="drawerWidth"
|
||||||
|
:body-style="{ padding: 0, display: 'flex', flexDirection: 'column', height: '100%' }"
|
||||||
|
:header-style="{ display: 'none' }" @close="closeDrawer">
|
||||||
|
<div class="drawer-header">
|
||||||
|
<span class="drawer-brand">3X-UI</span>
|
||||||
|
<button class="drawer-close" type="button" :aria-label="t('close')" @click="closeDrawer">
|
||||||
|
<CloseOutlined />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<a-menu :theme="currentTheme" mode="inline" :selected-keys="activeTab" @click="({ key }) => openLink(key)">
|
<a-menu :theme="currentTheme" mode="inline" :selected-keys="activeTab" class="drawer-menu drawer-nav"
|
||||||
<a-menu-item v-for="tab in tabs" :key="tab.key">
|
@click="({ key }) => openLink(key)">
|
||||||
|
<a-menu-item v-for="tab in navTabs" :key="tab.key">
|
||||||
|
<component :is="iconByName[tab.icon]" />
|
||||||
|
<span>{{ tab.title }}</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
<a-menu :theme="currentTheme" mode="inline" :selected-keys="activeTab" class="drawer-menu drawer-utility"
|
||||||
|
@click="({ key }) => openLink(key)">
|
||||||
|
<a-menu-item v-for="tab in utilTabs" :key="tab.key">
|
||||||
<component :is="iconByName[tab.icon]" />
|
<component :is="iconByName[tab.icon]" />
|
||||||
<span>{{ tab.title }}</span>
|
<span>{{ tab.title }}</span>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
|
||||||
<button class="drawer-handle" type="button" @click="toggleDrawer">
|
<button v-show="!drawerOpen" class="drawer-handle" type="button" :aria-label="t('menu.dashboard')"
|
||||||
<CloseOutlined v-if="drawerOpen" />
|
@click="toggleDrawer">
|
||||||
<MenuFoldOutlined v-else />
|
<MenuOutlined />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -123,21 +154,96 @@ function closeDrawer() {
|
||||||
height: 100%;
|
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 {
|
.drawer-handle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 16px;
|
top: 12px;
|
||||||
left: 16px;
|
left: 12px;
|
||||||
z-index: 1100;
|
z-index: 1100;
|
||||||
background: rgba(0, 0, 0, 0.55);
|
background: rgba(0, 0, 0, 0.55);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
width: 36px;
|
width: 40px;
|
||||||
height: 36px;
|
height: 40px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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) {
|
@media (max-width: 768px) {
|
||||||
|
|
@ -161,3 +267,48 @@ function closeDrawer() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Non-scoped so the rules survive AD-Vue teleporting the drawer body
|
||||||
|
* outside the AppSidebar element's scope id. Without this the Vue
|
||||||
|
* `:global(body.dark) .drawer-brand` form did not produce the expected
|
||||||
|
* `body.dark .drawer-brand[data-v-xxx]` selector reliably, and the
|
||||||
|
* drawer brand stayed at the light-theme dark colour on the navy
|
||||||
|
* drawer surface. Class names are specific enough that no collision is
|
||||||
|
* expected; AppSidebar owns the only drawer in the app. */
|
||||||
|
body.dark .drawer-brand,
|
||||||
|
body.dark .sider-brand {
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='ultra-dark'] .drawer-brand,
|
||||||
|
html[data-theme='ultra-dark'] .sider-brand {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark .drawer-close {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='ultra-dark'] .drawer-close {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pin the drawer surface to the same colour the desktop sider uses
|
||||||
|
* (Layout.colorBgHeader / Menu.colorItemBg from useTheme.js) so the
|
||||||
|
* header, empty body region, and menu items read as one continuous
|
||||||
|
* panel. AD-Vue's CSS-in-JS tokens otherwise leave the drawer at
|
||||||
|
* colorBgElevated (#2d2d30 in dark) which clashes with the #252526
|
||||||
|
* menu rows. `!important` is required to beat the CSS-in-JS rule
|
||||||
|
* specificity; AppSidebar owns the only drawer in the app so this
|
||||||
|
* doesn't collide with anything else. */
|
||||||
|
body.dark .ant-drawer .ant-drawer-content,
|
||||||
|
body.dark .ant-drawer .ant-drawer-body {
|
||||||
|
background: #252526 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-content,
|
||||||
|
html[data-theme='ultra-dark'] .ant-drawer .ant-drawer-body {
|
||||||
|
background: #0a0a0a !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -235,8 +235,8 @@ function onAntChange(next) {
|
||||||
/* ===== Dark (navy) ======================================================= */
|
/* ===== Dark (navy) ======================================================= */
|
||||||
|
|
||||||
body.dark .persian-datepicker-input {
|
body.dark .persian-datepicker-input {
|
||||||
background: #142340;
|
background: #252526;
|
||||||
border-color: #1f3358;
|
border-color: #3c3c3c;
|
||||||
color: rgba(255, 255, 255, 0.88);
|
color: rgba(255, 255, 255, 0.88);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,14 +251,14 @@ body.dark .persian-datepicker-input:focus {
|
||||||
|
|
||||||
body.dark .vpd-main .vpd-icon-btn {
|
body.dark .vpd-main .vpd-icon-btn {
|
||||||
background: rgba(255, 255, 255, 0.04) !important;
|
background: rgba(255, 255, 255, 0.04) !important;
|
||||||
border: 1px solid #1f3358 !important;
|
border: 1px solid #3c3c3c !important;
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
border-radius: 6px 0 0 6px !important;
|
border-radius: 6px 0 0 6px !important;
|
||||||
color: rgba(255, 255, 255, 0.75) !important;
|
color: rgba(255, 255, 255, 0.75) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark .vpd-wrapper .vpd-content {
|
body.dark .vpd-wrapper .vpd-content {
|
||||||
background: #1a2c4d;
|
background: #2d2d30;
|
||||||
color: rgba(255, 255, 255, 0.88);
|
color: rgba(255, 255, 255, 0.88);
|
||||||
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.32),
|
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.32),
|
||||||
0 3px 6px -4px rgba(0, 0, 0, 0.48),
|
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 {
|
body.dark .vpd-wrapper .vpd-body {
|
||||||
background: #1a2c4d;
|
background: #2d2d30;
|
||||||
color: rgba(255, 255, 255, 0.88);
|
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,
|
||||||
body.dark .vpd-wrapper .vpd-addon-list-content {
|
body.dark .vpd-wrapper .vpd-addon-list-content {
|
||||||
background: #1a2c4d;
|
background: #2d2d30;
|
||||||
color: rgba(255, 255, 255, 0.88);
|
color: rgba(255, 255, 255, 0.88);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,15 @@ export const currentTheme = computed(() => (theme.isDark ? 'dark' : 'light'));
|
||||||
|
|
||||||
// AD-Vue 4 theme config consumed by every page's <a-config-provider>.
|
// AD-Vue 4 theme config consumed by every page's <a-config-provider>.
|
||||||
// Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla
|
// Three modes — light / dark / ultra-dark — all share AD-Vue's vanilla
|
||||||
// blue primary. Dark uses a navy palette across page/cards/modals so
|
// blue primary. Dark uses a neutral grey palette modelled on VS Code's
|
||||||
// the sidebar blends with the rest of the surface; ultra-dark stays
|
// Dark+ chrome (`#1e1e1e` editor, `#252526` sidebar, `#2d2d30` panel),
|
||||||
// neutral black on top of darkAlgorithm.
|
// 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 = {
|
const DARK_TOKENS = {
|
||||||
colorBgBase: '#0a1426',
|
colorBgBase: '#1e1e1e',
|
||||||
colorBgLayout: '#0a1426',
|
colorBgLayout: '#1e1e1e',
|
||||||
colorBgContainer: '#142340',
|
colorBgContainer: '#252526',
|
||||||
colorBgElevated: '#1a2c4d',
|
colorBgElevated: '#2d2d30',
|
||||||
};
|
};
|
||||||
const ULTRA_DARK_TOKENS = {
|
const ULTRA_DARK_TOKENS = {
|
||||||
colorBgBase: '#000',
|
colorBgBase: '#000',
|
||||||
|
|
@ -47,13 +48,12 @@ const ULTRA_DARK_TOKENS = {
|
||||||
// + trigger backgrounds and `#001529` / `#000c17` as the dark Menu item
|
// + trigger backgrounds and `#001529` / `#000c17` as the dark Menu item
|
||||||
// backgrounds (see node_modules/ant-design-vue/es/{layout,menu}/style/
|
// backgrounds (see node_modules/ant-design-vue/es/{layout,menu}/style/
|
||||||
// index.js). Override at the component-token level so the sider blends
|
// index.js). Override at the component-token level so the sider blends
|
||||||
// with darkAlgorithm's neutral surfaces.
|
// with darkAlgorithm's neutral surfaces. Sider/trigger use the same
|
||||||
// Dark theme uses a refined navy for the sidebar — distinct from the
|
// `#252526` / `#333333` tones VS Code does for its activity bar.
|
||||||
// neutral ultra-dark and warmer than AD-Vue's stock #001529.
|
|
||||||
const DARK_LAYOUT_TOKENS = {
|
const DARK_LAYOUT_TOKENS = {
|
||||||
colorBgHeader: '#0d1d33',
|
colorBgHeader: '#252526',
|
||||||
colorBgTrigger: '#15294a',
|
colorBgTrigger: '#333333',
|
||||||
colorBgBody: '#000',
|
colorBgBody: '#1e1e1e',
|
||||||
};
|
};
|
||||||
const ULTRA_DARK_LAYOUT_TOKENS = {
|
const ULTRA_DARK_LAYOUT_TOKENS = {
|
||||||
colorBgHeader: '#0a0a0a',
|
colorBgHeader: '#0a0a0a',
|
||||||
|
|
@ -61,9 +61,9 @@ const ULTRA_DARK_LAYOUT_TOKENS = {
|
||||||
colorBgBody: '#000',
|
colorBgBody: '#000',
|
||||||
};
|
};
|
||||||
const DARK_MENU_TOKENS = {
|
const DARK_MENU_TOKENS = {
|
||||||
colorItemBg: '#0d1d33',
|
colorItemBg: '#252526',
|
||||||
colorSubItemBg: '#08142a',
|
colorSubItemBg: '#1e1e1e',
|
||||||
menuSubMenuBg: '#0d1d33',
|
menuSubMenuBg: '#252526',
|
||||||
};
|
};
|
||||||
const ULTRA_DARK_MENU_TOKENS = {
|
const ULTRA_DARK_MENU_TOKENS = {
|
||||||
colorItemBg: '#0a0a0a',
|
colorItemBg: '#0a0a0a',
|
||||||
|
|
|
||||||
|
|
@ -650,8 +650,8 @@ function onRowAction({ key, dbInbound }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbounds-page.is-dark {
|
.inbounds-page.is-dark {
|
||||||
--bg-page: #0a1222;
|
--bg-page: #1e1e1e;
|
||||||
--bg-card: #151f31;
|
--bg-card: #252526;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbounds-page.is-dark.is-ultra {
|
.inbounds-page.is-dark.is-ultra {
|
||||||
|
|
|
||||||
|
|
@ -336,8 +336,8 @@ async function openConfig() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-page.is-dark {
|
.index-page.is-dark {
|
||||||
--bg-page: #0a1222;
|
--bg-page: #1e1e1e;
|
||||||
--bg-card: #151f31;
|
--bg-card: #252526;
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-page.is-dark.is-ultra {
|
.index-page.is-dark.is-ultra {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ import ThemeSwitchLogin from '@/components/ThemeSwitchLogin.vue';
|
||||||
|
|
||||||
const { t } = useI18n();
|
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 headlineWords = computed(() => [t('pages.login.hello'), t('pages.login.title')]);
|
||||||
const HEADLINE_INTERVAL_MS = 2000;
|
const HEADLINE_INTERVAL_MS = 2000;
|
||||||
const headlineIndex = ref(0);
|
const headlineIndex = ref(0);
|
||||||
|
|
@ -28,23 +41,9 @@ onBeforeUnmount(() => {
|
||||||
if (headlineTimer != null) window.clearInterval(headlineTimer);
|
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 () => {
|
onMounted(async () => {
|
||||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||||
if (msg.success) {
|
if (msg.success) twoFactorEnable.value = !!msg.obj;
|
||||||
twoFactorEnable.value = !!msg.obj;
|
|
||||||
}
|
|
||||||
fetched.value = true;
|
fetched.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -52,9 +51,7 @@ async function login() {
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/login', user);
|
const msg = await HttpUtil.post('/login', user);
|
||||||
if (msg.success) {
|
if (msg.success) window.location.href = basePath + 'panel/';
|
||||||
window.location.href = basePath + 'panel/';
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -70,30 +67,9 @@ function onLangChange(next) {
|
||||||
<a-config-provider :theme="antdThemeConfig">
|
<a-config-provider :theme="antdThemeConfig">
|
||||||
<a-layout class="login-app" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
|
<a-layout class="login-app" :class="{ 'is-dark': themeState.isDark, 'is-ultra': themeState.isUltra }">
|
||||||
<a-layout-content class="login-content">
|
<a-layout-content class="login-content">
|
||||||
<div class="waves-header">
|
<!-- Floating settings (theme switcher + language picker) sits in
|
||||||
<div class="waves-inner-header"></div>
|
the viewport's top-right corner so the card stays uncluttered. -->
|
||||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
<div class="login-toolbar">
|
||||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
|
||||||
<defs>
|
|
||||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
|
||||||
</defs>
|
|
||||||
<g class="parallax">
|
|
||||||
<use xlink:href="#gentle-wave" x="48" y="0" />
|
|
||||||
<use xlink:href="#gentle-wave" x="48" y="3" />
|
|
||||||
<use xlink:href="#gentle-wave" x="48" y="5" />
|
|
||||||
<use xlink:href="#gentle-wave" x="48" y="7" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a-row type="flex" justify="center" align="middle" class="login-row">
|
|
||||||
<a-col class="login-card">
|
|
||||||
<div v-if="!fetched" class="login-loading">
|
|
||||||
<a-spin size="large" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<div class="login-settings">
|
|
||||||
<a-popover :overlay-class-name="currentTheme" :title="t('menu.settings')" placement="bottomRight"
|
<a-popover :overlay-class-name="currentTheme" :title="t('menu.settings')" placement="bottomRight"
|
||||||
trigger="click">
|
trigger="click">
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
@ -101,15 +77,14 @@ function onLangChange(next) {
|
||||||
<ThemeSwitchLogin />
|
<ThemeSwitchLogin />
|
||||||
<span>{{ t('pages.settings.language') }}</span>
|
<span>{{ t('pages.settings.language') }}</span>
|
||||||
<a-select v-model:value="lang" class="lang-select" @change="onLangChange">
|
<a-select v-model:value="lang" class="lang-select" @change="onLangChange">
|
||||||
<a-select-option v-for="l in LanguageManager.supportedLanguages" :key="l.value"
|
<a-select-option v-for="l in LanguageManager.supportedLanguages" :key="l.value" :value="l.value">
|
||||||
:value="l.value">
|
|
||||||
<span :aria-label="l.name">{{ l.icon }}</span>
|
<span :aria-label="l.name">{{ l.icon }}</span>
|
||||||
<span>{{ l.name }}</span>
|
<span>{{ l.name }}</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<a-button shape="circle">
|
<a-button shape="circle" class="toolbar-btn" :aria-label="t('menu.settings')">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SettingOutlined />
|
<SettingOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -117,19 +92,25 @@ function onLangChange(next) {
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a-row justify="center">
|
<div class="login-wrapper">
|
||||||
<a-col :span="24">
|
<div v-if="!fetched" class="login-loading">
|
||||||
<h2 class="login-title">
|
<a-spin size="large" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="login-card">
|
||||||
|
<div class="brand">
|
||||||
|
<span class="brand-name">3X-UI</span>
|
||||||
|
<span class="brand-accent" aria-hidden="true"></span>
|
||||||
|
</div>
|
||||||
|
<h2 class="welcome">
|
||||||
<Transition name="headline" mode="out-in">
|
<Transition name="headline" mode="out-in">
|
||||||
<b :key="headlineIndex">{{ headlineWords[headlineIndex] }}</b>
|
<b :key="headlineIndex">{{ headlineWords[headlineIndex] }}</b>
|
||||||
</Transition>
|
</Transition>
|
||||||
</h2>
|
</h2>
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<a-form layout="vertical" @submit.prevent="login">
|
<a-form layout="vertical" class="login-form" @submit.prevent="login">
|
||||||
<a-form-item>
|
<a-form-item :label="t('username')">
|
||||||
<a-input v-model:value="user.username" autocomplete="username" name="username"
|
<a-input v-model:value="user.username" autocomplete="username" name="username" size="large"
|
||||||
:placeholder="t('username')" autofocus required>
|
:placeholder="t('username')" autofocus required>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
|
|
@ -137,35 +118,34 @@ function onLangChange(next) {
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item :label="t('password')">
|
||||||
<a-input-password v-model:value="user.password" autocomplete="current-password" name="password"
|
<a-input-password v-model:value="user.password" autocomplete="current-password" name="password"
|
||||||
:placeholder="t('password')" required>
|
size="large" :placeholder="t('password')" required>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<LockOutlined />
|
<LockOutlined />
|
||||||
</template>
|
</template>
|
||||||
</a-input-password>
|
</a-input-password>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item v-if="twoFactorEnable">
|
<a-form-item v-if="twoFactorEnable" :label="t('twoFactorCode')">
|
||||||
<a-input v-model:value="user.twoFactorCode" autocomplete="one-time-code" name="twoFactorCode"
|
<a-input v-model:value="user.twoFactorCode" autocomplete="one-time-code" name="twoFactorCode"
|
||||||
:placeholder="t('twoFactorCode')" required>
|
size="large" :placeholder="t('twoFactorCode')" required>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<KeyOutlined />
|
<KeyOutlined />
|
||||||
</template>
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item class="submit-row">
|
||||||
<a-row justify="center">
|
<a-button type="primary" html-type="submit" :loading="submitting" size="large" block>
|
||||||
<a-button type="primary" html-type="submit" :loading="submitting" block>
|
|
||||||
{{ submitting ? '' : t('login') }}
|
{{ submitting ? '' : t('login') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-row>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
|
<div v-if="version" class="version">v{{ version }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
|
|
@ -173,59 +153,242 @@ function onLangChange(next) {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.login-app {
|
.login-app {
|
||||||
--bg-page: #c7ebe2;
|
--bg-page: #f5f7fa;
|
||||||
--bg-wave-header: #dbf5ed;
|
|
||||||
--bg-card: #ffffff;
|
--bg-card: #ffffff;
|
||||||
--color-title: #008771;
|
--color-text: rgba(0, 0, 0, 0.88);
|
||||||
--shadow-card: 0 2px 8px rgba(0, 0, 0, 0.09);
|
--color-text-subtle: rgba(0, 0, 0, 0.55);
|
||||||
--wave-fill: rgba(0, 135, 113, 0.12);
|
--color-accent: #1677ff;
|
||||||
--wave-fill-bottom: #c7ebe2;
|
--color-border: rgba(0, 0, 0, 0.08);
|
||||||
|
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04), 0 8px 24px rgba(0, 0, 0, 0.06);
|
||||||
|
--blob-1: rgba(99, 102, 241, 0.45);
|
||||||
|
--blob-2: rgba(236, 72, 153, 0.38);
|
||||||
|
--blob-3: rgba(20, 184, 166, 0.32);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-app.is-dark {
|
.login-app.is-dark {
|
||||||
--bg-page: #222d42;
|
--bg-page: #1e1e1e;
|
||||||
--bg-wave-header: #0a1222;
|
--bg-card: #252526;
|
||||||
--bg-card: #151f31;
|
--color-text: rgba(255, 255, 255, 0.92);
|
||||||
--color-title: rgba(255, 255, 255, 0.92);
|
--color-text-subtle: rgba(255, 255, 255, 0.55);
|
||||||
--shadow-card: 0 4px 16px rgba(0, 0, 0, 0.45);
|
--color-accent: #4096ff;
|
||||||
--wave-fill: #222d42;
|
--color-border: rgba(255, 255, 255, 0.08);
|
||||||
--wave-fill-bottom: #222d42;
|
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.3), 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
--blob-1: rgba(64, 150, 255, 0.40);
|
||||||
|
--blob-2: rgba(168, 85, 247, 0.34);
|
||||||
|
--blob-3: rgba(34, 211, 238, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-app.is-dark.is-ultra {
|
.login-app.is-dark.is-ultra {
|
||||||
--bg-page: #0f2d32;
|
--bg-page: #000;
|
||||||
--bg-wave-header: #0a2227;
|
--bg-card: #141414;
|
||||||
--bg-card: #0c0e12;
|
--color-border: rgba(255, 255, 255, 0.06);
|
||||||
--wave-fill: #1f4d52;
|
--blob-1: rgba(64, 150, 255, 0.22);
|
||||||
--wave-fill-bottom: #0f2d32;
|
--blob-2: rgba(168, 85, 247, 0.18);
|
||||||
|
--blob-3: rgba(34, 211, 238, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Three blurred blobs slowly drift across the page; ::before and
|
||||||
|
* ::after carry two of them, the third lives on .login-content::before
|
||||||
|
* so we can animate it independently. */
|
||||||
|
.login-app::before,
|
||||||
|
.login-app::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 70vw;
|
||||||
|
height: 70vw;
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 900px;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(90px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-app::before {
|
||||||
|
top: -25vw;
|
||||||
|
left: -20vw;
|
||||||
|
background: radial-gradient(circle, var(--blob-1) 0%, transparent 65%);
|
||||||
|
animation: blob-drift-a 24s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-app::after {
|
||||||
|
bottom: -25vw;
|
||||||
|
right: -20vw;
|
||||||
|
background: radial-gradient(circle, var(--blob-2) 0%, transparent 65%);
|
||||||
|
animation: blob-drift-b 30s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 30%;
|
||||||
|
left: 50%;
|
||||||
|
width: 50vw;
|
||||||
|
height: 50vw;
|
||||||
|
max-width: 700px;
|
||||||
|
max-height: 700px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, var(--blob-3) 0%, transparent 65%);
|
||||||
|
filter: blur(90px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
will-change: transform;
|
||||||
|
animation: blob-drift-c 36s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blob-drift-a {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(18vw, 10vh) scale(1.15); }
|
||||||
|
100% { transform: translate(34vw, 22vh) scale(1.25); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blob-drift-b {
|
||||||
|
0% { transform: translate(0, 0) scale(1); }
|
||||||
|
50% { transform: translate(-16vw, -10vh) scale(1.12); }
|
||||||
|
100% { transform: translate(-30vw, -22vh) scale(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blob-drift-c {
|
||||||
|
0% { transform: translate(-50%, -50%) scale(1); }
|
||||||
|
50% { transform: translate(-20%, -20%) scale(1.1); }
|
||||||
|
100% { transform: translate(-80%, -10%) scale(1.05); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.login-app::before,
|
||||||
|
.login-app::after,
|
||||||
|
.login-content::before {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-app,
|
|
||||||
.login-app :deep(.ant-layout-content) {
|
.login-app :deep(.ant-layout-content) {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-app {
|
.login-content {
|
||||||
background: var(--bg-page);
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-toolbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-wrapper {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-loading {
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px 32px 28px;
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
@media (max-width: 480px) {
|
||||||
color: var(--color-title);
|
.login-card {
|
||||||
|
padding: 32px 20px 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-settings {
|
.brand {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-accent {
|
||||||
|
display: block;
|
||||||
|
width: 40px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
min-height: 42px;
|
||||||
|
margin: 12px 0 28px;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome b {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline-enter-active,
|
||||||
|
.headline-leave-active {
|
||||||
|
transition: opacity 280ms ease, transform 280ms ease;
|
||||||
|
}
|
||||||
|
.headline-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
.headline-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form :deep(.ant-form-item-label > label) {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-row {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-subtle);
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-popover {
|
.settings-popover {
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
}
|
}
|
||||||
|
|
@ -233,118 +396,4 @@ function onLangChange(next) {
|
||||||
.lang-select {
|
.lang-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-content {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-row {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 24px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-card {
|
|
||||||
width: clamp(280px, 90vw, 300px);
|
|
||||||
border-radius: 2rem;
|
|
||||||
padding: clamp(2rem, 5vw, 4rem) 1.5rem;
|
|
||||||
transition: background 0.3s, box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 500;
|
|
||||||
min-height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-title b {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline-enter-active,
|
|
||||||
.headline-leave-active {
|
|
||||||
transition: opacity 0.4s ease, transform 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waves-header {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0 0 auto 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
background: var(--bg-wave-header);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waves-inner-header {
|
|
||||||
height: 50vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waves {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 15vh;
|
|
||||||
min-height: 100px;
|
|
||||||
max-height: 150px;
|
|
||||||
margin-bottom: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use {
|
|
||||||
fill: var(--wave-fill);
|
|
||||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(1) {
|
|
||||||
animation-delay: -2s;
|
|
||||||
animation-duration: 4s;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(2) {
|
|
||||||
animation-delay: -3s;
|
|
||||||
animation-duration: 7s;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(3) {
|
|
||||||
animation-delay: -4s;
|
|
||||||
animation-duration: 10s;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(4) {
|
|
||||||
animation-delay: -5s;
|
|
||||||
animation-duration: 13s;
|
|
||||||
fill: var(--wave-fill-bottom);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes move-forever {
|
|
||||||
0% {
|
|
||||||
transform: translate3d(-90px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate3d(85px, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,8 @@ async function onToggleEnable(node, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodes-page.is-dark {
|
.nodes-page.is-dark {
|
||||||
--bg-page: #0a1222;
|
--bg-page: #1e1e1e;
|
||||||
--bg-card: #151f31;
|
--bg-card: #252526;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nodes-page.is-dark.is-ultra {
|
.nodes-page.is-dark.is-ultra {
|
||||||
|
|
|
||||||
|
|
@ -256,8 +256,8 @@ const alertVisible = ref(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-page.is-dark {
|
.settings-page.is-dark {
|
||||||
--bg-page: #0a1222;
|
--bg-page: #1e1e1e;
|
||||||
--bg-card: #151f31;
|
--bg-card: #252526;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-page.is-dark.is-ultra {
|
.settings-page.is-dark.is-ultra {
|
||||||
|
|
|
||||||
|
|
@ -299,8 +299,8 @@ const themeClass = computed(() => ({
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscription-page.is-dark {
|
.subscription-page.is-dark {
|
||||||
--bg-page: #0a1222;
|
--bg-page: #1e1e1e;
|
||||||
--bg-card: #151f31;
|
--bg-card: #252526;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscription-page.is-dark.is-ultra {
|
.subscription-page.is-dark.is-ultra {
|
||||||
|
|
|
||||||
|
|
@ -339,8 +339,8 @@ function confirmRestart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.xray-page.is-dark {
|
.xray-page.is-dark {
|
||||||
--bg-page: #0a1222;
|
--bg-page: #1e1e1e;
|
||||||
--bg-card: #151f31;
|
--bg-card: #252526;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xray-page.is-dark.is-ultra {
|
.xray-page.is-dark.is-ultra {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue