feat: login page
This commit is contained in:
parent
d25f7447ac
commit
452672dedc
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import Modal from './components/Modal.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
<Modal />
|
||||
</template>
|
||||
<style>
|
||||
.font-deserta {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,12 @@ export const login = async (credentials: LoginRequest): Promise<LoginResponse> =
|
|||
|
||||
// 用户注册
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import axios from 'axios'
|
|||
|
||||
// 创建axios实例
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:8000', // 后端API地址
|
||||
baseURL: 'http://124.221.145.135', // 后端API地址
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'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: '/write', name: 'Writing', component: () => import('../views/WritingPage.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 { useRouter } from 'vue-router'
|
||||
import { useAuth } from '../composables/useAuth'
|
||||
import { useModal } from '../composables/useModal'
|
||||
|
||||
const router = useRouter()
|
||||
const { login, register } = useAuth()
|
||||
const { showSuccess, showError } = useModal()
|
||||
|
||||
const isLogin = ref(true)
|
||||
|
||||
|
|
@ -121,7 +123,8 @@ const loginForm = ref({
|
|||
const registerForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
lang_pref: 'fr' as 'jp' | 'fr' | 'private'
|
||||
lang_pref: 'fr' as 'jp' | 'fr' | 'private',
|
||||
portrait: ''
|
||||
})
|
||||
|
||||
const confirmPassword = ref('')
|
||||
|
|
@ -159,11 +162,25 @@ const handleLogin = async () => {
|
|||
error.value = ''
|
||||
|
||||
try {
|
||||
await login(loginForm.value)
|
||||
router.push('/dict')
|
||||
const response = await login(loginForm.value)
|
||||
|
||||
// 检查登录是否成功
|
||||
if (response && response.access_token) {
|
||||
// 显示成功modal
|
||||
showSuccess('登录成功!正在跳转...', 2000)
|
||||
|
||||
// 2秒后跳转到字典页面
|
||||
setTimeout(() => {
|
||||
router.push('/dict')
|
||||
}, 2000)
|
||||
} else {
|
||||
showError('登录失败,请检查用户名和密码')
|
||||
}
|
||||
} catch (err: any) {
|
||||
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 {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
@ -192,24 +209,37 @@ const handleRegister = async () => {
|
|||
|
||||
try {
|
||||
const username = registerForm.value.username // 保存用户名
|
||||
await register(registerForm.value)
|
||||
successMessage.value = '注册成功!请登录'
|
||||
const response = await register(registerForm.value)
|
||||
|
||||
// 将用户名填入登录表单
|
||||
loginForm.value.name = username
|
||||
// 检查注册是否成功
|
||||
if (response && response.id) {
|
||||
// 显示成功modal
|
||||
showSuccess('注册成功!2秒后将切换到登录页面', 2000)
|
||||
|
||||
// 切换到登录模式并清空注册表单
|
||||
isLogin.value = true
|
||||
registerForm.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
lang_pref: 'fr'
|
||||
// 将用户名填入登录表单
|
||||
loginForm.value.name = username
|
||||
|
||||
// 2秒后切换到登录模式并清空注册表单
|
||||
setTimeout(() => {
|
||||
isLogin.value = true
|
||||
registerForm.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
lang_pref: 'fr',
|
||||
portrait: ''
|
||||
}
|
||||
confirmPassword.value = ''
|
||||
}, 2000)
|
||||
} else {
|
||||
// 注册失败
|
||||
showError('注册失败,请检查返回信息')
|
||||
}
|
||||
confirmPassword.value = ''
|
||||
|
||||
} catch (err: any) {
|
||||
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 {
|
||||
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