|
@@ -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) {
|
|
|
|
|
- twoFactorEnable.value = !!msg.obj;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (msg.success) 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) {
|
|
|
|
|
- window.location.href = basePath + 'panel/';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (msg.success) window.location.href = basePath + 'panel/';
|
|
|
} finally {
|
|
} finally {
|
|
|
submitting.value = false;
|
|
submitting.value = false;
|
|
|
}
|
|
}
|
|
@@ -70,102 +67,85 @@ 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">
|
|
|
|
|
- <div class="waves-inner-header"></div>
|
|
|
|
|
- <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
|
|
|
- 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>
|
|
|
|
|
|
|
+ <!-- Floating settings (theme switcher + language picker) sits in
|
|
|
|
|
+ the viewport's top-right corner so the card stays uncluttered. -->
|
|
|
|
|
+ <div class="login-toolbar">
|
|
|
|
|
+ <a-popover :overlay-class-name="currentTheme" :title="t('menu.settings')" placement="bottomRight"
|
|
|
|
|
+ trigger="click">
|
|
|
|
|
+ <template #content>
|
|
|
|
|
+ <a-space direction="vertical" :size="10" class="settings-popover">
|
|
|
|
|
+ <ThemeSwitchLogin />
|
|
|
|
|
+ <span>{{ t('pages.settings.language') }}</span>
|
|
|
|
|
+ <a-select v-model:value="lang" class="lang-select" @change="onLangChange">
|
|
|
|
|
+ <a-select-option v-for="l in LanguageManager.supportedLanguages" :key="l.value" :value="l.value">
|
|
|
|
|
+ <span :aria-label="l.name">{{ l.icon }}</span>
|
|
|
|
|
+ <span>{{ l.name }}</span>
|
|
|
|
|
+ </a-select-option>
|
|
|
|
|
+ </a-select>
|
|
|
|
|
+ </a-space>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <a-button shape="circle" class="toolbar-btn" :aria-label="t('menu.settings')">
|
|
|
|
|
+ <template #icon>
|
|
|
|
|
+ <SettingOutlined />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ </a-popover>
|
|
|
</div>
|
|
</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 class="login-wrapper">
|
|
|
|
|
+ <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"
|
|
|
|
|
- trigger="click">
|
|
|
|
|
- <template #content>
|
|
|
|
|
- <a-space direction="vertical" :size="10" class="settings-popover">
|
|
|
|
|
- <ThemeSwitchLogin />
|
|
|
|
|
- <span>{{ t('pages.settings.language') }}</span>
|
|
|
|
|
- <a-select v-model:value="lang" class="lang-select" @change="onLangChange">
|
|
|
|
|
- <a-select-option v-for="l in LanguageManager.supportedLanguages" :key="l.value"
|
|
|
|
|
- :value="l.value">
|
|
|
|
|
- <span :aria-label="l.name">{{ l.icon }}</span>
|
|
|
|
|
- <span>{{ l.name }}</span>
|
|
|
|
|
- </a-select-option>
|
|
|
|
|
- </a-select>
|
|
|
|
|
- </a-space>
|
|
|
|
|
- </template>
|
|
|
|
|
- <a-button shape="circle">
|
|
|
|
|
- <template #icon>
|
|
|
|
|
- <SettingOutlined />
|
|
|
|
|
- </template>
|
|
|
|
|
- </a-button>
|
|
|
|
|
- </a-popover>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <a-row justify="center">
|
|
|
|
|
- <a-col :span="24">
|
|
|
|
|
- <h2 class="login-title">
|
|
|
|
|
- <Transition name="headline" mode="out-in">
|
|
|
|
|
- <b :key="headlineIndex">{{ headlineWords[headlineIndex] }}</b>
|
|
|
|
|
- </Transition>
|
|
|
|
|
- </h2>
|
|
|
|
|
- </a-col>
|
|
|
|
|
- </a-row>
|
|
|
|
|
-
|
|
|
|
|
- <a-form layout="vertical" @submit.prevent="login">
|
|
|
|
|
- <a-form-item>
|
|
|
|
|
- <a-input v-model:value="user.username" autocomplete="username" name="username"
|
|
|
|
|
- :placeholder="t('username')" autofocus required>
|
|
|
|
|
- <template #prefix>
|
|
|
|
|
- <UserOutlined />
|
|
|
|
|
- </template>
|
|
|
|
|
- </a-input>
|
|
|
|
|
- </a-form-item>
|
|
|
|
|
-
|
|
|
|
|
- <a-form-item>
|
|
|
|
|
- <a-input-password v-model:value="user.password" autocomplete="current-password" name="password"
|
|
|
|
|
- :placeholder="t('password')" required>
|
|
|
|
|
- <template #prefix>
|
|
|
|
|
- <LockOutlined />
|
|
|
|
|
- </template>
|
|
|
|
|
- </a-input-password>
|
|
|
|
|
- </a-form-item>
|
|
|
|
|
-
|
|
|
|
|
- <a-form-item v-if="twoFactorEnable">
|
|
|
|
|
- <a-input v-model:value="user.twoFactorCode" autocomplete="one-time-code" name="twoFactorCode"
|
|
|
|
|
- :placeholder="t('twoFactorCode')" required>
|
|
|
|
|
- <template #prefix>
|
|
|
|
|
- <KeyOutlined />
|
|
|
|
|
- </template>
|
|
|
|
|
- </a-input>
|
|
|
|
|
- </a-form-item>
|
|
|
|
|
-
|
|
|
|
|
- <a-form-item>
|
|
|
|
|
- <a-row justify="center">
|
|
|
|
|
- <a-button type="primary" html-type="submit" :loading="submitting" block>
|
|
|
|
|
- {{ submitting ? '' : t('login') }}
|
|
|
|
|
- </a-button>
|
|
|
|
|
- </a-row>
|
|
|
|
|
- </a-form-item>
|
|
|
|
|
- </a-form>
|
|
|
|
|
|
|
+ <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>
|
|
</div>
|
|
|
- </a-col>
|
|
|
|
|
- </a-row>
|
|
|
|
|
|
|
+ <h2 class="welcome">
|
|
|
|
|
+ <Transition name="headline" mode="out-in">
|
|
|
|
|
+ <b :key="headlineIndex">{{ headlineWords[headlineIndex] }}</b>
|
|
|
|
|
+ </Transition>
|
|
|
|
|
+ </h2>
|
|
|
|
|
+
|
|
|
|
|
+ <a-form layout="vertical" class="login-form" @submit.prevent="login">
|
|
|
|
|
+ <a-form-item :label="t('username')">
|
|
|
|
|
+ <a-input v-model:value="user.username" autocomplete="username" name="username" size="large"
|
|
|
|
|
+ :placeholder="t('username')" autofocus required>
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <UserOutlined />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-input>
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <a-form-item :label="t('password')">
|
|
|
|
|
+ <a-input-password v-model:value="user.password" autocomplete="current-password" name="password"
|
|
|
|
|
+ size="large" :placeholder="t('password')" required>
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <LockOutlined />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-input-password>
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <a-form-item v-if="twoFactorEnable" :label="t('twoFactorCode')">
|
|
|
|
|
+ <a-input v-model:value="user.twoFactorCode" autocomplete="one-time-code" name="twoFactorCode"
|
|
|
|
|
+ size="large" :placeholder="t('twoFactorCode')" required>
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <KeyOutlined />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </a-input>
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+
|
|
|
|
|
+ <a-form-item class="submit-row">
|
|
|
|
|
+ <a-button type="primary" html-type="submit" :loading="submitting" size="large" block>
|
|
|
|
|
+ {{ submitting ? '' : t('login') }}
|
|
|
|
|
+ </a-button>
|
|
|
|
|
+ </a-form-item>
|
|
|
|
|
+ </a-form>
|
|
|
|
|
+
|
|
|
|
|
+ <div v-if="version" class="version">v{{ version }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</a-layout-content>
|
|
</a-layout-content>
|
|
|
</a-layout>
|
|
</a-layout>
|
|
|
</a-config-provider>
|
|
</a-config-provider>
|
|
@@ -173,178 +153,247 @@ function onLangChange(next) {
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
.login-app {
|
|
.login-app {
|
|
|
- --bg-page: #c7ebe2;
|
|
|
|
|
- --bg-wave-header: #dbf5ed;
|
|
|
|
|
|
|
+ --bg-page: #f5f7fa;
|
|
|
--bg-card: #ffffff;
|
|
--bg-card: #ffffff;
|
|
|
- --color-title: #008771;
|
|
|
|
|
- --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.09);
|
|
|
|
|
- --wave-fill: rgba(0, 135, 113, 0.12);
|
|
|
|
|
- --wave-fill-bottom: #c7ebe2;
|
|
|
|
|
|
|
+ --color-text: rgba(0, 0, 0, 0.88);
|
|
|
|
|
+ --color-text-subtle: rgba(0, 0, 0, 0.55);
|
|
|
|
|
+ --color-accent: #1677ff;
|
|
|
|
|
+ --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-wave-header: #0a1222;
|
|
|
|
|
- --bg-card: #151f31;
|
|
|
|
|
- --color-title: rgba(255, 255, 255, 0.92);
|
|
|
|
|
- --shadow-card: 0 4px 16px rgba(0, 0, 0, 0.45);
|
|
|
|
|
- --wave-fill: #222d42;
|
|
|
|
|
- --wave-fill-bottom: #222d42;
|
|
|
|
|
|
|
+ --bg-page: #1e1e1e;
|
|
|
|
|
+ --bg-card: #252526;
|
|
|
|
|
+ --color-text: rgba(255, 255, 255, 0.92);
|
|
|
|
|
+ --color-text-subtle: rgba(255, 255, 255, 0.55);
|
|
|
|
|
+ --color-accent: #4096ff;
|
|
|
|
|
+ --color-border: rgba(255, 255, 255, 0.08);
|
|
|
|
|
+ --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-wave-header: #0a2227;
|
|
|
|
|
- --bg-card: #0c0e12;
|
|
|
|
|
- --wave-fill: #1f4d52;
|
|
|
|
|
- --wave-fill-bottom: #0f2d32;
|
|
|
|
|
|
|
+ --bg-page: #000;
|
|
|
|
|
+ --bg-card: #141414;
|
|
|
|
|
+ --color-border: rgba(255, 255, 255, 0.06);
|
|
|
|
|
+ --blob-1: rgba(64, 150, 255, 0.22);
|
|
|
|
|
+ --blob-2: rgba(168, 85, 247, 0.18);
|
|
|
|
|
+ --blob-3: rgba(34, 211, 238, 0.12);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.login-app,
|
|
|
|
|
-.login-app :deep(.ant-layout-content) {
|
|
|
|
|
- background: transparent;
|
|
|
|
|
|
|
+/* 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 {
|
|
|
|
|
- background: var(--bg-page);
|
|
|
|
|
|
|
+.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-card {
|
|
|
|
|
- background: var(--bg-card);
|
|
|
|
|
- box-shadow: var(--shadow-card);
|
|
|
|
|
|
|
+.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-title {
|
|
|
|
|
- color: var(--color-title);
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.login-settings {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: flex-end;
|
|
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
|
|
+@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); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.settings-popover {
|
|
|
|
|
- min-width: 220px;
|
|
|
|
|
|
|
+@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); }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.lang-select {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
|
|
+@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 :deep(.ant-layout-content) {
|
|
|
|
|
+ background: transparent;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.login-content {
|
|
.login-content {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.login-row {
|
|
|
|
|
|
|
+.login-content > * {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
z-index: 1;
|
|
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-toolbar {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 16px;
|
|
|
|
|
+ right: 16px;
|
|
|
|
|
+ z-index: 10;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.login-loading {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 40px 0;
|
|
|
|
|
|
|
+.toolbar-btn {
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 40px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.login-title {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- margin-bottom: 32px;
|
|
|
|
|
- font-size: 2rem;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
- min-height: 2.5rem;
|
|
|
|
|
|
|
+.login-wrapper {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 24px 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.login-title b {
|
|
|
|
|
- display: inline-block;
|
|
|
|
|
|
|
+.login-loading {
|
|
|
|
|
+ text-align: center;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.headline-enter-active,
|
|
|
|
|
-.headline-leave-active {
|
|
|
|
|
- transition: opacity 0.4s ease, transform 0.4s ease;
|
|
|
|
|
|
|
+.login-card {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: 400px;
|
|
|
|
|
+ background: var(--bg-card);
|
|
|
|
|
+ border: 1px solid var(--color-border);
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 40px 32px 28px;
|
|
|
|
|
+ box-shadow: var(--shadow-card);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.headline-enter-from {
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- transform: translateY(-12px);
|
|
|
|
|
|
|
+@media (max-width: 480px) {
|
|
|
|
|
+ .login-card {
|
|
|
|
|
+ padding: 32px 20px 24px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.headline-leave-to {
|
|
|
|
|
- opacity: 0;
|
|
|
|
|
- transform: translateY(12px);
|
|
|
|
|
|
|
+.brand {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.waves-header {
|
|
|
|
|
- position: fixed;
|
|
|
|
|
- inset: 0 0 auto 0;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- z-index: 0;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
- background: var(--bg-wave-header);
|
|
|
|
|
|
|
+.brand-name {
|
|
|
|
|
+ font-size: 28px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 1.5px;
|
|
|
|
|
+ color: var(--color-text);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.waves-inner-header {
|
|
|
|
|
- height: 50vh;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
|
|
+.brand-accent {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: 40px;
|
|
|
|
|
+ height: 3px;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ background: var(--color-accent);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.waves {
|
|
|
|
|
- position: relative;
|
|
|
|
|
- display: block;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 15vh;
|
|
|
|
|
- min-height: 100px;
|
|
|
|
|
- max-height: 150px;
|
|
|
|
|
- margin-bottom: -8px;
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.parallax>use {
|
|
|
|
|
- fill: var(--wave-fill);
|
|
|
|
|
- animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
|
|
|
|
|
|
+.welcome b {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ font-weight: inherit;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.parallax>use:nth-child(1) {
|
|
|
|
|
- animation-delay: -2s;
|
|
|
|
|
- animation-duration: 4s;
|
|
|
|
|
- opacity: 0.2;
|
|
|
|
|
|
|
+.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);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.parallax>use:nth-child(2) {
|
|
|
|
|
- animation-delay: -3s;
|
|
|
|
|
- animation-duration: 7s;
|
|
|
|
|
- opacity: 0.4;
|
|
|
|
|
|
|
+.login-form :deep(.ant-form-item-label > label) {
|
|
|
|
|
+ color: var(--color-text);
|
|
|
|
|
+ font-weight: 500;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.parallax>use:nth-child(3) {
|
|
|
|
|
- animation-delay: -4s;
|
|
|
|
|
- animation-duration: 10s;
|
|
|
|
|
- opacity: 0.6;
|
|
|
|
|
|
|
+.submit-row {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.parallax>use:nth-child(4) {
|
|
|
|
|
- animation-delay: -5s;
|
|
|
|
|
- animation-duration: 13s;
|
|
|
|
|
- fill: var(--wave-fill-bottom);
|
|
|
|
|
- opacity: 1;
|
|
|
|
|
|
|
+.version {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: var(--color-text-subtle);
|
|
|
|
|
+ margin-top: 16px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-@keyframes move-forever {
|
|
|
|
|
- 0% {
|
|
|
|
|
- transform: translate3d(-90px, 0, 0);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.settings-popover {
|
|
|
|
|
+ min-width: 220px;
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- 100% {
|
|
|
|
|
- transform: translate3d(85px, 0, 0);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+.lang-select {
|
|
|
|
|
+ width: 100%;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|