feat: 多机构用户登录时选择机构(600临时token二次登录);设置密码页增加警示文案(Alert新增warning样式)
This commit is contained in:
@@ -9,6 +9,19 @@ export const login = (data = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 多机构用户选择机构后二次登录(/login 返回 600 + 机构列表时使用)
|
||||
export const loginByOrg = (tempToken: string, orgId: string | number) => {
|
||||
return http.post(
|
||||
"/login_token_user",
|
||||
{ tempToken, orgId },
|
||||
{
|
||||
headers: {
|
||||
"content-type": HTTP_CONTENT_TYPE.APPLICATION_FORM_URLENCODED,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// 登出
|
||||
export const logout = () => {
|
||||
return http.post("/logout");
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import { initUserPassword } from "@/apis/user";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
@@ -69,10 +71,17 @@ export function InitPassword() {
|
||||
</div>
|
||||
<div className="w-[480px] p-10 bg-white rounded-xl">
|
||||
<div className="text-center text-2xl">设置密码</div>
|
||||
<Alert variant="warning" className="mt-4">
|
||||
<TriangleAlert />
|
||||
<AlertTitle>首次登录请设置新密码</AlertTitle>
|
||||
<AlertDescription>
|
||||
设置成功后即可正常使用系统。密码长度 6-32 位,支持字母、数字和特殊字符
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 mt-12"
|
||||
className="space-y-8 mt-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -11,6 +11,8 @@ const alertVariants = cva(
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
warning:
|
||||
"text-amber-600 bg-amber-500/10 border-amber-500/30 [&>svg]:text-current *:data-[slot=alert-description]:text-amber-600/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -10,12 +10,16 @@ import {
|
||||
FormControl,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { login } from "@/apis/user";
|
||||
import { login, loginByOrg } from "@/apis/user";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useState } from "react";
|
||||
import logo from "@/assets/image/logo.png";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
|
||||
@@ -32,10 +36,18 @@ const formSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
type PendingOrg = {
|
||||
tempToken: string;
|
||||
orgs: { orgId: number; orgName: string; administrator?: boolean }[];
|
||||
};
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
|
||||
function RouteComponent() {
|
||||
const router = useRouter();
|
||||
// 多机构用户:密码校验通过后先选机构再登录
|
||||
const [pendingOrg, setPendingOrg] = useState<PendingOrg | null>(null);
|
||||
const [selectedOrgId, setSelectedOrgId] = useState("");
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -45,28 +57,64 @@ function RouteComponent() {
|
||||
},
|
||||
});
|
||||
|
||||
const finishLogin = (res: any) => {
|
||||
useUserStore.setState({ token: res.data.token });
|
||||
router.navigate({
|
||||
to: decodeURIComponent(
|
||||
decodeURIComponent(router.state.location.search.redirect || "/"),
|
||||
),
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
const { mutateAsync, isPending } = useMutation({
|
||||
mutationFn: login,
|
||||
onSuccess(res) {
|
||||
useUserStore.setState({ token: res.data.token });
|
||||
router.navigate({
|
||||
to: decodeURIComponent(
|
||||
decodeURIComponent(router.state.location.search.redirect || "/"),
|
||||
),
|
||||
replace: true,
|
||||
});
|
||||
mutationFn: async (values: any) => {
|
||||
try {
|
||||
return await login(values);
|
||||
} catch (err: any) {
|
||||
// 多机构用户:600 + 机构列表 + 临时token,进入选择机构步骤
|
||||
if (err?.code === "600" && err?.errorData?.orgs?.length) {
|
||||
return { needSelectOrg: true, ...err.errorData };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
onSuccess(res: any) {
|
||||
if (res?.needSelectOrg) {
|
||||
setSelectedOrgId(String(res.orgs[0].orgId));
|
||||
setPendingOrg({ tempToken: res.tempToken, orgs: res.orgs });
|
||||
return;
|
||||
}
|
||||
finishLogin(res);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: selectOrgMutateAsync, isPending: selectIsPending } =
|
||||
useMutation({
|
||||
mutationFn: (params: { tempToken: string; orgId: string }) =>
|
||||
loginByOrg(params.tempToken, params.orgId),
|
||||
onSuccess(res) {
|
||||
finishLogin(res);
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
console.log(
|
||||
"router.state.location.search.redirect=======",
|
||||
router.state.location.search.redirect,
|
||||
);
|
||||
const promise = mutateAsync({
|
||||
...values,
|
||||
userType: 1,
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "登录中...",
|
||||
success: (res: any) => (res?.needSelectOrg ? "请选择机构" : "登录成功"),
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmitOrg = () => {
|
||||
if (!selectedOrgId || !pendingOrg) return;
|
||||
const promise = selectOrgMutateAsync({
|
||||
tempToken: pendingOrg.tempToken,
|
||||
orgId: selectedOrgId,
|
||||
});
|
||||
toast.promise(promise, {
|
||||
loading: "登录中...",
|
||||
success: "登录成功",
|
||||
@@ -85,44 +133,117 @@ function RouteComponent() {
|
||||
<div className="text-3xl">{title}</div>
|
||||
</div>
|
||||
<div className="w-[480px] p-10 bg-white rounded-xl">
|
||||
<div className="text-center text-2xl">用户登录</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 mt-12"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>用户名</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="用户名" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="密码" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full" disabled={isPending}>
|
||||
{isPending && <Spinner />}
|
||||
提交
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
{pendingOrg ? (
|
||||
<>
|
||||
<div className="text-center text-2xl">选择机构</div>
|
||||
<div className="text-center text-sm text-black/50 mt-2">
|
||||
当前账号属于多个机构,请选择要登录的机构
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={selectedOrgId}
|
||||
onValueChange={setSelectedOrgId}
|
||||
className="space-y-1 mt-8"
|
||||
>
|
||||
{pendingOrg.orgs.map((org) => (
|
||||
<div
|
||||
key={org.orgId}
|
||||
onClick={() => setSelectedOrgId(String(org.orgId))}
|
||||
className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
|
||||
selectedOrgId === String(org.orgId)
|
||||
? "border-primary bg-primary/10"
|
||||
: "hover:bg-black/5"
|
||||
}`}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={String(org.orgId)}
|
||||
id={`org-${org.orgId}`}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`org-${org.orgId}`}
|
||||
className="flex items-center gap-2 flex-1 cursor-pointer"
|
||||
>
|
||||
<span>{org.orgName}</span>
|
||||
{org.administrator && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-primary border-primary"
|
||||
>
|
||||
管理员
|
||||
</Badge>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<div className="space-y-3 mt-8">
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={selectIsPending || !selectedOrgId}
|
||||
onClick={onSubmitOrg}
|
||||
>
|
||||
{selectIsPending && <Spinner />}
|
||||
进入系统
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={selectIsPending}
|
||||
onClick={() => setPendingOrg(null)}
|
||||
>
|
||||
返回登录
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center text-2xl">用户登录</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-8 mt-12"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>用户名</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="用户名" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner />}
|
||||
提交
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user