feat(web): migrate course adjustment workflow

This commit is contained in:
biss authored and biss committed 2026-10-03 21:38:14 +08:00
1 parent 2ac0a82bb8
commit c5b6954f9c
8 files changed
+209 -1

No files matched your search

+2
View File
@@ -49,6 +49,7 @@ const PersonnelRegistryPage = lazy(() => import('../pages/PersonnelRegistryPage'
const TeachingTasksPage = lazy(() => import('../pages/TeachingTasksPage'))
const SchedulesPage = lazy(() => import('../pages/SchedulesPage'))
const TimetableQueryPage = lazy(() => import('../pages/TimetableQueryPage'))
const CourseAdjustmentsPage = lazy(() => import('../pages/CourseAdjustmentsPage'))
const AccountActivationPage = lazy(() => import('../pages/AccountActivationPage'))
const OfficialDocumentVerifyPage = lazy(() => import('../pages/OfficialDocumentVerifyPage'))
const ClassroomRepairReportPage = lazy(() => import('../pages/ClassroomRepairReportPage'))
@@ -146,6 +147,7 @@ function AppRoutes() {
<Route path="/schedules" element={<SchedulesPage />} />
<Route path="/class-timetable" element={<TimetableQueryPage />} />
<Route path="/teacher-timetable/:teacherId" element={<TimetableQueryPage />} />
<Route path="/course-adjustments" element={<CourseAdjustmentsPage />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Route>
<Route path="*" element={<Navigate to="/dashboard" replace />} />
+1
View File
@@ -110,6 +110,7 @@ export default function AppShell() {
] : []),
...(roles.some((role) => ['Teacher', 'SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)) ? [
{ key: '/teaching-preferences', icon: <ReadOutlined />, label: roles.includes('Teacher') && !roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)) ? '授课科目申报' : '授课资格审核' },
{ key: '/course-adjustments', icon: <SwapOutlined />, label: '调停课管理' },
] : []),
...(roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role)) ? [
{ key: '/teaching-tasks', icon: <ScheduleOutlined />, label: '教学任务' },
@@ -0,0 +1,147 @@
import { useEffect, useMemo, useState } from 'react'
import { CheckOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { Alert, App, Button, Checkbox, Empty, Form, Input, InputNumber, Modal, Radio, Select, Skeleton, Tabs, Tag } from 'antd'
import { useQuery } from '@tanstack/react-query'
import { useSearchParams } from 'react-router-dom'
import http, { apiErrorMessage } from '../api/http'
import PageHeader from '../components/PageHeader'
import { useAuthStore } from '../stores/auth'
import './course-adjustments.css'
type AdjustmentType = 'Reschedule' | 'Cancel' | 'Makeup' | 'Substitute'
type AdjustmentStatus = 'Draft' | 'Submitted' | 'Approved' | 'Rejected'
interface Term { id: string; name: string; academicYear?: string; isCurrent?: boolean; isArchived?: boolean; isEnabled?: boolean }
interface TaskOption { id: string; taskNumber: string; name: string; academicTermId: string; termName: string; courseCode: string; courseName: string; classNames: string[] }
interface TeacherOption { id: string; teacherNumber: string; name: string; title?: string; collegeName: string }
interface Session { scheduleEntryId: string; week: number; date: string; dayOfWeek: number; startPeriod: number; periodCount: number; classroomId?: string; classroomName?: string; buildingName?: string; hasExistingAdjustment: boolean }
interface Classroom { id: string; name: string; buildingName: string; campusName: string; capacity: number }
interface Period { periodNumber: number; name: string; startsAt: string; endsAt: string }
interface SessionOptions { task: { id: string; taskNumber: string; courseName: string; academicTermId: string; termStartDate: string; termEndDate: string }; sessions: Session[]; classrooms: Classroom[]; periods: Period[] }
interface Adjustment {
id: string; teachingTaskId: string; taskNumber: string; taskName: string; courseCode: string; courseName: string; collegeName: string;
teacherNames: string[]; type: AdjustmentType; status: AdjustmentStatus; reason: string; reviewComment?: string;
sourceWeek?: number; sourceDate?: string; sourceStartPeriod?: number; sourcePeriodCount?: number; sourceClassroomName?: string;
targetDate?: string; dayOfWeek?: number; startPeriod?: number; periodCount?: number; classroomName?: string; buildingName?: string;
substituteTeacherName?: string; cancelWeek?: number; submittedAt?: string; createdAt: string
}
interface CursorPage { items: Adjustment[]; nextCursor?: string; hasMore: boolean; pageSize: number }
interface CreateValues {
teachingTaskId: string; sourceKey?: string; sourceScheduleEntryId?: string; sourceWeek?: number; type: AdjustmentType;
reason: string; submit: boolean; targetDate?: string; startPeriod?: number; periodCount?: number; classroomId?: string;
substituteTeacherId?: string
}
const typeLabels: Record<AdjustmentType, string> = { Reschedule: '调课', Cancel: '停课', Makeup: '补课', Substitute: '代课' }
const statusLabels: Record<AdjustmentStatus, string> = { Draft: '草稿', Submitted: '待审核', Approved: '已通过', Rejected: '已退回' }
const typeColors: Record<AdjustmentType, string> = { Reschedule: 'blue', Cancel: 'red', Makeup: 'green', Substitute: 'gold' }
const statusColors: Record<AdjustmentStatus, string> = { Draft: 'default', Submitted: 'orange', Approved: 'green', Rejected: 'red' }
const weekday = (day?: number) => day ? `周${['', '一', '二', '三', '四', '五', '六', '日'][day]}` : ''
const termLabel = (term: Term) => `${term.name}${term.isCurrent ? ' · 当前' : term.isArchived ? ' · 已归档' : ''}`
const defaultTerm = (terms: Term[]) => terms.find((item) => item.isCurrent)?.id ?? terms.find((item) => item.isEnabled !== false && !item.isArchived)?.id ?? terms[0]?.id
const sessionKey = (session: Session) => `${session.scheduleEntryId}:${session.week}`
const sessionLabel = (session: Session) => `第 ${session.week} 周 · ${session.date} ${weekday(session.dayOfWeek)} · 第 ${session.startPeriod}—${session.startPeriod + session.periodCount - 1} 节 · ${[session.buildingName, session.classroomName].filter(Boolean).join(' ') || '无固定教室'}`
function AdjustmentCard({ item, reviewing, busy, onSubmit, onApprove, onReject }: { item: Adjustment; reviewing?: boolean; busy?: boolean; onSubmit: (item: Adjustment) => void; onApprove: (item: Adjustment) => void; onReject: (item: Adjustment) => void }) {
const hasSource = !!item.sourceDate && !!item.sourceStartPeriod && !!item.sourcePeriodCount
const targetRoom = item.classroomName ? [item.buildingName, item.classroomName].filter(Boolean).join(' ') : item.sourceClassroomName || '无固定教室'
return <article className={`adjustment-card status-${item.status.toLowerCase()}`}>
<header><div><span>{item.courseCode} · {item.taskNumber}</span><h3>{item.courseName}</h3><p>{item.teacherNames.join('、') || '未分配教师'} · {item.collegeName}</p></div><div><Tag color={typeColors[item.type]}>{typeLabels[item.type]}</Tag>{!reviewing && <Tag color={statusColors[item.status]}>{statusLabels[item.status]}</Tag>}</div></header>
<div className="adjustment-card__body"><p><b>申请原因:</b>{item.reason}</p>
{hasSource && <div className="adjustment-route"><div><small>原课次</small><strong>第 {item.sourceWeek} 周 · {item.sourceDate} · 第 {item.sourceStartPeriod}—{item.sourceStartPeriod! + item.sourcePeriodCount! - 1} 节</strong><span>{item.sourceClassroomName || '无固定教室'}</span></div><i>{item.type === 'Cancel' ? '×' : '→'}</i>{item.type === 'Cancel' ? <div><small>处理结果</small><strong>仅停掉本次课程</strong><span>其他周次保持不变</span></div> : <div><small>{item.type === 'Makeup' ? '新增补课' : '调整至'}</small><strong>{item.targetDate} · 第 {item.startPeriod}—{(item.startPeriod ?? 0) + (item.periodCount ?? 1) - 1} 节</strong><span>{targetRoom}</span></div>}</div>}
{!hasSource && <div className="adjustment-facts">{item.targetDate && <span><b>日期</b>{item.targetDate}</span>}{item.dayOfWeek && <span><b>星期</b>{weekday(item.dayOfWeek)}</span>}{item.startPeriod && <span><b>节次</b>第 {item.startPeriod}—{item.startPeriod + (item.periodCount ?? 1) - 1} 节</span>}{item.substituteTeacherName && <span><b>代课教师</b>{item.substituteTeacherName}</span>}</div>}
{item.reviewComment && <Alert type="warning" showIcon message={`审核意见:${item.reviewComment}`} />}
<small className="adjustment-time">{item.submittedAt ? `提交于 ${new Date(item.submittedAt).toLocaleString('zh-CN', { hour12: false })}` : `创建于 ${new Date(item.createdAt).toLocaleString('zh-CN', { hour12: false })}`}</small>
</div>
{(reviewing || item.status === 'Draft') && <footer>{reviewing ? <><Button danger disabled={busy} onClick={() => onReject(item)}>退回</Button><Button type="primary" icon={<CheckOutlined />} loading={busy} onClick={() => onApprove(item)}>通过</Button></> : <Button type="primary" loading={busy} onClick={() => onSubmit(item)}>提交审核</Button>}</footer>}
</article>
}
export default function CourseAdjustmentsPage() {
const { message, modal } = App.useApp()
const user = useAuthStore((state) => state.user)
const roles = user?.roles ?? []
const isManager = roles.some((role) => ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'].includes(role))
const [params, setParams] = useSearchParams()
const targetId = params.get('adjustmentId') || undefined
const [activeTab, setActiveTab] = useState(isManager ? 'reviews' : 'mine')
const [termId, setTermId] = useState<string>()
const [status, setStatus] = useState<AdjustmentStatus>()
const [mineCursors, setMineCursors] = useState<string[]>([])
const [reviewCursors, setReviewCursors] = useState<string[]>([])
const [createOpen, setCreateOpen] = useState(false)
const [rejecting, setRejecting] = useState<Adjustment>()
const [rejectComment, setRejectComment] = useState('')
const [busyId, setBusyId] = useState<string>()
const [form] = Form.useForm<CreateValues>()
const type = Form.useWatch('type', form) ?? 'Reschedule'
const taskId = Form.useWatch('teachingTaskId', form)
const sourceKeyValue = Form.useWatch('sourceKey', form)
const targetDate = Form.useWatch('targetDate', form)
const targetPeriod = Form.useWatch('startPeriod', form)
const submitNow = Form.useWatch('submit', form)
const sourceRequired = ['Reschedule', 'Cancel', 'Makeup'].includes(type)
const references = useQuery({
queryKey: ['course-adjustment-references'],
queryFn: async () => {
const [terms, teachers] = await Promise.all([http.get<Term[]>('/base-data/terms'), http.get<TeacherOption[]>('/course-adjustments/teacher-options')])
return { terms: terms.data, teachers: teachers.data }
},
})
useEffect(() => { if (references.data?.terms.length) setTermId((current) => current ?? defaultTerm(references.data!.terms)) }, [references.data?.terms])
useEffect(() => {
if (targetId && isManager) setActiveTab('reviews')
if (params.get('create') === '1') { setCreateOpen(true); setParams({}, { replace: true }) }
}, [targetId, isManager]) // eslint-disable-line react-hooks/exhaustive-deps
const tasks = useQuery({ queryKey: ['course-adjustment-tasks', termId], enabled: references.isSuccess, queryFn: async () => (await http.get<TaskOption[]>('/course-adjustments/task-options', { params: { academicTermId: termId } })).data })
const sessions = useQuery({ queryKey: ['course-adjustment-sessions', taskId], enabled: !!taskId && createOpen, queryFn: async () => (await http.get<SessionOptions>(`/course-adjustments/tasks/${taskId}/session-options`)).data })
const mineCursor = mineCursors.at(-1)
const reviewCursor = reviewCursors.at(-1)
const mine = useQuery({ queryKey: ['course-adjustments-mine', termId, status, mineCursor], enabled: activeTab === 'mine', queryFn: async () => (await http.get<CursorPage>('/course-adjustments/mine', { params: { academicTermId: termId, status, cursor: mineCursor, pageSize: 20 } })).data })
const reviews = useQuery({ queryKey: ['course-adjustments-reviews', targetId, termId, reviewCursor], enabled: isManager && activeTab === 'reviews', queryFn: async () => (await http.get<CursorPage>('/course-adjustments/pending-reviews', { params: { adjustmentId: targetId, academicTermId: targetId ? undefined : termId, cursor: reviewCursor, pageSize: 20 } })).data })
const selectedSession = useMemo(() => sessions.data?.sessions.find((item) => sessionKey(item) === sourceKeyValue), [sessions.data?.sessions, sourceKeyValue])
const periods = sessions.data?.periods.length ? sessions.data.periods.map((item) => ({ value: item.periodNumber, label: `第 ${item.periodNumber} 节 · ${item.name} ${item.startsAt}—${item.endsAt}` })) : Array.from({ length: 12 }, (_, index) => ({ value: index + 1, label: `第 ${index + 1} 节` }))
function openCreate() {
form.resetFields()
form.setFieldsValue({ type: 'Reschedule', submit: true, periodCount: 2 })
setCreateOpen(true)
}
function resetPages(nextTerm?: string) { setTermId(nextTerm); setMineCursors([]); setReviewCursors([]) }
async function refresh() { await Promise.all([mine.refetch(), isManager ? reviews.refetch() : Promise.resolve()]) }
async function create(values: CreateValues) {
if (selectedSession?.hasExistingAdjustment) { message.warning('该课次已有待审核或已通过的申请'); return }
const { sourceKey: _, ...payload } = values
try { setBusyId('create'); await http.post('/course-adjustments', { ...payload, targetDate: payload.targetDate || null, startPeriod: payload.startPeriod || null, classroomId: payload.classroomId || null, substituteTeacherId: payload.substituteTeacherId || null }); message.success(payload.submit ? '申请已提交审核' : '草稿已保存'); setCreateOpen(false); setActiveTab('mine'); await mine.refetch() }
catch (error) { message.error(apiErrorMessage(error)) } finally { setBusyId(undefined) }
}
async function submitDraft(item: Adjustment) { try { setBusyId(item.id); await http.post(`/course-adjustments/${item.id}/submit`); message.success('已提交审核'); await refresh() } catch (error) { message.error(apiErrorMessage(error)) } finally { setBusyId(undefined) } }
function approve(item: Adjustment) { modal.confirm({ title: `通过${typeLabels[item.type]}申请?`, content: '服务端会再次检查教师、班级和教室冲突;通过后将立即更新正式课表并通知相关人员。', okText: '确认通过', onOk: async () => { try { setBusyId(item.id); await http.post(`/course-adjustments/${item.id}/approve`); message.success('申请已通过'); await refresh() } catch (error) { message.error(apiErrorMessage(error)); throw error } finally { setBusyId(undefined) } } }) }
async function reject() { if (!rejecting) return; try { setBusyId(rejecting.id); await http.post(`/course-adjustments/${rejecting.id}/reject`, { comment: rejectComment.trim() || null }); message.success('申请已退回'); setRejecting(undefined); setRejectComment(''); await refresh() } catch (error) { message.error(apiErrorMessage(error)) } finally { setBusyId(undefined) } }
const page = activeTab === 'reviews' ? reviews : mine
const data = page.data?.items ?? []
const referenceData = references.data ?? { terms: [], teachers: [] }
if (references.isLoading) return <main className="page-container"><Skeleton active paragraph={{ rows: 10 }} /></main>
if (references.isError) return <main className="page-container"><Alert type="error" showIcon message="调停课基础数据无法加载" description={apiErrorMessage(references.error)} /></main>
return <main className="page-container course-adjustments-page">
<PageHeader kicker="COURSE ADJUSTMENT" title="调停课管理" description="按具体课次发起调课、停课与补课,或为教学班申请代课教师;审核通过后同步正式课表并通知相关人员。" actions={<div className="adjustment-actions"><Button icon={<ReloadOutlined />} onClick={() => void refresh()}>刷新</Button><Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>提交申请</Button></div>} />
<section className="adjustment-control"><Select allowClear value={termId} placeholder="全部学期" options={referenceData.terms.map((item) => ({ value: item.id, label: termLabel(item), disabled: item.isEnabled === false }))} onChange={resetPages} />{activeTab === 'mine' && <Select allowClear value={status} placeholder="全部状态" options={Object.entries(statusLabels).map(([value, label]) => ({ value, label }))} onChange={(value) => { setStatus(value); setMineCursors([]) }} />}<span>{targetId && activeTab === 'reviews' ? '已按通知定位单条待审核申请' : '每批最多显示 20 条'}</span></section>
<Tabs activeKey={activeTab} onChange={setActiveTab} items={[{ key: 'mine', label: '我的申请' }, ...(isManager ? [{ key: 'reviews', label: '待我审核' }] : [])]} />
{page.isError ? <Alert type="error" showIcon message="调停课记录无法加载" description={apiErrorMessage(page.error)} /> : page.isLoading ? <Skeleton active paragraph={{ rows: 8 }} /> : <section className="adjustment-list">{data.map((item) => <AdjustmentCard key={item.id} item={item} reviewing={activeTab === 'reviews'} busy={busyId === item.id} onSubmit={(value) => void submitDraft(value)} onApprove={approve} onReject={(value) => { setRejecting(value); setRejectComment('') }} />)}{!data.length && <Empty description={activeTab === 'reviews' ? '暂无待审核申请' : '暂无调停课申请'} />}</section>}
{((activeTab === 'mine' ? mineCursors : reviewCursors).length > 0 || page.data?.hasMore) && <footer className="adjustment-pager"><Button disabled={page.isFetching || !(activeTab === 'mine' ? mineCursors : reviewCursors).length} onClick={() => activeTab === 'mine' ? setMineCursors((items) => items.slice(0, -1)) : setReviewCursors((items) => items.slice(0, -1))}>上一批</Button><span>第 {(activeTab === 'mine' ? mineCursors : reviewCursors).length + 1} 批</span><Button type="primary" disabled={page.isFetching || !page.data?.hasMore} onClick={() => { const cursor = page.data?.nextCursor; if (!cursor) return; activeTab === 'mine' ? setMineCursors((items) => [...items, cursor]) : setReviewCursors((items) => [...items, cursor]) }}>下一批</Button></footer>}
<Modal open={createOpen} title="发起单次课程调整" width={780} okText={submitNow === false ? '保存草稿' : '提交审核'} confirmLoading={busyId === 'create'} onCancel={() => setCreateOpen(false)} onOk={() => form.submit()}>
<Form form={form} layout="vertical" className="adjustment-form" onFinish={(values) => void create(values)}>
<section><header><b>1</b><div><strong>选择业务类型</strong><span>每次申请只处理一个具体事项。</span></div></header><Form.Item name="type" label="申请类型" rules={[{ required: true }]}><Radio.Group optionType="button" buttonStyle="solid" options={Object.entries(typeLabels).map(([value, label]) => ({ value, label }))} onChange={() => form.setFieldsValue({ targetDate: undefined, startPeriod: undefined, classroomId: selectedSession?.classroomId, substituteTeacherId: undefined })} /></Form.Item><Alert type="info" showIcon message={type === 'Reschedule' ? '把选中的原课次移到另一个日期和节次。' : type === 'Cancel' ? '只停掉选中的这一次课,不影响其他周次。' : type === 'Makeup' ? '保留原课次,另外增加一次补课。' : '为教学班增加一位在职代课教师。'} /></section>
<section><header><b>2</b><div><strong>{sourceRequired ? '锁定原课次' : '选择教学班'}</strong><span>{sourceRequired ? '从已发布课表中选择,不再手填周次和星期。' : '代课申请将在审核通过后把教师加入教学班。'}</span></div></header><Form.Item name="teachingTaskId" label="教学班" rules={[{ required: true, message: '请选择教学班' }]}><Select showSearch optionFilterProp="label" loading={tasks.isLoading} options={(tasks.data ?? []).map((item) => ({ value: item.id, label: `${item.taskNumber} · ${item.courseName} · ${item.classNames.join('、') || '未关联行政班'}` }))} onChange={() => form.setFieldsValue({ sourceKey: undefined, sourceScheduleEntryId: undefined, sourceWeek: undefined, classroomId: undefined })} /></Form.Item>{sourceRequired && <Form.Item name="sourceKey" label="原课次" rules={[{ required: true, message: '请选择要处理的原课次' }]}><Select showSearch optionFilterProp="label" loading={sessions.isLoading} placeholder="选择第几周、哪一天、哪一节课" options={(sessions.data?.sessions ?? []).map((item) => ({ value: sessionKey(item), label: `${sessionLabel(item)}${item.hasExistingAdjustment ? ' · 已有申请' : ''}`, disabled: item.hasExistingAdjustment }))} onChange={(value) => { const selected = sessions.data?.sessions.find((item) => sessionKey(item) === value); if (selected) form.setFieldsValue({ sourceScheduleEntryId: selected.scheduleEntryId, sourceWeek: selected.week, periodCount: selected.periodCount, classroomId: selected.classroomId }) }} /></Form.Item>}{taskId && sourceRequired && !sessions.isLoading && !sessions.data?.sessions.length && <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="该教学班没有可用的已发布课次" />}</section>
{(type === 'Reschedule' || type === 'Makeup') && <section className="adjustment-target"><header><b>3</b><div><strong>{type === 'Reschedule' ? '安排调课后课次' : '安排补课课次'}</strong><span>课程时长沿用原课次,审批时再次检查教师、班级和教室冲突。</span></div></header>{selectedSession && <div className="adjustment-preview"><span>{sessionLabel(selectedSession)}</span><i>→</i><span>{targetDate || '待选日期'} · {targetPeriod ? `第 ${targetPeriod} 节起` : '待选节次'}</span></div>}<div className="adjustment-grid"><Form.Item name="targetDate" label="目标日期" rules={[{ required: true, message: '请选择目标日期' }]}><Input type="date" min={sessions.data?.task.termStartDate} max={sessions.data?.task.termEndDate} /></Form.Item><Form.Item name="startPeriod" label="目标起始节次" rules={[{ required: true, message: '请选择目标起始节次' }]}><Select options={periods} /></Form.Item></div><Form.Item name="classroomId" label="目标教室"><Select allowClear showSearch optionFilterProp="label" placeholder="留空则沿用原教室" options={(sessions.data?.classrooms ?? []).map((item) => ({ value: item.id, label: `${item.campusName} · ${item.buildingName} ${item.name} · ${item.capacity} 人` }))} /></Form.Item></section>}
{type === 'Substitute' && <section><header><b>3</b><div><strong>选择代课教师</strong><span>仅列出在职且未归档教师。</span></div></header><Form.Item name="substituteTeacherId" label="代课教师" rules={[{ required: true, message: '请选择代课教师' }]}><Select showSearch optionFilterProp="label" options={referenceData.teachers.map((item) => ({ value: item.id, label: `${item.teacherNumber} · ${item.name}${item.title ? ` · ${item.title}` : ''} · ${item.collegeName}` }))} /></Form.Item></section>}
<Form.Item name="reason" label="申请原因" rules={[{ required: true, whitespace: true, message: '请填写申请原因' }]}><Input.TextArea rows={3} maxLength={500} showCount placeholder="请详细说明调停课原因" /></Form.Item><Form.Item name="submit" valuePropName="checked"><Checkbox>保存后直接提交审核</Checkbox></Form.Item><Form.Item name="sourceScheduleEntryId" hidden><Input /></Form.Item><Form.Item name="sourceWeek" hidden><InputNumber /></Form.Item><Form.Item name="periodCount" hidden><InputNumber /></Form.Item>
</Form>
</Modal>
<Modal open={!!rejecting} title={`退回${rejecting ? typeLabels[rejecting.type] : ''}申请`} okText="确认退回" okButtonProps={{ danger: true }} confirmLoading={busyId === rejecting?.id} onCancel={() => setRejecting(undefined)} onOk={() => void reject()}><Alert type="warning" showIcon message="退回后申请人可以看到审核意见,但当前版本不支持编辑原申请。" /><label className="reject-field">退回意见<Input.TextArea rows={4} maxLength={500} showCount value={rejectComment} onChange={(event) => setRejectComment(event.target.value)} placeholder="说明冲突或需要补充的材料" /></label></Modal>
</main>
}
Loaded 3 of 8 files, more files were not shown because too many files have changed in this diff. Show more