This commit is contained in:
2026-07-21 10:34:23 +08:00
parent ea06edec3e
commit 5538ddaa43
4 changed files with 60 additions and 30 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-Ph2QIO8X.js"></script>
<script type="module" crossorigin src="/assets/index-W0SXIbZy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
</head>
<body>
+52 -25
View File
@@ -29,6 +29,7 @@ const AdminDashboard: React.FC = () => {
const [stats, setStats] = useState<AdminStats | null>(null);
const [loading, setLoading] = useState(true);
const [startDate, setStartDate] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null]>([dayjs().startOf('day'), dayjs()]);
const [activeRange, setActiveRange] = useState<string>('today');
const [siteName, setSiteName] = useState<string>('数据概览');
const load = async () => {
@@ -55,21 +56,22 @@ const AdminDashboard: React.FC = () => {
useEffect(() => { load(); loadSiteName(); }, []);
const loadWithDates = (dates: [dayjs.Dayjs, dayjs.Dayjs]) => {
const loadWithDates = (dates: [dayjs.Dayjs, dayjs.Dayjs], range?: string) => {
setStartDate(dates);
if (range) setActiveRange(range);
const start = dates[0]?.format('YYYY-MM-DD') || undefined;
const end = dates[1]?.format('YYYY-MM-DD') || undefined;
setLoading(true);
getAdminStats(start, end).then(data => { setStats(data); setLoading(false); }).catch(() => setLoading(false));
};
const handleToday = () => loadWithDates([dayjs().startOf('day'), dayjs()]);
const handleYesterday = () => { const y = dayjs().subtract(1, 'day'); loadWithDates([y.startOf('day'), y.endOf('day')]); };
const handleWeek = () => loadWithDates([dayjs().startOf('week'), dayjs()]);
const handleMonth = () => loadWithDates([dayjs().startOf('month'), dayjs()]);
const handleToday = () => loadWithDates([dayjs().startOf('day'), dayjs()], 'today');
const handleYesterday = () => { const y = dayjs().subtract(1, 'day'); loadWithDates([y.startOf('day'), y.endOf('day')], 'yesterday'); };
const handleWeek = () => loadWithDates([dayjs().startOf('week'), dayjs()], 'week');
const handleMonth = () => loadWithDates([dayjs().startOf('month'), dayjs()], 'month');
const handleDateChange = (dates: any) => {
if (dates) loadWithDates([dates[0], dates[1]]);
if (dates) { setActiveRange(''); loadWithDates([dates[0], dates[1]]); }
};
return (
@@ -77,10 +79,10 @@ const AdminDashboard: React.FC = () => {
{/* 日期筛选 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 20 }}>
<Space>
<Button type="primary" size="small" onClick={handleToday}></Button>
<Button size="small" onClick={handleYesterday}></Button>
<Button size="small" onClick={handleWeek}></Button>
<Button size="small" onClick={handleMonth}></Button>
<Button type={activeRange === 'today' ? 'primary' : 'default'} size="small" onClick={() => setActiveRange('today')}></Button>
<Button type={activeRange === 'yesterday' ? 'primary' : 'default'} size="small" onClick={() => setActiveRange('yesterday')}></Button>
<Button type={activeRange === 'week' ? 'primary' : 'default'} size="small" onClick={() => setActiveRange('week')}></Button>
<Button type={activeRange === 'month' ? 'primary' : 'default'} size="small" onClick={() => setActiveRange('month')}></Button>
</Space>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, background: '#fff', border: '1px solid #e2e8f0', padding: '4px 12px', borderRadius: 8 }}>
<CalendarOutlined style={{ color: '#64748b' }} />
@@ -207,7 +209,7 @@ const LineChart: React.FC<{ data: { date: string; module: string; credits: numbe
);
};
// ── 模块积分占比(堆叠式横条)──
// ── 模块积分占比(饼图)──
const ModulePie: React.FC<{ data: { module: string; credits: number }[] }> = ({ data }) => {
if (!data.length) return <EmptyChart />;
const moduleMap = new Map<string, number>();
@@ -215,22 +217,47 @@ const ModulePie: React.FC<{ data: { module: string; credits: number }[] }> = ({
const modules = Array.from(moduleMap.entries()).sort((a, b) => b[1] - a[1]);
const total = modules.reduce((s, [, v]) => s + v, 0) || 1;
// 计算饼图扇形路径
const size = 160;
const cx = size / 2;
const cy = size / 2;
const r = 68;
let cumAngle = -90; // 从顶部开始
const slices = modules.map(([mod, val], i) => {
const pct = val / total;
const angle = pct * 360;
const startAngle = cumAngle;
cumAngle += angle;
const endAngle = cumAngle;
const startRad = (startAngle * Math.PI) / 180;
const endRad = (endAngle * Math.PI) / 180;
const largeArc = angle > 180 ? 1 : 0;
const x1 = cx + r * Math.cos(startRad);
const y1 = cy + r * Math.sin(startRad);
const x2 = cx + r * Math.cos(endRad);
const y2 = cy + r * Math.sin(endRad);
const d = `M${cx},${cy} L${x1},${y1} A${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`;
return { d, color: COLORS[i % COLORS.length], label: MODULE_LABELS[mod] || mod, val, pct };
});
return (
<div style={{ height: 220, display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 10 }}>
{/* 堆叠条 */}
<div style={{ height: 28, borderRadius: 6, display: 'flex', overflow: 'hidden', background: '#f1f5f9' }}>
{modules.map(([mod, val], i) => (
<div key={mod} style={{ width: `${(val / total) * 100}%`, background: COLORS[i % COLORS.length], minWidth: val > 0 ? 4 : 0, transition: 'width 0.3s' }} />
<div style={{ height: 220, display: 'flex', alignItems: 'center', gap: 16 }}>
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ flexShrink: 0 }}>
{slices.map((s, i) => (
<path key={i} d={s.d} fill={s.color} stroke="#fff" strokeWidth={2} />
))}
</div>
{/* 图例 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 14px' }}>
{modules.map(([mod, val], i) => (
<div key={mod} style={{ display: 'flex', alignItems: 'center', gap: 5, fontSize: 12, color: '#64748b' }}>
<div style={{ width: 10, height: 10, borderRadius: 2, background: COLORS[i % COLORS.length] }} />
<span>{MODULE_LABELS[mod] || mod}</span>
<span style={{ fontWeight: 600, color: '#1e293b' }}>{val.toFixed(0)}</span>
<span style={{ fontSize: 10 }}>({((val / total) * 100).toFixed(1)}%)</span>
<circle cx={cx} cy={cy} r={36} fill="#fff" />
<text x={cx} y={cy - 4} textAnchor="middle" fontSize={11} fill="#64748b"></text>
<text x={cx} y={cy + 12} textAnchor="middle" fontSize={13} fontWeight={700} fill="#1e293b">{total.toFixed(0)}</text>
</svg>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{slices.map((s, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#64748b' }}>
<div style={{ width: 10, height: 10, borderRadius: 3, background: s.color, flexShrink: 0 }} />
<span>{s.label}</span>
<span style={{ fontWeight: 600, color: '#1e293b' }}>{s.val.toFixed(0)}</span>
<span style={{ fontSize: 10 }}>{(s.pct * 100).toFixed(1)}%</span>
</div>
))}
</div>
+6 -3
View File
@@ -1856,9 +1856,12 @@ async def get_stats(
)
)).scalar() or 0
# ── 每日各模块积分消耗(按日期+模块分组
# ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示
from sqlalchemy import Date, cast as sa_cast
_day_expr = sa_cast(CreditRecord.created_at, Date)
# 图表固定展示 [date_end - 6天, date_end] 共7天
_chart_end_dt = date_end
_chart_start_dt = _chart_end_dt - timedelta(days=6)
_inner = (
select(
_day_expr.label('date'),
@@ -1867,8 +1870,8 @@ async def get_stats(
)
.where(
CreditRecord.type == "consume",
CreditRecord.created_at >= date_start,
CreditRecord.created_at <= date_end,
CreditRecord.created_at >= _chart_start_dt,
CreditRecord.created_at <= _chart_end_dt,
)
.group_by(_day_expr, CreditRecord.source_module)
.subquery()