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)/公司 两种目标,切换注释即可
This commit is contained in:
99
src/hooks/use-fetch-files.ts
Normal file
99
src/hooks/use-fetch-files.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getFilePage } from "@/apis/file";
|
||||
import { useReactTable } from "@tanstack/react-table";
|
||||
import { getCoreRowModel } from "@tanstack/react-table";
|
||||
|
||||
type UseFetchFilesProps = {
|
||||
useColumns: any;
|
||||
queryKey: any[];
|
||||
params: any;
|
||||
};
|
||||
|
||||
export const useFetchFiles = ({
|
||||
useColumns,
|
||||
queryKey,
|
||||
params,
|
||||
}: UseFetchFilesProps) => {
|
||||
// 当前打开的目录层级
|
||||
const [openDirectorys, setOpenDirectorys] = useState([]);
|
||||
// 文件目录ID
|
||||
const [parentId, setParentId] = useState(0);
|
||||
// 分页
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
// 查询数据
|
||||
const query = useQuery<any>({
|
||||
queryKey: [...queryKey, pagination, parentId, params],
|
||||
queryFn: () =>
|
||||
getFilePage({
|
||||
parentId: parentId || 0,
|
||||
pageSize: pagination.pageSize,
|
||||
pageNo: pagination.pageIndex + 1,
|
||||
...params,
|
||||
}).then((res) => res.data),
|
||||
});
|
||||
|
||||
// 切换上级ID
|
||||
const handleChangeParentId = (record: any) => {
|
||||
setParentId(record.id);
|
||||
// 重置分页
|
||||
table.setPageIndex(0);
|
||||
// @ts-ignore
|
||||
setOpenDirectorys((prev: any) => [...prev, record]);
|
||||
};
|
||||
|
||||
// 表格
|
||||
const columns = useColumns(handleChangeParentId);
|
||||
const table = useReactTable({
|
||||
data: query?.data?.entityList || [],
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: false,
|
||||
rowCount: query?.data?.totalCount || 0,
|
||||
pageCount: query?.data?.pageCount || 0,
|
||||
onPaginationChange: setPagination,
|
||||
autoResetPageIndex: false,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
const changeOpenDirectorys = (record?: any) => {
|
||||
setParentId(record?.id);
|
||||
// 重置分页
|
||||
table.setPageIndex(0);
|
||||
// @ts-ignore
|
||||
setOpenDirectorys((prev: any[]) => {
|
||||
const index = prev.findIndex((item: any) => item.id === record?.id);
|
||||
if (index > -1) {
|
||||
return [...prev.slice(0, index + 1)];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
setParentId(0);
|
||||
// 重置分页
|
||||
table.setPageIndex(0);
|
||||
// @ts-ignore
|
||||
setOpenDirectorys([]);
|
||||
};
|
||||
|
||||
return {
|
||||
parentId,
|
||||
setParentId,
|
||||
handleChangeParentId,
|
||||
openDirectorys,
|
||||
changeOpenDirectorys,
|
||||
pagination,
|
||||
table,
|
||||
query,
|
||||
reset,
|
||||
};
|
||||
};
|
||||
26
src/hooks/use-file-category-name.ts
Normal file
26
src/hooks/use-file-category-name.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
const fileCategorys = [
|
||||
{
|
||||
value: "image",
|
||||
name: "图片",
|
||||
},
|
||||
{
|
||||
value: "video",
|
||||
name: "视频",
|
||||
},
|
||||
{
|
||||
value: "document",
|
||||
name: "文档",
|
||||
},
|
||||
{
|
||||
value: "other",
|
||||
name: "其他",
|
||||
},
|
||||
{
|
||||
value: "unknown",
|
||||
name: "未知",
|
||||
},
|
||||
];
|
||||
|
||||
export const useFileCategoryName = (value: string) => {
|
||||
return fileCategorys.find((item) => item.value === value)?.name || "未知";
|
||||
};
|
||||
19
src/hooks/use-mobile.ts
Normal file
19
src/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
23
src/hooks/use-move-flie.ts
Normal file
23
src/hooks/use-move-flie.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { modifyFile } from "@/apis/file";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const useMoveFile = (mutationOptions?: any) => {
|
||||
const { mutateAsync: moveMutateAsync, isPending: moveIsPending } =
|
||||
useMutation({
|
||||
mutationFn: modifyFile,
|
||||
...(mutationOptions || {}),
|
||||
});
|
||||
|
||||
const move = (values: any) => {
|
||||
toast.promise(moveMutateAsync(values), {
|
||||
loading: "移动中",
|
||||
success: "移动成功",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
move,
|
||||
moveIsPending,
|
||||
};
|
||||
};
|
||||
23
src/hooks/use-remove-file.ts
Normal file
23
src/hooks/use-remove-file.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { modifyFileStatus } from "@/apis/file";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const useRemoveFile = (mutationOptions?: any) => {
|
||||
const { mutateAsync: removeMutateAsync, isPending: removeIsPending } =
|
||||
useMutation({
|
||||
mutationFn: modifyFileStatus,
|
||||
...(mutationOptions || {}),
|
||||
});
|
||||
|
||||
const remove = (values: any) => {
|
||||
toast.promise(removeMutateAsync(values), {
|
||||
loading: "删除中",
|
||||
success: "删除成功",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
remove,
|
||||
removeIsPending,
|
||||
};
|
||||
};
|
||||
37
src/hooks/use-upload-config.ts
Normal file
37
src/hooks/use-upload-config.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUploadConfig } from "@/apis/upload-config";
|
||||
|
||||
export type UploadConfig = {
|
||||
orgId: number;
|
||||
uploadEnabled: boolean;
|
||||
allowAllType: boolean;
|
||||
allowedSuffixes: string[] | null;
|
||||
maxFileSize: number | null;
|
||||
configured: boolean;
|
||||
};
|
||||
|
||||
// 当前机构的上传配置(未配置过的机构 = 默认策略:允许上传、允许全部类型、不限大小)
|
||||
export const useUploadConfig = () => {
|
||||
return useQuery<UploadConfig>({
|
||||
queryKey: ["uploadConfig"],
|
||||
queryFn: () => getUploadConfig().then((res: any) => res?.data),
|
||||
});
|
||||
};
|
||||
|
||||
// 允许的后缀 → 文件选择器 accept(带点);不限制时返回 undefined
|
||||
export const toAccept = (config?: UploadConfig | null) => {
|
||||
if (!config || config.allowAllType || !config.allowedSuffixes?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return config.allowedSuffixes
|
||||
.map((suffix) => `.${String(suffix).replace(/^\.+/, "").toLowerCase()}`)
|
||||
.join(",");
|
||||
};
|
||||
|
||||
// 单文件大小上限(字节);不限制时返回 undefined
|
||||
export const toMaxSize = (config?: UploadConfig | null) => {
|
||||
if (config?.maxFileSize && config.maxFileSize > 0) {
|
||||
return config.maxFileSize * 1024 * 1024;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
41
src/hooks/use-user-status-actions.ts
Normal file
41
src/hooks/use-user-status-actions.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { adminCancelUser, disableUser, enableUser } from "@/apis/user";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const useUserStatusActions = (mutationOptions?: any) => {
|
||||
const { mutateAsync: enableMutateAsync, isPending: enableIsPending } =
|
||||
useMutation<any, Error, number>({
|
||||
mutationFn: enableUser,
|
||||
...(mutationOptions || {}),
|
||||
});
|
||||
|
||||
const { mutateAsync: disableMutateAsync, isPending: disableIsPending } =
|
||||
useMutation<any, Error, number>({
|
||||
mutationFn: disableUser,
|
||||
...(mutationOptions || {}),
|
||||
});
|
||||
|
||||
const { mutateAsync: cancelMutateAsync, isPending: cancelIsPending } =
|
||||
useMutation<any, Error, number>({
|
||||
mutationFn: adminCancelUser,
|
||||
...(mutationOptions || {}),
|
||||
});
|
||||
|
||||
const wrap = (mutateAsync: (userId: number) => Promise<any>) => {
|
||||
return (userId: number) => {
|
||||
toast.promise(mutateAsync(userId), {
|
||||
loading: "提交中...",
|
||||
success: "操作成功",
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
enable: wrap(enableMutateAsync),
|
||||
disable: wrap(disableMutateAsync),
|
||||
adminCancel: wrap(cancelMutateAsync),
|
||||
enableIsPending,
|
||||
disableIsPending,
|
||||
cancelIsPending,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user