feat: login page

This commit is contained in:
KirisameVanilla 2025-09-09 14:45:57 +08:00
parent d25f7447ac
commit 452672dedc
No known key found for this signature in database
GPG Key ID: A68EE6C617D68238
8 changed files with 357 additions and 19 deletions

View File

@ -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 {

View File

@ -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
} }

View File

@ -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',

161
src/components/Modal.vue Normal file
View File

@ -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>

View File

@ -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
}
}

View File

@ -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') },
], ],
}) })

View File

@ -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
} }

View File

@ -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>