chore: commit oxfmt formatting changes and verify artifacts
This commit is contained in:
@@ -1,5 +1,18 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { DatePicker, Card, Row, Col, Statistic, Tag, Space, Button, Modal, Spin, Empty, Tooltip } from 'antd';
|
||||
import {
|
||||
DatePicker,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Tag,
|
||||
Space,
|
||||
Button,
|
||||
Modal,
|
||||
Spin,
|
||||
Empty,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { CalendarOutlined, FileTextOutlined } from '@ant-design/icons';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import api from '../../api';
|
||||
@@ -11,7 +24,10 @@ interface ScheduleData {
|
||||
classrooms: any[];
|
||||
tenants: any[];
|
||||
matrix: Record<number, Record<number, any>>;
|
||||
summary: Record<number, { totalDays: number; rentedDays: number; idleDays: number; occupancyRate: number }>;
|
||||
summary: Record<
|
||||
number,
|
||||
{ totalDays: number; rentedDays: number; idleDays: number; occupancyRate: number }
|
||||
>;
|
||||
}
|
||||
|
||||
const ClassroomSchedulePage: React.FC = () => {
|
||||
@@ -27,11 +43,15 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
params: { year: month.year(), month: month.month() + 1 },
|
||||
});
|
||||
setData(res);
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [month]);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [month]);
|
||||
|
||||
// 按楼栋+楼层分组教室
|
||||
const groups = useMemo(() => {
|
||||
@@ -64,15 +84,21 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
try {
|
||||
const res: any = await api.get(`/classroom-rentals/${rentalId}`);
|
||||
setDetailModal(res);
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadContract = (id: number, filename?: string) => {
|
||||
const baseURL = import.meta.env.PROD ? '/api' : `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const baseURL = import.meta.env.PROD
|
||||
? '/api'
|
||||
: `http://localhost:${import.meta.env.VITE_API_PORT || 3002}/api`;
|
||||
const token = localStorage.getItem('token');
|
||||
fetch(`${baseURL}/classroom-rentals/${id}/contract`, { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
fetch(`${baseURL}/classroom-rentals/${id}/contract`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -84,33 +110,80 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<CalendarOutlined style={{ fontSize: 20 }} />
|
||||
<h3 style={{ margin: 0 }}>教室排期总览</h3>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button onClick={() => setMonth(month.subtract(1, 'month'))}>上月</Button>
|
||||
<DatePicker picker="month" value={month} onChange={(v) => v && setMonth(v)} allowClear={false} placeholder="选择月份" format="YYYY年M月" />
|
||||
<DatePicker
|
||||
picker="month"
|
||||
value={month}
|
||||
onChange={(v) => v && setMonth(v)}
|
||||
allowClear={false}
|
||||
placeholder="选择月份"
|
||||
format="YYYY年M月"
|
||||
/>
|
||||
<Button onClick={() => setMonth(month.add(1, 'month'))}>下月</Button>
|
||||
<Button type="primary" onClick={() => setMonth(dayjs())}>回到本月</Button>
|
||||
<Button type="primary" onClick={() => setMonth(dayjs())}>
|
||||
回到本月
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={12} sm={6}><Card size="small"><Statistic title="教室总数" value={data?.classrooms.length || 0} /></Card></Col>
|
||||
<Col xs={12} sm={6}><Card size="small"><Statistic title="本月天数" value={data?.days || 0} /></Card></Col>
|
||||
<Col xs={12} sm={6}><Card size="small"><Statistic title="总占用天数" value={overall.rented} suffix={`/${overall.total}`} /></Card></Col>
|
||||
<Col xs={12} sm={6}><Card size="small"><Statistic title="整体占用率" value={overall.rate} suffix="%" valueStyle={{ color: overall.rate > 70 ? '#cf1322' : overall.rate > 40 ? '#fa8c16' : '#3f8600' }} /></Card></Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="教室总数" value={data?.classrooms.length || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="本月天数" value={data?.days || 0} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card size="small">
|
||||
<Statistic title="总占用天数" value={overall.rented} suffix={`/${overall.total}`} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={6}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="整体占用率"
|
||||
value={overall.rate}
|
||||
suffix="%"
|
||||
valueStyle={{
|
||||
color: overall.rate > 70 ? '#cf1322' : overall.rate > 40 ? '#fa8c16' : '#3f8600',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 租赁方图例 */}
|
||||
{data && data.tenants.length > 0 && (
|
||||
<Card size="small" style={{ marginBottom: 16 }} title="租赁方图例">
|
||||
<Space wrap>
|
||||
{data.tenants.map(t => (
|
||||
<Tag key={t.id} color={t.color} style={{ background: t.color, color: '#fff', borderColor: t.color }}>{t.name}</Tag>
|
||||
{data.tenants.map((t) => (
|
||||
<Tag
|
||||
key={t.id}
|
||||
color={t.color}
|
||||
style={{ background: t.color, color: '#fff', borderColor: t.color }}
|
||||
>
|
||||
{t.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
@@ -121,7 +194,7 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
<Empty description="暂无教室数据" />
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
{groups.map(group => (
|
||||
{groups.map((group) => (
|
||||
<Card
|
||||
key={group.name}
|
||||
size="small"
|
||||
@@ -132,25 +205,88 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#fafafa' }}>
|
||||
<th style={{ position: 'sticky', left: 0, background: '#fafafa', zIndex: 2, padding: '8px', border: '1px solid #f0f0f0', minWidth: 120, textAlign: 'left' }}>教室</th>
|
||||
<th style={{ padding: '8px 6px', border: '1px solid #f0f0f0', minWidth: 60 }}>类型</th>
|
||||
<th style={{ padding: '8px 6px', border: '1px solid #f0f0f0', minWidth: 70 }}>占用率</th>
|
||||
{Array.from({ length: data.days }, (_, i) => i + 1).map(d => (
|
||||
<th key={d} style={{ padding: '8px 4px', border: '1px solid #f0f0f0', minWidth: 26, textAlign: 'center' }}>{d}</th>
|
||||
<th
|
||||
style={{
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
background: '#fafafa',
|
||||
zIndex: 2,
|
||||
padding: '8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
minWidth: 120,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
教室
|
||||
</th>
|
||||
<th style={{ padding: '8px 6px', border: '1px solid #f0f0f0', minWidth: 60 }}>
|
||||
类型
|
||||
</th>
|
||||
<th style={{ padding: '8px 6px', border: '1px solid #f0f0f0', minWidth: 70 }}>
|
||||
占用率
|
||||
</th>
|
||||
{Array.from({ length: data.days }, (_, i) => i + 1).map((d) => (
|
||||
<th
|
||||
key={d}
|
||||
style={{
|
||||
padding: '8px 4px',
|
||||
border: '1px solid #f0f0f0',
|
||||
minWidth: 26,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{d}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{group.classrooms.map(c => {
|
||||
const sum = data.summary[c.id] || { rentedDays: 0, totalDays: data.days, occupancyRate: 0 };
|
||||
{group.classrooms.map((c) => {
|
||||
const sum = data.summary[c.id] || {
|
||||
rentedDays: 0,
|
||||
totalDays: data.days,
|
||||
occupancyRate: 0,
|
||||
};
|
||||
return (
|
||||
<tr key={c.id}>
|
||||
<td style={{ position: 'sticky', left: 0, background: '#fff', zIndex: 1, padding: '6px 8px', border: '1px solid #f0f0f0', fontWeight: 500 }}>{c.name}</td>
|
||||
<td style={{ padding: '6px', border: '1px solid #f0f0f0', textAlign: 'center' }}>{c.roomType}</td>
|
||||
<td style={{ padding: '6px', border: '1px solid #f0f0f0', textAlign: 'center', color: sum.occupancyRate > 0.7 ? '#cf1322' : sum.occupancyRate > 0.4 ? '#fa8c16' : '#3f8600' }}>
|
||||
<td
|
||||
style={{
|
||||
position: 'sticky',
|
||||
left: 0,
|
||||
background: '#fff',
|
||||
zIndex: 1,
|
||||
padding: '6px 8px',
|
||||
border: '1px solid #f0f0f0',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{c.name}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '6px',
|
||||
border: '1px solid #f0f0f0',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{c.roomType}
|
||||
</td>
|
||||
<td
|
||||
style={{
|
||||
padding: '6px',
|
||||
border: '1px solid #f0f0f0',
|
||||
textAlign: 'center',
|
||||
color:
|
||||
sum.occupancyRate > 0.7
|
||||
? '#cf1322'
|
||||
: sum.occupancyRate > 0.4
|
||||
? '#fa8c16'
|
||||
: '#3f8600',
|
||||
}}
|
||||
>
|
||||
{Math.round(sum.occupancyRate * 100)}%
|
||||
</td>
|
||||
{Array.from({ length: data.days }, (_, i) => i + 1).map(d => {
|
||||
{Array.from({ length: data.days }, (_, i) => i + 1).map((d) => {
|
||||
const cell = data.matrix[c.id]?.[d];
|
||||
return (
|
||||
<td
|
||||
@@ -166,7 +302,9 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
{cell && (
|
||||
<Tooltip title={`${cell.tenantName}${cell.hasContract ? ' · 有合同' : ''}`}>
|
||||
<Tooltip
|
||||
title={`${cell.tenantName}${cell.hasContract ? ' · 有合同' : ''}`}
|
||||
>
|
||||
<span style={{ color: '#fff', fontSize: 10, fontWeight: 600 }}>
|
||||
{cell.hasContract ? '📄' : ''}
|
||||
</span>
|
||||
@@ -195,28 +333,64 @@ const ClassroomSchedulePage: React.FC = () => {
|
||||
>
|
||||
{detailModal && (
|
||||
<div style={{ lineHeight: 2 }}>
|
||||
<div><strong>教室:</strong>{detailModal.classroom?.building} · {detailModal.classroom?.name}({detailModal.classroom?.roomType})</div>
|
||||
<div><strong>租赁方:</strong>
|
||||
<Tag color={detailModal.tenant?.color} style={{ background: detailModal.tenant?.color, color: '#fff', borderColor: detailModal.tenant?.color }}>
|
||||
<div>
|
||||
<strong>教室:</strong>
|
||||
{detailModal.classroom?.building} · {detailModal.classroom?.name}(
|
||||
{detailModal.classroom?.roomType})
|
||||
</div>
|
||||
<div>
|
||||
<strong>租赁方:</strong>
|
||||
<Tag
|
||||
color={detailModal.tenant?.color}
|
||||
style={{
|
||||
background: detailModal.tenant?.color,
|
||||
color: '#fff',
|
||||
borderColor: detailModal.tenant?.color,
|
||||
}}
|
||||
>
|
||||
{detailModal.tenant?.name}
|
||||
</Tag>
|
||||
</div>
|
||||
<div><strong>联系人:</strong>{detailModal.tenant?.contact || '-'} {detailModal.tenant?.phone || ''}</div>
|
||||
<div><strong>起止日期:</strong>{detailModal.startDate} ~ {detailModal.endDate}({dayjs(detailModal.endDate).diff(dayjs(detailModal.startDate), 'day') + 1}天)</div>
|
||||
{detailModal.dailyRate && <div><strong>日租金:</strong>¥{detailModal.dailyRate}</div>}
|
||||
{detailModal.totalAmount && <div><strong>合同总额:</strong>¥{detailModal.totalAmount}</div>}
|
||||
{detailModal.notes && <div><strong>备注:</strong>{detailModal.notes}</div>}
|
||||
<div>
|
||||
<strong>联系人:</strong>
|
||||
{detailModal.tenant?.contact || '-'} {detailModal.tenant?.phone || ''}
|
||||
</div>
|
||||
<div>
|
||||
<strong>起止日期:</strong>
|
||||
{detailModal.startDate} ~ {detailModal.endDate}(
|
||||
{dayjs(detailModal.endDate).diff(dayjs(detailModal.startDate), 'day') + 1}天)
|
||||
</div>
|
||||
{detailModal.dailyRate && (
|
||||
<div>
|
||||
<strong>日租金:</strong>¥{detailModal.dailyRate}
|
||||
</div>
|
||||
)}
|
||||
{detailModal.totalAmount && (
|
||||
<div>
|
||||
<strong>合同总额:</strong>¥{detailModal.totalAmount}
|
||||
</div>
|
||||
)}
|
||||
{detailModal.notes && (
|
||||
<div>
|
||||
<strong>备注:</strong>
|
||||
{detailModal.notes}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<strong>合同文件:</strong>
|
||||
{detailModal.contractPath ? (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => handleDownloadContract(detailModal.id, detailModal.contractOriginalName)}
|
||||
onClick={() =>
|
||||
handleDownloadContract(detailModal.id, detailModal.contractOriginalName)
|
||||
}
|
||||
>
|
||||
{detailModal.contractOriginalName || '下载'}
|
||||
</Button>
|
||||
) : '未上传'}
|
||||
) : (
|
||||
'未上传'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user