fix: update home page background image and remove unnecessary redirect code chore: update pnpm lock file with new dependencies for auto-animate and svg spinners delete: remove unused images from public directory refactor: modify course and user types for better clarity and structure feat: implement course API with CRUD operations and teacher team management feat: create user authentication page with login functionality and validation feat: add login state management with Pinia for user session handling style: create reusable UI components for cards and tabs chore: implement HTTP utility for API requests with error handling
195 lines
5.6 KiB
Vue
195 lines
5.6 KiB
Vue
<script lang="ts" setup>
|
|
import { toTypedSchema } from "@vee-validate/zod";
|
|
import { useForm } from "vee-validate";
|
|
import { toast } from "vue-sonner";
|
|
import * as z from "zod";
|
|
import { userLogin, type LoginResponse } from "~/api";
|
|
import type { FetchError } from "ofetch";
|
|
|
|
const loginState = useLoginState();
|
|
const {
|
|
query: { redirect },
|
|
} = useRoute();
|
|
const router = useRouter();
|
|
|
|
const redirectBack = () => {
|
|
router.replace(redirect ? (redirect as string) : "/");
|
|
};
|
|
|
|
const pending = ref(false);
|
|
|
|
const passwordLoginSchema = toTypedSchema(
|
|
z.object({
|
|
username: z.string().min(3, "请输入用户名"),
|
|
password: z.string().min(6, "请输入密码"),
|
|
})
|
|
);
|
|
|
|
const passwordLoginForm = useForm({
|
|
validationSchema: passwordLoginSchema,
|
|
initialValues: {
|
|
username: "",
|
|
password: "",
|
|
},
|
|
});
|
|
|
|
const onPasswordLoginSubmit = passwordLoginForm.handleSubmit((values) => {
|
|
pending.value = true;
|
|
toast.promise(
|
|
userLogin({
|
|
account: values.username,
|
|
password: values.password,
|
|
loginType: "admin",
|
|
}),
|
|
{
|
|
loading: "登录中...",
|
|
success: async (data: LoginResponse) => {
|
|
if (data.code !== 200) {
|
|
toast.error(`登录失败:${data.msg}`);
|
|
return "登录中...";
|
|
}
|
|
loginState.token = data.token;
|
|
const userInfo = await loginState.checkLogin();
|
|
if (!userInfo) {
|
|
toast.error(`获取用户信息失败`);
|
|
return "登录中...";
|
|
}
|
|
redirectBack();
|
|
return `登录成功`;
|
|
},
|
|
error: (error: FetchError) => {
|
|
if (error.status === 401) {
|
|
return "用户名或密码错误";
|
|
}
|
|
return `登录失败:${error.message}`;
|
|
},
|
|
finally: () => {
|
|
pending.value = false;
|
|
},
|
|
}
|
|
);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="w-full h-full flex justify-between bg-img">
|
|
<div class="w-full flex-1">
|
|
<!-- <NuxtImg
|
|
src="/images/bg_home.jpg"
|
|
alt="背景图"
|
|
class="w-full h-full object-cover"
|
|
/> -->
|
|
</div>
|
|
<div
|
|
class="flex flex-col justify-center items-center px-48 gap-4 bg-background"
|
|
>
|
|
<h1 class="text-4xl font-medium drop-shadow-xl text-ai-gradient mb-12">
|
|
AI 智慧课程平台
|
|
</h1>
|
|
<Tabs default-value="account" class="w-[480px]">
|
|
<TabsList class="grid w-full grid-cols-3">
|
|
<TabsTrigger value="account">
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="tabler:key" size="16px" />
|
|
密码登录
|
|
</div>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="otp">
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="tabler:password-mobile-phone" size="16px" />
|
|
验证码登录
|
|
</div>
|
|
</TabsTrigger>
|
|
<TabsTrigger value="recovery">
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="tabler:lock-question" size="16px" />
|
|
找回密码
|
|
</div>
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="account">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>密码登录</CardTitle>
|
|
<CardDescription>
|
|
使用您的用户名和密码登录到您的帐户。
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form
|
|
id="password-login-form"
|
|
class="space-y-4"
|
|
keep-values
|
|
@submit="onPasswordLoginSubmit"
|
|
>
|
|
<FormField v-slot="{ componentField }" name="username">
|
|
<FormItem v-auto-animate>
|
|
<FormLabel>用户名</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="text"
|
|
placeholder="请输入用户名"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
<FormField v-slot="{ componentField }" name="password">
|
|
<FormItem v-auto-animate>
|
|
<FormLabel>密码</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="password"
|
|
placeholder="请输入密码"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</form>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button
|
|
form="password-login-form"
|
|
type="submit"
|
|
:disabled="pending"
|
|
>
|
|
<Icon
|
|
v-if="pending"
|
|
name="svg-spinners:90-ring-with-bg"
|
|
size="16px"
|
|
/>
|
|
登录
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
<div>
|
|
<Button variant="link">
|
|
<Icon name="tabler:user-plus" size="16px" />
|
|
注册新账号
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.text-ai-gradient {
|
|
background: linear-gradient(90deg, rgb(94, 222, 255), rgb(136, 99, 253));
|
|
background-clip: text;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.bg-img {
|
|
background-image: url("/images/bg_home.jpg");
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
}
|
|
</style>
|