151 lines
5.2 KiB
Vue
151 lines
5.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from "vue"
|
|
import { useRoute, useRouter } from "vue-router"
|
|
import { useForm } from "vee-validate"
|
|
import { toast } from "vue-sonner"
|
|
import { loginApi } from "@/services/api"
|
|
import { useAuthStore } from "@/stores/auth"
|
|
import { Building2, GalleryVerticalEnd, KeyRound, LoaderCircle, User } from "@lucide/vue"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Form,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormControl,
|
|
FormMessage,
|
|
} from "@/components/ui/form"
|
|
import { toTypedSchema } from "@vee-validate/zod"
|
|
import { z } from "zod"
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const authStore = useAuthStore()
|
|
|
|
const loginHint = computed(() => {
|
|
const reason = String(route.query.reason || "")
|
|
if (reason === "session-replaced") return "账号已在其他设备登录,请重新登录"
|
|
if (reason === "expired") return "登录已过期,请重新登录"
|
|
return ""
|
|
})
|
|
|
|
const formSchema = toTypedSchema(
|
|
z.object({
|
|
orgId: z.string({ required_error: "请输入机构码" }).min(1, "机构码不能为空"),
|
|
userId: z.string({ required_error: "请输入账号" }).min(1, "账号不能为空"),
|
|
password: z.string().optional(),
|
|
}),
|
|
)
|
|
|
|
const form = useForm({
|
|
validationSchema: formSchema,
|
|
})
|
|
|
|
const submitting = ref(false)
|
|
const orgIdInput = ref<InstanceType<typeof Input> | null>(null)
|
|
|
|
onMounted(() => {
|
|
form.setValues({
|
|
orgId: authStore.orgId || '',
|
|
userId: authStore.user?.account || '',
|
|
password: '',
|
|
})
|
|
orgIdInput.value?.$el?.focus()
|
|
})
|
|
|
|
async function onSubmit(values: Record<string, unknown>) {
|
|
submitting.value = true
|
|
try {
|
|
const { orgId, userId, password } = values as { orgId: string; userId: string; password?: string }
|
|
const res = await loginApi(orgId, userId, password ?? "")
|
|
authStore.setSession(
|
|
{ token: res.data.token, user: { ...res.data.user, account: userId } },
|
|
res.data.orgid ?? orgId,
|
|
)
|
|
toast.success('登录成功')
|
|
router.replace(String(route.query.redirect || "/dashboard"))
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) alert(e.message)
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function login() {
|
|
form.handleSubmit(onSubmit)()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex min-h-svh flex-col items-center justify-center bg-background p-6 md:p-10">
|
|
<div class="w-full max-w-sm">
|
|
<Form class="flex flex-col gap-6" :form="form">
|
|
<!-- Header -->
|
|
<div class="flex flex-col items-center gap-2 text-center">
|
|
<a href="#" class="flex flex-col items-center gap-2 font-medium">
|
|
<div class="flex size-8 items-center justify-center rounded-md">
|
|
<GalleryVerticalEnd class="size-6" />
|
|
</div>
|
|
<span class="sr-only">FMS</span>
|
|
</a>
|
|
<h1 class="text-xl font-bold">登录 FMS</h1>
|
|
</div>
|
|
|
|
<!-- Hint -->
|
|
<div
|
|
v-if="loginHint"
|
|
class="rounded-md bg-amber-50 px-4 py-2.5 text-center text-sm text-amber-700 dark:bg-amber-950 dark:text-amber-300"
|
|
>{{ loginHint }}</div>
|
|
|
|
<!-- orgId -->
|
|
<FormField v-slot="{ componentField }" name="orgId">
|
|
<FormItem>
|
|
<FormLabel>机构码 <span class="text-destructive">*</span></FormLabel>
|
|
<FormControl>
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-3 flex items-center text-muted-foreground"><Building2 class="size-4" /></span>
|
|
<Input id="orgId" ref="orgIdInput" v-bind="componentField" placeholder="请输入机构码" class="pl-9" :disabled="submitting" />
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- userId -->
|
|
<FormField v-slot="{ componentField }" name="userId">
|
|
<FormItem>
|
|
<FormLabel>账号 <span class="text-destructive">*</span></FormLabel>
|
|
<FormControl>
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-3 flex items-center text-muted-foreground"><User class="size-4" /></span>
|
|
<Input id="userId" v-bind="componentField" placeholder="请输入账号" class="pl-9" :disabled="submitting" />
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<!-- password -->
|
|
<FormField v-slot="{ componentField }" name="password">
|
|
<FormItem>
|
|
<FormLabel>密码</FormLabel>
|
|
<FormControl>
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-3 flex items-center text-muted-foreground"><KeyRound class="size-4" /></span>
|
|
<Input id="password" v-bind="componentField" type="password" placeholder="请输入密码" class="pl-9" :disabled="submitting" />
|
|
</div>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<Button class="w-full cursor-pointer" :disabled="submitting" @click="login">
|
|
<LoaderCircle v-if="submitting" class="size-4 animate-spin" />
|
|
登录
|
|
</Button>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
</template>
|