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 = () => {
|
export const logout = () => {
|
||||||
return http.post("/logout");
|
return http.post("/logout");
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user