Files
senmesh-zc/src/routes/admin/org-account/index.tsx
leijiaping d92d7d6752 feat: 管理后台增强——用户启停操作、shadcn日期区间/后缀标签组件
1. 用户管理页:新增启用状态列与启用/禁用操作按钮(对接 user/enable|disable)
2. 机构账号/记录查询页:时间区间改用官方 shadcn Range Picker 模式
   (Popover+Calendar mode=range 双月面板,新增 components/date-range-picker.tsx)
3. 上传配置页:文件类型限制改为标签式输入(新增 suffix-tag-input.tsx,
   Badge标签+回车添加+常用后缀快捷点选),替换自由文本框
4. 修复:.env 的 VITE_APP_TITLE 加引号,规避 Bun 1.3.10 env 解析
   对行尾多字节中文的截断(标题乱码"森睿智�")
5. 新增共享 hook hooks/use-user-status-actions.ts(从 org-account 迁出)
6. vite 代理保留 本地Docker(localhost:8080)/公司 两种目标,切换注释即可
2026-09-15 12:16:14 +08:00

328 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { RotateCcw, Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { DataTable } from "@/components/data-table";
import { Pagination } from "@/components/pagination";
import { useFetchList } from "./-hooks/useFetchList";
import { useUserStatusActions } from "@/hooks/use-user-status-actions";
import { UserActionConfirmDialog } from "./-components/UserActionConfirmDialog";
import { useUserStore } from "@/stores/user";
import { DateRangePicker } from "@/components/date-range-picker";
export const Route = createFileRoute("/admin/org-account/")({
component: RouteComponent,
});
const initialFilters = {
orgNameLike: "",
usernameLike: "",
realNameLike: "",
phoneLike: "",
enabled: "",
status: "",
createTimeStart: "",
createTimeEnd: "",
};
export function useColumns({
handleEnable,
handleDisable,
handleCancelUser,
}: any) {
const currentUserId = useUserStore((state: any) => state.user?.id);
return [
{
accessorKey: "orgName",
header: "机构名称",
cell: ({ row }: any) => (
<div className="flex items-center gap-1">
<div>{row.getValue("orgName")}</div>
{row.original.administrator && (
<Badge
variant="outline"
className="text-primary border-primary shrink-0"
>
管理员
</Badge>
)}
</div>
),
},
{
accessorKey: "username",
header: "账号",
cell: ({ row }: any) => <div>{row.getValue("username")}</div>,
},
{
accessorKey: "realName",
header: "姓名",
cell: ({ row }: any) => (
<div>{row.getValue("realName") || row.original.nickname || "-"}</div>
),
},
{
accessorKey: "phone",
header: "手机号",
cell: ({ row }: any) => <div>{row.getValue("phone") || "-"}</div>,
},
{
accessorKey: "userType",
header: "用户类型",
cell: ({ row }: any) => (
<div>{row.getValue("userType") === 2 ? "个人用户" : "企业用户"}</div>
),
},
{
accessorKey: "enabled",
header: "启用状态",
cell: ({ row }: any) =>
row.getValue("enabled") ? (
<Badge className="bg-green-500/10 text-green-600">启用</Badge>
) : (
<Badge className="bg-red-500/10 text-red-500">禁用</Badge>
),
},
{
accessorKey: "status",
header: "状态",
cell: ({ row }: any) => {
const status = Number(row.getValue("status"));
return (
<div className="text-xs">
{status === 1 ? (
<Badge variant="secondary">待激活</Badge>
) : status === 2 ? (
<Badge variant="secondary" className="text-green-600">
已激活
</Badge>
) : (
<Badge variant="secondary" className="text-red-600">
已注销
</Badge>
)}
</div>
);
},
},
{
accessorKey: "createTime",
header: "创建时间",
cell: ({ row }: any) => <div>{row.getValue("createTime")}</div>,
},
{
id: "actions",
enableHiding: false,
header: "操作",
cell: ({ row }: any) => {
const record = row.original;
// 已注销或当前登录人自己,不展示操作
if (Number(record.status) === 3 || record.userId === currentUserId) {
return null;
}
return (
<div className="space-x-2">
{record.enabled ? (
<Button
size="sm"
variant="outline"
onClick={() => handleDisable(record)}
>
禁用
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => handleEnable(record)}
>
启用
</Button>
)}
<Button
size="sm"
variant="destructive"
onClick={() => handleCancelUser(record)}
>
注销
</Button>
</div>
);
},
},
];
}
function RouteComponent() {
const [currentEntity, setCurrentEntity] = useState<any>(null);
// 启用(直接操作)
const { enable } = useUserStatusActions({
onSuccess: () => query?.refetch(),
});
const handleEnable = (record: any) => {
enable(record.userId);
};
// 禁用/注销(二次确认)
const [confirmAction, setConfirmAction] = useState<
"disable" | "adminCancel"
>("disable");
const [openConfirmDialog, setOpenConfirmDialog] = useState(false);
const handleDisable = (record: any) => {
setCurrentEntity(record);
setConfirmAction("disable");
setOpenConfirmDialog(true);
};
const handleCancelUser = (record: any) => {
setCurrentEntity(record);
setConfirmAction("adminCancel");
setOpenConfirmDialog(true);
};
const { table, query, search } = useFetchList(() =>
useColumns({
handleEnable,
handleDisable,
handleCancelUser,
}),
);
// 筛选条件(点击查询后生效)
const [filters, setFilters] = useState<any>(initialFilters);
const setFilter = (key: string, value: string) => {
setFilters((prev: any) => ({ ...prev, [key]: value }));
};
const handleSearch = () => {
search({
...filters,
createTimeStart: filters.createTimeStart
? `${filters.createTimeStart} 00:00:00`
: "",
createTimeEnd: filters.createTimeEnd
? `${filters.createTimeEnd} 23:59:59`
: "",
});
};
const handleReset = () => {
setFilters(initialFilters);
search({});
};
return (
<>
<UserActionConfirmDialog
open={openConfirmDialog}
setOpen={setOpenConfirmDialog}
action={confirmAction}
data={currentEntity}
refetch={query?.refetch}
/>
<div className="p-4 box-border min-h-full space-y-4">
<div>
<div className="text-lg">机构账号</div>
<div className="text-xs text-black/50">
查看各机构下的账号信息,启用/禁用/注销账号
</div>
</div>
<div className="p-4 rounded-lg border bg-white space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Input
placeholder="机构名称"
className="w-[160px]"
value={filters.orgNameLike}
onChange={(e: any) => setFilter("orgNameLike", e.target.value)}
/>
<Input
placeholder="账号"
className="w-[140px]"
value={filters.usernameLike}
onChange={(e: any) => setFilter("usernameLike", e.target.value)}
/>
<Input
placeholder="真实姓名"
className="w-[120px]"
value={filters.realNameLike}
onChange={(e: any) => setFilter("realNameLike", e.target.value)}
/>
<Input
placeholder="手机号"
className="w-[140px]"
value={filters.phoneLike}
onChange={(e: any) => setFilter("phoneLike", e.target.value)}
/>
<Select
value={filters.enabled || "all"}
onValueChange={(value: string) =>
setFilter("enabled", value === "all" ? "" : value)
}
>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="启用状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">启用状态</SelectItem>
<SelectItem value="true">启用</SelectItem>
<SelectItem value="false">禁用</SelectItem>
</SelectContent>
</Select>
<Select
value={filters.status || "all"}
onValueChange={(value: string) =>
setFilter("status", value === "all" ? "" : value)
}
>
<SelectTrigger className="w-[110px]">
<SelectValue placeholder="用户状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">用户状态</SelectItem>
<SelectItem value="1">待激活</SelectItem>
<SelectItem value="2">已激活</SelectItem>
<SelectItem value="3">已注销</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center gap-1 text-sm text-black/60">
创建时间
<DateRangePicker
from={filters.createTimeStart}
to={filters.createTimeEnd}
onChange={(range) => {
setFilter("createTimeStart", range.from ?? "");
setFilter("createTimeEnd", range.to ?? "");
}}
/>
</div>
<Button onClick={handleSearch}>
<Search />
查询
</Button>
<Button variant="outline" onClick={handleReset}>
<RotateCcw />
重置
</Button>
</div>
<div className="relative overflow-hidden">
<DataTable table={table} query={query} />
<Pagination
table={table}
className="justify-end p-2 sticky bottom-0 left-0 right-0 z-10 bg-white"
/>
</div>
</div>
</div>
</>
);
}