@cqsjjb/course-res-design 0.0.1

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.
@@ -0,0 +1,62 @@
1
+ import { default as React } from 'react';
2
+
3
+
4
+ /**
5
+ * 关闭回调数据
6
+ */
7
+ export interface DesignableCloseData {
8
+ /** 课程资源ID */
9
+ id: string;
10
+ }
11
+
12
+ /**
13
+ * Designable 组件 Props
14
+ */
15
+ export interface DesignableProps {
16
+ /** 自定义样式 */
17
+ style?: React.CSSProperties;
18
+ /** 主机地址(协议 + 域名 + 端口),用于构建 iframe 的源地址 */
19
+ host?: string;
20
+ /** 课程资源ID,未传则认为是新增,传入则认为是编辑 */
21
+ id?: string | number;
22
+ /** 额外参数,会传递给 iframe 内的编辑器 */
23
+ extraParams?: Record<string, any>;
24
+ /** 关闭事件回调 */
25
+ onClose?: (data: DesignableCloseData) => void;
26
+ /** iframe 的源地址,如果提供则优先级高于 host */
27
+ src?: string;
28
+ /** 是否显示组件 */
29
+ visible?: boolean;
30
+ }
31
+
32
+ /**
33
+ * 打开课程资源设计器的消息类型
34
+ */
35
+ export interface OpenCourseResDesignMessage {
36
+ /** 消息类型 */
37
+ type: 'EVENT_OPEN_COURSE_RES_DESIGN' | 'EVENT_CLOSE_COURSE_RES_DESIGN';
38
+ /** 课程资源ID */
39
+ id: string;
40
+ /** 额外参数 */
41
+ extraParams: Record<string, any>;
42
+ }
43
+
44
+ /**
45
+ * 关闭课程资源设计器的消息类型
46
+ */
47
+ export interface CloseCourseResDesignMessage {
48
+ /** 消息类型 */
49
+ type: 'EVENT_CLOSE_COURSE_RES_DESIGN';
50
+ /** 课程资源ID */
51
+ id: string;
52
+ }
53
+
54
+ /**
55
+ * 课程资源设计器组件
56
+ *
57
+ * @description 通过 iframe 嵌入课程资源设计器功能,支持与 iframe 内的编辑器进行 postMessage 通信
58
+ */
59
+ declare const Designable: React.FC<DesignableProps>;
60
+
61
+ export default Designable;
62
+
package/Preview.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { default as React } from 'react';
2
+ import { CourseInfo, ResourceLabel } from './types';
3
+
4
+
5
+ /**
6
+ * 试题选项
7
+ */
8
+ export interface TestExamOption {
9
+ /** 选项标识(如 A、B、C、D) */
10
+ optionItem: string;
11
+ /** 选项内容(HTML字符串) */
12
+ optionContent: string;
13
+ }
14
+
15
+ /**
16
+ * 试题数据
17
+ */
18
+ export interface TestExamData {
19
+ /** 试题ID */
20
+ id: string | number;
21
+ /** 试题题目(HTML字符串) */
22
+ titleName: string;
23
+ /** 试题选项列表 */
24
+ optionList?: TestExamOption[];
25
+ /** 试题类型:'1'=单选, '2'=多选, '3'=判断 */
26
+ type: string | number;
27
+ /** 难度类型:'1'=简单, '2'=中等, '3'=困难 */
28
+ difficultType: string | number;
29
+ /** 正确答案 */
30
+ questionsOptionsIds?: string;
31
+ /** 解析(HTML字符串,可选) */
32
+ analysis?: string;
33
+ /** 创建时间 */
34
+ createTime?: string;
35
+ }
36
+
37
+ /**
38
+ * 课程章节节点
39
+ */
40
+ export interface CourseChapterNode {
41
+ /** 节点ID */
42
+ id: string | number;
43
+ /** 课程菜单名称 */
44
+ courseMenuName: string;
45
+ /** 课程菜单类型:1=文件夹, 2=视频, 3=文件 */
46
+ courseMenuType: 1 | 2 | 3;
47
+ /** 子节点列表 */
48
+ children?: CourseChapterNode[];
49
+ /** 资源类型(如 'video') */
50
+ resType?: string;
51
+ /** 资源总时长(秒) */
52
+ resourceTotalIden?: string | number;
53
+ /** 参考学时 */
54
+ resourceHour?: string | number;
55
+ /** 资源标签列表 */
56
+ resourceLabel?: ResourceLabel[];
57
+ /** 描述 */
58
+ description?: string;
59
+ /** 试题JSON字符串 */
60
+ testExamJson?: string;
61
+ /** 试看时长(分钟) */
62
+ auditionTime?: string | number;
63
+ /** 节点类型(如 'root', 'fileLib') */
64
+ type?: string;
65
+ /** 标题(用于 fileLib 类型) */
66
+ title?: string;
67
+ /** 层级(内部使用,扁平化后添加) */
68
+ level?: number;
69
+ }
70
+
71
+ /**
72
+ * Preview 组件 Props
73
+ */
74
+ export interface PreviewProps {
75
+ /** 课程章节列表(树形结构) */
76
+ courseChapterList: CourseChapterNode[];
77
+ /** 课程信息 */
78
+ courseInfo: CourseInfo;
79
+ /** 是否启用预览扫码功能 */
80
+ enablePreview?: boolean;
81
+ }
82
+
83
+ /**
84
+ * 课程预览组件
85
+ *
86
+ * @description 用于展示课程章节的预览界面,支持树形结构展示、视频预览、试题查看等功能
87
+ */
88
+ declare class Preview extends React.Component<PreviewProps> {}
89
+
90
+ export default Preview;
91
+
@@ -0,0 +1,91 @@
1
+ import { default as React } from 'react';
2
+ import { CourseInfo, ResourceLabel } from './types';
3
+
4
+
5
+ /**
6
+ * 资源类型枚举(与 ResourceLabel 相同,保留别名以保持兼容性)
7
+ */
8
+ export type ResTypeEnum = ResourceLabel;
9
+
10
+ /**
11
+ * 字幕项
12
+ */
13
+ export interface CaptionItem {
14
+ /** 字幕ID */
15
+ id?: string | number;
16
+ /** 字幕内容 */
17
+ content?: string;
18
+ /** 开始时间 */
19
+ startTime?: number;
20
+ /** 结束时间 */
21
+ endTime?: number;
22
+ [key: string]: any;
23
+ }
24
+
25
+ /**
26
+ * 视频资源数据
27
+ */
28
+ export interface VideoResourceValue {
29
+ /** 资源URL */
30
+ resourceUrl?: string;
31
+ /** 文件ID */
32
+ fileId?: string | number;
33
+ /** 资源类型枚举 */
34
+ resTypeEnum?: ResourceLabel | null;
35
+ /** 视频时长(秒) */
36
+ duration?: number;
37
+ /** 资源类型(如 'video') */
38
+ resType?: string;
39
+ /** 参考学时 */
40
+ resourceHour?: number | null;
41
+ /** 字幕ID */
42
+ captionId?: string;
43
+ /** 字幕列表 */
44
+ captionList?: CaptionItem[];
45
+ /** 课程菜单名称 */
46
+ courseMenuName?: string;
47
+ /** 试题JSON字符串 */
48
+ testExamJson?: string;
49
+ [key: string]: any;
50
+ }
51
+
52
+ /**
53
+ * 资源选择器返回的文件项
54
+ */
55
+ export interface ResSelectorFileItem {
56
+ /** 文件ID */
57
+ id: string | number;
58
+ /** 文件URL */
59
+ fileUrl: string;
60
+ /** 资源名称 */
61
+ resName: string;
62
+ /** 视频时长(秒) */
63
+ duration: number;
64
+ /** 资源类型枚举 */
65
+ resTypeEnum?: ResourceLabel;
66
+ [key: string]: any;
67
+ }
68
+
69
+ /**
70
+ * PreviewVideo 组件 Props
71
+ */
72
+ export interface PreviewVideoProps {
73
+ /** 视频资源数据 */
74
+ value?: VideoResourceValue;
75
+ /** 课程信息 */
76
+ courseInfo?: CourseInfo;
77
+ /** 是否禁用(禁用时不显示"重新选择"按钮) */
78
+ disabled?: boolean;
79
+ /** 变化回调,当重新选择资源时触发 */
80
+ onChange?: (value: VideoResourceValue) => void;
81
+ }
82
+
83
+ /**
84
+ * 预览视频组件
85
+ *
86
+ * @description 用于预览和重新选择视频资源,支持视频预览、资源库选择等功能
87
+ */
88
+ declare const PreviewVideo: React.FC<PreviewVideoProps>;
89
+
90
+ export default PreviewVideo;
91
+
package/README.md ADDED
@@ -0,0 +1,322 @@
1
+ # @cqsjjb/scene-engine
2
+
3
+ 场景引擎组件库,通过 iframe 嵌入场景引擎功能,支持富文本编辑和内容渲染。
4
+
5
+ ## 📦 安装
6
+
7
+ ```bash
8
+ npm install @cqsjjb/scene-engine
9
+ # 或
10
+ yarn add @cqsjjb/scene-engine
11
+ # 或
12
+ pnpm add @cqsjjb/scene-engine
13
+ ```
14
+
15
+ ## 🔧 依赖要求
16
+
17
+ 该组件库需要以下 peer dependencies:
18
+
19
+ - `react`: ^16.8.0 || ^17.0.0 || ^18.0.0
20
+ - `react-dom`: ^16.8.0 || ^17.0.0 || ^18.0.0
21
+ - `react-quill`: ^1.3.0 || ^2.0.0
22
+
23
+ ## 🚀 快速开始
24
+
25
+ ### SceneEngine - 场景引擎
26
+
27
+ 场景引擎组件通过 iframe 嵌入场景引擎功能,支持新增和编辑场景。
28
+
29
+ ```tsx
30
+ import React, { useState } from 'react';
31
+ import { SceneEngine } from '@cqsjjb/scene-engine';
32
+
33
+ const App = () => {
34
+ const [visible, setVisible] = useState(false);
35
+ const [sceneId, setSceneId] = useState<string>();
36
+
37
+ const handleOpenEditor = (id?: string) => {
38
+ setSceneId(id);
39
+ setVisible(true);
40
+ };
41
+
42
+ const handleClose = (data: {
43
+ id: string;
44
+ contentTitle: string;
45
+ content: string;
46
+ settingConfig: {
47
+ [key: string]: any;
48
+ };
49
+ }) => {
50
+ console.log('场景数据:', data);
51
+ setVisible(false);
52
+ // 处理保存逻辑
53
+ };
54
+
55
+ return (
56
+ <div>
57
+ <button onClick={() => handleOpenEditor()}>新增场景</button>
58
+ <button onClick={() => handleOpenEditor('scene-123')}>编辑场景</button>
59
+
60
+ <SceneEngine
61
+ visible={visible}
62
+ id={sceneId}
63
+ host="http://localhost:5173"
64
+ onClose={handleClose}
65
+ style={{ zIndex: 9999 }}
66
+ />
67
+ </div>
68
+ );
69
+ };
70
+ ```
71
+
72
+ ### ContentRenderer - 内容渲染器
73
+
74
+ 内容渲染器组件用于渲染富文本内容,适用于预览场景内容。
75
+
76
+ ```tsx
77
+ import React from 'react';
78
+ import { ContentRenderer } from '@cqsjjb/scene-engine';
79
+
80
+ const Preview = () => {
81
+ const htmlContent = '<p>这是富文本内容</p>';
82
+
83
+ return (
84
+ <div>
85
+ <h2>场景预览</h2>
86
+ <ContentRenderer content={htmlContent} />
87
+ </div>
88
+ );
89
+ };
90
+ ```
91
+
92
+ ## 📚 API 文档
93
+
94
+ ### SceneEngine
95
+
96
+ 场景引擎组件,通过 iframe 嵌入场景引擎功能。
97
+
98
+ #### Props
99
+
100
+ | 属性名 | 类型 | 必填 | 默认值 | 说明 |
101
+ |--------|------|------|--------|------|
102
+ | `style` | `React.CSSProperties` | 否 | - | 自定义样式,默认组件使用固定定位全屏显示(z-index: 9999) |
103
+ | `host` | `string` | 否 | `window.location.origin` | 主机地址(协议 + 域名 + 端口),用于构建 iframe 的源地址 |
104
+ | `src` | `string` | 否 | `host + '/article-layout'` | iframe 的源地址,如果提供则优先级高于 host |
105
+ | `id` | `string` | 否 | - | 场景 ID,未传则认为是新增场景,传入则认为是编辑场景 |
106
+ | `visible` | `boolean` | 否 | `true` | 是否显示组件 |
107
+ | `onClose` | `(data: CloseData) => void` | 否 | - | 关闭事件回调 |
108
+
109
+ #### CloseData 类型
110
+
111
+ ```typescript
112
+ interface CloseData {
113
+ id: string; // 场景 ID
114
+ contentTitle: string; // 场景的标题
115
+ content: string; // 场景的内容(富文本 HTML)
116
+ settingConfig: {
117
+ [key: string]: any; // 场景的设置配置
118
+ };
119
+ }
120
+ ```
121
+
122
+ #### 事件通信
123
+
124
+ 组件通过 `postMessage` 与 iframe 内的编辑器进行通信:
125
+
126
+ - **打开编辑器**: 组件会在 iframe 加载完成后发送 `EVENT_OPEN_SCENE_EDITOR` 消息,消息格式:
127
+ ```typescript
128
+ {
129
+ type: 'EVENT_OPEN_SCENE_EDITOR',
130
+ id: string // 场景 ID,新增时为空字符串
131
+ }
132
+ ```
133
+
134
+ - **关闭编辑器**: iframe 内编辑器发送 `EVENT_CLOSE_SCENE_EDITOR` 消息时触发 `onClose` 回调,消息格式:
135
+ ```typescript
136
+ {
137
+ type: 'EVENT_CLOSE_SCENE_EDITOR',
138
+ id: string,
139
+ contentTitle: string,
140
+ content: string,
141
+ settingConfig: Record<string, any>
142
+ }
143
+ ```
144
+
145
+ 组件会验证消息来源,只处理来自 iframe 源地址的消息,确保通信安全。
146
+
147
+ ### ContentRenderer
148
+
149
+ 内容渲染器组件,用于渲染富文本内容。组件会自动清洗 HTML 内容,确保内容以只读模式展示:
150
+
151
+ - 移除所有包含 `ql-material-image--selected` 类的元素(清理图片选中状态)
152
+ - 移除所有 `contenteditable` 属性(确保内容不可编辑)
153
+
154
+ #### Props
155
+
156
+ | 属性名 | 类型 | 必填 | 默认值 | 说明 |
157
+ |--------|------|------|--------|------|
158
+ | `content` | `string` | 否 | `''` | 要渲染的 HTML 内容(富文本) |
159
+
160
+ ## 💡 使用示例
161
+
162
+ ### 完整示例
163
+
164
+ ```tsx
165
+ import React, { useState } from 'react';
166
+ import { SceneEngine, ContentRenderer } from '@cqsjjb/scene-engine';
167
+
168
+ interface SceneData {
169
+ id: string;
170
+ title: string;
171
+ content: string;
172
+ settingConfig: Record<string, any>;
173
+ }
174
+
175
+ const SceneManager = () => {
176
+ const [editorVisible, setEditorVisible] = useState(false);
177
+ const [editingSceneId, setEditingSceneId] = useState<string>();
178
+ const [scenes, setScenes] = useState<SceneData[]>([]);
179
+ const [previewContent, setPreviewContent] = useState<string>('');
180
+
181
+ // 打开编辑器
182
+ const handleOpenEditor = (sceneId?: string) => {
183
+ setEditingSceneId(sceneId);
184
+ setEditorVisible(true);
185
+ };
186
+
187
+ // 关闭编辑器并保存数据
188
+ const handleCloseEditor = (data: {
189
+ id: string;
190
+ contentTitle: string;
191
+ content: string;
192
+ settingConfig: Record<string, any>;
193
+ }) => {
194
+ const sceneData: SceneData = {
195
+ id: data.id,
196
+ title: data.contentTitle,
197
+ content: data.content,
198
+ settingConfig: data.settingConfig,
199
+ };
200
+
201
+ // 更新场景列表
202
+ const existingIndex = scenes.findIndex((s) => s.id === sceneData.id);
203
+ if (existingIndex >= 0) {
204
+ const updatedScenes = [...scenes];
205
+ updatedScenes[existingIndex] = sceneData;
206
+ setScenes(updatedScenes);
207
+ } else {
208
+ setScenes([...scenes, sceneData]);
209
+ }
210
+
211
+ setEditorVisible(false);
212
+ setEditingSceneId(undefined);
213
+ };
214
+
215
+ // 预览场景
216
+ const handlePreview = (scene: SceneData) => {
217
+ setPreviewContent(scene.content);
218
+ };
219
+
220
+ return (
221
+ <div>
222
+ <div>
223
+ <h2>场景列表</h2>
224
+ <button onClick={() => handleOpenEditor()}>新增场景</button>
225
+ {scenes.map((scene) => (
226
+ <div key={scene.id}>
227
+ <h3>{scene.title}</h3>
228
+ <button onClick={() => handleOpenEditor(scene.id)}>编辑</button>
229
+ <button onClick={() => handlePreview(scene)}>预览</button>
230
+ </div>
231
+ ))}
232
+ </div>
233
+
234
+ {previewContent && (
235
+ <div>
236
+ <h2>预览</h2>
237
+ <ContentRenderer content={previewContent} />
238
+ </div>
239
+ )}
240
+
241
+ <SceneEngine
242
+ visible={editorVisible}
243
+ id={editingSceneId}
244
+ host="http://localhost:5173"
245
+ onClose={handleCloseEditor}
246
+ style={{ zIndex: 9999 }}
247
+ />
248
+ </div>
249
+ );
250
+ };
251
+
252
+ export default SceneManager;
253
+ ```
254
+
255
+ ### 自定义 iframe 地址
256
+
257
+ ```tsx
258
+ <SceneEngine
259
+ visible={true}
260
+ src="https://example.com/custom-editor-path"
261
+ onClose={handleClose}
262
+ />
263
+ ```
264
+
265
+ ### 自定义样式
266
+
267
+ ```tsx
268
+ <SceneEngine
269
+ visible={true}
270
+ style={{
271
+ zIndex: 10000,
272
+ backgroundColor: '#f5f5f5',
273
+ }}
274
+ onClose={handleClose}
275
+ />
276
+ ```
277
+
278
+ ## 🏗️ 构建
279
+
280
+ 该组件库使用 Vite 构建,支持 ES Module 和 CommonJS 两种格式。
281
+
282
+ ### 开发构建
283
+
284
+ ```bash
285
+ npm run build
286
+ ```
287
+
288
+ 构建产物将输出到 `publish` 目录,包括:
289
+
290
+ - `index.esm.js` - ES Module 格式
291
+ - `index.cjs.js` - CommonJS 格式
292
+ - `index.d.ts` - TypeScript 类型定义
293
+ - `index.css` - 样式文件
294
+
295
+ ### 发布
296
+
297
+ ```bash
298
+ npm publish
299
+ ```
300
+
301
+ 发布前会自动执行 `prepublishOnly` 脚本进行构建。
302
+
303
+ ## 📝 注意事项
304
+
305
+ 1. **iframe 通信**: 组件通过 `postMessage` 与 iframe 内的编辑器通信,确保 iframe 的源地址与 `host` 或 `src` 配置一致。组件会验证消息来源,只处理来自 iframe 源的消息。
306
+
307
+ 2. **样式隔离**: 组件使用 `createPortal` 将编辑器渲染到 `document.body`,确保样式不受父组件影响。组件默认使用固定定位全屏显示(z-index: 9999)。
308
+
309
+ 3. **富文本内容**: `ContentRenderer` 组件使用 `dangerouslySetInnerHTML` 渲染 HTML 内容,请确保内容来源可信,避免 XSS 攻击。组件会自动清洗 HTML 内容:
310
+ - 移除所有包含 `ql-material-image--selected` 类的元素(清理图片选中状态)
311
+ - 移除所有 `contenteditable` 属性(确保内容不可编辑)
312
+
313
+ 4. **react-quill 样式**: 组件已引入 `react-quill/dist/quill.snow.css` 和自定义样式,无需额外引入。
314
+
315
+ 5. **浏览器兼容性**: 组件依赖现代浏览器 API(如 `postMessage`、`createPortal`),建议在支持 ES2020 的浏览器中使用。
316
+
317
+ 6. **默认路径**: 如果未提供 `src` 属性,组件会使用 `host + '/scene-engine'` 作为 iframe 的默认路径。
318
+
319
+ ## 🔒 安全提示
320
+
321
+ - 使用 `ContentRenderer` 渲染用户输入内容时,请确保对 HTML 内容进行安全过滤和转义
322
+ - 在生产环境中,建议使用 CSP (Content Security Policy) 限制 iframe 的源地址
@@ -0,0 +1,63 @@
1
+ import { default as React } from 'react';
2
+ import { CourseInfo } from './types';
3
+
4
+
5
+ /**
6
+ * 视频源配置
7
+ */
8
+ export interface VideoSource {
9
+ /** 视频源地址 */
10
+ src: string;
11
+ /** 清晰度标签(如 '高清'、'标清' 等) */
12
+ label: string;
13
+ /** 是否为默认源 */
14
+ default?: boolean;
15
+ }
16
+
17
+ /**
18
+ * 字幕轨道配置
19
+ */
20
+ export interface VideoTrack {
21
+ /** 字幕文件地址(Blob URL) */
22
+ src: string;
23
+ /** 字幕类型 */
24
+ kind: 'subtitles' | 'captions' | 'descriptions' | 'chapters' | 'metadata';
25
+ /** 字幕语言代码 */
26
+ srcLang: string;
27
+ /** 字幕标签 */
28
+ label: string;
29
+ /** 是否为默认字幕 */
30
+ default?: boolean;
31
+ }
32
+
33
+ /**
34
+ * VideoPlayer 组件使用的课程信息(扩展 CourseInfo)
35
+ */
36
+ export interface VideoPlayerCourseInfo extends CourseInfo {
37
+ /** 源枚举类型(如 'OUT' 表示外部源) */
38
+ sourceEnum?: string;
39
+ }
40
+
41
+ /**
42
+ * VideoPlayer 组件 Props
43
+ */
44
+ export interface VideoPlayerProps {
45
+ /** 视频源地址(直接传入时使用) */
46
+ src?: string;
47
+ /** 文件ID(用于获取视频源和字幕) */
48
+ id?: string | number;
49
+ /** 课程信息 */
50
+ courseInfo?: VideoPlayerCourseInfo;
51
+ /** 其他传递给 WebVideoPlayer 的 props */
52
+ [key: string]: any;
53
+ }
54
+
55
+ /**
56
+ * 视频播放器组件
57
+ *
58
+ * @description 基于 WebVideoPlayer 封装的视频播放器,支持多清晰度切换、字幕显示等功能
59
+ */
60
+ declare const VideoPlayer: React.FC<VideoPlayerProps>;
61
+
62
+ export default VideoPlayer;
63
+
@@ -0,0 +1 @@
1
+ .course-preview{height:calc(100% - 78px);display:flex;gap:16px}.course-preview__container{height:100%;display:flex;gap:16px}.course-preview__sidebar,.course-preview__main{height:100%;border:1px solid #f0f0f0;border-radius:8px;overflow:hidden}.course-preview__sidebar{width:482px;overflow-y:auto}.course-preview__main{flex:1;padding:16px;overflow-y:auto}.course-preview__main-content{border:1px solid #ddd;border-radius:8px;overflow:hidden}.course-preview__sidebar-header{background:#f7f7f7;height:50px;display:flex;gap:10px;align-items:center;padding:0 10px;position:relative;border-bottom:1px solid #f0f0f0}.course-preview__item{padding:16px;background-color:#f7f7f7}.course-preview__item:not(:last-child){border-bottom:1px solid #ddd}.course-preview__item-title{font-size:16px;font-weight:600}.course-preview__item-description{font-size:14px;color:#999;margin-top:8px}.course-preview__item-video,.course-preview__item-file{display:flex;align-items:center;justify-content:space-between}.course-preview__item-video-name,.course-preview__item-file-name{font-size:16px;font-weight:600;line-height:22px;margin-right:10px}.course-preview__item-video-info,.course-preview__item-file-info{display:flex;flex-direction:column;justify-content:center;gap:4px;font-size:14px;align-items:flex-start}.course-preview__item-video-description,.course-preview__item-file-description{color:#999;font-size:14px;margin-top:8px}.course-preview__item-video-content,.course-preview__item-file-content{display:flex;gap:24px;white-space:nowrap;align-items:center}.course-preview__sidebar-content{height:calc(100% - 50px);overflow-y:auto;padding:16px}.course-preview__title{padding:16px;background:#f7f7f7;border:1px solid #f0f0f0;border-radius:8px;margin-bottom:16px}.course-preview__title-total{display:flex;gap:20px}.course-preview__title-total-item{display:flex;gap:5px}.course-preview__label-container{display:flex;align-items:center}.course-preview__label-icon{color:#2c5de5}.course-preview__label{font-size:16px;color:#4d4d4d}.course-preview__value{font-size:18px;color:#000;font-weight:600}.course-res-designable-container{position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;background:#fff}.course-res-designable-iframe{width:100%;height:100%;border:none;display:block}