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 }} /> ({ value: t.id, label: ( {t.color && ( )} {t.name} ), })), ]} />
{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') && (
} loading={inspectionSaving} onClick={submitInspection} > 提交查寝
)}
) : (
当前无住户,可安排入住
)}
)}
); }; export default RoomVisualPage;