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:
2026-09-15 12:16:14 +08:00
commit d92d7d6752
532 changed files with 177420 additions and 0 deletions

View 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,
};
};

View 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
View 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
}

View 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,
};
};

View 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,
};
};

View 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;
};

View 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,
};
};