feat: 多机构用户登录时选择机构(600临时token二次登录);设置密码页增加警示文案(Alert新增warning样式)

This commit is contained in:
2026-10-27 15:18:17 +08:00
parent 78a1e6c60a
commit 45b62ba131
4 changed files with 198 additions and 53 deletions

View File

@@ -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 = () => { export const logout = () => {
return http.post("/logout"); return http.post("/logout");

View File

@@ -13,6 +13,8 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner"; 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 { initUserPassword } from "@/apis/user";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -69,10 +71,17 @@ export function InitPassword() {
</div> </div>
<div className="w-[480px] p-10 bg-white rounded-xl"> <div className="w-[480px] p-10 bg-white rounded-xl">
<div className="text-center text-2xl">设置密码</div> <div className="text-center text-2xl">设置密码</div>
<Alert variant="warning" className="mt-4">
<TriangleAlert />
<AlertTitle>首次登录请设置新密码</AlertTitle>
<AlertDescription>
设置成功后即可正常使用系统。密码长度 6-32 位,支持字母、数字和特殊字符
</AlertDescription>
</Alert>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8 mt-12" className="space-y-8 mt-6"
> >
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -11,6 +11,8 @@ const alertVariants = cva(
default: "bg-card text-card-foreground", default: "bg-card text-card-foreground",
destructive: destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", "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: { defaultVariants: {

View File

@@ -10,12 +10,16 @@ import {
FormControl, FormControl,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; 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 { Spinner } from "@/components/ui/spinner";
import { login } from "@/apis/user"; import { login, loginByOrg } from "@/apis/user";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState } from "react";
import logo from "@/assets/image/logo.png"; import logo from "@/assets/image/logo.png";
import { useUserStore } from "@/stores/user"; 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; const title = import.meta.env.VITE_APP_TITLE;
function RouteComponent() { function RouteComponent() {
const router = useRouter(); const router = useRouter();
// 多机构用户:密码校验通过后先选机构再登录
const [pendingOrg, setPendingOrg] = useState<PendingOrg | null>(null);
const [selectedOrgId, setSelectedOrgId] = useState("");
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(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({ const { mutateAsync, isPending } = useMutation({
mutationFn: login, mutationFn: async (values: any) => {
onSuccess(res) { try {
useUserStore.setState({ token: res.data.token }); return await login(values);
router.navigate({ } catch (err: any) {
to: decodeURIComponent( // 多机构用户:600 + 机构列表 + 临时token,进入选择机构步骤
decodeURIComponent(router.state.location.search.redirect || "/"), if (err?.code === "600" && err?.errorData?.orgs?.length) {
), return { needSelectOrg: true, ...err.errorData };
replace: true, }
}); 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>) => { const onSubmit = (values: z.infer<typeof formSchema>) => {
console.log(
"router.state.location.search.redirect=======",
router.state.location.search.redirect,
);
const promise = mutateAsync({ const promise = mutateAsync({
...values, ...values,
userType: 1, 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, { toast.promise(promise, {
loading: "登录中...", loading: "登录中...",
success: "登录成功", success: "登录成功",
@@ -85,44 +133,117 @@ function RouteComponent() {
<div className="text-3xl">{title}</div> <div className="text-3xl">{title}</div>
</div> </div>
<div className="w-[480px] p-10 bg-white rounded-xl"> <div className="w-[480px] p-10 bg-white rounded-xl">
<div className="text-center text-2xl">用户登录</div> {pendingOrg ? (
<Form {...form}> <>
<form <div className="text-center text-2xl">选择机构</div>
onSubmit={form.handleSubmit(onSubmit)} <div className="text-center text-sm text-black/50 mt-2">
className="space-y-8 mt-12" 当前账号属于多个机构,请选择要登录的机构
> </div>
<FormField <RadioGroup
control={form.control} value={selectedOrgId}
name="username" onValueChange={setSelectedOrgId}
render={({ field }) => ( className="space-y-1 mt-8"
<FormItem> >
<FormLabel>用户名</FormLabel> {pendingOrg.orgs.map((org) => (
<FormControl> <div
<Input placeholder="用户名" {...field} /> key={org.orgId}
</FormControl> onClick={() => setSelectedOrgId(String(org.orgId))}
<FormMessage /> className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
</FormItem> selectedOrgId === String(org.orgId)
)} ? "border-primary bg-primary/10"
/> : "hover:bg-black/5"
<FormField }`}
control={form.control} >
name="password" <RadioGroupItem
render={({ field }) => ( value={String(org.orgId)}
<FormItem> id={`org-${org.orgId}`}
<FormLabel>密码</FormLabel> />
<FormControl> <Label
<Input type="password" placeholder="密码" {...field} /> htmlFor={`org-${org.orgId}`}
</FormControl> className="flex items-center gap-2 flex-1 cursor-pointer"
<FormMessage /> >
</FormItem> <span>{org.orgName}</span>
)} {org.administrator && (
/> <Badge
<Button type="submit" className="w-full" disabled={isPending}> variant="outline"
{isPending && <Spinner />} className="text-primary border-primary"
提交 >
</Button> 管理员
</form> </Badge>
</Form> )}
</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> </div>
</div> </div>