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

View File

@@ -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}

View File

@@ -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: {

View File

@@ -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,9 +57,7 @@ function RouteComponent() {
},
});
const { mutateAsync, isPending } = useMutation({
mutationFn: login,
onSuccess(res) {
const finishLogin = (res: any) => {
useUserStore.setState({ token: res.data.token });
router.navigate({
to: decodeURIComponent(
@@ -55,18 +65,56 @@ function RouteComponent() {
),
replace: true,
});
};
const { mutateAsync, isPending } = useMutation({
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,6 +133,69 @@ function RouteComponent() {
<div className="text-3xl">{title}</div>
</div>
<div className="w-[480px] p-10 bg-white rounded-xl">
{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
@@ -111,18 +222,28 @@ function RouteComponent() {
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input type="password" placeholder="密码" {...field} />
<Input
type="password"
placeholder="密码"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isPending}>
<Button
type="submit"
className="w-full"
disabled={isPending}
>
{isPending && <Spinner />}
提交
</Button>
</form>
</Form>
</>
)}
</div>
</div>
</div>