1
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user