mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 13:44:24 +00:00
Turnstile iframe (~300px min width) overflowed its container on mobile due to large login card padding and no overflow handling. Reduce mobile padding, center the widget wrapper, and use compact mode below 480px.
351 lines
14 KiB
HTML
351 lines
14 KiB
HTML
{{ template "page/head_start" .}}
|
|
{{ template "page/head_end" .}}
|
|
|
|
{{ template "page/body_start" .}}
|
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
|
<transition name="list" appear>
|
|
<a-layout-content class="under min-h-0">
|
|
<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" fill="rgba(0, 135, 113, 0.08)" />
|
|
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
|
|
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
|
|
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
|
|
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
|
<template v-if="!loadingStates.fetched">
|
|
<div class="text-center">
|
|
<a-spin size="large" />
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="setting-section">
|
|
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
|
placement="bottomRight" trigger="click">
|
|
<template slot="content">
|
|
<a-space direction="vertical" :size="10">
|
|
<a-theme-switch-login></a-theme-switch-login>
|
|
<span>{{ i18n "pages.settings.language" }}</span>
|
|
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
|
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
|
<span v-text="l.name"></span>
|
|
</a-select-option>
|
|
</a-select>
|
|
</a-space>
|
|
</template>
|
|
<a-button shape="circle" icon="setting"></a-button>
|
|
</a-popover>
|
|
</div>
|
|
<a-row type="flex" justify="center">
|
|
<a-col :style="{ width: '100%' }">
|
|
<h2 class="title headline zoom">
|
|
<span class="words-wrapper">
|
|
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
|
<b>{{ i18n "pages.login.title" }}</b>
|
|
</span>
|
|
</h2>
|
|
</a-col>
|
|
</a-row>
|
|
<a-row type="flex" justify="center">
|
|
<a-col span="24">
|
|
<a-tabs :active-key="activeTab" @change="switchTab" centered>
|
|
<a-tab-pane key="login" tab='{{ i18n "pages.login.loginTab" }}'>
|
|
<a-form @submit.prevent="doLogin">
|
|
<a-space direction="vertical" size="middle">
|
|
<a-form-item>
|
|
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
|
placeholder='{{ i18n "username" }}' autofocus required>
|
|
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
|
</a-input>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
|
placeholder='{{ i18n "password" }}' required>
|
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
|
</a-input-password>
|
|
</a-form-item>
|
|
<a-form-item v-if="twoFactorEnable">
|
|
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
|
placeholder='{{ i18n "twoFactorCode" }}' required>
|
|
<a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
|
|
</a-input>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<a-row justify="center" class="centered">
|
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
|
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
|
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
|
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
|
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
|
</a-button>
|
|
</div>
|
|
</a-row>
|
|
</a-form-item>
|
|
</a-space>
|
|
</a-form>
|
|
</a-tab-pane>
|
|
<a-tab-pane key="register" tab='{{ i18n "pages.login.registerTab" }}'>
|
|
<a-form @submit.prevent="doRegister">
|
|
<a-space direction="vertical" size="middle">
|
|
<a-form-item>
|
|
<a-input autocomplete="username" name="reg-username" v-model.trim="regUser.username"
|
|
placeholder='{{ i18n "username" }}' required>
|
|
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
|
</a-input>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<a-input-password autocomplete="new-password" name="reg-password" v-model.trim="regUser.password"
|
|
placeholder='{{ i18n "password" }}' required>
|
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
|
</a-input-password>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<a-input-password autocomplete="new-password" name="reg-confirm-password" v-model.trim="regUser.confirmPassword"
|
|
placeholder='{{ i18n "pages.login.confirmPassword" }}' required>
|
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
|
</a-input-password>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<div class="cf-turnstile-wrapper">
|
|
<div class="cf-turnstile" :data-sitekey="turnstileSiteKey" data-callback="onTurnstileCallback" :data-size="turnstileSize"></div>
|
|
</div>
|
|
</a-form-item>
|
|
<a-form-item>
|
|
<a-row justify="center" class="centered">
|
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
|
:style="loadingStates.registerSpinning ? 'width: 52px' : 'display: inline-block'">
|
|
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.registerSpinning"
|
|
:icon="loadingStates.registerSpinning ? 'poweroff' : undefined" html-type="submit">
|
|
[[ loadingStates.registerSpinning ? '' : '{{ i18n "pages.login.registerTab" }}' ]]
|
|
</a-button>
|
|
</div>
|
|
</a-row>
|
|
</a-form-item>
|
|
</a-space>
|
|
</a-form>
|
|
</a-tab-pane>
|
|
</a-tabs>
|
|
</a-col>
|
|
</a-row>
|
|
</template>
|
|
</a-col>
|
|
</a-row>
|
|
</a-layout-content>
|
|
</transition>
|
|
</a-layout>
|
|
{{template "page/body_scripts" .}}
|
|
{{template "component/aThemeSwitch" .}}
|
|
<script>
|
|
var turnstileToken = '';
|
|
|
|
function onTurnstileCallback(token) {
|
|
turnstileToken = token;
|
|
}
|
|
|
|
const app = new Vue({
|
|
delimiters: ['[[', ']]'],
|
|
el: '#app',
|
|
data: {
|
|
themeSwitcher,
|
|
loadingStates: { fetched: false, spinning: false, registerSpinning: false },
|
|
activeTab: 'login',
|
|
user: { username: "", password: "", twoFactorCode: "" },
|
|
regUser: { username: "", password: "", confirmPassword: "" },
|
|
twoFactorEnable: false,
|
|
lang: "",
|
|
animationStarted: false,
|
|
turnstileSiteKey: '',
|
|
},
|
|
async mounted() {
|
|
this.lang = LanguageManager.getLanguage();
|
|
this.twoFactorEnable = await this.getTwoFactorEnable();
|
|
this.turnstileSiteKey = await this.getTurnstileSiteKey();
|
|
if (this.turnstileSiteKey) {
|
|
this.loadTurnstileScript();
|
|
}
|
|
},
|
|
computed: {
|
|
turnstileSize() {
|
|
return window.innerWidth < 480 ? 'compact' : 'normal';
|
|
},
|
|
},
|
|
methods: {
|
|
switchTab(key) {
|
|
this.activeTab = key;
|
|
},
|
|
async doLogin() {
|
|
this.loadingStates.spinning = true;
|
|
const msg = await HttpUtil.post('/login', this.user);
|
|
if (msg.success) {
|
|
location.href = basePath + 'panel/';
|
|
}
|
|
this.loadingStates.spinning = false;
|
|
},
|
|
async doRegister() {
|
|
if (this.regUser.password !== this.regUser.confirmPassword) {
|
|
this.$message.error('{{ i18n "pages.login.passwordMismatch" }}');
|
|
return;
|
|
}
|
|
if (!turnstileToken) {
|
|
this.$message.error('{{ i18n "pages.login.turnstileRequired" }}');
|
|
return;
|
|
}
|
|
this.loadingStates.registerSpinning = true;
|
|
const msg = await HttpUtil.post('/register', {
|
|
username: this.regUser.username,
|
|
password: this.regUser.password,
|
|
turnstileToken: turnstileToken,
|
|
});
|
|
if (msg.success) {
|
|
this.$message.success('{{ i18n "pages.login.toasts.successRegister" }}');
|
|
this.activeTab = 'login';
|
|
this.user.username = this.regUser.username;
|
|
this.regUser = { username: "", password: "", confirmPassword: "" };
|
|
turnstileToken = '';
|
|
if (window.turnstile) {
|
|
turnstile.reset('.cf-turnstile');
|
|
}
|
|
}
|
|
this.loadingStates.registerSpinning = false;
|
|
},
|
|
loadTurnstileScript() {
|
|
const script = document.createElement('script');
|
|
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
|
|
script.async = true;
|
|
script.defer = true;
|
|
document.head.appendChild(script);
|
|
},
|
|
async getTurnstileSiteKey() {
|
|
const msg = await HttpUtil.post('/getTurnstileSiteKey');
|
|
if (msg.success) {
|
|
return msg.obj;
|
|
}
|
|
return '';
|
|
},
|
|
async getTwoFactorEnable() {
|
|
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
|
if (msg.success) {
|
|
this.twoFactorEnable = msg.obj;
|
|
this.loadingStates.fetched = true;
|
|
this.$nextTick(() => {
|
|
if (!this.animationStarted) {
|
|
this.animationStarted = true;
|
|
this.initHeadline();
|
|
}
|
|
});
|
|
return msg.obj;
|
|
}
|
|
},
|
|
initHeadline() {
|
|
const animationDelay = 2000;
|
|
const headlines = this.$el.querySelectorAll('.headline');
|
|
headlines.forEach((headline) => {
|
|
const first = headline.querySelector('.is-visible');
|
|
if (!first) return;
|
|
setTimeout(() => this.hideWord(first, animationDelay), animationDelay);
|
|
});
|
|
},
|
|
hideWord(word, delay) {
|
|
const nextWord = this.takeNext(word);
|
|
this.switchWord(word, nextWord);
|
|
setTimeout(() => this.hideWord(nextWord, delay), delay);
|
|
},
|
|
takeNext(word) {
|
|
return word.nextElementSibling || word.parentElement.firstElementChild;
|
|
},
|
|
switchWord(oldWord, newWord) {
|
|
oldWord.classList.remove('is-visible');
|
|
oldWord.classList.add('is-hidden');
|
|
newWord.classList.remove('is-hidden');
|
|
newWord.classList.add('is-visible');
|
|
}
|
|
},
|
|
});
|
|
|
|
const pm_input_selector = 'input.ant-input, textarea.ant-input';
|
|
const pm_strip_props = [
|
|
'background',
|
|
'background-color',
|
|
'background-image',
|
|
'color'
|
|
];
|
|
|
|
const pm_observed_forms = new WeakSet();
|
|
|
|
function pm_strip_inline(el) {
|
|
if (!el || el.nodeType !== 1 || !el.matches?.(pm_input_selector)) return;
|
|
|
|
let did_change = false;
|
|
for (const prop of pm_strip_props) {
|
|
if (el.style.getPropertyValue(prop)) {
|
|
el.style.removeProperty(prop);
|
|
did_change = true;
|
|
}
|
|
}
|
|
|
|
if (did_change && el.style.length === 0) {
|
|
el.removeAttribute('style');
|
|
}
|
|
}
|
|
|
|
function pm_attach_observer(form) {
|
|
if (pm_observed_forms.has(form)) return;
|
|
pm_observed_forms.add(form);
|
|
|
|
form.querySelectorAll(pm_input_selector).forEach(pm_strip_inline);
|
|
|
|
const pm_mo = new MutationObserver(mutations => {
|
|
for (const m of mutations) {
|
|
if (m.type === 'attributes') {
|
|
pm_strip_inline(m.target);
|
|
} else if (m.type === 'childList') {
|
|
for (const n of m.addedNodes) {
|
|
if (n.nodeType !== 1) continue;
|
|
if (n.matches?.(pm_input_selector)) pm_strip_inline(n);
|
|
n.querySelectorAll?.(pm_input_selector).forEach(pm_strip_inline);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
pm_mo.observe(form, {
|
|
attributes: true,
|
|
attributeFilter: ['style'],
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
|
|
function pm_init() {
|
|
document.querySelectorAll('form.ant-form').forEach(pm_attach_observer);
|
|
const pm_host = document.getElementById('login') || document.body;
|
|
const pm_wait_for_forms = new MutationObserver(mutations => {
|
|
for (const m of mutations) {
|
|
for (const n of m.addedNodes) {
|
|
if (n.nodeType !== 1) continue;
|
|
if (n.matches?.('form.ant-form')) pm_attach_observer(n);
|
|
n.querySelectorAll?.('form.ant-form').forEach(pm_attach_observer);
|
|
}
|
|
}
|
|
});
|
|
pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', pm_init, { once: true });
|
|
} else {
|
|
pm_init();
|
|
}
|
|
</script>
|
|
{{ template "page/body_end" .}}
|