zt-admin-template 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +11 -0
  2. package/template/.env.development +2 -0
  3. package/template/.env.production +2 -0
  4. package/template/.env.test +2 -0
  5. package/template/.kiro/specs/course-backend-integration/.config.kiro +1 -0
  6. package/template/.kiro/specs/course-backend-integration/design.md +234 -0
  7. package/template/.kiro/specs/course-backend-integration/requirements.md +116 -0
  8. package/template/.kiro/specs/course-backend-integration/tasks.md +0 -0
  9. package/template/COMPLETION_CHECKLIST.md +305 -0
  10. package/template/DEPLOYMENT_GUIDE.md +391 -0
  11. package/template/FINAL_SUMMARY.md +428 -0
  12. package/template/IMPLEMENTATION_SUMMARY.md +382 -0
  13. package/template/INTEGRATION_GUIDE.md +458 -0
  14. package/template/PROJECT_OVERVIEW.md +343 -0
  15. package/template/QUICK_START.md +273 -0
  16. package/template/RBAC_Tutorial.md +424 -0
  17. package/template/README.md +16 -0
  18. package/template/React_Antd_TS_Tutorial.md +279 -0
  19. package/template/START_ALL.md +163 -0
  20. package/template/SYSTEM_MANAGEMENT.md +247 -0
  21. package/template/eslint.config.js +29 -0
  22. package/template/index.html +13 -0
  23. package/template/koa-server/README.md +65 -0
  24. package/template/koa-server/app.js +625 -0
  25. package/template/koa-server/package-lock.json +1547 -0
  26. package/template/koa-server/package.json +26 -0
  27. package/template/koa-server/public/assets/index-B1Cj4mG9.css +1 -0
  28. package/template/koa-server/public/assets/index-Mgxg-xqT.js +503 -0
  29. package/template/koa-server/public/favicon.svg +1 -0
  30. package/template/koa-server/public/icons.svg +24 -0
  31. package/template/koa-server/public/index.html +14 -0
  32. package/template/koa-server/uploads/1774265088480-962006467.png +0 -0
  33. package/template/koa-server/uploads/file-1774346891704-610962013.png +0 -0
  34. package/template/koa-server/uploads/file-1774346898887-58636533.png +0 -0
  35. package/template/koa-server/uploads/file-1774346912676-771862547.png +0 -0
  36. package/template/koa-server/uploads/file-1774347025308-130037894.png +0 -0
  37. package/template/koa-server/uploads/file-1774347031104-766499773.png +0 -0
  38. package/template/koa-server/uploads/file-1774347094969-731402203.png +0 -0
  39. package/template/koa-server/uploads/file-1774347101948-330296656.png +0 -0
  40. package/template/koa-server/uploads/file-1774351682377-932868720.png +0 -0
  41. package/template/koa-server/uploads/file-1774352037654-877426905.png +0 -0
  42. package/template/koa-server/uploads/file-1774352175463-386248997.png +0 -0
  43. package/template/koa-server/uploads/file-1774361446433-405859961.png +0 -0
  44. package/template/koa-server/uploads/file-1774361512207-465806267.png +0 -0
  45. package/template/lianxi.html +15 -0
  46. package/template/package-lock.json +6307 -0
  47. package/template/package.json +36 -0
  48. package/template/public/favicon.svg +1 -0
  49. package/template/public/icons.svg +24 -0
  50. package/template/src/App.css +184 -0
  51. package/template/src/App.tsx +44 -0
  52. package/template/src/api/course.ts +86 -0
  53. package/template/src/api/menu.ts +55 -0
  54. package/template/src/api/role.ts +58 -0
  55. package/template/src/api/user.ts +58 -0
  56. package/template/src/assets/hero.png +0 -0
  57. package/template/src/assets/react.svg +1 -0
  58. package/template/src/assets/vite.svg +1 -0
  59. package/template/src/components/Child.tsx +10 -0
  60. package/template/src/components/MainLayout.tsx +169 -0
  61. package/template/src/components/SunZi.tsx +13 -0
  62. package/template/src/contexts/ThemeContext.tsx +33 -0
  63. package/template/src/hooks/usePermission.tsx +62 -0
  64. package/template/src/index.css +111 -0
  65. package/template/src/main.tsx +13 -0
  66. package/template/src/pages/Dashboard.tsx +39 -0
  67. package/template/src/pages/Users.tsx +95 -0
  68. package/template/src/pages/banner/BannerList.tsx +182 -0
  69. package/template/src/pages/course/Course.tsx +586 -0
  70. package/template/src/pages/course/CourseList.tsx +168 -0
  71. package/template/src/pages/system/menu/Menu.tsx +501 -0
  72. package/template/src/pages/system/role/Role.tsx +458 -0
  73. package/template/src/pages/system/user/User.tsx +364 -0
  74. package/template/src/types/permission.ts +21 -0
  75. package/template/src/utils/request.tsx +94 -0
  76. package/template/src/vite-env.d.ts +1 -0
  77. package/template/tsconfig.app.json +32 -0
  78. package/template/tsconfig.json +7 -0
  79. package/template/tsconfig.node.json +13 -0
  80. package/template/vite.config.ts +30 -0
