import React, { useEffect, useState, useCallback } from 'react';
import {
Row,
Col,
Card,
Tag,
Select,
Statistic,
Modal,
Spin,
Badge,
Tooltip,
DatePicker,
Alert,
Button,
Switch,
Space,
} from 'antd';
import {
HomeOutlined,
UserOutlined,
CalendarOutlined,
BankOutlined,
HistoryOutlined,
ShopOutlined,
CheckCircleOutlined,
SaveOutlined,
} from '@ant-design/icons';
import dayjs, { Dayjs } from 'dayjs';
import api from '../../api';
import { message } from '../../ui/app-message';
import PermissionButton from '../../components/PermissionButton';
import { usePermission } from '../../hooks/usePermission';
import { getInitialPresentOccupancyIds, togglePresentOccupancy } from './inspection-state';
function getCardStyle(room: any): React.CSSProperties {
let base: React.CSSProperties;
if (room.status === 'maintenance') base = { background: '#f5f5f5', borderColor: '#d9d9d9' };
else if (room.currentCount === 0) base = { background: '#f6ffed', borderColor: '#b7eb8f' };
else if (room.currentCount >= room.capacity)
base = { background: '#fff2f0', borderColor: '#ffccc7' };
else base = { background: '#e6f4ff', borderColor: '#91caff' };
if (room.organizationColor) {
return {
...base,
background: `color-mix(in srgb, ${room.organizationColor} 15%, ${base.background || '#fff'} 85%)`,
};
}
return base;
}
function getStatusLabel(room: any) {
if (room.status === 'maintenance') return 维修中;
if (room.currentCount === 0) return 空闲;
if (room.currentCount >= room.capacity) return 满员;
return 部分入住;
}
function getOrganizationTags(occupants: any[]) {
const organizationList = [
...new Map(
occupants
.filter((o: any) => o.organizationName)
.map((o: any) => [
o.organizationId,
{ name: o.organizationName, color: o.organizationColor },
]),
).values(),
] as { name: string; color: string | null }[];
if (organizationList.length === 0) return null;
return (
{organizationList.map((t) => (
}
>
{t.name}
))}
);
}
const RoomVisualPage: React.FC = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [selectedBuilding, setSelectedBuilding] = useState('all');
const [selectedOrganization, setSelectedOrganization] = useState('all');
const [detailRoom, setDetailRoom] = useState(null);
const [asOf, setAsOf] = useState(null);
const [presentOccupancyIds, setPresentOccupancyIds] = useState([]);
const [inspectionSaving, setInspectionSaving] = useState(false);
const { hasPermission } = usePermission();
const isHistorical = !!asOf && !asOf.isSame(dayjs(), 'day');
const fetchData = useCallback(async () => {
setLoading(true);
try {
const params = isHistorical ? { asOf: asOf!.format('YYYY-MM-DD') } : undefined;
const res: any = await api.get('/rooms/visual', { params });
setData(res);
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '加载失败,请稍后重试');
}
setLoading(false);
}, [isHistorical, asOf]);
useEffect(() => {
fetchData();
}, [fetchData]);
useEffect(() => {
if (!detailRoom) {
setPresentOccupancyIds([]);
return;
}
setPresentOccupancyIds(
getInitialPresentOccupancyIds(
detailRoom.occupants || [],
detailRoom.inspection?.submitted === true,
),
);
}, [detailRoom]);
const inspectionDate = (asOf || dayjs()).format('YYYY-MM-DD');
const submitInspection = async () => {
if (!detailRoom || isHistorical) return;
setInspectionSaving(true);
try {
await api.put(`/rooms/${detailRoom.id}/inspections/${inspectionDate}`, {
presentOccupancyIds,
});
message.success(detailRoom.inspection?.submitted ? '查寝记录已更新' : '查寝已提交');
const params = isHistorical ? { asOf: inspectionDate } : undefined;
const res: any = await api.get('/rooms/visual', { params });
setData(res);
const updatedRoom = res.rooms.find((room: any) => room.id === detailRoom.id);
if (updatedRoom) setDetailRoom(updatedRoom);
} catch (e: unknown) {
const err = e as { message?: string };
message.error(err?.message || '查寝提交失败');
} finally {
setInspectionSaving(false);
}
};
if (!data) return ;
const rooms = data.rooms.filter((r: any) => {
if (selectedBuilding !== 'all' && r.building !== selectedBuilding) return false;
if (selectedOrganization !== 'all' && !(r.organizationIds || []).includes(selectedOrganization))
return false;
return true;
});
const totalRooms = rooms.length;
const emptyRooms = rooms.filter(
(r: any) => r.currentCount === 0 && r.status !== 'maintenance',
).length;
const totalBeds = rooms.reduce((sum: number, r: any) => sum + (r.totalBeds || 0), 0);
const occupiedBeds = rooms.reduce((sum: number, r: any) => sum + (r.occupiedBeds || 0), 0);
const availableBedsCount = totalBeds - occupiedBeds;
const fullRooms = rooms.filter((r: any) => r.currentCount >= r.capacity).length;
/* getCardStyle, getStatusLabel, getOrganizationTags are now standalone functions outside the component */
return (
宿舍总览
}
disabledDate={(d) => d && d.isAfter(dayjs(), 'day')}
style={{ width: 180 }}
/>
{isHistorical && (
}
style={{ marginBottom: 16 }}
title={`正在查看 ${asOf!.format('YYYY年M月D日')} 的历史入住情况(含当日已归档房间),非实时数据`}
action={
}
/>
)}
{loading &&
}
{/* 统计栏 */}
} />
{/* 房态网格 */}
{rooms.map((room: any) => (
setDetailRoom(room)}
>
{room.organizationColor && (
)}
{room.roomNumber}
{getStatusLabel(room)}
{room.building && {room.building} }
{room.floor && {room.floor}F}
{room.totalBeds > 0 && (
= room.totalBeds ? '#FF3B30' : '#34C759',
marginBottom: 6,
}}
>
床位: {room.occupiedBeds}/{room.totalBeds}
)}
= room.capacity
? '#FF3B30'
: room.currentCount > 0
? '#007AFF'
: '#34C759',
fontSize: 12,
}}
/>
{room.orgLabel && (
}>
{room.orgLabel}
)}
{getOrganizationTags(room.occupants)}
{room.occupants.length > 0 && (
{room.occupants.slice(0, 4).map((o: any) => (
}
>
{o.studentName}
))}
{room.occupants.length > 4 && +{room.occupants.length - 4}}
)}
{room.occupants.length > 0 && (
{room.inspection?.submitted ? (
{room.inspection.source === 'automatic' ? '自动补记' : '已查寝'}
) : (
未查寝
)}
)}
))}
{/* 详情弹窗 */}
setDetailRoom(null)}
footer={null}
width={500}
>
{detailRoom && (
位置:{detailRoom.building || '-'} {detailRoom.floor ? `${detailRoom.floor}F` : ''}
{detailRoom.organizationColor && (
{detailRoom.occupants[0]?.organizationName || '机构'}
)}
{getStatusLabel(detailRoom)}
{detailRoom.occupants.length > 0 ? (
床位查寝
{detailRoom.inspection?.submitted && (
{detailRoom.inspection.source === 'automatic' ? '自动补记' : '已提交'} ·{' '}
{detailRoom.inspection.inspectorName}
)}
{detailRoom.occupants.map((o: any) => (
{o.studentName}
{o.organization && (
{o.organization}
)}
{isHistorical ? (
{o.inspectionStatus === 'present'
? '在寝'
: o.inspectionStatus === 'absent'
? '缺勤'
: '无记录'}
) : (
{presentOccupancyIds.includes(o.occupancyId) ? '在寝' : '缺勤'}
setPresentOccupancyIds((current) =>
togglePresentOccupancy(current, o.occupancyId, checked),
)
}
/>
)}
床位:{o.bedNumber || '未分配'} |{' '}
入住:{o.checkInDate} | 计费起:{o.billingStartDate}
{o.supervisor && (
负责人:{o.supervisor}
)}
))}
{!isHistorical && hasPermission('room:inspect') && (
}
onClick={() =>
setPresentOccupancyIds(
detailRoom.occupants.map((occupant: any) => occupant.occupancyId),
)
}
>
本宿舍一键打卡
}
loading={inspectionSaving}
onClick={submitInspection}
>
提交查寝
)}
) : (
当前无住户,可安排入住
)}
)}
);
};
export default RoomVisualPage;