feat(task1): restructure directories for turborepo monorepo

- Move backend/ to apps/server/ via git mv
- Move frontend/ to apps/admin/ via git mv
- Create packages/typescript-config/ with base, nestjs, and react-vite presets
This commit is contained in:
2026-07-02 15:05:12 +08:00
parent 4704adcba1
commit 46a817503e
137 changed files with 52 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
import React, { useEffect, useState } from 'react';
import { Row, Col, Card, Tag, Select, Statistic, Modal, Spin, Badge, Tooltip } from 'antd';
import { HomeOutlined, UserOutlined, CalendarOutlined, BankOutlined } from '@ant-design/icons';
import api from '../../api';
const RoomVisualPage: React.FC = () => {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [selectedBuilding, setSelectedBuilding] = useState<string>('all');
const [detailRoom, setDetailRoom] = useState<any>(null);
const fetchData = async () => {
setLoading(true);
try {
const res: any = await api.get('/rooms/visual');
setData(res);
} catch (e) { console.error(e); }
setLoading(false);
};
useEffect(() => { fetchData(); }, []);
if (loading || !data) return <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />;
const rooms = selectedBuilding === 'all'
? data.rooms
: data.rooms.filter((r: any) => r.building === selectedBuilding);
const totalRooms = rooms.length;
const emptyRooms = rooms.filter((r: any) => r.currentCount === 0 && r.status !== 'maintenance').length;
const availableBeds = rooms.reduce((sum: number, r: any) => r.status !== 'maintenance' ? sum + (r.capacity - r.currentCount) : sum, 0);
const fullRooms = rooms.filter((r: any) => r.currentCount >= r.capacity).length;
const getCardStyle = (room: any): React.CSSProperties => {
if (room.status === 'maintenance') return { background: '#f5f5f5', borderColor: '#d9d9d9' };
if (room.currentCount === 0) return { background: '#f6ffed', borderColor: '#b7eb8f' };
if (room.currentCount >= room.capacity) return { background: '#fff2f0', borderColor: '#ffccc7' };
return { background: '#e6f4ff', borderColor: '#91caff' };
};
const getStatusLabel = (room: any) => {
if (room.status === 'maintenance') return <Tag color="default"></Tag>;
if (room.currentCount === 0) return <Tag color="success"></Tag>;
if (room.currentCount >= room.capacity) return <Tag color="error"></Tag>;
return <Tag color="processing"></Tag>;
};
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
<h2 style={{ margin: 0 }}>宿</h2>
<Select
value={selectedBuilding}
onChange={setSelectedBuilding}
style={{ width: 160 }}
options={[
{ value: 'all', label: '全部楼栋' },
...data.buildings.map((b: string) => ({ value: b, label: b })),
]}
/>
</div>
{/* 统计栏 */}
<Row gutter={[12, 12]} style={{ marginBottom: 20 }}>
<Col xs={12} sm={6}>
<Card size="small"><Statistic title="宿舍总数" value={totalRooms} prefix={<HomeOutlined />} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small"><Statistic title="空闲房间" value={emptyRooms} valueStyle={{ color: '#34C759' }} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small"><Statistic title="可安排床位" value={availableBeds} valueStyle={{ color: '#007AFF' }} /></Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small"><Statistic title="满员房间" value={fullRooms} valueStyle={{ color: '#FF3B30' }} /></Card>
</Col>
</Row>
{/* 房态网格 */}
<Row gutter={[12, 12]}>
{rooms.map((room: any) => (
<Col xs={12} sm={8} md={6} lg={4} key={room.id}>
<Card
size="small"
hoverable
style={{ ...getCardStyle(room), borderRadius: 12, borderWidth: 2, cursor: 'pointer', height: '100%' }}
onClick={() => setDetailRoom(room)}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<span style={{ fontSize: 16, fontWeight: 600, color: '#1d1d1f' }}>{room.roomNumber}</span>
{getStatusLabel(room)}
</div>
<div style={{ color: '#86868b', fontSize: 12, marginBottom: 6 }}>
{room.building && <span>{room.building} </span>}
{room.floor && <span>{room.floor}F</span>}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 8 }}>
<Badge
count={`${room.currentCount}/${room.capacity}`}
showZero
style={{
backgroundColor: room.currentCount >= room.capacity ? '#FF3B30' : room.currentCount > 0 ? '#007AFF' : '#34C759',
fontSize: 11,
}}
/>
</div>
{room.orgLabel && (
<div style={{ marginBottom: 6 }}>
<Tag color="purple" style={{ fontSize: 11 }} icon={<BankOutlined />}>{room.orgLabel}</Tag>
</div>
)}
{room.occupants.length > 0 && (
<div style={{ borderTop: '1px solid rgba(0,0,0,0.06)', paddingTop: 6 }}>
{room.occupants.slice(0, 4).map((o: any) => (
<Tooltip key={o.studentId} title={`入住 ${o.days} 天 (${o.checkInDate} 起)`}>
<Tag style={{ margin: '0 4px 4px 0', fontSize: 11 }} icon={<UserOutlined />}>
{o.studentName}
</Tag>
</Tooltip>
))}
{room.occupants.length > 4 && <Tag>+{room.occupants.length - 4}</Tag>}
</div>
)}
</Card>
</Col>
))}
</Row>
{/* 详情弹窗 */}
<Modal
title={`宿舍 ${detailRoom?.roomNumber} 详情`}
open={!!detailRoom}
onCancel={() => setDetailRoom(null)}
footer={null}
width={500}
>
{detailRoom && (
<div>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={8}><Statistic title="额定人数" value={detailRoom.capacity} /></Col>
<Col span={8}><Statistic title="当前入住" value={detailRoom.currentCount} /></Col>
<Col span={8}><Statistic title="剩余床位" value={Math.max(0, detailRoom.capacity - detailRoom.currentCount)} /></Col>
</Row>
<div style={{ marginBottom: 8, fontWeight: 500 }}>
{detailRoom.building || '-'} {detailRoom.floor ? `${detailRoom.floor}F` : ''}
</div>
<div style={{ marginBottom: 16 }}>{getStatusLabel(detailRoom)}</div>
{detailRoom.occupants.length > 0 ? (
<div>
<h4 style={{ marginBottom: 8 }}></h4>
{detailRoom.occupants.map((o: any) => (
<Card key={o.studentId} size="small" style={{ marginBottom: 8, borderRadius: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<UserOutlined style={{ marginRight: 6 }} />
<strong>{o.studentName}</strong>
{o.organization && <Tag color="purple" style={{ marginLeft: 6, fontSize: 11 }}>{o.organization}</Tag>}
</div>
<Tag color="blue">{o.days} </Tag>
</div>
<div style={{ color: '#86868b', fontSize: 12, marginTop: 4 }}>
<CalendarOutlined style={{ marginRight: 4 }} />
{o.checkInDate} | {o.billingStartDate}
{o.supervisor && <span style={{ marginLeft: 8 }}>{o.supervisor}</span>}
</div>
</Card>
))}
</div>
) : (
<div style={{ textAlign: 'center', padding: 24, color: '#86868b' }}></div>
)}
</div>
)}
</Modal>
</div>
);
};
export default RoomVisualPage;