feat: login page
This commit is contained in:
parent
d25f7447ac
commit
452672dedc
|
|
@ -1,8 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Modal from './components/Modal.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<Modal />
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style>
|
||||||
.font-deserta {
|
.font-deserta {
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,12 @@ export const login = async (credentials: LoginRequest): Promise<LoginResponse> =
|
||||||
|
|
||||||
// 用户注册
|
// 用户注册
|
||||||
export const register = async (userData: RegisterRequest): Promise<RegisterResponse> => {
|
export const register = async (userData: RegisterRequest): Promise<RegisterResponse> => {
|
||||||
const response = await apiClient.post('/users/register', userData)
|
// 确保 portrait 字段存在,如果没有则设置为空字符串
|
||||||
|
const requestData = {
|
||||||
|
...userData,
|
||||||
|
portrait: userData.portrait || ''
|
||||||
|
}
|
||||||
|
const response = await apiClient.post('/users/register', requestData)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import axios from 'axios'
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: 'http://localhost:8000', // 后端API地址
|
baseURL: 'http://124.221.145.135', // 后端API地址
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="modal">
|
||||||
|
<div
|
||||||
|
v-if="modalState.isVisible"
|
||||||
|
class="z-50 fixed inset-0 flex justify-center items-center bg-transparent bg-opacity-50"
|
||||||
|
@click="handleBackdropClick"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-white shadow-xl mx-4 rounded-lg w-full max-w-md transition-all transform"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex justify-between items-center p-6 border-gray-200 border-b">
|
||||||
|
<h3 class="font-semibold text-gray-900 text-lg">
|
||||||
|
{{ modalState.title || getDefaultTitle() }}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
v-if="modalState.showCloseButton"
|
||||||
|
@click="closeModal"
|
||||||
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<!-- Icon -->
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'w-8 h-8 rounded-full flex items-center justify-center',
|
||||||
|
{
|
||||||
|
'bg-green-100 text-green-600': modalState.type === 'success',
|
||||||
|
'bg-red-100 text-red-600': modalState.type === 'error',
|
||||||
|
'bg-yellow-100 text-yellow-600': modalState.type === 'warning',
|
||||||
|
'bg-blue-100 text-blue-600': modalState.type === 'info'
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- Success Icon -->
|
||||||
|
<svg
|
||||||
|
v-if="modalState.type === 'success'"
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Error Icon -->
|
||||||
|
<svg
|
||||||
|
v-else-if="modalState.type === 'error'"
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Warning Icon -->
|
||||||
|
<svg
|
||||||
|
v-else-if="modalState.type === 'warning'"
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.664-.833-2.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Info Icon -->
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
class="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-gray-600 text-sm">
|
||||||
|
{{ modalState.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="flex justify-end bg-gray-50 px-6 py-3 rounded-b-lg">
|
||||||
|
<button
|
||||||
|
@click="closeModal"
|
||||||
|
class="bg-white hover:bg-gray-50 shadow-sm px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 font-medium text-gray-700 text-sm"
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useModal } from '../composables/useModal'
|
||||||
|
|
||||||
|
const { modalState, closeModal } = useModal()
|
||||||
|
|
||||||
|
const getDefaultTitle = () => {
|
||||||
|
switch (modalState.type) {
|
||||||
|
case 'success':
|
||||||
|
return '成功'
|
||||||
|
case 'error':
|
||||||
|
return '错误'
|
||||||
|
case 'warning':
|
||||||
|
return '警告'
|
||||||
|
case 'info':
|
||||||
|
default:
|
||||||
|
return '提示'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBackdropClick = () => {
|
||||||
|
if (modalState.showCloseButton) {
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-enter-active,
|
||||||
|
.modal-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from,
|
||||||
|
.modal-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-active .bg-white,
|
||||||
|
.modal-leave-active .bg-white {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter-from .bg-white,
|
||||||
|
.modal-leave-to .bg-white {
|
||||||
|
transform: scale(0.9) translateY(-10px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
interface ModalConfig {
|
||||||
|
title?: string
|
||||||
|
message: string
|
||||||
|
type?: 'success' | 'error' | 'warning' | 'info'
|
||||||
|
duration?: number
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalState = reactive({
|
||||||
|
isVisible: false,
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
type: 'info' as 'success' | 'error' | 'warning' | 'info',
|
||||||
|
showCloseButton: true
|
||||||
|
})
|
||||||
|
|
||||||
|
let closeTimer: number | null = null
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const showModal = (config: ModalConfig) => {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (closeTimer) {
|
||||||
|
clearTimeout(closeTimer)
|
||||||
|
closeTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
modalState.isVisible = true
|
||||||
|
modalState.title = config.title || ''
|
||||||
|
modalState.message = config.message
|
||||||
|
modalState.type = config.type || 'info'
|
||||||
|
modalState.showCloseButton = config.showCloseButton !== false
|
||||||
|
|
||||||
|
// 如果设置了持续时间,自动关闭
|
||||||
|
if (config.duration && config.duration > 0) {
|
||||||
|
closeTimer = setTimeout(() => {
|
||||||
|
closeModal()
|
||||||
|
}, config.duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
modalState.isVisible = false
|
||||||
|
if (closeTimer) {
|
||||||
|
clearTimeout(closeTimer)
|
||||||
|
closeTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSuccess = (message: string, duration?: number) => {
|
||||||
|
showModal({
|
||||||
|
type: 'success',
|
||||||
|
message,
|
||||||
|
duration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showError = (message: string, duration?: number) => {
|
||||||
|
showModal({
|
||||||
|
type: 'error',
|
||||||
|
message,
|
||||||
|
duration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showWarning = (message: string, duration?: number) => {
|
||||||
|
showModal({
|
||||||
|
type: 'warning',
|
||||||
|
message,
|
||||||
|
duration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showInfo = (message: string, duration?: number) => {
|
||||||
|
showModal({
|
||||||
|
type: 'info',
|
||||||
|
message,
|
||||||
|
duration
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modalState,
|
||||||
|
showModal,
|
||||||
|
closeModal,
|
||||||
|
showSuccess,
|
||||||
|
showError,
|
||||||
|
showWarning,
|
||||||
|
showInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ const router = createRouter({
|
||||||
{ path: '/trans', name: 'Translation', component: () => import('../views/TranslationPage.vue') },
|
{ path: '/trans', name: 'Translation', component: () => import('../views/TranslationPage.vue') },
|
||||||
{ path: '/write', name: 'Writing', component: () => import('../views/WritingPage.vue') },
|
{ path: '/write', name: 'Writing', component: () => import('../views/WritingPage.vue') },
|
||||||
{ path: '/login', name: 'Login', component: () => import('../views/LoginPage.vue') },
|
{ path: '/login', name: 'Login', component: () => import('../views/LoginPage.vue') },
|
||||||
|
{ path: '/modal-test', name: 'ModalTest', component: () => import('../views/ModalTestPage.vue') },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,11 @@
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuth } from '../composables/useAuth'
|
import { useAuth } from '../composables/useAuth'
|
||||||
|
import { useModal } from '../composables/useModal'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { login, register } = useAuth()
|
const { login, register } = useAuth()
|
||||||
|
const { showSuccess, showError } = useModal()
|
||||||
|
|
||||||
const isLogin = ref(true)
|
const isLogin = ref(true)
|
||||||
|
|
||||||
|
|
@ -121,7 +123,8 @@ const loginForm = ref({
|
||||||
const registerForm = ref({
|
const registerForm = ref({
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
lang_pref: 'fr' as 'jp' | 'fr' | 'private'
|
lang_pref: 'fr' as 'jp' | 'fr' | 'private',
|
||||||
|
portrait: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const confirmPassword = ref('')
|
const confirmPassword = ref('')
|
||||||
|
|
@ -159,11 +162,25 @@ const handleLogin = async () => {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await login(loginForm.value)
|
const response = await login(loginForm.value)
|
||||||
|
|
||||||
|
// 检查登录是否成功
|
||||||
|
if (response && response.access_token) {
|
||||||
|
// 显示成功modal
|
||||||
|
showSuccess('登录成功!正在跳转...', 2000)
|
||||||
|
|
||||||
|
// 2秒后跳转到字典页面
|
||||||
|
setTimeout(() => {
|
||||||
router.push('/dict')
|
router.push('/dict')
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
showError('登录失败,请检查用户名和密码')
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Login error:', err)
|
console.error('Login error:', err)
|
||||||
error.value = err.response?.data?.detail || '登录失败,请检查用户名和密码'
|
const errorMessage = err.response?.data?.detail || err.response?.data?.message || '登录失败,请检查用户名和密码'
|
||||||
|
showError(errorMessage)
|
||||||
|
error.value = errorMessage
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -192,24 +209,37 @@ const handleRegister = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const username = registerForm.value.username // 保存用户名
|
const username = registerForm.value.username // 保存用户名
|
||||||
await register(registerForm.value)
|
const response = await register(registerForm.value)
|
||||||
successMessage.value = '注册成功!请登录'
|
|
||||||
|
// 检查注册是否成功
|
||||||
|
if (response && response.id) {
|
||||||
|
// 显示成功modal
|
||||||
|
showSuccess('注册成功!2秒后将切换到登录页面', 2000)
|
||||||
|
|
||||||
// 将用户名填入登录表单
|
// 将用户名填入登录表单
|
||||||
loginForm.value.name = username
|
loginForm.value.name = username
|
||||||
|
|
||||||
// 切换到登录模式并清空注册表单
|
// 2秒后切换到登录模式并清空注册表单
|
||||||
|
setTimeout(() => {
|
||||||
isLogin.value = true
|
isLogin.value = true
|
||||||
registerForm.value = {
|
registerForm.value = {
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
lang_pref: 'fr'
|
lang_pref: 'fr',
|
||||||
|
portrait: ''
|
||||||
}
|
}
|
||||||
confirmPassword.value = ''
|
confirmPassword.value = ''
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
// 注册失败
|
||||||
|
showError('注册失败,请检查返回信息')
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Register error:', err)
|
console.error('Register error:', err)
|
||||||
error.value = err.response?.data?.detail || '注册失败,请稍后重试'
|
const errorMessage = err.response?.data?.detail || err.response?.data?.message || '注册失败,请稍后重试'
|
||||||
|
showError(errorMessage)
|
||||||
|
error.value = errorMessage
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div class="p-8">
|
||||||
|
<h1 class="mb-8 text-2xl">Modal 测试页面</h1>
|
||||||
|
|
||||||
|
<div class="space-x-4">
|
||||||
|
<button
|
||||||
|
@click="showSuccess('这是一个成功消息!', 3000)"
|
||||||
|
class="bg-green-500 hover:bg-green-600 px-4 py-2 rounded text-white"
|
||||||
|
>
|
||||||
|
测试成功Modal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="showError('这是一个错误消息!', 3000)"
|
||||||
|
class="bg-red-500 hover:bg-red-600 px-4 py-2 rounded text-white"
|
||||||
|
>
|
||||||
|
测试错误Modal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="showWarning('这是一个警告消息!', 3000)"
|
||||||
|
class="bg-yellow-500 hover:bg-yellow-600 px-4 py-2 rounded text-white"
|
||||||
|
>
|
||||||
|
测试警告Modal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="showInfo('这是一个信息消息!', 3000)"
|
||||||
|
class="bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded text-white"
|
||||||
|
>
|
||||||
|
测试信息Modal
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<router-link to="/login" class="text-blue-600 underline">
|
||||||
|
回到登录页面
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useModal } from '../composables/useModal'
|
||||||
|
|
||||||
|
const { showSuccess, showError, showWarning, showInfo } = useModal()
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue