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 ClassroomRentalsPage from './pages/ClassroomRentals';
|
||||
import ClassroomSchedulePage from './pages/ClassroomSchedule';
|
||||
import SchedulesPage from './pages/Schedules';
|
||||
import RolesPage from './pages/Roles';
|
||||
import PermissionsPage from './pages/Permissions';
|
||||
import PermissionRoute from './components/PermissionRoute';
|
||||
@@ -200,6 +201,15 @@ const App: React.FC = () => {
|
||||
</PermissionRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="schedules"
|
||||
element={
|
||||
<PermissionRoute permission="schedule:view">
|
||||
<SchedulesPage />
|
||||
</PermissionRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -77,6 +77,7 @@ const allMenuItems: MenuItemType[] = [
|
||||
{ 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: '/roles', icon: <SafetyOutlined />, 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