feat(auth): 重构登录页面并添加忘记密码路由
- 重构登录表单 UI,使用 DaisyUI 组件美化界面 - 添加记住我功能和加载状态提示 - 集成 Toast 提示组件用于显示登录结果 - 新增忘记密码页面路由 - 实现全局 Toast 消息提醒功能 - 在根布局中引入 ToastContainer 组件
This commit is contained in:
34
src/lib/components/ToastContainer.svelte
Normal file
34
src/lib/components/ToastContainer.svelte
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toast } from '$lib/stores/toastStore';
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import { flip } from 'svelte/animate';
|
||||||
|
|
||||||
|
// 映射 Toast 类型到 daisyUI 的 alert 样式类
|
||||||
|
const alertStyles = {
|
||||||
|
info: 'alert-info',
|
||||||
|
success: 'alert-success',
|
||||||
|
warning: 'alert-warning',
|
||||||
|
error: 'alert-error'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 映射 Toast 类型到图标 (可选,为了更像 AntD)
|
||||||
|
const icons = {
|
||||||
|
info: 'ℹ️',
|
||||||
|
success: '✅',
|
||||||
|
warning: '⚠️',
|
||||||
|
error: '❌'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="toast toast-top toast-center z-50">
|
||||||
|
{#each $toast as t (t.id)}
|
||||||
|
<div
|
||||||
|
animate:flip={{ duration: 300 }}
|
||||||
|
transition:fly={{ x: 100, duration: 300 }}
|
||||||
|
class="alert {alertStyles[t.type]} shadow-lg min-w-[200px] flex justify-start"
|
||||||
|
>
|
||||||
|
<span>{icons[t.type]}</span>
|
||||||
|
<span>{t.message}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
39
src/lib/stores/toastStore.ts
Normal file
39
src/lib/stores/toastStore.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||||
|
|
||||||
|
export interface ToastMessage {
|
||||||
|
id: string;
|
||||||
|
type: ToastType;
|
||||||
|
message: string;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createToastStore = () => {
|
||||||
|
|
||||||
|
const {subscribe, update} = writable<ToastMessage[]>([]);
|
||||||
|
const send = (message:string, type:ToastType = 'info',duration = 3000)=> {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
update((toasts) => [...toasts,{id,type,message,duration}])
|
||||||
|
if (duration > 0){
|
||||||
|
setTimeout(()=>{
|
||||||
|
remove(id);
|
||||||
|
},duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const remove = (id:string) => {
|
||||||
|
update((toasts) => toasts.filter(toast => toast.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
success: (msg: string, duration = 3000) => send(msg, 'success', duration),
|
||||||
|
error: (msg: string, duration = 3000) => send(msg, 'error', duration),
|
||||||
|
warning: (msg: string, duration = 3000) => send(msg, 'warning', duration),
|
||||||
|
info: (msg: string, duration = 3000) => send(msg, 'info', duration),
|
||||||
|
remove
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const toast = createToastStore();
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { sidebarStore } from '$lib/stores/sidebarStore.ts';
|
import { sidebarStore } from '$lib/stores/sidebarStore.ts';
|
||||||
import Sprite from '$lib/components/icon/Sprite.svelte';
|
import Sprite from '$lib/components/icon/Sprite.svelte';
|
||||||
|
import ToastContainer from '$lib/components/ToastContainer.svelte';
|
||||||
|
|
||||||
const MD_BREAKPOINT = '(min-width: 768px)';
|
const MD_BREAKPOINT = '(min-width: 768px)';
|
||||||
|
|
||||||
@@ -69,5 +70,6 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div data-theme={$themeStore} class="text-base-content">
|
<div data-theme={$themeStore} class="text-base-content">
|
||||||
|
<ToastContainer />
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
7
src/routes/auth/forgetPassword/+page.svelte
Normal file
7
src/routes/auth/forgetPassword/+page.svelte
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="h-screen w-screen bg-base-300">
|
||||||
|
123
|
||||||
|
</div>
|
||||||
@@ -1,43 +1,119 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { authService } from '$lib/api/services/authService.ts';
|
||||||
|
import type { LoginPayload } from '$lib/types/auth.ts';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { authStore } from '$lib/stores/authStore.ts';
|
||||||
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
|
import { toast } from '$lib/stores/toastStore.ts';
|
||||||
|
|
||||||
import { authService } from '$lib/api/services/authService.ts';
|
// 使用 bind:value 直接绑定,不需要手动写 handleChange
|
||||||
import type { LoginPayload } from '$lib/types/auth.ts';
|
let loginPayload: LoginPayload = {
|
||||||
import { goto } from '$app/navigation';
|
username: '',
|
||||||
import { resolve } from '$app/paths';
|
password: ''
|
||||||
import { get } from 'svelte/store';
|
};
|
||||||
import { authStore } from '$lib/stores/authStore.ts';
|
|
||||||
|
|
||||||
const loginPayload:LoginPayload = {
|
let rememberMe = false; // 变量名语义更清晰,原 isSaving 容易歧义
|
||||||
username: '',
|
let isLoading = false; // 新增:控制按钮加载状态
|
||||||
password: ''
|
|
||||||
}
|
const handleSubmit = async () => {
|
||||||
const handleSubmit = async (e: Event) => {
|
if (isLoading) return;
|
||||||
e.preventDefault();
|
isLoading = true;
|
||||||
try{
|
|
||||||
await authService.login(loginPayload);
|
try {
|
||||||
if(get(authStore).isAuthenticated){
|
// 模拟延时效果,让用户感觉到正在处理 (可选)
|
||||||
await goto(resolve('/'));
|
// await new Promise(r => setTimeout(r, 500));
|
||||||
|
|
||||||
|
await authService.login(loginPayload);
|
||||||
|
if (get(authStore).isAuthenticated) {
|
||||||
|
toast.success('登录成功,正在跳转到首页')
|
||||||
|
setTimeout( async () => {
|
||||||
|
|
||||||
|
await goto(resolve('/app/dashboard'));
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// 这里建议加上一个 Toast 提示错误,比如 daisyUI 的 alert
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
}catch (e){
|
};
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const handleChange = (e: Event) => {
|
|
||||||
const target = e.target as HTMLInputElement;
|
|
||||||
loginPayload[target.name] = target.value;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-screen w-screen bg-base-300 flex justify-center items-center">
|
<div class="min-h-screen bg-base-200 flex items-center justify-center p-4">
|
||||||
<form id="loginForm" class="flex flex-col gap-4">
|
|
||||||
|
|
||||||
<label class="" for="username" >用户名</label>
|
<div class="card w-full max-w-sm bg-base-100 shadow-2xl">
|
||||||
<input class="input" type="text" name="username" on:change={handleChange} >
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<h2 class="text-2xl font-bold flex justify-center items-center gap-2">
|
||||||
|
<Icon id="logo" size="40" className="inline-block"></Icon>
|
||||||
|
<span>IT DTMS登录</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="" for="password" >密码</label>
|
<form on:submit|preventDefault={handleSubmit} class="space-y-4">
|
||||||
<input class="input " type="password" name="password" on:change={handleChange} >
|
|
||||||
|
|
||||||
<button class="btn btn-wide" type="submit" on:click="{handleSubmit}" > 登录</button>
|
<div class="form-control">
|
||||||
</form>
|
<label class="label">
|
||||||
|
<span class="label-text">用户名</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="username"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
bind:value={loginPayload.username}
|
||||||
|
/>
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 opacity-70"><path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM12.735 14c.618 0 1.093-.561.872-1.139a6.002 6.002 0 0 0-11.215 0c-.22.578.254 1.139.872 1.139h9.47Z" /></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">密码</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder="password"
|
||||||
|
class="input input-bordered w-full pl-10"
|
||||||
|
bind:value={loginPayload.password}
|
||||||
|
/>
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/50">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-4 h-4 opacity-70"><path fill-rule="evenodd" d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z" clip-rule="evenodd" /></svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mt-6 flex justify-between">
|
||||||
|
<label class="label cursor-pointer justify-start gap-2">
|
||||||
|
<input type="checkbox" bind:checked={rememberMe} class="checkbox checkbox-sm checkbox-primary" />
|
||||||
|
<span class="label-text">记住我</span>
|
||||||
|
</label>
|
||||||
|
<div class="label" >
|
||||||
|
<a href={resolve('/auth/forgetPassword')} class="label-text-alt link link-hover">忘记密码?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mt-2">
|
||||||
|
<button class="btn btn-primary w-full" disabled={isLoading}>
|
||||||
|
{#if isLoading}
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
登录中...
|
||||||
|
{:else}
|
||||||
|
登录
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Reference in New Issue
Block a user