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:
150
src/routes/(user)/-components/ChartPieDonutText.tsx
Normal file
150
src/routes/(user)/-components/ChartPieDonutText.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user