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-button>
|
||||||
|
|
||||||
<!-- 用户菜单 -->
|
<!-- 用户菜单 -->
|
||||||
<n-dropdown
|
<UserMenu />
|
||||||
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>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, h, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
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 { Icon } from '@iconify/vue'
|
||||||
import { useAuthStore } from '../../shared/stores/auth'
|
import { useAuthStore } from '../../shared/stores/auth'
|
||||||
import { t } from '../../shared/i18n'
|
import { t } from '../../shared/i18n'
|
||||||
|
import UserMenu from '../../shared/components/UserMenu.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const message = useMessage()
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
// 搜索相关状态
|
// 搜索相关状态
|
||||||
@ -93,7 +77,6 @@ const searchQuery = ref('')
|
|||||||
// 从 localStorage 读取应用名称
|
// 从 localStorage 读取应用名称
|
||||||
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
||||||
|
|
||||||
const user = computed(() => authStore.user)
|
|
||||||
const isSystemUser = computed(() => authStore.user?.user_type === 'System User')
|
const isSystemUser = computed(() => authStore.user?.user_type === 'System User')
|
||||||
|
|
||||||
const breadcrumbItems = computed(() => {
|
const breadcrumbItems = computed(() => {
|
||||||
@ -148,43 +131,6 @@ const breadcrumbItems = computed(() => {
|
|||||||
return items
|
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 = () => {
|
const handleSearch = () => {
|
||||||
@ -224,10 +170,6 @@ const handleSearchClear = () => {
|
|||||||
min-width: 0; /* 允许flex子项收缩 */
|
min-width: 0; /* 允许flex子项收缩 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
|
||||||
margin-left: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 搜索框响应式样式 */
|
/* 搜索框响应式样式 */
|
||||||
.search-input {
|
.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">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
import { computed, ref, watch, onMounted, onUnmounted, nextTick, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { NButton, NSpace, useMessage, NModal, NForm, NFormItem, NInput, NText } from 'naive-ui'
|
||||||
import { NButton, NSpace, useMessage } from 'naive-ui'
|
import { Icon } from '@iconify/vue'
|
||||||
import { useSEO } from '@/shared/composables/useSEO'
|
import { useSEO } from '@/shared/composables/useSEO'
|
||||||
import { t } from '@/shared/i18n'
|
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'
|
import axios from 'axios'
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const authStore = useAuthStore()
|
||||||
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
const appName = computed(() => localStorage.getItem('appName') || 'Jingrow')
|
||||||
const currentYear = computed(() => new Date().getFullYear())
|
const currentYear = computed(() => new Date().getFullYear())
|
||||||
const logoUrl = computed(() => '/logo.svg')
|
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({
|
useSEO({
|
||||||
title: t('Remove Background - Free AI Background Removal Tool'),
|
title: t('Remove Background - Free AI Background Removal Tool'),
|
||||||
@ -30,6 +204,8 @@ interface HistoryItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null)
|
const fileInputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
// urlInputRef 在模板中使用,lint 警告是误报
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const urlInputRef = ref<HTMLInputElement | null>(null)
|
const urlInputRef = ref<HTMLInputElement | null>(null)
|
||||||
const uploadedImage = ref<File | null>(null)
|
const uploadedImage = ref<File | null>(null)
|
||||||
const uploadedImageUrl = ref<string>('')
|
const uploadedImageUrl = ref<string>('')
|
||||||
@ -497,9 +673,25 @@ const removeHistoryItem = (index: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
window.addEventListener('paste', handlePaste)
|
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(() => {
|
onUnmounted(() => {
|
||||||
@ -524,8 +716,20 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<n-space :size="12">
|
<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>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -711,6 +915,182 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -1649,4 +2029,100 @@ onUnmounted(() => {
|
|||||||
padding: 40px 0 20px;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user