@@ -0,0 +1,586 @@
1
+ import React, { useState } from 'react';
2
+ import { Form, Input, Button, Upload, Steps, Modal, Space, InputNumber, Switch, message, Select } from 'antd';
3
+ import { UploadOutlined, ReadOutlined, CheckCircleOutlined, LoadingOutlined } from '@ant-design/icons';
4
+ import MDEditor from '@uiw/react-md-editor';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import { addCourseBasicInfo, uploadFile, addCourseOutline } from '@/api/course';
7
+
8
+ interface Chapter {
9
+ id: string;
10
+ name: string;
11
+ sortOrder?: string;
12
+ }
13
+
14
+ interface Section {
15
+ id: string;
16
+ name: string;
17
+ sortOrder: string;
18
+ isFree: boolean;
19
+ videoCover?: string;
20
+ videoFile?: string;
21
+ chapterId: string;
22
+ }
23
+
24
+ const Course = () => {
25
+ const [form] = Form.useForm();
26
+ const [chapterForm] = Form.useForm();
27
+ const [sectionForm] = Form.useForm();
28
+ const [mdValue, setMdValue] = useState<string | undefined>('');
29
+ const [currentStep, setCurrentStep] = useState(0);
30
+ const [isChapterModalVisible, setIsChapterModalVisible] = useState(false);
31
+ const [isSectionModalVisible, setIsSectionModalVisible] = useState(false);
32
+ const [chapters, setChapters] = useState<Chapter[]>([]);
33
+ const [sections, setSections] = useState<Section[]>([]);
34
+ const [currentChapterId, setCurrentChapterId] = useState<string>('');
35
+ const [coverImageUrl, setCoverImageUrl] = useState<string>('');
36
+ const [coverUploading, setCoverUploading] = useState(false);
37
+ const [videoCoverUrl, setVideoCoverUrl] = useState<string>('');
38
+ const [videoCoverUploading, setVideoCoverUploading] = useState(false);
39
+ const [videoFileUrl, setVideoFileUrl] = useState<string>('');
40
+ const [videoFileUploading, setVideoFileUploading] = useState(false);
41
+ const [step1Data, setStep1Data] = useState<any>(null); // 保存步骤一表单数据
42
+ const navigate = useNavigate();
43
+
44
+ const handleCoverUpload = async (file: File) => {
45
+ setCoverUploading(true);
46
+ try {
47
+ const url = await uploadFile(file);
48
+ setCoverImageUrl(url);
49
+ message.success('封面上传成功');
50
+ } catch {
51
+ message.error('封面上传失败');
52
+ } finally {
53
+ setCoverUploading(false);
54
+ }
55
+ return false;
56
+ };
57
+
58
+ const handleVideoCoverUpload = async (file: File) => {
59
+ setVideoCoverUploading(true);
60
+ try {
61
+ const url = await uploadFile(file);
62
+ setVideoCoverUrl(url);
63
+ message.success('视频封面上传成功');
64
+ } catch {
65
+ message.error('视频封面上传失败');
66
+ } finally {
67
+ setVideoCoverUploading(false);
68
+ }
69
+ return false;
70
+ };
71
+
72
+ const handleVideoFileUpload = async (file: File) => {
73
+ setVideoFileUploading(true);
74
+ try {
75
+ const url = await uploadFile(file);
76
+ setVideoFileUrl(url);
77
+ message.success('视频上传成功');
78
+ } catch {
79
+ message.error('视频上传失败');
80
+ } finally {
81
+ setVideoFileUploading(false);
82
+ }
83
+ return false;
84
+ };
85
+
86
+ // 老师选项数据
87
+ const teacherOptions = [
88
+ { value: 'teacher1', label: '张老师' },
89
+ { value: 'teacher2', label: '李老师' },
90
+ { value: 'teacher3', label: '王老师' },
91
+ { value: 'teacher4', label: '刘老师' },
92
+ { value: 'teacher5', label: '陈老师' }
93
+ ];
94
+
95
+ const onFinish = (values: any) => {
96
+ setStep1Data(values); // 保存步骤一数据
97
+ setCurrentStep(1);
98
+ };
99
+
100
+ const handleAddChapter = () => {
101
+ chapterForm.validateFields().then(values => {
102
+ console.log('Add Chapter:', values);
103
+ const newChapter: Chapter = {
104
+ id: Date.now().toString(),
105
+ name: values.chapterName,
106
+ sortOrder: values.sortOrder,
107
+ };
108
+ setChapters([...chapters, newChapter]);
109
+ setIsChapterModalVisible(false);
110
+ chapterForm.resetFields();
111
+ });
112
+ };
113
+
114
+ const handleCancelModal = () => {
115
+ setIsChapterModalVisible(false);
116
+ chapterForm.resetFields();
117
+ };
118
+
119
+ const handleDeleteChapter = (id: string) => {
120
+ setChapters(chapters.filter(chapter => chapter.id !== id));
121
+ setSections(sections.filter(section => section.chapterId !== id));
122
+ };
123
+
124
+ const handleAddSection = (chapterId: string) => {
125
+ setCurrentChapterId(chapterId);
126
+ sectionForm.resetFields();
127
+ setIsSectionModalVisible(true);
128
+ };
129
+
130
+ const handleSubmitSection = () => {
131
+ sectionForm.validateFields().then(values => {
132
+ const newSection: Section = {
133
+ id: Date.now().toString(),
134
+ name: values.sectionName,
135
+ sortOrder: values.sortOrder,
136
+ isFree: values.isFree ?? false,
137
+ videoCover: videoCoverUrl,
138
+ videoFile: videoFileUrl,
139
+ chapterId: currentChapterId,
140
+ };
141
+ setSections([...sections, newSection]);
142
+ setIsSectionModalVisible(false);
143
+ sectionForm.resetFields();
144
+ setVideoCoverUrl('');
145
+ setVideoFileUrl('');
146
+ });
147
+ };
148
+
149
+ const handleCancelSectionModal = () => {
150
+ setIsSectionModalVisible(false);
151
+ sectionForm.resetFields();
152
+ };
153
+
154
+ const renderStepContent = () => {
155
+ switch (currentStep) {
156
+ case 0:
157
+ return (
158
+ <div style={{ backgroundColor: '#fff', borderRadius: '8px', padding: '40px', minHeight: '600px' }}>
159
+ <Form
160
+ form={form}
161
+ layout="vertical"
162
+ onFinish={onFinish}
163
+ style={{ maxWidth: 800, margin: '0 auto' }}
164
+ colon={false}
165
+ initialValues={{
166
+ coverImage: [],
167
+ videoCover: [],
168
+ videoFile: []
169
+ }}
170
+ >
171
+ <Form.Item
172
+ name="courseTitle"
173
+ label={<span style={{ fontWeight: 500 }}>课程标题</span>}
174
+ rules={[{ required: true, message: '请输入课程标题' }]}
175
+ >
176
+ <Input />
177
+ </Form.Item>
178
+
179
+ <Form.Item name="instructorName" label={<span style={{ fontWeight: 500 }}>课程主讲老师</span>} rules={[{ required: true, message: '请选择课程主讲老师' }]}>
180
+ <Select
181
+ placeholder="请选择主讲老师"
182
+ style={{ width: '100%' }}
183
+ >
184
+ {teacherOptions.map(option => (
185
+ <Select.Option key={option.value} value={option.label}>
186
+ {option.label}
187
+ </Select.Option>
188
+ ))}
189
+ </Select>
190
+ </Form.Item>
191
+
192
+ <Form.Item name="totalHours" label={<span style={{ fontWeight: 500 }}>课程总时长</span>}>
193
+ <Input />
194
+ </Form.Item>
195
+
196
+ <Form.Item label={<span style={{ fontWeight: 500 }}>课程简介</span>}>
197
+ <div data-color-mode="light">
198
+ <MDEditor
199
+ value={mdValue}
200
+ onChange={setMdValue}
201
+ height={300}
202
+ preview="live"
203
+ hideToolbar={false}
204
+ />
205
+ </div>
206
+ </Form.Item>
207
+
208
+ <Form.Item label={<span style={{ fontWeight: 500 }}>课程封面</span>}>
209
+ <Upload
210
+ listType="picture-card"
211
+ showUploadList={false}
212
+ beforeUpload={handleCoverUpload}
213
+ accept="image/*"
214
+ >
215
+ {coverImageUrl ? (
216
+ <img src={coverImageUrl} alt="封面" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
217
+ ) : (
218
+ <div>
219
+ {coverUploading ? <LoadingOutlined /> : <UploadOutlined />}
220
+ <div style={{ marginTop: 8 }}>点击上传</div>
221
+ </div>
222
+ )}
223
+ </Upload>
224
+ </Form.Item>
225
+
226
+ <Form.Item name="price" label={<span style={{ fontWeight: 500 }}>课程价格</span>}>
227
+ <Input />
228
+ </Form.Item>
229
+
230
+ <Form.Item style={{ textAlign: 'right', marginTop: '40px' }}>
231
+ <Button type="primary" htmlType="submit">
232
+ 下一步
233
+ </Button>
234
+ </Form.Item>
235
+ </Form>
236
+ </div>
237
+ );
238
+ case 1:
239
+ return (
240
+ <div style={{ minHeight: '600px', display: 'flex', flexDirection: 'column' }}>
241
+ <div style={{ flex: 1, backgroundColor: '#fff', borderRadius: '8px', padding: '24px' }}>
242
+
243
+ {/* 课程大纲标题和添加章节按钮 */}
244
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
245
+ <h2 style={{ margin: 0, fontSize: '18px', fontWeight: 600 }}>课程大纲</h2>
246
+ <Button type="primary" onClick={() => setIsChapterModalVisible(true)}>添加章节</Button>
247
+ </div>
248
+
249
+ {/* 章节列表 */}
250
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
251
+ {chapters.map((chapter, index) => (
252
+ <div key={chapter.id} style={{ border: '1px solid #d9d9d9', borderRadius: '4px' }}>
253
+ {/* 章节标题栏 */}
254
+ <div style={{
255
+ display: 'flex',
256
+ justifyContent: 'space-between',
257
+ alignItems: 'center',
258
+ padding: '12px 16px',
259
+ backgroundColor: '#fafafa',
260
+ borderBottom: '1px solid #d9d9d9'
261
+ }}>
262
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
263
+ <span style={{ cursor: 'pointer' }}>▼</span>
264
+ <span>章节:{chapter.name}</span>
265
+ </div>
266
+ <Space size="small">
267
+ <a style={{ color: '#1890ff' }} onClick={() => handleAddSection(chapter.id)}>添加小节</a>
268
+ <a style={{ color: '#1890ff' }}>修改</a>
269
+ <a style={{ color: '#ff4d4f' }} onClick={() => handleDeleteChapter(chapter.id)}>删除</a>
270
+ </Space>
271
+ </div>
272
+
273
+ {/* 小节列表 */}
274
+ <div style={{ padding: '16px' }}>
275
+ {sections.filter(section => section.chapterId === chapter.id).map((section, sectionIndex) => (
276
+ <div key={section.id} style={{
277
+ display: 'flex',
278
+ alignItems: 'center',
279
+ gap: '12px',
280
+ padding: '8px 0',
281
+ borderBottom: '1px solid #f0f0f0'
282
+ }}>
283
+ <span style={{ width: '24px', textAlign: 'center' }}>{sectionIndex + 1}</span>
284
+ <span style={{ flex: 1 }}>{section.name}</span>
285
+ {section.isFree && (
286
+ <span style={{
287
+ color: '#52c41a',
288
+ fontSize: '12px',
289
+ backgroundColor: '#f6ffed',
290
+ padding: '2px 8px',
291
+ borderRadius: '10px',
292
+ border: '1px solid #b7eb8f'
293
+ }}>免费</span>
294
+ )}
295
+ <Space size="small" style={{ marginLeft: 'auto' }}>
296
+ <a style={{ color: '#1890ff' }}>修改</a>
297
+ <a style={{ color: '#ff4d4f' }}>删除</a>
298
+ </Space>
299
+ </div>
300
+ ))}
301
+ {sections.filter(section => section.chapterId === chapter.id).length === 0 && (
302
+ <div style={{
303
+ textAlign: 'center',
304
+ padding: '24px',
305
+ color: '#999',
306
+ fontSize: '14px'
307
+ }}>
308
+ 暂无小节,点击"添加小节"按钮添加
309
+ </div>
310
+ )}
311
+ </div>
312
+ </div>
313
+ ))}
314
+
315
+ {chapters.length === 0 && (
316
+ <div style={{
317
+ textAlign: 'center',
318
+ padding: '48px',
319
+ color: '#999',
320
+ fontSize: '14px',
321
+ border: '1px dashed #d9d9d9',
322
+ borderRadius: '4px'
323
+ }}>
324
+ 暂无章节,点击"添加章节"按钮添加
325
+ </div>
326
+ )}
327
+ </div>
328
+
329
+ {/* 底部按钮 */}
330
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '32px' }}>
331
+ <Button onClick={() => setCurrentStep(0)}>上一步</Button>
332
+ <Button type="primary" onClick={() => setCurrentStep(2)}>下一步</Button>
333
+ </div>
334
+ </div>
335
+ </div>
336
+ );
337
+ case 2:
338
+ return (
339
+ <div style={{ minHeight: '600px', display: 'flex', flexDirection: 'column' }}>
340
+ <div style={{ flex: 1, backgroundColor: '#fff', borderRadius: '8px', padding: '40px' }}>
341
+ <div style={{ textAlign: 'center', marginBottom: '40px' }}>
342
+ <CheckCircleOutlined style={{ fontSize: '64px', color: '#52c41a', marginBottom: '16px' }} />
343
+ <h2 style={{ margin: '0 0 8px 0' }}>课程信息已完善</h2>
344
+ <p style={{ color: '#666' }}>请确认以下信息无误后提交审核</p>
345
+ </div>
346
+
347
+ <div style={{ maxWidth: '600px', margin: '0 auto' }}>
348
+ <div style={{ marginBottom: '24px' }}>
349
+ <h3 style={{ margin: '0 0 16px 0', fontSize: '16px', fontWeight: 600 }}>课程信息</h3>
350
+ <div style={{ border: '1px solid #f0f0f0', borderRadius: '4px', padding: '16px' }}>
351
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
352
+ <span style={{ color: '#666' }}>课程标题:</span>
353
+ <span>{step1Data?.courseTitle || '未设置'}</span>
354
+ </div>
355
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
356
+ <span style={{ color: '#666' }}>课程价格:</span>
357
+ <span>¥{step1Data?.price || '0'}</span>
358
+ </div>
359
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
360
+ <span style={{ color: '#666' }}>主讲老师:</span>
361
+ <span>{step1Data?.instructorName || '未设置'}</span>
362
+ </div>
363
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
364
+ <span style={{ color: '#666' }}>章节数量:</span>
365
+ <span>{chapters.length} 章</span>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <div style={{ textAlign: 'center', marginTop: '40px' }}>
371
+ <Button style={{ marginRight: '16px' }} onClick={() => setCurrentStep(1)}>
372
+ 上一步
373
+ </Button>
374
+ <Button type="primary" onClick={handleSubmitAudit}>
375
+ 提交审核
376
+ </Button>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ );
382
+ default:
383
+ return null;
384
+ }
385
+ };
386
+
387
+ const handleSubmitAudit = () => {
388
+ const data = step1Data || {};
389
+ const newCourse = {
390
+ id: Date.now().toString(),
391
+ title: data.courseTitle || '未命名课程',
392
+ price: data.price || 0,
393
+ instructorName: data.instructorName || '',
394
+ coverImage: coverImageUrl,
395
+ introduction: mdValue || '',
396
+ totalDuration: data.totalHours || 0,
397
+ chapters: chapters.map(chapter => ({
398
+ id: chapter.id,
399
+ title: chapter.name,
400
+ sections: sections
401
+ .filter(section => section.chapterId === chapter.id)
402
+ .map(section => ({
403
+ id: section.id,
404
+ title: section.name,
405
+ duration: 0,
406
+ videoFile: section.videoFile || ''
407
+ }))
408
+ }))
409
+ };
410
+
411
+ try {
412
+ const existing = localStorage.getItem('courses');
413
+ const list = existing ? JSON.parse(existing) : [];
414
+ list.push(newCourse);
415
+ localStorage.setItem('courses', JSON.stringify(list));
416
+ message.success('课程提交审核成功!');
417
+ navigate('/course/list');
418
+ } catch {
419
+ message.error('提交审核失败,请重试');
420
+ }
421
+ };
422
+
423
+ return (
424
+ <div style={{ padding: '24px', backgroundColor: '#f0f2f5', minHeight: '100vh' }}>
425
+ {/* 顶部区域:动态渲染以匹配不同步骤的UI */}
426
+ {currentStep === 0 ? (
427
+ <div style={{ backgroundColor: '#fff', padding: '16px 24px', marginBottom: '24px', borderRadius: '4px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
428
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
429
+ <ReadOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
430
+ <span style={{ fontSize: '18px', fontWeight: 600 }}>课程创建向导</span>
431
+ <span style={{ color: '#1890ff', fontSize: '12px', backgroundColor: '#e6f7ff', padding: '2px 8px', borderRadius: '10px' }}>当前步骤 1 / 3</span>
432
+ </div>
433
+ <div style={{ maxWidth: '800px' }}>
434
+ <Steps
435
+ current={currentStep}
436
+ items={[
437
+ { title: '填写课程基本信息' },
438
+ { title: '创建课程大纲' },
439
+ { title: '提交审核' },
440
+ ]}
441
+ />
442
+ </div>
443
+ </div>
444
+ ) : currentStep === 1 ? (
445
+ <div style={{ backgroundColor: '#fff', padding: '16px 24px', marginBottom: '24px', borderRadius: '4px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
446
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
447
+ <ReadOutlined style={{ fontSize: '20px', color: '#1890ff' }} />
448
+ <span style={{ fontSize: '18px', fontWeight: 600 }}>课程创建向导</span>
449
+ <span style={{ color: '#1890ff', fontSize: '12px', backgroundColor: '#e6f7ff', padding: '2px 8px', borderRadius: '10px' }}>当前步骤 2 / 3</span>
450
+ </div>
451
+ <div style={{ maxWidth: '800px' }}>
452
+ <Steps
453
+ current={currentStep}
454
+ items={[
455
+ { title: '填写课程基本信息' },
456
+ { title: '创建课程大纲' },
457
+ { title: '提交审核' },
458
+ ]}
459
+ />
460
+ </div>
461
+ </div>
462
+ ) : null}
463
+
464
+ {/* 渲染对应步骤的内容 */}
465
+ {renderStepContent()}
466
+
467
+ {/* 添加章节弹窗 */}
468
+ <Modal
469
+ title="添加章节"
470
+ open={isChapterModalVisible}
471
+ onCancel={handleCancelModal}
472
+ footer={[
473
+ <Button key="cancel" onClick={handleCancelModal}>
474
+ 取消
475
+ </Button>,
476
+ <Button key="submit" type="primary" onClick={handleAddChapter}>
477
+ 确定
478
+ </Button>
479
+ ]}
480
+ width={500}
481
+ >
482
+ <Form
483
+ form={chapterForm}
484
+ layout="horizontal"
485
+ labelCol={{ span: 5 }}
486
+ wrapperCol={{ span: 19 }}
487
+ style={{ marginTop: '24px' }}
488
+ colon={true}
489
+ >
490
+ <Form.Item
491
+ name="chapterName"
492
+ label="章节名称"
493
+ rules={[{ required: true, message: '请输入章节名称' }]}
494
+ >
495
+ <Input />
496
+ </Form.Item>
497
+ <Form.Item
498
+ name="sortOrder"
499
+ label="章节排序"
500
+ >
501
+ <InputNumber style={{ width: 120 }} />
502
+ </Form.Item>
503
+ </Form>
504
+ </Modal>
505
+ {/* 添加小节弹窗 */}
506
+ <Modal
507
+ title="添加小节"
508
+ open={isSectionModalVisible}
509
+ onCancel={handleCancelSectionModal}
510
+ footer={[
511
+ <Button key="cancel" onClick={handleCancelSectionModal}>
512
+ 取消
513
+ </Button>,
514
+ <Button key="submit" type="primary" onClick={handleSubmitSection}>
515
+ 确定
516
+ </Button>
517
+ ]}
518
+ width={500}
519
+ >
520
+ <Form
521
+ form={sectionForm}
522
+ layout="horizontal"
523
+ labelCol={{ span: 5 }}
524
+ wrapperCol={{ span: 19 }}
525
+ style={{ marginTop: '24px' }}
526
+ colon={true}
527
+ initialValues={{
528
+ videoCover: [],
529
+ videoFile: []
530
+ }}
531
+ >
532
+ <Form.Item
533
+ name="sectionName"
534
+ label="课时名称"
535
+ rules={[{ required: true, message: '请输入课时名称' }]}
536
+ >
537
+ <Input />
538
+ </Form.Item>
539
+ <Form.Item
540
+ name="sortOrder"
541
+ label="课时排序"
542
+ rules={[{ required: true, message: '请输入课时排序' }]}
543
+ >
544
+ <InputNumber style={{ width: '100%' }} />
545
+ </Form.Item>
546
+ <Form.Item
547
+ name="isFree"
548
+ label="是否免费"
549
+ valuePropName="checked"
550
+ initialValue={false}
551
+ >
552
+ <Switch />
553
+ </Form.Item>
554
+ <Form.Item label="视频封面">
555
+ <Upload
556
+ listType="picture"
557
+ showUploadList={false}
558
+ beforeUpload={handleVideoCoverUpload}
559
+ accept="image/*"
560
+ >
561
+ <Button icon={videoCoverUploading ? <LoadingOutlined /> : <UploadOutlined />}>
562
+ {videoCoverUrl ? '重新上传' : '点击上传'}
563
+ </Button>
564
+ {videoCoverUrl && <span style={{ marginLeft: 8, color: '#52c41a' }}>已上传</span>}
565
+ </Upload>
566
+ </Form.Item>
567
+ <Form.Item label="视频文件">
568
+ <Upload
569
+ listType="text"
570
+ showUploadList={false}
571
+ beforeUpload={handleVideoFileUpload}
572
+ accept="video/*"
573
+ >
574
+ <Button icon={videoFileUploading ? <LoadingOutlined /> : <UploadOutlined />}>
575
+ {videoFileUrl ? '重新上传' : '点击上传'}
576
+ </Button>
577
+ {videoFileUrl && <span style={{ marginLeft: 8, color: '#52c41a' }}>已上传</span>}
578
+ </Upload>
579
+ </Form.Item>
580
+ </Form>
581
+ </Modal>
582
+ </div>
583
+ );
584
+ };
585
+
586
+ export default Course;