+ {/* ─── 顶部标题栏 ─── */}
{
gap: 12,
}}
>
-
数据面板{refreshLoading && }
+ 工作台{refreshLoading && }
{
/>
+ {/* ═══════════ 待办与异常 ═══════════ */}
+
+
+ {/* 今日缺勤 */}
+
+ 0 ? TODO_CARD_WARN : TODO_CARD_OK}
+ styles={{ body: { padding: 16 } }}
+ onClick={() => navigate('/attendance')}
+ >
+
+
0 ? '#FF9500' : '#999' }}
+ />
+
+
+
+
0 ? '#FF9500' : '#999' }}>
+ {absentCount}
+
+
今日缺勤人数
+ {absentCount > 0 ? (
+
需要关注
+ ) : (
+
全员到齐
+ )}
+
+
+
+
+ {/* 待处理账单 */}
+
+ 0 ? TODO_CARD_DRAFT : TODO_CARD_OK}
+ styles={{ body: { padding: 16 } }}
+ onClick={() => navigate('/bills')}
+ >
+
+
0 ? '#AF52DE' : '#999' }}
+ />
+
+
+
+
0 ? '#AF52DE' : '#999' }}>
+ {draftCount}
+
+
待处理账单
+
0 ? '#AF52DE' : '#999', marginTop: 4 }}>
+ {draftCount > 0 ? `合计 ¥${draftTotal.toLocaleString()}` : '暂无待处理'}
+
+
+
+
+
+ {/* 待退押金 */}
+
+ 0 ? TODO_CARD_DANGER : TODO_CARD_OK}
+ styles={{ body: { padding: 16 } }}
+ onClick={() => navigate('/deposits')}
+ >
+
+
0 ? '#FF3B30' : '#999' }}
+ />
+
+
+
+
0 ? '#FF3B30' : '#999' }}>
+ ¥{pendingDeposits.toLocaleString()}
+
+
待退押金
+
0 ? '#FF3B30' : '#999', marginTop: 4 }}>
+ {pendingDeposits > 0 ? '需要处理' : '暂无待退'}
+
+
+
+
+
+
+
+ {/* ═══════════ 核心 KPI ═══════════ */}
-
-
- } />
-
-
-
-
- }
- />
-
-
-
-
- }
- />
-
-
-
+
{
/>
-
-
-
-
-
- } />
-
-
-
+
}
+ title="当前在住"
+ value={stats?.occupiedBeds || 0}
+ suffix={`/ ${stats?.totalCapacity || 0}`}
+ prefix={}
/>
-
+
{
/>
-
+
+
+ }
+ />
+
+
+
{
/>
-
-
-
-
-
- } />
-
-
-
-
- } />
-
-
-
-
-
-
-
-
-
- } />
-
-
-
-
- }
- />
-
+
+ {/* 占位:保持 6 列布局最后一个空位 */}
-
-
-
- } />
-
-
- } />
-
-
- }
- styles={{ value: { color: Number(classroomUtil?.utilizationRate ?? 0) > 70 ? '#34C759' : '#FF9500' } }}
- />
-
-
- } />
-
-
-
+ {/* ═══════════ 更多指标(折叠) ═══════════ */}
+
+
+
+
+ } />
+
+
+
+
+ } />
+
+
+
+
+ } />
+
+
+
+
+ } />
+
+
+
+
+
+
+ } />
+
+
+
+
+
+
+
+
+
+ } />
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+ } />
+
+
+ } />
+
+
+ }
+ styles={{ value: { color: Number(classroomUtil?.utilizationRate ?? 0) > 70 ? '#34C759' : '#FF9500' } }}
+ />
+
+
+ } />
+
+
+
+ >
+ ),
+ }]}
+ />
+ {/* ═══════════ 图表:考勤趋势 + 出勤分布 ═══════════ */}
@@ -495,6 +685,7 @@ const DashboardPage: React.FC = () => {
+ {/* ═══════════ 图表:班级出勤排行 ═══════════ */}
@@ -516,35 +707,7 @@ const DashboardPage: React.FC = () => {
-
-
-
- {classroomOccupancy.length > 0 ? (
- `${p.name}
排课: ${p.data.scheduleDays}天 租赁: ${p.data.rentalCount}个 占用率: ${(p.data.occupancy * 100).toFixed(0)}%`,
- },
- grid: { left: 100, right: 20, bottom: 30, top: 10 },
- xAxis: { type: 'value', max: 1 },
- yAxis: { type: 'category', data: classroomOccupancy.map((r) => r.name), inverse: true },
- visualMap: {
- min: 0, max: 1, orient: 'horizontal', left: 'center', bottom: 0,
- inRange: { color: ['#e6f4ff', '#91caff', '#40a9ff', '#0050b3', '#002c8c'] },
- },
- series: [{
- type: 'bar',
- data: classroomOccupancy.map((r) => ({ name: r.name, value: r.occupancy, scheduleDays: r.scheduleDays, rentalCount: r.rentalCount, occupancy: r.occupancy })),
- itemStyle: { borderRadius: [0, 4, 4, 0] },
- label: { show: true, position: 'right', formatter: (p: any) => `${(p.data.occupancy * 100).toFixed(0)}%` },
- }],
- }} style={{ width: '100%', height: isMobile ? 300 : 400 }} />
- ) : (
- 暂无教室数据
- )}
-
-
-
-
+ {/* ═══════════ 图表:费用分布 + 宿舍排行 ═══════════ */}
@@ -571,6 +734,7 @@ const DashboardPage: React.FC = () => {
+ {/* ═══════════ 图表:月度收入趋势 ═══════════ */}
@@ -583,17 +747,65 @@ const DashboardPage: React.FC = () => {
-
-
-
- {ganttData.length > 0 ? (
-
- ) : (
- 暂无入住数据
- )}
+ {/* ═══════════ 图表:教室占用热力图(懒加载) ═══════════ */}
+
+ {classroomHeatmapVp.inView ? (
+
+
+
+ {classroomOccupancy.length > 0 ? (
+
+ `${p.name}
排课: ${p.data.scheduleDays}天 租赁: ${p.data.rentalCount}个 占用率: ${(p.data.occupancy * 100).toFixed(0)}%`,
+ },
+ grid: { left: 100, right: 20, bottom: 30, top: 10 },
+ xAxis: { type: 'value', max: 1 },
+ yAxis: { type: 'category', data: classroomOccupancy.map((r) => r.name), inverse: true },
+ visualMap: {
+ min: 0, max: 1, orient: 'horizontal', left: 'center', bottom: 0,
+ inRange: { color: ['#e6f4ff', '#91caff', '#40a9ff', '#0050b3', '#002c8c'] },
+ },
+ series: [{
+ type: 'bar',
+ data: classroomOccupancy.map((r) => ({ name: r.name, value: r.occupancy, scheduleDays: r.scheduleDays, rentalCount: r.rentalCount, occupancy: r.occupancy })),
+ itemStyle: { borderRadius: [0, 4, 4, 0] },
+ label: { show: true, position: 'right', formatter: (p: { data: { occupancy: number } }) => `${(p.data.occupancy * 100).toFixed(0)}%` },
+ }],
+ }} style={{ width: '100%', height: isMobile ? 300 : 400 }} />
+ ) : (
+ 暂无教室数据
+ )}
+
+
+
+ ) : (
+
+ 加载中…
-
-
+ )}
+
+
+ {/* ═══════════ 图表:入住时间线甘特图(懒加载) ═══════════ */}
+
+ {ganttVp.inView ? (
+
+
+
+ {ganttData.length > 0 ? (
+
+ ) : (
+ 暂无入住数据
+ )}
+
+
+
+ ) : (
+
+ 加载中…
+
+ )}
+
);
};
diff --git a/apps/admin/src/pages/Occupancies/index.tsx b/apps/admin/src/pages/Occupancies/index.tsx
index 8f86cc5..9a757b3 100644
--- a/apps/admin/src/pages/Occupancies/index.tsx
+++ b/apps/admin/src/pages/Occupancies/index.tsx
@@ -16,6 +16,7 @@ import {
Switch,
Tooltip,
Empty,
+ Alert,
} from 'antd';
import {
PlusOutlined,
@@ -99,8 +100,8 @@ const OccupanciesPage: React.FC = () => {
}
try {
const [beds, lockers] = await Promise.all([
- api.get(`/rooms/${roomId}/beds/available`),
- api.get(`/rooms/${roomId}/lockers/available`),
+ api.get