2025-06-21 08:38:43 +00:00
|
|
|
{{ template "page/head_start" .}}
|
2026-04-02 19:01:18 +00:00
|
|
|
<script data-cfasync="false" src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
2025-06-21 08:38:43 +00:00
|
|
|
{{ template "page/head_end" .}}
|
|
|
|
|
|
|
|
|
|
{{ template "page/body_start" .}}
|
2025-09-12 16:46:20 +00:00
|
|
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
2025-06-21 08:38:43 +00:00
|
|
|
<transition name="list" appear>
|
2025-09-24 18:41:32 +00:00
|
|
|
<a-layout-content class="under min-h-0">
|
2025-06-21 08:38:43 +00:00
|
|
|
<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>
|
2025-09-24 18:41:32 +00:00
|
|
|
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
|
2025-09-12 16:46:20 +00:00
|
|
|
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
2025-09-04 10:11:52 +00:00
|
|
|
<template v-if="!loadingStates.fetched">
|
2025-09-12 16:46:20 +00:00
|
|
|
<div class="text-center">
|
2025-09-04 10:11:52 +00:00
|
|
|
<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>
|
2025-09-24 18:41:32 +00:00
|
|
|
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
|
|
|
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
2025-09-04 10:11:52 +00:00
|
|
|
<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">
|
2026-04-02 12:18:48 +00:00
|
|
|
<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-space>
|
2026-04-07 09:32:57 +00:00
|
|
|
<a-form-item style="display:flex; justify-content:center; margin-top: 16px;">
|
|
|
|
|
<div class="cf-turnstile-wrapper" style="display:inline-flex; max-width:100%; overflow-x:auto;">
|
|
|
|
|
<div class="cf-turnstile" id="turnstile-widget"></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>
|
2026-04-02 12:18:48 +00:00
|
|
|
</a-form>
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
</a-tabs>
|
2025-09-04 10:11:52 +00:00
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
</template>
|
2025-06-21 08:38:43 +00:00
|
|
|
</a-col>
|
|
|
|
|
</a-row>
|
|
|
|
|
</a-layout-content>
|
|
|
|
|
</transition>
|
|
|
|
|
</a-layout>
|
|
|
|
|
{{template "page/body_scripts" .}}
|
|
|
|
|
{{template "component/aThemeSwitch" .}}
|
|
|
|
|
<script>
|
2026-04-02 12:18:48 +00:00
|
|
|
var turnstileToken = '';
|
2026-04-04 18:55:28 +00:00
|
|
|
var turnstileWidgetId = null;
|
2026-04-08 07:18:00 +00:00
|
|
|
var turnstileContainer = null;
|
2026-04-02 12:18:48 +00:00
|
|
|
|
2025-06-21 08:38:43 +00:00
|
|
|
const app = new Vue({
|
|
|
|
|
delimiters: ['[[', ']]'],
|
|
|
|
|
el: '#app',
|
|
|
|
|
data: {
|
|
|
|
|
themeSwitcher,
|
2026-04-02 12:18:48 +00:00
|
|
|
loadingStates: { fetched: false, spinning: false, registerSpinning: false },
|
|
|
|
|
activeTab: 'login',
|
2025-09-25 13:16:50 +00:00
|
|
|
user: { username: "", password: "", twoFactorCode: "" },
|
2026-04-02 12:18:48 +00:00
|
|
|
regUser: { username: "", password: "", confirmPassword: "" },
|
2025-06-21 08:38:43 +00:00
|
|
|
twoFactorEnable: false,
|
2025-09-25 13:16:50 +00:00
|
|
|
lang: "",
|
2026-04-02 12:18:48 +00:00
|
|
|
animationStarted: false,
|
|
|
|
|
turnstileSiteKey: '',
|
2025-06-21 08:38:43 +00:00
|
|
|
},
|
|
|
|
|
async mounted() {
|
|
|
|
|
this.lang = LanguageManager.getLanguage();
|
2026-04-06 13:00:02 +00:00
|
|
|
try {
|
|
|
|
|
this.twoFactorEnable = await this.getTwoFactorEnable();
|
|
|
|
|
this.turnstileSiteKey = await this.getTurnstileSiteKey();
|
|
|
|
|
} finally {
|
|
|
|
|
this.loadingStates.fetched = true;
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
if (!this.animationStarted) {
|
|
|
|
|
this.animationStarted = true;
|
|
|
|
|
this.initHeadline();
|
|
|
|
|
}
|
2026-04-08 07:34:50 +00:00
|
|
|
if (this.turnstileSiteKey && this.activeTab === 'register') {
|
2026-04-06 13:00:02 +00:00
|
|
|
this.ensureTurnstileRendered();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-02 12:18:48 +00:00
|
|
|
}
|
2025-06-21 08:38:43 +00:00
|
|
|
},
|
2026-04-02 16:30:55 +00:00
|
|
|
computed: {
|
|
|
|
|
turnstileSize() {
|
|
|
|
|
return window.innerWidth < 480 ? 'compact' : 'normal';
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-06-21 08:38:43 +00:00
|
|
|
methods: {
|
2026-04-02 12:18:48 +00:00
|
|
|
switchTab(key) {
|
2026-04-08 07:34:50 +00:00
|
|
|
if (key !== 'register') {
|
|
|
|
|
this.destroyTurnstile();
|
|
|
|
|
}
|
2026-04-02 12:18:48 +00:00
|
|
|
this.activeTab = key;
|
2026-04-04 18:55:28 +00:00
|
|
|
if (key === 'register') {
|
|
|
|
|
this.$nextTick(() => this.ensureTurnstileRendered());
|
|
|
|
|
}
|
2026-04-02 12:18:48 +00:00
|
|
|
},
|
2026-04-08 07:34:50 +00:00
|
|
|
destroyTurnstile() {
|
|
|
|
|
turnstileToken = '';
|
|
|
|
|
turnstileContainer = null;
|
|
|
|
|
if (window.turnstile && turnstileWidgetId !== null) {
|
|
|
|
|
turnstile.remove(turnstileWidgetId);
|
|
|
|
|
}
|
|
|
|
|
turnstileWidgetId = null;
|
|
|
|
|
},
|
2026-04-02 12:18:48 +00:00
|
|
|
async doLogin() {
|
2025-09-04 10:11:52 +00:00
|
|
|
this.loadingStates.spinning = true;
|
2025-06-21 08:38:43 +00:00
|
|
|
const msg = await HttpUtil.post('/login', this.user);
|
|
|
|
|
if (msg.success) {
|
|
|
|
|
location.href = basePath + 'panel/';
|
|
|
|
|
}
|
2025-09-04 10:11:52 +00:00
|
|
|
this.loadingStates.spinning = false;
|
2024-04-20 18:45:36 +00:00
|
|
|
},
|
2026-04-02 12:18:48 +00:00
|
|
|
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: "" };
|
2026-04-08 07:34:50 +00:00
|
|
|
this.destroyTurnstile();
|
2026-04-02 12:18:48 +00:00
|
|
|
}
|
|
|
|
|
this.loadingStates.registerSpinning = false;
|
|
|
|
|
},
|
2026-04-08 07:34:50 +00:00
|
|
|
isTurnstileContainerVisible(container) {
|
|
|
|
|
var pane = container.closest('.ant-tabs-tabpane');
|
|
|
|
|
if (!pane) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
var paneStyle = window.getComputedStyle(pane);
|
|
|
|
|
return paneStyle.display !== 'none' && paneStyle.visibility !== 'hidden';
|
|
|
|
|
},
|
2026-04-04 18:55:28 +00:00
|
|
|
ensureTurnstileRendered(retries = 20) {
|
2026-04-08 07:18:00 +00:00
|
|
|
if (!this.turnstileSiteKey) {
|
2026-04-04 18:55:28 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var container = document.getElementById('turnstile-widget');
|
|
|
|
|
if (!container || !window.turnstile) {
|
|
|
|
|
if (retries > 0) {
|
|
|
|
|
setTimeout(() => this.ensureTurnstileRendered(retries - 1), 150);
|
2026-04-02 19:01:18 +00:00
|
|
|
}
|
2026-04-04 18:55:28 +00:00
|
|
|
return;
|
2026-04-02 18:46:20 +00:00
|
|
|
}
|
2026-04-08 07:34:50 +00:00
|
|
|
if (!this.isTurnstileContainerVisible(container)) {
|
|
|
|
|
if (retries > 0) {
|
|
|
|
|
setTimeout(() => this.ensureTurnstileRendered(retries - 1), 150);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-08 07:18:00 +00:00
|
|
|
if (
|
|
|
|
|
turnstileWidgetId !== null &&
|
|
|
|
|
(turnstileContainer !== container || container.childElementCount === 0)
|
|
|
|
|
) {
|
|
|
|
|
turnstile.remove(turnstileWidgetId);
|
|
|
|
|
turnstileWidgetId = null;
|
|
|
|
|
turnstileContainer = null;
|
|
|
|
|
turnstileToken = '';
|
|
|
|
|
}
|
|
|
|
|
if (turnstileWidgetId !== null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
turnstileContainer = container;
|
2026-04-04 18:55:28 +00:00
|
|
|
turnstileWidgetId = turnstile.render(container, {
|
|
|
|
|
sitekey: this.turnstileSiteKey,
|
|
|
|
|
callback: function(token) { turnstileToken = token; },
|
2026-04-08 07:18:00 +00:00
|
|
|
'expired-callback': function() { turnstileToken = ''; },
|
|
|
|
|
'error-callback': function() { turnstileToken = ''; },
|
2026-04-04 18:55:28 +00:00
|
|
|
size: this.turnstileSize,
|
|
|
|
|
});
|
2026-04-02 12:18:48 +00:00
|
|
|
},
|
|
|
|
|
async getTurnstileSiteKey() {
|
|
|
|
|
const msg = await HttpUtil.post('/getTurnstileSiteKey');
|
|
|
|
|
if (msg.success) {
|
|
|
|
|
return msg.obj;
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
},
|
2025-06-21 08:38:43 +00:00
|
|
|
async getTwoFactorEnable() {
|
|
|
|
|
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
|
|
|
|
if (msg.success) {
|
|
|
|
|
this.twoFactorEnable = msg.obj;
|
|
|
|
|
return msg.obj;
|
|
|
|
|
}
|
2026-04-06 13:00:02 +00:00
|
|
|
return false;
|
2025-04-01 12:24:03 +00:00
|
|
|
},
|
2025-09-25 13:16:50 +00:00
|
|
|
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');
|
|
|
|
|
}
|
2025-06-21 08:38:43 +00:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-17 11:47:01 +00:00
|
|
|
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();
|
|
|
|
|
}
|
2025-06-21 08:38:43 +00:00
|
|
|
</script>
|
2025-09-25 13:16:50 +00:00
|
|
|
{{ template "page/body_end" .}}
|