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,150 @@
import { Label, Pie, PieChart, Sector } from "recharts";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import { formatFileSize } from "@/lib/utils";
import { useMemo } from "react";
import type { PieSectorDataItem } from "recharts/types/polar/Pie";
// 总量
const total = 10 * 1024 * 1024 * 1024;
export const CustomTooltip = ({ active, payload }: any) => {
const isVisible = active && payload && payload.length;
const data = payload?.[0]?.payload;
return (
<div
className="p-4 bg-white/50 text-black backdrop-blur-lg rounded-md shadow z-10 border"
style={{
visibility: isVisible ? "visible" : "hidden",
borderColor: isVisible ? data?.fill : "transparent",
}}
>
{isVisible && (
<div className="flex items-center gap-2 min-w-[150px]">
<span
className="flex-none inline-block h-4 w-4 rounded-full bg-black"
style={{ backgroundColor: data.fill }}
/>
<span className="flex-none">{data.name}</span>
<span className="flex-auto text-right">{data.sizeString}</span>
</div>
)}
</div>
);
};
type Props = {
data: any[];
categorys: any[];
};
export function ChartPieDonutText({ data, categorys }: Props) {
// 已使用
const usedTotal = useMemo(() => {
if (!data?.length) {
return 0;
}
return data.reduce((count, current) => count + current.totalSize, 0);
}, [data]);
// 未使用
const unusedTotal = useMemo(() => total - usedTotal, [usedTotal]);
const chartData = useMemo(() => {
if (!data?.length) {
return [];
}
return [
...data,
{
totalSize: unusedTotal,
fileCategory: "unused",
key: "unused",
name: "未使用",
color: "hsl(var(--muted))",
sizeString: formatFileSize(unusedTotal),
proportion: Math.round((unusedTotal / total) * 100),
fill: `var(--color-unused)`,
},
];
}, [data, unusedTotal]);
const chartConfig = useMemo(() => {
if (!categorys?.length) {
return {};
}
return Object.fromEntries(
categorys.map((item) => [item.key, { ...item, label: item.name }])
);
}, [categorys]);
if (!data?.length) {
return null;
}
return (
<div className="w-full h-full">
<ChartContainer
config={chartConfig}
className="mx-auto aspect-square h-[240px]"
>
<PieChart>
<ChartTooltip cursor={false} content={CustomTooltip} />
<Pie
data={chartData}
dataKey="totalSize"
nameKey="fileCategory"
innerRadius={60}
strokeWidth={4}
activeIndex={0}
activeShape={({ outerRadius = 0, ...props }: PieSectorDataItem) => (
<Sector {...props} outerRadius={outerRadius + 10} />
)}
>
<Label
content={({ viewBox }) => {
if (viewBox && "cx" in viewBox && "cy" in viewBox) {
return (
<text
x={viewBox.cx}
y={viewBox.cy}
textAnchor="middle"
dominantBaseline="middle"
>
<tspan
x={viewBox.cx}
y={viewBox.cy}
className="fill-foreground text-2xl font-bold"
>
{formatFileSize(unusedTotal)}
</tspan>
<tspan
x={viewBox.cx}
y={(viewBox.cy || 0) + 24}
className="fill-muted-foreground"
>
可用容量
</tspan>
</text>
);
}
}}
/>
</Pie>
</PieChart>
</ChartContainer>
<div className="mt-5 flex items-center justify-center flex-col">
<div className="text-base text-black/60 flex items-center justify-between w-[240px] border-b border-b-solid border-b-black/10 py-4">
<span>总量:</span>
<span>{formatFileSize(total)}</span>
</div>
<div className="text-base text-black/60 flex items-center justify-between w-[240px] border-b border-b-solid border-b-black/10 py-4">
<span>已用:</span>
<span>{formatFileSize(usedTotal)}</span>
</div>
<div className="text-base text-black/60 flex items-center justify-between w-[240px] border-b border-b-solid border-b-black/10 py-4">
<span>可用:</span>
<span>{formatFileSize(unusedTotal)}</span>
</div>
</div>
</div>
);
}