mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
fix: use native login inputs
This commit is contained in:
@@ -285,6 +285,8 @@ export default {
|
|||||||
copyright: '© {year} MoviePilot',
|
copyright: '© {year} MoviePilot',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
|
showPassword: 'Show password',
|
||||||
|
hidePassword: 'Hide password',
|
||||||
otpCode: 'Verification Code',
|
otpCode: 'Verification Code',
|
||||||
stayLoggedIn: 'Stay Logged In',
|
stayLoggedIn: 'Stay Logged In',
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ export default {
|
|||||||
copyright: '© {year} MoviePilot',
|
copyright: '© {year} MoviePilot',
|
||||||
username: '用户名',
|
username: '用户名',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
|
showPassword: '显示密码',
|
||||||
|
hidePassword: '隐藏密码',
|
||||||
otpCode: '验证码',
|
otpCode: '验证码',
|
||||||
stayLoggedIn: '保持登录',
|
stayLoggedIn: '保持登录',
|
||||||
login: '登录',
|
login: '登录',
|
||||||
|
|||||||
@@ -282,6 +282,8 @@ export default {
|
|||||||
copyright: '© {year} MoviePilot',
|
copyright: '© {year} MoviePilot',
|
||||||
username: '用戶名',
|
username: '用戶名',
|
||||||
password: '密碼',
|
password: '密碼',
|
||||||
|
showPassword: '顯示密碼',
|
||||||
|
hidePassword: '隱藏密碼',
|
||||||
otpCode: '驗證碼',
|
otpCode: '驗證碼',
|
||||||
stayLoggedIn: '保持登錄',
|
stayLoggedIn: '保持登錄',
|
||||||
login: '登錄',
|
login: '登錄',
|
||||||
|
|||||||
+130
-80
@@ -1,9 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { VForm } from 'vuetify/components/VForm'
|
|
||||||
import { useAuthStore, useUserStore } from '@/stores'
|
import { useAuthStore, useUserStore } from '@/stores'
|
||||||
import { authState, userState } from '@/stores/types'
|
import { authState, userState } from '@/stores/types'
|
||||||
import { requiredValidator } from '@/@validators'
|
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import logo from '@images/logo.png'
|
import logo from '@images/logo.png'
|
||||||
@@ -43,7 +41,7 @@ const form = ref({
|
|||||||
remember: true,
|
remember: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const refForm = ref<InstanceType<typeof VForm> | null>(null)
|
const refForm = ref<HTMLFormElement | null>(null)
|
||||||
|
|
||||||
// 密码输入
|
// 密码输入
|
||||||
const isPasswordVisible = ref(false)
|
const isPasswordVisible = ref(false)
|
||||||
@@ -61,12 +59,6 @@ const mfaDialog = ref(false)
|
|||||||
const mfaPasskeyLoading = ref(false)
|
const mfaPasskeyLoading = ref(false)
|
||||||
let mfaDialogController: ReturnType<typeof openSharedDialog> | null = null
|
let mfaDialogController: ReturnType<typeof openSharedDialog> | null = null
|
||||||
|
|
||||||
// 用户名称输入框
|
|
||||||
const usernameInput = ref()
|
|
||||||
|
|
||||||
// 登录自动填充同步的延时任务
|
|
||||||
const autofillSyncTimerIds: number[] = []
|
|
||||||
|
|
||||||
// 语言选择菜单
|
// 语言选择菜单
|
||||||
const langMenu = ref(false)
|
const langMenu = ref(false)
|
||||||
|
|
||||||
@@ -133,8 +125,7 @@ const showPasskeyLogin = computed(() => !!systemPasskeyProvider.value?.enabled)
|
|||||||
|
|
||||||
// 获取登录表单中的原生账号和密码输入框。
|
// 获取登录表单中的原生账号和密码输入框。
|
||||||
function getLoginCredentialInputs() {
|
function getLoginCredentialInputs() {
|
||||||
const formElement = refForm.value?.$el as HTMLFormElement | undefined
|
const root = refForm.value || document
|
||||||
const root = formElement || document
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
username: root.querySelector<HTMLInputElement>('input[name="username"]'),
|
username: root.querySelector<HTMLInputElement>('input[name="username"]'),
|
||||||
@@ -155,21 +146,6 @@ function syncLoginCredentialValues() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理尚未执行的登录自动填充同步任务。
|
|
||||||
function clearLoginAutofillSyncTimers() {
|
|
||||||
autofillSyncTimerIds.forEach(timerId => window.clearTimeout(timerId))
|
|
||||||
autofillSyncTimerIds.length = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 延迟多次同步自动填充值,兼容浏览器或密码管理器不派发事件的自动填充。
|
|
||||||
function scheduleLoginAutofillSync() {
|
|
||||||
clearLoginAutofillSyncTimers()
|
|
||||||
syncLoginCredentialValues()
|
|
||||||
autofillSyncTimerIds.push(window.setTimeout(syncLoginCredentialValues, 50))
|
|
||||||
autofillSyncTimerIds.push(window.setTimeout(syncLoginCredentialValues, 250))
|
|
||||||
autofillSyncTimerIds.push(window.setTimeout(syncLoginCredentialValues, 1000))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 MFA 共享弹窗使用的最新 props。
|
// 生成 MFA 共享弹窗使用的最新 props。
|
||||||
function getMfaDialogProps() {
|
function getMfaDialogProps() {
|
||||||
return {
|
return {
|
||||||
@@ -671,10 +647,6 @@ onMounted(async () => {
|
|||||||
// 加载系统和插件声明的未登录认证入口
|
// 加载系统和插件声明的未登录认证入口
|
||||||
await loadAuthProviders()
|
await loadAuthProviders()
|
||||||
|
|
||||||
// 捕获页面加载后由浏览器或密码管理器写入的静默自动填充值。
|
|
||||||
await nextTick()
|
|
||||||
scheduleLoginAutofillSync()
|
|
||||||
|
|
||||||
// 初始化 Conditional UI 的 PassKey 自动填充
|
// 初始化 Conditional UI 的 PassKey 自动填充
|
||||||
await initConditionalPasskey()
|
await initConditionalPasskey()
|
||||||
})
|
})
|
||||||
@@ -718,7 +690,6 @@ onUnmounted(() => {
|
|||||||
manualAbortController.abort()
|
manualAbortController.abort()
|
||||||
manualAbortController = null
|
manualAbortController = null
|
||||||
}
|
}
|
||||||
clearLoginAutofillSyncTimers()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -777,48 +748,59 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VCardText class="login-body">
|
<VCardText class="login-body">
|
||||||
<VForm ref="refForm" autocomplete="on" @submit.prevent="login">
|
<form
|
||||||
|
ref="refForm"
|
||||||
|
class="login-form"
|
||||||
|
method="post"
|
||||||
|
action="/login/access-token"
|
||||||
|
autocomplete="on"
|
||||||
|
@submit.prevent="login"
|
||||||
|
>
|
||||||
<VRow>
|
<VRow>
|
||||||
<!-- username -->
|
<!-- username -->
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VTextField
|
<div class="native-login-field login-input">
|
||||||
ref="usernameInput"
|
<VIcon icon="mdi-account-outline" class="native-login-field__icon" aria-hidden="true" />
|
||||||
v-model="form.username"
|
<input
|
||||||
:label="t('login.username')"
|
id="username"
|
||||||
type="text"
|
v-model="form.username"
|
||||||
name="username"
|
class="native-login-field__input"
|
||||||
id="username"
|
type="text"
|
||||||
autocomplete="username"
|
name="username"
|
||||||
:rules="[requiredValidator]"
|
autocomplete="username"
|
||||||
hide-details
|
autocapitalize="none"
|
||||||
variant="outlined"
|
spellcheck="false"
|
||||||
density="comfortable"
|
enterkeyhint="next"
|
||||||
prepend-inner-icon="mdi-account-outline"
|
:placeholder="t('login.username')"
|
||||||
class="login-input"
|
:aria-label="t('login.username')"
|
||||||
@input="scheduleLoginAutofillSync"
|
required
|
||||||
@change="scheduleLoginAutofillSync"
|
/>
|
||||||
/>
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
<!-- password -->
|
<!-- password -->
|
||||||
<VCol cols="12">
|
<VCol cols="12">
|
||||||
<VTextField
|
<div class="native-login-field native-login-field--password login-input">
|
||||||
v-model="form.password"
|
<VIcon icon="mdi-lock-outline" class="native-login-field__icon" aria-hidden="true" />
|
||||||
:label="t('login.password')"
|
<input
|
||||||
:type="isPasswordVisible ? 'text' : 'password'"
|
id="password"
|
||||||
name="password"
|
v-model="form.password"
|
||||||
id="password"
|
class="native-login-field__input"
|
||||||
autocomplete="current-password"
|
:type="isPasswordVisible ? 'text' : 'password'"
|
||||||
prepend-inner-icon="mdi-lock-outline"
|
name="password"
|
||||||
:append-inner-icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
autocomplete="current-password"
|
||||||
:rules="[requiredValidator]"
|
:placeholder="t('login.password')"
|
||||||
hide-details
|
:aria-label="t('login.password')"
|
||||||
variant="outlined"
|
required
|
||||||
density="comfortable"
|
/>
|
||||||
class="login-input"
|
<button
|
||||||
@click:append-inner="isPasswordVisible = !isPasswordVisible"
|
class="native-login-field__toggle"
|
||||||
@input="scheduleLoginAutofillSync"
|
type="button"
|
||||||
@change="scheduleLoginAutofillSync"
|
:aria-label="isPasswordVisible ? t('login.hidePassword') : t('login.showPassword')"
|
||||||
/>
|
@click="isPasswordVisible = !isPasswordVisible"
|
||||||
|
>
|
||||||
|
<VIcon :icon="isPasswordVisible ? 'mdi-eye-off-outline' : 'mdi-eye-outline'" size="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol cols="12" class="py-0">
|
<VCol cols="12" class="py-0">
|
||||||
<!-- remember me checkbox -->
|
<!-- remember me checkbox -->
|
||||||
@@ -876,7 +858,7 @@ onUnmounted(() => {
|
|||||||
</VAlert>
|
</VAlert>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</form>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<!-- 卡片页脚:版权 + 版本 -->
|
<!-- 卡片页脚:版权 + 版本 -->
|
||||||
@@ -1167,24 +1149,92 @@ onUnmounted(() => {
|
|||||||
padding-block: 8px !important;
|
padding-block: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框增强样式 */
|
/* 原生登录输入框:保留标准 input DOM,便于密码管理器识别。 */
|
||||||
:deep(.login-input .v-field) {
|
.native-login-field {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(var(--v-border-color), 0.38);
|
||||||
|
min-block-size: 56px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
background: transparent;
|
||||||
transition:
|
transition:
|
||||||
box-shadow 200ms ease,
|
border-color 150ms ease,
|
||||||
border-color 200ms ease;
|
box-shadow 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框聚焦时增加主色光晕 */
|
.native-login-field:focus-within {
|
||||||
:deep(.login-body .v-field.v-field--focused) {
|
border-color: rgb(var(--v-theme-primary));
|
||||||
box-shadow:
|
box-shadow: inset 0 0 0 1px rgb(var(--v-theme-primary));
|
||||||
0 0 0 3px rgba(var(--v-theme-primary), 0.12),
|
|
||||||
0 4px 12px rgba(var(--v-theme-primary), 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入框悬停 */
|
.native-login-field__icon {
|
||||||
:deep(.login-input .v-field:hover:not(.v-field--focused)) {
|
position: absolute;
|
||||||
border-color: rgba(var(--v-theme-primary), 0.4);
|
z-index: 1;
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
inset-block-start: 16px;
|
||||||
|
inset-inline-start: 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-field__input {
|
||||||
|
display: block;
|
||||||
|
border: 0;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
block-size: 54px;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
font: inherit;
|
||||||
|
inline-size: 100%;
|
||||||
|
line-height: 1.5;
|
||||||
|
outline: none;
|
||||||
|
padding-block: 0;
|
||||||
|
padding-inline: 48px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-field__input::placeholder {
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-field--password .native-login-field__input {
|
||||||
|
padding-inline-end: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-field__input:-webkit-autofill,
|
||||||
|
.native-login-field__input:-webkit-autofill:hover,
|
||||||
|
.native-login-field__input:-webkit-autofill:focus {
|
||||||
|
-webkit-text-fill-color: rgb(var(--v-theme-on-surface));
|
||||||
|
caret-color: rgb(var(--v-theme-on-surface));
|
||||||
|
transition: background-color 9999s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-field__toggle {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: transparent;
|
||||||
|
block-size: 40px;
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
cursor: pointer;
|
||||||
|
inline-size: 40px;
|
||||||
|
inset-block-start: 8px;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
padding: 0;
|
||||||
|
transition:
|
||||||
|
background 150ms ease,
|
||||||
|
color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.native-login-field__toggle:hover,
|
||||||
|
.native-login-field__toggle:focus-visible {
|
||||||
|
background: rgba(var(--v-theme-on-surface), 0.08);
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remember me 复选框样式优化 */
|
/* Remember me 复选框样式优化 */
|
||||||
|
|||||||
Reference in New Issue
Block a user