feat: 批量创建支持Excel导入与模板下载;机构账号页新增修改用户(手机号/姓名),移除修改姓名入口
This commit is contained in:
@@ -23,6 +23,11 @@ export const createUser = (data: any, config?: any) => {
|
|||||||
return http.post("/user/create_org_user", data, config);
|
return http.post("/user/create_org_user", data, config);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 批量创建用户(单条失败不影响其他条目,返回与入参顺序对应的逐条结果)
|
||||||
|
export const batchCreateUser = (data: any[]) => {
|
||||||
|
return http.post("/user/batch_create_org_user", data);
|
||||||
|
};
|
||||||
|
|
||||||
export const getUserOrg = () => {
|
export const getUserOrg = () => {
|
||||||
return http.get("/uc/org/find");
|
return http.get("/uc/org/find");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useSaveUser } from "../-hooks/useSaveUser";
|
import { useSaveUser } from "@/hooks/useSaveUser";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
|
||||||
@@ -57,7 +57,9 @@ export function EditUserDialog({
|
|||||||
refetch,
|
refetch,
|
||||||
data,
|
data,
|
||||||
}: EditUserDialogProps) {
|
}: EditUserDialogProps) {
|
||||||
const isNewUser = useMemo(() => !data?.id, [data]);
|
// 用户管理页行主键为 id,机构账号页行主键为 userId
|
||||||
|
const recordId = data?.userId ?? data?.id;
|
||||||
|
const isNewUser = useMemo(() => !recordId, [recordId]);
|
||||||
const formSchema = useMemo(() => getFormSchema(isNewUser), [isNewUser]);
|
const formSchema = useMemo(() => getFormSchema(isNewUser), [isNewUser]);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
@@ -89,7 +91,7 @@ export function EditUserDialog({
|
|||||||
}, [open, data, form]);
|
}, [open, data, form]);
|
||||||
|
|
||||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||||
save({ id: data?.id, ...values });
|
save({ id: recordId, ...values });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -17,6 +17,7 @@ import { useFetchList } from "./-hooks/useFetchList";
|
|||||||
import { useUserStatusActions } from "@/hooks/use-user-status-actions";
|
import { useUserStatusActions } from "@/hooks/use-user-status-actions";
|
||||||
import { UserActionConfirmDialog } from "./-components/UserActionConfirmDialog";
|
import { UserActionConfirmDialog } from "./-components/UserActionConfirmDialog";
|
||||||
import { ResetUserPasswordDialog } from "@/components/reset-user-password-dialog";
|
import { ResetUserPasswordDialog } from "@/components/reset-user-password-dialog";
|
||||||
|
import { EditUserDialog } from "@/components/edit-user-dialog";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { DateRangePicker } from "@/components/date-range-picker";
|
import { DateRangePicker } from "@/components/date-range-picker";
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ export function useColumns({
|
|||||||
handleDisable,
|
handleDisable,
|
||||||
handleCancelUser,
|
handleCancelUser,
|
||||||
handleResetPassword,
|
handleResetPassword,
|
||||||
|
handleEditUser,
|
||||||
}: any) {
|
}: any) {
|
||||||
const currentUserId = useUserStore((state: any) => state.user?.id);
|
const currentUserId = useUserStore((state: any) => state.user?.id);
|
||||||
return [
|
return [
|
||||||
@@ -133,6 +135,13 @@ export function useColumns({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleEditUser(record)}
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -206,12 +215,20 @@ function RouteComponent() {
|
|||||||
setOpenResetPasswordDialog(true);
|
setOpenResetPasswordDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 修改用户(手机号/姓名)
|
||||||
|
const [openEditUserDialog, setOpenEditUserDialog] = useState(false);
|
||||||
|
const handleEditUser = (record: any) => {
|
||||||
|
setCurrentEntity(record);
|
||||||
|
setOpenEditUserDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
const { table, query, search } = useFetchList(() =>
|
const { table, query, search } = useFetchList(() =>
|
||||||
useColumns({
|
useColumns({
|
||||||
handleEnable,
|
handleEnable,
|
||||||
handleDisable,
|
handleDisable,
|
||||||
handleCancelUser,
|
handleCancelUser,
|
||||||
handleResetPassword,
|
handleResetPassword,
|
||||||
|
handleEditUser,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -252,6 +269,12 @@ function RouteComponent() {
|
|||||||
setOpen={setOpenResetPasswordDialog}
|
setOpen={setOpenResetPasswordDialog}
|
||||||
data={currentEntity}
|
data={currentEntity}
|
||||||
/>
|
/>
|
||||||
|
<EditUserDialog
|
||||||
|
open={openEditUserDialog}
|
||||||
|
setOpen={setOpenEditUserDialog}
|
||||||
|
data={currentEntity}
|
||||||
|
refetch={query?.refetch}
|
||||||
|
/>
|
||||||
<div className="p-4 box-border min-h-full space-y-4">
|
<div className="p-4 box-border min-h-full space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg">机构账号</div>
|
<div className="text-lg">机构账号</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { createUser } from "@/apis/user";
|
import * as XLSX from "xlsx";
|
||||||
|
import { Download, FileSpreadsheet, Upload } from "lucide-react";
|
||||||
|
import { batchCreateUser } from "@/apis/user";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -12,6 +14,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
|
||||||
type BatchCreateUserDialogProps = {
|
type BatchCreateUserDialogProps = {
|
||||||
@@ -25,10 +28,17 @@ type FailedItem = {
|
|||||||
msg: string;
|
msg: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ParsedRow = {
|
||||||
|
phone: string;
|
||||||
|
realName: string;
|
||||||
|
line: string;
|
||||||
|
};
|
||||||
|
|
||||||
const PHONE_RE = /^1\d{10}$/;
|
const PHONE_RE = /^1\d{10}$/;
|
||||||
|
const MAX_BATCH = 500;
|
||||||
|
|
||||||
const parseText = (text: string) => {
|
const parseText = (text: string) => {
|
||||||
const rows: { phone: string; realName: string; line: string }[] = [];
|
const rows: ParsedRow[] = [];
|
||||||
const failed: FailedItem[] = [];
|
const failed: FailedItem[] = [];
|
||||||
|
|
||||||
text.split("\n").forEach((raw) => {
|
text.split("\n").forEach((raw) => {
|
||||||
@@ -49,22 +59,59 @@ const parseText = (text: string) => {
|
|||||||
return { rows, failed };
|
return { rows, failed };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Excel 行解析:取前两列为手机号/姓名,自动跳过模板表头行
|
||||||
|
const parseExcel = (matrix: unknown[][]) => {
|
||||||
|
const rows: ParsedRow[] = [];
|
||||||
|
const failed: FailedItem[] = [];
|
||||||
|
|
||||||
|
let data = (matrix || [])
|
||||||
|
.map((row) => (row || []).map((cell) => String(cell ?? "").trim()))
|
||||||
|
.filter((cells) => cells.some((cell) => cell !== ""));
|
||||||
|
if (data.length > 0 && data[0][0].includes("手机")) {
|
||||||
|
data = data.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.forEach((cells) => {
|
||||||
|
const line = cells.join(", ");
|
||||||
|
const [phone, realName] = cells;
|
||||||
|
if (!phone || !realName) {
|
||||||
|
failed.push({ line, msg: "格式错误,应为「手机号,姓名」" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PHONE_RE.test(phone)) {
|
||||||
|
failed.push({ line, msg: "手机号格式不正确" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rows.push({ phone, realName, line });
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rows, failed };
|
||||||
|
};
|
||||||
|
|
||||||
export function BatchCreateUserDialog({
|
export function BatchCreateUserDialog({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
refetch,
|
refetch,
|
||||||
}: BatchCreateUserDialogProps) {
|
}: BatchCreateUserDialogProps) {
|
||||||
|
const [mode, setMode] = useState<"text" | "excel">("text");
|
||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [isPending, setIsPending] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [progress, setProgress] = useState({ done: 0, total: 0 });
|
|
||||||
const [failed, setFailed] = useState<FailedItem[]>([]);
|
const [failed, setFailed] = useState<FailedItem[]>([]);
|
||||||
|
const [fileName, setFileName] = useState("");
|
||||||
|
const [excelResult, setExcelResult] = useState<{
|
||||||
|
rows: ParsedRow[];
|
||||||
|
failed: FailedItem[];
|
||||||
|
}>({ rows: [], failed: [] });
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
setMode("text");
|
||||||
setText("");
|
setText("");
|
||||||
setIsPending(false);
|
setIsPending(false);
|
||||||
setProgress({ done: 0, total: 0 });
|
|
||||||
setFailed([]);
|
setFailed([]);
|
||||||
|
setFileName("");
|
||||||
|
setExcelResult({ rows: [], failed: [] });
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
@@ -78,42 +125,96 @@ export function BatchCreateUserDialog({
|
|||||||
setOpen(nextOpen);
|
setOpen(nextOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const downloadTemplate = () => {
|
||||||
|
const worksheet = XLSX.utils.aoa_to_sheet([
|
||||||
|
["手机号", "姓名"],
|
||||||
|
["13800000001", "张三"],
|
||||||
|
["13800000002", "李四"],
|
||||||
|
]);
|
||||||
|
worksheet["!cols"] = [{ wch: 20 }, { wch: 24 }];
|
||||||
|
const workbook = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(workbook, worksheet, "用户列表");
|
||||||
|
XLSX.writeFile(workbook, "批量创建用户模板.xlsx");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFilePicked = async (file?: File) => {
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const workbook = XLSX.read(buffer, { type: "array" });
|
||||||
|
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||||
|
if (!worksheet) {
|
||||||
|
toast.error("文件中没有工作表");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const matrix = XLSX.utils.sheet_to_json<unknown[]>(worksheet, {
|
||||||
|
header: 1,
|
||||||
|
defval: "",
|
||||||
|
});
|
||||||
|
const result = parseExcel(matrix);
|
||||||
|
if (result.rows.length === 0 && result.failed.length === 0) {
|
||||||
|
toast.error("文件中没有用户数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFileName(file.name);
|
||||||
|
setExcelResult(result);
|
||||||
|
} catch {
|
||||||
|
toast.error("文件解析失败,请使用下载的模板填写");
|
||||||
|
} finally {
|
||||||
|
if (fileRef.current) {
|
||||||
|
fileRef.current.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
const { rows, failed: invalid } = parseText(text);
|
const { rows, failed: invalid } =
|
||||||
|
mode === "text" ? parseText(text) : excelResult;
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
if (invalid.length > 0) {
|
if (invalid.length > 0) {
|
||||||
setFailed(invalid);
|
setFailed(invalid);
|
||||||
} else {
|
} else {
|
||||||
toast.error("请输入用户数据");
|
toast.error(mode === "text" ? "请输入用户数据" : "请先选择 Excel 文件");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (rows.length + invalid.length > MAX_BATCH) {
|
||||||
const failures: FailedItem[] = [...invalid];
|
toast.error(`单次批量创建不能超过 ${MAX_BATCH} 条`);
|
||||||
setIsPending(true);
|
return;
|
||||||
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);
|
setIsPending(true);
|
||||||
refetch?.();
|
try {
|
||||||
|
const res: any = await batchCreateUser(
|
||||||
|
rows.map(({ phone, realName }) => ({ phone, realName })),
|
||||||
|
);
|
||||||
|
// 返回结果与入参顺序一一对应,失败项回显原始行内容
|
||||||
|
const results: any[] = res?.data || [];
|
||||||
|
const failures: FailedItem[] = [...invalid];
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (!result?.success) {
|
||||||
|
failures.push({
|
||||||
|
line: rows[index]?.line ?? "",
|
||||||
|
msg: result?.msg || "创建失败",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const failedCount = failures.length;
|
refetch?.();
|
||||||
const successCount = rows.length - (failedCount - invalid.length);
|
|
||||||
if (failedCount > 0) {
|
if (failures.length > 0) {
|
||||||
setFailed(failures);
|
setFailed(failures);
|
||||||
toast.error(`创建完成:成功 ${successCount} 条,失败 ${failedCount} 条`);
|
toast.error(
|
||||||
} else {
|
`创建完成:成功 ${results.length - (failures.length - invalid.length)} 条,失败 ${failures.length} 条`,
|
||||||
toast.success(`创建成功:共 ${rows.length} 条`);
|
);
|
||||||
setOpen(false);
|
} else {
|
||||||
|
toast.success(`创建成功:共 ${results.length} 条`);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 请求整体失败已由拦截器统一 toast
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +228,7 @@ export function BatchCreateUserDialog({
|
|||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{failed.length > 0
|
{failed.length > 0
|
||||||
? `以下 ${failed.length} 条数据创建失败,修正后可重新提交`
|
? `以下 ${failed.length} 条数据创建失败,修正后可重新提交`
|
||||||
: "每行一个用户,格式为「手机号,姓名」,支持逗号、空格或制表符分隔"}
|
: "支持贴内容或上传 Excel 文件导入,每条用户为「手机号,姓名」"}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -147,23 +248,117 @@ export function BatchCreateUserDialog({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<Tabs value={mode} onValueChange={(v) => setMode(v as any)}>
|
||||||
<Textarea
|
<TabsList className="w-full mb-2">
|
||||||
value={text}
|
<TabsTrigger value="text" disabled={isPending} className="flex-1">
|
||||||
onChange={(event) => setText(event.target.value)}
|
贴内容导入
|
||||||
placeholder={"13800000001,张三\n13800000002,李四"}
|
</TabsTrigger>
|
||||||
className="h-[240px] resize-none font-mono"
|
<TabsTrigger value="excel" disabled={isPending} className="flex-1">
|
||||||
disabled={isPending}
|
Excel 导入
|
||||||
/>
|
</TabsTrigger>
|
||||||
<div className="flex items-center justify-between text-xs text-black/50">
|
</TabsList>
|
||||||
<span>已输入 {lineCount} 条</span>
|
|
||||||
{isPending && (
|
<TabsContent value="text" className="space-y-2">
|
||||||
<span>
|
<Textarea
|
||||||
正在创建 {progress.done}/{progress.total}...
|
value={text}
|
||||||
</span>
|
onChange={(event) => setText(event.target.value)}
|
||||||
|
placeholder={"13800000001,张三\n13800000002,李四"}
|
||||||
|
className="h-[240px] resize-none font-mono"
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between text-xs text-black/50">
|
||||||
|
<span>已输入 {lineCount} 条</span>
|
||||||
|
{isPending && <span>正在创建...</span>}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="excel" className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={downloadTemplate}
|
||||||
|
disabled={isPending}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<Download />
|
||||||
|
下载模板
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => fileRef.current?.click()}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<Upload />
|
||||||
|
{excelResult.rows.length > 0 || excelResult.failed.length > 0
|
||||||
|
? "重新选择文件"
|
||||||
|
: "选择文件"}
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
accept=".xlsx,.xls"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(event) =>
|
||||||
|
onFilePicked(event.target.files?.[0] as File | undefined)
|
||||||
|
}
|
||||||
|
disabled={isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fileName ? (
|
||||||
|
<div className="h-[200px] rounded-md border p-3 space-y-2 overflow-auto">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<FileSpreadsheet className="size-4 text-green-600" />
|
||||||
|
<span className="break-all">{fileName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-black/50">
|
||||||
|
<div>
|
||||||
|
解析出有效数据{" "}
|
||||||
|
<span className="text-black font-medium">
|
||||||
|
{excelResult.rows.length}
|
||||||
|
</span>{" "}
|
||||||
|
条
|
||||||
|
{excelResult.failed.length > 0 && (
|
||||||
|
<>
|
||||||
|
,格式错误{" "}
|
||||||
|
<span className="text-red-600 font-medium">
|
||||||
|
{excelResult.failed.length}
|
||||||
|
</span>{" "}
|
||||||
|
条(提交后将展示明细)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{excelResult.rows.length > 0 && (
|
||||||
|
<div className="mt-1 max-h-[130px] overflow-auto font-mono">
|
||||||
|
{excelResult.rows
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((row) => (
|
||||||
|
<div key={row.line}>
|
||||||
|
{row.phone}, {row.realName}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{excelResult.rows.length > 5 && (
|
||||||
|
<div>...共 {excelResult.rows.length} 条</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-[200px] rounded-md border border-dashed flex flex-col items-center justify-center gap-1 text-xs text-black/50">
|
||||||
|
<span>请先下载模板,按模板格式填写后上传</span>
|
||||||
|
<span>支持 .xlsx / .xls 文件,最多 {MAX_BATCH} 条</span>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
<div className="flex items-center justify-between text-xs text-black/50">
|
||||||
|
<span />
|
||||||
|
{isPending && <span>正在创建...</span>}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { DataTable } from "@/components/data-table";
|
import { DataTable } from "@/components/data-table";
|
||||||
import { useFetchList } from "./-hooks/useFetchList";
|
import { useFetchList } from "./-hooks/useFetchList";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { EditUserDialog } from "./-components/EditUserDialog";
|
import { EditUserDialog } from "@/components/edit-user-dialog";
|
||||||
import { BatchCreateUserDialog } from "./-components/BatchCreateUserDialog";
|
import { BatchCreateUserDialog } from "./-components/BatchCreateUserDialog";
|
||||||
import { CancelUserConfirmDialog } from "./-components/CancelUserConfirmDialog";
|
import { CancelUserConfirmDialog } from "./-components/CancelUserConfirmDialog";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|||||||
Reference in New Issue
Block a user