feat(schedules): implement frontend schedule week view page
- Create SchedulesPage with week selector, classroom/class filters - Matrix: rows=classrooms, cols=Mon-Sun, cells show subject/teacher/time - Click empty cell -> create modal with schedule form - Click occupied cell -> detail modal with delete button - Conflict: backend 409/400 -> message.error with conflict info - Route /schedules with PermissionRoute (schedule:view) - Menu item '排课管理' with CalendarOutlined icon
This commit is contained in:
@@ -20,6 +20,7 @@ import ClassDetailPage from './pages/Classes/detail';
|
|||||||
import TenantsPage from './pages/Tenants';
|
import TenantsPage from './pages/Tenants';
|
||||||
import ClassroomRentalsPage from './pages/ClassroomRentals';
|
import ClassroomRentalsPage from './pages/ClassroomRentals';
|
||||||
import ClassroomSchedulePage from './pages/ClassroomSchedule';
|
import ClassroomSchedulePage from './pages/ClassroomSchedule';
|
||||||
|
import SchedulesPage from './pages/Schedules';
|
||||||
import RolesPage from './pages/Roles';
|
import RolesPage from './pages/Roles';
|
||||||
import PermissionsPage from './pages/Permissions';
|
import PermissionsPage from './pages/Permissions';
|
||||||
import PermissionRoute from './components/PermissionRoute';
|
import PermissionRoute from './components/PermissionRoute';
|
||||||
@@ -200,6 +201,15 @@ const App: React.FC = () => {
|
|||||||
</PermissionRoute>
|
</PermissionRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="schedules"
|
||||||
|
element={
|
||||||
|
<PermissionRoute permission="schedule:view">
|
||||||
|
<SchedulesPage />
|
||||||
|
</PermissionRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const allMenuItems: MenuItemType[] = [
|
|||||||
{ key: '/tenants', icon: <TagsOutlined />, label: '租赁方', permission: 'tenant:view' },
|
{ key: '/tenants', icon: <TagsOutlined />, label: '租赁方', permission: 'tenant:view' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{ key: '/schedules', icon: <CalendarOutlined />, label: '排课管理', permission: 'schedule:view' },
|
||||||
{ key: '/operation-logs', icon: <AuditOutlined />, label: '操作日志', permission: 'log:view' },
|
{ key: '/operation-logs', icon: <AuditOutlined />, label: '操作日志', permission: 'log:view' },
|
||||||
{ key: '/roles', icon: <SafetyOutlined />, label: '角色管理', permission: 'role:view' },
|
{ key: '/roles', icon: <SafetyOutlined />, label: '角色管理', permission: 'role:view' },
|
||||||
{ key: '/permissions', icon: <KeyOutlined />, label: '权限一览', permission: 'role:view' },
|
{ key: '/permissions', icon: <KeyOutlined />, label: '权限一览', permission: 'role:view' },
|
||||||
|
|||||||
622
apps/admin/src/pages/Schedules/index.tsx
Normal file
622
apps/admin/src/pages/Schedules/index.tsx
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
import React, { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Card, Button, Select, Modal, Form, Input, DatePicker, TimePicker,
|
||||||
|
Popconfirm, message, Space, Spin, Empty, Tag, Tooltip,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
CalendarOutlined,
|
||||||
|
LeftOutlined,
|
||||||
|
RightOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import api from '../../api';
|
||||||
|
import PermissionButton from '../../components/PermissionButton';
|
||||||
|
|
||||||
|
// ---- Types ----
|
||||||
|
|
||||||
|
interface ClassScheduleItem {
|
||||||
|
id: number;
|
||||||
|
classId: number;
|
||||||
|
classroomId: number;
|
||||||
|
weekDay: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
subject: string;
|
||||||
|
teacherId: number | null;
|
||||||
|
scheduleType: string;
|
||||||
|
status: string;
|
||||||
|
notes: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClassroomItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
building: string;
|
||||||
|
floor: number;
|
||||||
|
roomType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClassItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScheduleFormValues {
|
||||||
|
classId: number;
|
||||||
|
subject: string;
|
||||||
|
teacherId?: number;
|
||||||
|
timeRange: [Dayjs, Dayjs];
|
||||||
|
dateRange: [Dayjs, Dayjs];
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEEKDAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||||
|
const WEEKDAY_NUMBERS = [1, 2, 3, 4, 5, 6, 7];
|
||||||
|
|
||||||
|
// ---- Component ----
|
||||||
|
|
||||||
|
const SchedulesPage: React.FC = () => {
|
||||||
|
// Week navigation
|
||||||
|
const [weekStart, setWeekStart] = useState<Dayjs>(() => dayjs().weekday(1).startOf('day'));
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const [classrooms, setClassrooms] = useState<ClassroomItem[]>([]);
|
||||||
|
const [classes, setClasses] = useState<ClassItem[]>([]);
|
||||||
|
const [matrix, setMatrix] = useState<Record<number, Record<number, ClassScheduleItem[]>>>({});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const [filterClassroomIds, setFilterClassroomIds] = useState<number[]>([]);
|
||||||
|
const [filterClassId, setFilterClassId] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [modalMode, setModalMode] = useState<'create' | 'detail'>('create');
|
||||||
|
const [selectedCell, setSelectedCell] = useState<{
|
||||||
|
classroomId: number;
|
||||||
|
weekDay: number;
|
||||||
|
} | null>(null);
|
||||||
|
const [selectedSchedules, setSelectedSchedules] = useState<ClassScheduleItem[]>([]);
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [form] = Form.useForm<ScheduleFormValues>();
|
||||||
|
|
||||||
|
// Derived week info
|
||||||
|
const weekEnd = useMemo(() => weekStart.add(6, 'day'), [weekStart]);
|
||||||
|
const weekNum = useMemo(() => weekStart.week(), [weekStart]);
|
||||||
|
const weekYear = useMemo(() => weekStart.year(), [weekStart]);
|
||||||
|
const startDateStr = useMemo(() => weekStart.format('YYYY-MM-DD'), [weekStart]);
|
||||||
|
const endDateStr = useMemo(() => weekEnd.format('YYYY-MM-DD'), [weekEnd]);
|
||||||
|
|
||||||
|
// ---- Data fetching ----
|
||||||
|
|
||||||
|
const fetchData = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const [classroomsRes, classesRes, schedulesRes] = await Promise.all([
|
||||||
|
api.get('/classrooms') as Promise<ClassroomItem[]>,
|
||||||
|
api.get('/classes') as Promise<ClassItem[]>,
|
||||||
|
api.get('/class-schedules/weekly', {
|
||||||
|
params: {
|
||||||
|
startDate: startDateStr,
|
||||||
|
endDate: endDateStr,
|
||||||
|
...(filterClassroomIds.length === 1 ? { classroomId: filterClassroomIds[0] } : {}),
|
||||||
|
},
|
||||||
|
}) as Promise<Record<string, Record<string, ClassScheduleItem[]>>>,
|
||||||
|
]);
|
||||||
|
|
||||||
|
setClassrooms(classroomsRes);
|
||||||
|
setClasses(classesRes);
|
||||||
|
|
||||||
|
// Convert string keys to numbers
|
||||||
|
const typedMatrix: Record<number, Record<number, ClassScheduleItem[]>> = {};
|
||||||
|
for (const [cId, dayMap] of Object.entries(schedulesRes)) {
|
||||||
|
const classroomId = Number(cId);
|
||||||
|
typedMatrix[classroomId] = {};
|
||||||
|
for (const [wd, schedules] of Object.entries(dayMap)) {
|
||||||
|
typedMatrix[classroomId][Number(wd)] = schedules;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setMatrix(typedMatrix);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '加载排课数据失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [startDateStr, endDateStr, filterClassroomIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
// ---- Filtered classrooms ----
|
||||||
|
|
||||||
|
const filteredClassrooms = useMemo(() => {
|
||||||
|
if (filterClassroomIds.length === 0) return classrooms;
|
||||||
|
const idSet = new Set(filterClassroomIds);
|
||||||
|
return classrooms.filter((c) => idSet.has(c.id));
|
||||||
|
}, [classrooms, filterClassroomIds]);
|
||||||
|
|
||||||
|
// Apply class filter to the matrix
|
||||||
|
const displayMatrix = useMemo(() => {
|
||||||
|
if (filterClassId == null) return matrix;
|
||||||
|
const filtered: Record<number, Record<number, ClassScheduleItem[]>> = {};
|
||||||
|
for (const [cId, dayMap] of Object.entries(matrix)) {
|
||||||
|
const classroomId = Number(cId);
|
||||||
|
filtered[classroomId] = {};
|
||||||
|
for (const [wd, schedules] of Object.entries(dayMap)) {
|
||||||
|
const matched = schedules.filter((s) => s.classId === filterClassId);
|
||||||
|
if (matched.length > 0) {
|
||||||
|
filtered[classroomId][Number(wd)] = matched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}, [matrix, filterClassId]);
|
||||||
|
|
||||||
|
// ---- Cell click handlers ----
|
||||||
|
const handleCellClick = (classroomId: number, weekDay: number) => {
|
||||||
|
const schedules = displayMatrix[classroomId]?.[weekDay] || [];
|
||||||
|
setSelectedCell({ classroomId, weekDay });
|
||||||
|
|
||||||
|
if (schedules.length > 0) {
|
||||||
|
setSelectedSchedules(schedules);
|
||||||
|
setModalMode('detail');
|
||||||
|
setModalOpen(true);
|
||||||
|
} else {
|
||||||
|
setSelectedSchedules([]);
|
||||||
|
setModalMode('create');
|
||||||
|
setModalOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Create schedule ----
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!selectedCell) return;
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
classId: values.classId,
|
||||||
|
subject: values.subject,
|
||||||
|
teacherId: values.teacherId,
|
||||||
|
classroomId: selectedCell.classroomId,
|
||||||
|
weekDay: selectedCell.weekDay,
|
||||||
|
startTime: values.timeRange[0].format('HH:mm'),
|
||||||
|
endTime: values.timeRange[1].format('HH:mm'),
|
||||||
|
startDate: values.dateRange[0].format('YYYY-MM-DD'),
|
||||||
|
endDate: values.dateRange[1].format('YYYY-MM-DD'),
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.post('/class-schedules', payload);
|
||||||
|
message.success('排课创建成功');
|
||||||
|
setModalOpen(false);
|
||||||
|
fetchData();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const err = e as { message?: string; status?: number };
|
||||||
|
message.error(err?.message || '创建排课失败');
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ---- Delete schedule ----
|
||||||
|
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/class-schedules/${id}`);
|
||||||
|
message.success('排课已删除');
|
||||||
|
// Refresh the cell's schedules
|
||||||
|
if (selectedCell) {
|
||||||
|
const remaining = selectedSchedules.filter((s) => s.id !== id);
|
||||||
|
setSelectedSchedules(remaining);
|
||||||
|
if (remaining.length === 0) {
|
||||||
|
setModalOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const err = e as { message?: string };
|
||||||
|
message.error(err?.message || '删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Classroom select options ----
|
||||||
|
|
||||||
|
const classroomOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
classrooms.map((c) => ({
|
||||||
|
value: c.id,
|
||||||
|
label: `${c.building ? c.building + ' · ' : ''}${c.name}`,
|
||||||
|
})),
|
||||||
|
[classrooms],
|
||||||
|
);
|
||||||
|
|
||||||
|
const classOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
classes.map((c) => ({
|
||||||
|
value: c.id,
|
||||||
|
label: `${c.name}${c.code ? ` (${c.code})` : ''}`,
|
||||||
|
})),
|
||||||
|
[classes],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---- Render ----
|
||||||
|
|
||||||
|
const selectedClassroom = selectedCell
|
||||||
|
? classrooms.find((c) => c.id === selectedCell.classroomId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Header */}
|
||||||
|
<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
|
||||||
|
icon={<LeftOutlined />}
|
||||||
|
onClick={() => setWeekStart(weekStart.subtract(7, 'day'))}
|
||||||
|
>
|
||||||
|
上一周
|
||||||
|
</Button>
|
||||||
|
<span style={{ fontWeight: 500, fontSize: 15 }}>
|
||||||
|
{weekYear} W{weekNum}
|
||||||
|
<span style={{ color: '#8c8c8c', fontWeight: 400, fontSize: 13, marginLeft: 6 }}>
|
||||||
|
({startDateStr} ~ {endDateStr})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
icon={<RightOutlined />}
|
||||||
|
onClick={() => setWeekStart(weekStart.add(7, 'day'))}
|
||||||
|
>
|
||||||
|
下一周
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
|
<Space wrap>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="筛选教室"
|
||||||
|
allowClear
|
||||||
|
style={{ minWidth: 200 }}
|
||||||
|
value={filterClassroomIds}
|
||||||
|
onChange={(v) => setFilterClassroomIds(v)}
|
||||||
|
options={classroomOptions}
|
||||||
|
maxTagCount={3}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="筛选班级"
|
||||||
|
allowClear
|
||||||
|
style={{ minWidth: 160 }}
|
||||||
|
value={filterClassId}
|
||||||
|
onChange={(v) => setFilterClassId(v)}
|
||||||
|
options={classOptions}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Matrix */}
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
{classrooms.length === 0 ? (
|
||||||
|
<Empty description="暂无教室数据" />
|
||||||
|
) : (
|
||||||
|
<div style={{ overflowX: 'auto' }}>
|
||||||
|
<table
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
borderCollapse: 'collapse',
|
||||||
|
fontSize: 13,
|
||||||
|
tableLayout: 'fixed',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ background: '#fafafa' }}>
|
||||||
|
<th
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0,
|
||||||
|
background: '#fafafa',
|
||||||
|
zIndex: 2,
|
||||||
|
padding: '10px 12px',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
width: 150,
|
||||||
|
textAlign: 'left',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
教室
|
||||||
|
</th>
|
||||||
|
{WEEKDAYS.map((day, idx) => (
|
||||||
|
<th
|
||||||
|
key={day}
|
||||||
|
style={{
|
||||||
|
padding: '10px 8px',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
textAlign: 'center',
|
||||||
|
background: '#fafafa',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredClassrooms.map((classroom) => (
|
||||||
|
<tr key={classroom.id}>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0,
|
||||||
|
background: '#fff',
|
||||||
|
zIndex: 1,
|
||||||
|
padding: '8px 12px',
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{classroom.name}</div>
|
||||||
|
{classroom.building && (
|
||||||
|
<div style={{ fontSize: 11, color: '#8c8c8c', marginTop: 2 }}>
|
||||||
|
{classroom.building}
|
||||||
|
{classroom.floor ? ` ${classroom.floor}F` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
{WEEKDAY_NUMBERS.map((wd) => {
|
||||||
|
const schedules = displayMatrix[classroom.id]?.[wd] || [];
|
||||||
|
const hasContent = schedules.length > 0;
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={wd}
|
||||||
|
onClick={() => handleCellClick(classroom.id, wd)}
|
||||||
|
style={{
|
||||||
|
padding: 4,
|
||||||
|
border: '1px solid #f0f0f0',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
cursor: 'pointer',
|
||||||
|
minHeight: 56,
|
||||||
|
transition: 'background 0.15s',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
(e.currentTarget as HTMLElement).style.background = '#f6f8fa';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
(e.currentTarget as HTMLElement).style.background = '';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasContent ? (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
{schedules.map((s) => (
|
||||||
|
<Tooltip
|
||||||
|
key={s.id}
|
||||||
|
title={`${s.subject} · ${s.startTime}-${s.endTime} · ${s.startDate}~${s.endDate}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: '#e6f4ff',
|
||||||
|
border: '1px solid #91caff',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: '2px 6px',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: '18px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 600, color: '#1677ff' }}>
|
||||||
|
{s.subject}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: '#595959' }}>
|
||||||
|
{s.startTime}-{s.endTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: '#d9d9d9',
|
||||||
|
fontSize: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: '44px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
modalMode === 'create'
|
||||||
|
? `新增排课 — ${selectedClassroom?.name || ''} · ${selectedCell ? WEEKDAYS[selectedCell.weekDay - 1] : ''}`
|
||||||
|
: `排课详情 — ${selectedClassroom?.name || ''} · ${selectedCell ? WEEKDAYS[selectedCell.weekDay - 1] : ''}`
|
||||||
|
}
|
||||||
|
open={modalOpen}
|
||||||
|
onCancel={() => setModalOpen(false)}
|
||||||
|
onOk={modalMode === 'create' ? handleSubmit : undefined}
|
||||||
|
confirmLoading={submitting}
|
||||||
|
okText={modalMode === 'create' ? '创建' : undefined}
|
||||||
|
footer={
|
||||||
|
modalMode === 'create'
|
||||||
|
? undefined // use default ok/cancel
|
||||||
|
: null // no footer for detail mode
|
||||||
|
}
|
||||||
|
width={600}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
{modalMode === 'create' ? (
|
||||||
|
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||||
|
<Form.Item
|
||||||
|
name="classId"
|
||||||
|
label="班级"
|
||||||
|
rules={[{ required: true, message: '请选择班级' }]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="选择班级"
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
options={classOptions}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="subject"
|
||||||
|
label="科目"
|
||||||
|
rules={[{ required: true, message: '请输入科目' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="如:数学、语文" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="teacherId"
|
||||||
|
label="教师ID(可选)"
|
||||||
|
normalize={(v) => (v ? Number(v) : undefined)}
|
||||||
|
>
|
||||||
|
<Input type="number" placeholder="输入教师用户ID" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="教室">
|
||||||
|
<Input value={selectedClassroom?.name || ''} disabled />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="星期">
|
||||||
|
<Input
|
||||||
|
value={selectedCell ? WEEKDAYS[selectedCell.weekDay - 1] : ''}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="timeRange"
|
||||||
|
label="上课时段"
|
||||||
|
rules={[{ required: true, message: '请选择时段' }]}
|
||||||
|
>
|
||||||
|
<TimePicker.RangePicker
|
||||||
|
format="HH:mm"
|
||||||
|
minuteStep={10}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={['开始时间', '结束时间']}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="dateRange"
|
||||||
|
label="日期范围"
|
||||||
|
rules={[{ required: true, message: '请选择日期范围' }]}
|
||||||
|
>
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={['开始日期', '结束日期']}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<div style={{ lineHeight: 2 }}>
|
||||||
|
{selectedSchedules.length === 0 ? (
|
||||||
|
<Empty description="该时段暂无排课" />
|
||||||
|
) : (
|
||||||
|
selectedSchedules.map((s) => (
|
||||||
|
<Card
|
||||||
|
key={s.id}
|
||||||
|
size="small"
|
||||||
|
style={{ marginBottom: 8 }}
|
||||||
|
styles={{ body: { padding: 12 } }}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<strong>科目:</strong>
|
||||||
|
<Tag color="blue">{s.subject}</Tag>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>班级:</strong>
|
||||||
|
{classes.find((c) => c.id === s.classId)?.name || `#${s.classId}`}
|
||||||
|
</div>
|
||||||
|
{s.teacherId != null && (
|
||||||
|
<div>
|
||||||
|
<strong>教师ID:</strong>
|
||||||
|
{s.teacherId}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<strong>时段:</strong>
|
||||||
|
{s.startTime} ~ {s.endTime}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>日期:</strong>
|
||||||
|
{s.startDate} ~ {s.endDate}
|
||||||
|
</div>
|
||||||
|
{s.notes && (
|
||||||
|
<div>
|
||||||
|
<strong>备注:</strong>
|
||||||
|
{s.notes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Tag color={s.scheduleType === 'RENTAL' ? 'orange' : 'green'}>
|
||||||
|
{s.scheduleType === 'RENTAL' ? '租赁' : '内部'}
|
||||||
|
</Tag>
|
||||||
|
<Tag color={s.status === 'active' ? 'green' : 'default'}>{s.status}</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认删除该排课?"
|
||||||
|
onConfirm={() => handleDelete(s.id)}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<PermissionButton
|
||||||
|
permission="schedule:delete"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</PermissionButton>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SchedulesPage;
|
||||||
Reference in New Issue
Block a user