From cea200ca5e6676f848e1a3be973f7f87d01b24cf Mon Sep 17 00:00:00 2001 From: leijiaping Date: Mon, 26 Oct 2026 16:50:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=B9=E9=87=8F=E5=88=9B=E5=BB=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=EF=BC=88=E9=80=90=E8=A1=8C=E8=A7=A3=E6=9E=90=E3=80=81?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E8=B7=B3=E8=BF=87=E5=B9=B6=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/apis/user.ts | 4 +- src/lib/http/interceptors.ts | 7 +- .../-components/BatchCreateUserDialog.tsx | 199 ++++++++++++++++++ src/routes/admin/user/index.tsx | 19 +- 4 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 src/routes/admin/user/-components/BatchCreateUserDialog.tsx diff --git a/src/apis/user.ts b/src/apis/user.ts index aef2e31..f2ba8c0 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -19,8 +19,8 @@ export const getUser = () => { return http.get("/user/info"); }; -export const createUser = (data: any) => { - return http.post("/user/create_org_user", data); +export const createUser = (data: any, config?: any) => { + return http.post("/user/create_org_user", data, config); }; export const getUserOrg = () => { diff --git a/src/lib/http/interceptors.ts b/src/lib/http/interceptors.ts index df50c6a..7a89759 100644 --- a/src/lib/http/interceptors.ts +++ b/src/lib/http/interceptors.ts @@ -56,8 +56,9 @@ export const setupInterceptors = (instance: AxiosInstance) => { res, ); + const silent = res.config?.silent === true; if (res.status !== 200) { - toast.error(res?.data?.msg || "请求失败,请稍后再试"); + if (!silent) toast.error(res?.data?.msg || "请求失败,请稍后再试"); return Promise.reject(res.data); } // 文件下载 @@ -75,7 +76,7 @@ export const setupInterceptors = (instance: AxiosInstance) => { ["401", "402"].includes(code) && router.state.location.pathname !== "/login" ) { - toast.error(msg || "未登录/无权限"); + if (!silent) toast.error(msg || "未登录/无权限"); useUserStore.setState({ token: null }); router.navigate({ to: @@ -92,7 +93,7 @@ export const setupInterceptors = (instance: AxiosInstance) => { } // 失败统一弹窗处理 if (["555", "666", "999", "401", "402", "700"].includes(code)) { - toast.error(msg || "系统错误,请稍后再试"); + if (!silent) toast.error(msg || "系统错误,请稍后再试"); } return Promise.reject(res.data); }, diff --git a/src/routes/admin/user/-components/BatchCreateUserDialog.tsx b/src/routes/admin/user/-components/BatchCreateUserDialog.tsx new file mode 100644 index 0000000..5d21510 --- /dev/null +++ b/src/routes/admin/user/-components/BatchCreateUserDialog.tsx @@ -0,0 +1,199 @@ +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { createUser } from "@/apis/user"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Spinner } from "@/components/ui/spinner"; +import { Textarea } from "@/components/ui/textarea"; + +type BatchCreateUserDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + refetch?: () => void; +}; + +type FailedItem = { + line: string; + msg: string; +}; + +const PHONE_RE = /^1\d{10}$/; + +const parseText = (text: string) => { + const rows: { phone: string; realName: string; line: string }[] = []; + const failed: FailedItem[] = []; + + text.split("\n").forEach((raw) => { + const line = raw.trim(); + if (!line) return; + const [phone, ...rest] = line.split(/[,,\t\s]+/).filter(Boolean); + if (!phone || rest.length === 0) { + failed.push({ line, msg: "格式错误,应为「手机号,姓名」" }); + return; + } + if (!PHONE_RE.test(phone)) { + failed.push({ line, msg: "手机号格式不正确" }); + return; + } + rows.push({ phone, realName: rest.join(" "), line }); + }); + + return { rows, failed }; +}; + +export function BatchCreateUserDialog({ + open, + setOpen, + refetch, +}: BatchCreateUserDialogProps) { + const [text, setText] = useState(""); + const [isPending, setIsPending] = useState(false); + const [progress, setProgress] = useState({ done: 0, total: 0 }); + const [failed, setFailed] = useState([]); + + useEffect(() => { + if (open) { + setText(""); + setIsPending(false); + setProgress({ done: 0, total: 0 }); + setFailed([]); + } + }, [open]); + + const lineCount = useMemo( + () => text.split("\n").filter((line) => line.trim()).length, + [text], + ); + + const onOpenChange = (nextOpen: boolean) => { + if (isPending) return; + setOpen(nextOpen); + }; + + const onSubmit = async () => { + const { rows, failed: invalid } = parseText(text); + if (rows.length === 0) { + if (invalid.length > 0) { + setFailed(invalid); + } else { + toast.error("请输入用户数据"); + } + return; + } + + const failures: FailedItem[] = [...invalid]; + setIsPending(true); + setProgress({ done: 0, total: rows.length }); + + for (let index = 0; index < rows.length; index++) { + const { phone, realName, line } = rows[index]; + try { + await createUser({ phone, realName }, { silent: true }); + } catch (err: any) { + failures.push({ line, msg: err?.msg || "创建失败" }); + } + setProgress({ done: index + 1, total: rows.length }); + } + + setIsPending(false); + refetch?.(); + + const failedCount = failures.length; + const successCount = rows.length - (failedCount - invalid.length); + if (failedCount > 0) { + setFailed(failures); + toast.error(`创建完成:成功 ${successCount} 条,失败 ${failedCount} 条`); + } else { + toast.success(`创建成功:共 ${rows.length} 条`); + setOpen(false); + } + }; + + return ( + + + + + {failed.length > 0 ? "批量创建失败项" : "批量创建用户"} + + + {failed.length > 0 + ? `以下 ${failed.length} 条数据创建失败,修正后可重新提交` + : "每行一个用户,格式为「手机号,姓名」,支持逗号、空格或制表符分隔"} + + + + {failed.length > 0 ? ( +
+ {failed.map((item, index) => ( +
+ {index + 1}. + + {item.line} + + {item.msg} +
+ ))} +
+ ) : ( +
+