add modal login/signup and unified UserMenu component for consistent user
This commit is contained in:
parent
3cfb936b54
commit
f8da7e84b6
@ -52,39 +52,23 @@
|
||||
</n-button>
|
||||
|
||||
<!-- 用户菜单 -->
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="userMenuOptions"
|
||||
@select="handleUserMenuSelect"
|
||||
>
|
||||
<n-button quaternary>
|
||||
<n-avatar
|
||||
round
|
||||
size="small"
|
||||
:src="user?.avatar"
|
||||
>
|
||||
{{ user?.username?.charAt(0).toUpperCase() }}
|
||||
</n-avatar>
|
||||
<span class="username">{{ user?.username }}</span>
|
||||
<Icon icon="tabler:chevron-down" />
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
<UserMenu />
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { NButton, NBreadcrumb, NBreadcrumbItem, NSpace, NDropdown, NAvatar, NInput, useMessage } from 'naive-ui'
|
||||
import { NButton, NBreadcrumb, NBreadcrumbItem, NSpace, NInput } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useAuthStore } from '../../shared/stores/auth'
|
||||
import { t } from '../../shared/i18n'
|
||||
import UserMenu from '../../shared/components/UserMenu.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 搜索相关状态
|
||||
@ -93,7 +77,6 @@ const searchQuery = ref('')
|
||||
// 从 localStorage 读取应用名称
|
||||
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
const isSystemUser = computed(() => authStore.user?.user_type === 'System User')
|
||||
|
||||
const breadcrumbItems = computed(() => {
|
||||
@ -148,43 +131,6 @@ const breadcrumbItems = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
const userMenuOptions = computed(() => {
|
||||
const options: any[] = [
|
||||
// 菜单管理和设置对所有用户可见
|
||||
{
|
||||
label: t('Menu Management'),
|
||||
key: 'menu-manager',
|
||||
icon: () => h(Icon, { icon: 'tabler:menu-2' })
|
||||
},
|
||||
{
|
||||
label: t('Settings'),
|
||||
key: 'settings',
|
||||
icon: () => h(Icon, { icon: 'tabler:settings' })
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: t('Logout'),
|
||||
key: 'logout',
|
||||
icon: () => h(Icon, { icon: 'tabler:logout' })
|
||||
}
|
||||
]
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const handleUserMenuSelect = async (key: string) => {
|
||||
if (key === 'logout') {
|
||||
await authStore.logout()
|
||||
message.success(t('Logged out'))
|
||||
router.push('/login')
|
||||
} else if (key === 'menu-manager') {
|
||||
router.push({ name: 'MenuManager' })
|
||||
} else if (key === 'settings') {
|
||||
router.push({ name: 'Settings' })
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理函数
|
||||
const handleSearch = () => {
|
||||
@ -224,10 +170,6 @@ const handleSearchClear = () => {
|
||||
min-width: 0; /* 允许flex子项收缩 */
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 搜索框响应式样式 */
|
||||
.search-input {
|
||||
|
||||
86
apps/jingrow/frontend/src/shared/components/UserMenu.vue
Normal file
86
apps/jingrow/frontend/src/shared/components/UserMenu.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<n-dropdown
|
||||
trigger="click"
|
||||
:options="userMenuOptions"
|
||||
@select="handleUserMenuSelect"
|
||||
>
|
||||
<n-button quaternary>
|
||||
<n-avatar
|
||||
round
|
||||
size="small"
|
||||
:src="user?.avatar"
|
||||
>
|
||||
{{ user?.username?.charAt(0).toUpperCase() }}
|
||||
</n-avatar>
|
||||
<span class="username">{{ user?.username }}</span>
|
||||
<Icon icon="tabler:chevron-down" />
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NButton, NDropdown, NAvatar, useMessage } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { t } from '../i18n'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const user = computed(() => authStore.user)
|
||||
|
||||
const userMenuOptions = computed(() => {
|
||||
const options: any[] = [
|
||||
{
|
||||
label: t('Menu Management'),
|
||||
key: 'menu-manager',
|
||||
icon: () => h(Icon, { icon: 'tabler:menu-2' })
|
||||
},
|
||||
{
|
||||
label: t('Settings'),
|
||||
key: 'settings',
|
||||
icon: () => h(Icon, { icon: 'tabler:settings' })
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: t('Logout'),
|
||||
key: 'logout',
|
||||
icon: () => h(Icon, { icon: 'tabler:logout' })
|
||||
}
|
||||
]
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const handleUserMenuSelect = async (key: string) => {
|
||||
if (key === 'logout') {
|
||||
await authStore.logout()
|
||||
message.success(t('Logged out'))
|
||||
router.push('/')
|
||||
} else if (key === 'menu-manager') {
|
||||
router.push({ name: 'MenuManager' })
|
||||
} else if (key === 'settings') {
|
||||
router.push({ name: 'Settings' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.username {
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 移动端隐藏用户名文字 */
|
||||
@media (max-width: 768px) {
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,19 +1,193 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NButton, NSpace, useMessage } from 'naive-ui'
|
||||
import { computed, ref, watch, onMounted, onUnmounted, nextTick, reactive } from 'vue'
|
||||
import { NButton, NSpace, useMessage, NModal, NForm, NFormItem, NInput, NText } from 'naive-ui'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useSEO } from '@/shared/composables/useSEO'
|
||||
import { t } from '@/shared/i18n'
|
||||
import { useAuthStore } from '@/shared/stores/auth'
|
||||
import { signupApi } from '@/shared/api/auth'
|
||||
import UserMenu from '@/shared/components/UserMenu.vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const authStore = useAuthStore()
|
||||
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
const logoUrl = computed(() => '/logo.svg')
|
||||
|
||||
const handleLogin = () => router.push('/login')
|
||||
const handleSignup = () => router.push('/signup')
|
||||
// 登录/注册弹窗状态
|
||||
const showLoginModal = ref(false)
|
||||
const showSignupModal = ref(false)
|
||||
const loginFormRef = ref()
|
||||
const signupFormRef = ref()
|
||||
const loginLoading = ref(false)
|
||||
const signupLoading = ref(false)
|
||||
const showSignupLink = ref(false)
|
||||
|
||||
// 登录表单
|
||||
const loginFormData = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loginRules = {
|
||||
username: [
|
||||
{ required: true, message: t('Please enter username'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('Please enter password'), trigger: 'blur' },
|
||||
{ min: 6, message: t('Password must be at least 6 characters'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 注册表单
|
||||
const signupFormData = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
email: '',
|
||||
phoneNumber: ''
|
||||
})
|
||||
|
||||
const validatePasswordMatch = (_rule: any, value: string) => {
|
||||
if (value !== signupFormData.password) {
|
||||
return new Error(t('Passwords do not match'))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const signupRules = {
|
||||
username: [
|
||||
{ required: true, message: t('Please enter username'), trigger: 'blur' },
|
||||
{ min: 3, message: t('Username must be at least 3 characters'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('Please enter password'), trigger: 'blur' },
|
||||
{ min: 6, message: t('Password must be at least 6 characters'), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: t('Please confirm password'), trigger: 'blur' },
|
||||
{ validator: validatePasswordMatch, trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{
|
||||
validator: (_rule: any, value: string) => {
|
||||
if (!value) return true
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
if (!emailRegex.test(value)) {
|
||||
return new Error(t('Please enter a valid email address'))
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
phoneNumber: [
|
||||
{ required: true, message: t('Please enter phone number'), trigger: 'blur' },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: t('Please enter a valid phone number'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const handleLogin = () => {
|
||||
showLoginModal.value = true
|
||||
}
|
||||
|
||||
const handleSignup = () => {
|
||||
showSignupModal.value = true
|
||||
}
|
||||
|
||||
const handleLoginSubmit = async () => {
|
||||
try {
|
||||
await loginFormRef.value?.validate()
|
||||
loginLoading.value = true
|
||||
|
||||
const result = await authStore.login(loginFormData.username, loginFormData.password)
|
||||
|
||||
if (result.success) {
|
||||
message.success(t('Login successful'))
|
||||
showLoginModal.value = false
|
||||
loginFormData.username = ''
|
||||
loginFormData.password = ''
|
||||
} else {
|
||||
message.error(result.error || t('Login failed'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error)
|
||||
message.error(t('Login failed, please check username and password'))
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSignupSubmit = async () => {
|
||||
try {
|
||||
await signupFormRef.value?.validate()
|
||||
signupLoading.value = true
|
||||
|
||||
const result = await signupApi({
|
||||
username: signupFormData.username,
|
||||
password: signupFormData.password,
|
||||
email: signupFormData.email || undefined,
|
||||
phone_number: signupFormData.phoneNumber
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
message.success(t('Sign up successful'))
|
||||
if (result.user) {
|
||||
authStore.user = result.user
|
||||
authStore.isAuthenticated = true
|
||||
localStorage.setItem('jingrow_user', JSON.stringify(result.user))
|
||||
localStorage.setItem('jingrow_authenticated', 'true')
|
||||
showSignupModal.value = false
|
||||
signupFormData.username = ''
|
||||
signupFormData.password = ''
|
||||
signupFormData.confirmPassword = ''
|
||||
signupFormData.email = ''
|
||||
signupFormData.phoneNumber = ''
|
||||
} else {
|
||||
const loginResult = await authStore.login(signupFormData.username, signupFormData.password)
|
||||
if (loginResult.success) {
|
||||
showSignupModal.value = false
|
||||
signupFormData.username = ''
|
||||
signupFormData.password = ''
|
||||
signupFormData.confirmPassword = ''
|
||||
signupFormData.email = ''
|
||||
signupFormData.phoneNumber = ''
|
||||
} else {
|
||||
message.warning(loginResult.error || t('注册成功,但自动登录失败,请手动登录'))
|
||||
showSignupModal.value = false
|
||||
showLoginModal.value = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const errorMsg = result.error || t('Sign up failed')
|
||||
console.error('注册失败:', errorMsg, result)
|
||||
message.error(errorMsg)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('注册异常:', error)
|
||||
message.error(error.message || t('Sign up failed, please try again'))
|
||||
} finally {
|
||||
signupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchToSignup = () => {
|
||||
showLoginModal.value = false
|
||||
showSignupModal.value = true
|
||||
}
|
||||
|
||||
const switchToLogin = () => {
|
||||
showSignupModal.value = false
|
||||
showLoginModal.value = true
|
||||
}
|
||||
|
||||
// 登录状态
|
||||
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
||||
|
||||
useSEO({
|
||||
title: t('Remove Background - Free AI Background Removal Tool'),
|
||||
@ -30,6 +204,8 @@ interface HistoryItem {
|
||||
}
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||
// urlInputRef 在模板中使用,lint 警告是误报
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const urlInputRef = ref<HTMLInputElement | null>(null)
|
||||
const uploadedImage = ref<File | null>(null)
|
||||
const uploadedImageUrl = ref<string>('')
|
||||
@ -497,9 +673,25 @@ const removeHistoryItem = (index: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('paste', handlePaste)
|
||||
|
||||
// 初始化认证状态
|
||||
await authStore.initAuth()
|
||||
|
||||
// 检查服务器配置,判断是否显示注册链接
|
||||
try {
|
||||
const response = await fetch('/jingrow/server-config')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.success && data.jingrow_server_url === 'https://cloud.jingrow.com') {
|
||||
showSignupLink.value = true
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get server config:', error)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -524,8 +716,20 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<n-space :size="12">
|
||||
<n-button quaternary @click="handleSignup">注册</n-button>
|
||||
<n-button type="primary" @click="handleLogin" class="login-btn">登录</n-button>
|
||||
<!-- 未登录状态:显示登录/注册按钮 -->
|
||||
<template v-if="!isLoggedIn">
|
||||
<n-button quaternary @click="handleSignup">注册</n-button>
|
||||
<n-button type="primary" @click="handleLogin" class="login-btn">登录</n-button>
|
||||
</template>
|
||||
<!-- 已登录状态:显示通知和用户菜单 -->
|
||||
<template v-else>
|
||||
<n-button quaternary circle>
|
||||
<template #icon>
|
||||
<Icon icon="tabler:bell" />
|
||||
</template>
|
||||
</n-button>
|
||||
<UserMenu />
|
||||
</template>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
@ -711,6 +915,182 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showLoginModal"
|
||||
preset="card"
|
||||
:title="appName"
|
||||
size="large"
|
||||
:bordered="false"
|
||||
:mask-closable="true"
|
||||
style="max-width: 500px;"
|
||||
class="auth-modal"
|
||||
>
|
||||
<n-form
|
||||
ref="loginFormRef"
|
||||
:model="loginFormData"
|
||||
:rules="loginRules"
|
||||
size="medium"
|
||||
:show-label="false"
|
||||
@keyup.enter="handleLoginSubmit"
|
||||
>
|
||||
<n-form-item path="username">
|
||||
<n-input
|
||||
v-model:value="loginFormData.username"
|
||||
:placeholder="t('Username')"
|
||||
:input-props="{ autocomplete: 'username' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:user" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="loginFormData.password"
|
||||
type="password"
|
||||
:placeholder="t('Password')"
|
||||
:input-props="{ autocomplete: 'current-password' }"
|
||||
show-password-on="click"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:lock" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:loading="loginLoading"
|
||||
@click="handleLoginSubmit"
|
||||
class="brand-button"
|
||||
>
|
||||
{{ t('Login') }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div class="auth-footer" v-if="showSignupLink">
|
||||
<n-text depth="3">
|
||||
{{ t("Don't have an account?") }}
|
||||
<a href="javascript:void(0)" class="auth-link" @click="switchToSignup">
|
||||
{{ t('Sign up') }}
|
||||
</a>
|
||||
</n-text>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<!-- 注册弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showSignupModal"
|
||||
preset="card"
|
||||
:title="appName"
|
||||
size="large"
|
||||
:bordered="false"
|
||||
:mask-closable="true"
|
||||
style="max-width: 500px;"
|
||||
class="auth-modal"
|
||||
>
|
||||
<n-form
|
||||
ref="signupFormRef"
|
||||
:model="signupFormData"
|
||||
:rules="signupRules"
|
||||
size="medium"
|
||||
:show-label="false"
|
||||
@keyup.enter="handleSignupSubmit"
|
||||
>
|
||||
<n-form-item path="username">
|
||||
<n-input
|
||||
v-model:value="signupFormData.username"
|
||||
:placeholder="t('Username')"
|
||||
:input-props="{ autocomplete: 'username' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:user" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="password">
|
||||
<n-input
|
||||
v-model:value="signupFormData.password"
|
||||
type="password"
|
||||
:placeholder="t('Password')"
|
||||
:input-props="{ autocomplete: 'new-password' }"
|
||||
show-password-on="click"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:lock" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="confirmPassword">
|
||||
<n-input
|
||||
v-model:value="signupFormData.confirmPassword"
|
||||
type="password"
|
||||
:placeholder="t('Confirm Password')"
|
||||
:input-props="{ autocomplete: 'new-password' }"
|
||||
show-password-on="click"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:lock" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="email">
|
||||
<n-input
|
||||
v-model:value="signupFormData.email"
|
||||
:placeholder="t('Email (Optional)')"
|
||||
:input-props="{ autocomplete: 'email', type: 'email' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:mail" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="phoneNumber">
|
||||
<n-input
|
||||
v-model:value="signupFormData.phoneNumber"
|
||||
:placeholder="t('Phone Number')"
|
||||
:input-props="{ autocomplete: 'tel' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="tabler:phone" />
|
||||
</template>
|
||||
</n-input>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item>
|
||||
<n-button
|
||||
type="primary"
|
||||
size="medium"
|
||||
block
|
||||
:loading="signupLoading"
|
||||
@click="handleSignupSubmit"
|
||||
class="brand-button"
|
||||
>
|
||||
{{ t('Sign up') }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<n-text depth="3">
|
||||
{{ t('Already have an account?') }}
|
||||
<a href="javascript:void(0)" class="auth-link" @click="switchToLogin">
|
||||
{{ t('Login') }}
|
||||
</a>
|
||||
</n-text>
|
||||
</div>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1649,4 +2029,100 @@ onUnmounted(() => {
|
||||
padding: 40px 0 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 登录/注册弹窗样式 */
|
||||
:deep(.auth-modal) {
|
||||
.n-card {
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.n-card-header {
|
||||
text-align: center;
|
||||
padding: 24px 24px 16px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.n-card__content {
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.n-form-item {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.n-form-item:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.n-form-item:not(.n-form-item--error) .n-form-item__feedback-wrapper {
|
||||
min-height: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
.n-form-item--error .n-form-item__feedback-wrapper {
|
||||
margin-top: 4px !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.auth-link {
|
||||
color: #1fc76f;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
transition: color 0.2s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1fc76f;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.brand-button) {
|
||||
background: #e6f8f0 !important;
|
||||
border: 1px solid #1fc76f !important;
|
||||
color: #0d684b !important;
|
||||
}
|
||||
|
||||
:deep(.brand-button .n-button__border),
|
||||
:deep(.brand-button .n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.brand-button:hover) {
|
||||
background: #dcfce7 !important;
|
||||
border-color: #1fc76f !important;
|
||||
color: #166534 !important;
|
||||
box-shadow: 0 2px 8px rgba(31, 199, 111, 0.15) !important;
|
||||
}
|
||||
|
||||
:deep(.brand-button:hover .n-button__border),
|
||||
:deep(.brand-button:hover .n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
:deep(.brand-button:active) {
|
||||
background: #1fc76f !important;
|
||||
border-color: #1fc76f !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 1px 4px rgba(31, 199, 111, 0.2) !important;
|
||||
}
|
||||
|
||||
:deep(.brand-button:active .n-button__border),
|
||||
:deep(.brand-button:active .n-button__state-border) {
|
||||
border: none !important;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user