1181 lines
28 KiB
Markdown
1181 lines
28 KiB
Markdown
|
|
# 可试看功能实现文档
|
|||
|
|
|
|||
|
|
## 📋 目录
|
|||
|
|
|
|||
|
|
1. [功能概述](#功能概述)
|
|||
|
|
2. [技术架构](#技术架构)
|
|||
|
|
3. [实现步骤](#实现步骤)
|
|||
|
|
4. [代码示例](#代码示例)
|
|||
|
|
5. [样式规范](#样式规范)
|
|||
|
|
6. [常见问题](#常见问题)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 功能概述
|
|||
|
|
|
|||
|
|
### 功能描述
|
|||
|
|
|
|||
|
|
"可试看"功能允许特定课程在未购买或未解锁的情况下,通过iframe方式预览课程内容。该功能分为两个场景:
|
|||
|
|
|
|||
|
|
1. **课程直播间场景**:在课程列表中显示可试看标签,点击后在视频播放器区域内嵌显示课程内容
|
|||
|
|
2. **课后作业场景**:在作业卡片上显示可试看标签,点击后全屏显示课程内容
|
|||
|
|
|
|||
|
|
### 核心特性
|
|||
|
|
|
|||
|
|
- ✅ 视觉标签:醒目的"可试看"标签提示
|
|||
|
|
- ✅ iframe内嵌:无缝内嵌外部网页内容
|
|||
|
|
- ✅ 全屏支持:支持全屏观看模式
|
|||
|
|
- ✅ 缩放适配:自动缩放适配显示区域
|
|||
|
|
- ✅ 权限控制:精准控制哪些课程可试看
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 技术架构
|
|||
|
|
|
|||
|
|
### 架构流程图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────┐
|
|||
|
|
│ 数据层 (Data Layer) │
|
|||
|
|
│ ├─ canPreview: boolean │
|
|||
|
|
│ ├─ previewUrl: string │
|
|||
|
|
│ └─ isShowCase: boolean │
|
|||
|
|
└─────────────────────────────────────────┘
|
|||
|
|
↓
|
|||
|
|
┌─────────────────────────────────────────┐
|
|||
|
|
│ 业务逻辑层 (Business Logic) │
|
|||
|
|
│ ├─ 判断是否可试看 │
|
|||
|
|
│ ├─ 控制UI展示状态 │
|
|||
|
|
│ └─ 处理点击事件 │
|
|||
|
|
└─────────────────────────────────────────┘
|
|||
|
|
↓
|
|||
|
|
┌─────────────────────────────────────────┐
|
|||
|
|
│ 展示层 (Presentation Layer) │
|
|||
|
|
│ ├─ 可试看标签组件 │
|
|||
|
|
│ ├─ iframe内嵌组件 │
|
|||
|
|
│ └─ 交互控制组件 │
|
|||
|
|
└─────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 技术栈
|
|||
|
|
|
|||
|
|
- **前端框架**: React 18+
|
|||
|
|
- **UI组件库**: Arco Design
|
|||
|
|
- **样式方案**: CSS Modules / Less
|
|||
|
|
- **状态管理**: React Hooks (useState, useRef)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 实现步骤
|
|||
|
|
|
|||
|
|
### 步骤 1: 数据结构设计
|
|||
|
|
|
|||
|
|
#### 1.1 课程直播数据结构
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 课程对象结构
|
|||
|
|
const courseObj = {
|
|||
|
|
courseId: "course_001",
|
|||
|
|
courseName: "展会主题与品牌定位",
|
|||
|
|
teacherName: "张老师",
|
|||
|
|
date: "2025-09-15",
|
|||
|
|
unitName: "消费电子展品牌策划与执行",
|
|||
|
|
|
|||
|
|
// 可试看相关字段
|
|||
|
|
canPreview: true, // 是否可试看
|
|||
|
|
previewUrl: "https://example.com/preview" // 试看页面URL
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.2 课后作业数据结构
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 作业对象结构
|
|||
|
|
const homeworkItem = {
|
|||
|
|
id: 142,
|
|||
|
|
name: "展会主题与品牌定位",
|
|||
|
|
level: "completed",
|
|||
|
|
imageUrl: "https://example.com/poster.jpg",
|
|||
|
|
|
|||
|
|
// 可试看相关字段
|
|||
|
|
isShowCase: true // 是否可试看(作业场景)
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 2: 数据标记逻辑
|
|||
|
|
|
|||
|
|
#### 2.1 在数据处理函数中添加标记
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 示例:处理课程数据时添加可试看标记
|
|||
|
|
const processCourseData = (rawData) => {
|
|||
|
|
const courses = rawData.map(item => {
|
|||
|
|
const courseObj = {
|
|||
|
|
courseId: item.id,
|
|||
|
|
courseName: item.title,
|
|||
|
|
// ... 其他字段
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 为特定课程添加可试看标记
|
|||
|
|
if (shouldEnablePreview(item)) {
|
|||
|
|
courseObj.canPreview = true;
|
|||
|
|
courseObj.previewUrl = getPreviewUrl(item);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return courseObj;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return courses;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 判断是否应该开启试看
|
|||
|
|
const shouldEnablePreview = (item) => {
|
|||
|
|
// 示例:根据课程名称和单元名称判断
|
|||
|
|
return item.title === "展会主题与品牌定位" &&
|
|||
|
|
item.unitName === "消费电子展品牌策划与执行";
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 获取试看URL
|
|||
|
|
const getPreviewUrl = (item) => {
|
|||
|
|
// 可以从配置文件、数据库或API获取
|
|||
|
|
const previewUrls = {
|
|||
|
|
"展会主题与品牌定位": "https://du9uay.github.io/zhanhui/"
|
|||
|
|
};
|
|||
|
|
return previewUrls[item.title] || "";
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 3: UI组件实现
|
|||
|
|
|
|||
|
|
#### 3.1 可试看标签组件
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// PreviewBadge.jsx
|
|||
|
|
import React from 'react';
|
|||
|
|
import './PreviewBadge.css';
|
|||
|
|
|
|||
|
|
const PreviewBadge = ({
|
|||
|
|
type = 'course', // 'course' | 'homework' | 'unit'
|
|||
|
|
className = ''
|
|||
|
|
}) => {
|
|||
|
|
const classNames = {
|
|||
|
|
course: 'preview-badge-course',
|
|||
|
|
homework: 'preview-badge-homework',
|
|||
|
|
unit: 'preview-badge-unit'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<span className={`preview-badge ${classNames[type]} ${className}`}>
|
|||
|
|
可试看
|
|||
|
|
</span>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default PreviewBadge;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**PreviewBadge.css**
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
.preview-badge {
|
|||
|
|
display: inline-block;
|
|||
|
|
font-weight: 600;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 课程列表中的标签 */
|
|||
|
|
.preview-badge-course {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 50%;
|
|||
|
|
top: 60%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
padding: 4px 12px;
|
|||
|
|
background: #4080ff;
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 12px;
|
|||
|
|
z-index: 10;
|
|||
|
|
box-shadow: 0 3px 8px rgba(64, 128, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 作业卡片上的标签 */
|
|||
|
|
.preview-badge-homework {
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
font-size: 11px;
|
|||
|
|
color: #ff8c00;
|
|||
|
|
background: rgba(255, 140, 0, 0.1);
|
|||
|
|
border: 1px solid rgba(255, 140, 0, 0.3);
|
|||
|
|
margin-top: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 单元标题上的标签 */
|
|||
|
|
.preview-badge-unit {
|
|||
|
|
margin-left: 8px;
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
background: #4080ff;
|
|||
|
|
color: #fff;
|
|||
|
|
font-size: 11px;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2 iframe播放器组件
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// IframePlayer.jsx
|
|||
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|||
|
|
import './IframePlayer.css';
|
|||
|
|
|
|||
|
|
const IframePlayer = ({
|
|||
|
|
url,
|
|||
|
|
title = '课程预览',
|
|||
|
|
zoom = 0.5,
|
|||
|
|
onClose
|
|||
|
|
}) => {
|
|||
|
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
|||
|
|
const containerRef = useRef(null);
|
|||
|
|
|
|||
|
|
// 全屏切换
|
|||
|
|
const handleFullscreen = () => {
|
|||
|
|
const container = containerRef.current;
|
|||
|
|
if (!container) return;
|
|||
|
|
|
|||
|
|
if (!isFullscreen) {
|
|||
|
|
// 进入全屏
|
|||
|
|
if (container.requestFullscreen) {
|
|||
|
|
container.requestFullscreen();
|
|||
|
|
} else if (container.webkitRequestFullscreen) {
|
|||
|
|
container.webkitRequestFullscreen();
|
|||
|
|
} else if (container.mozRequestFullScreen) {
|
|||
|
|
container.mozRequestFullScreen();
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 退出全屏
|
|||
|
|
if (document.exitFullscreen) {
|
|||
|
|
document.exitFullscreen();
|
|||
|
|
} else if (document.webkitExitFullscreen) {
|
|||
|
|
document.webkitExitFullscreen();
|
|||
|
|
} else if (document.mozCancelFullScreen) {
|
|||
|
|
document.mozCancelFullScreen();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 监听全屏状态变化
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleFullscreenChange = () => {
|
|||
|
|
setIsFullscreen(
|
|||
|
|
document.fullscreenElement === containerRef.current ||
|
|||
|
|
document.webkitFullscreenElement === containerRef.current ||
|
|||
|
|
document.mozFullScreenElement === containerRef.current
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|||
|
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|||
|
|
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|||
|
|
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
|
|||
|
|
document.removeEventListener('mozfullscreenchange', handleFullscreenChange);
|
|||
|
|
};
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div
|
|||
|
|
ref={containerRef}
|
|||
|
|
className="iframe-player-container"
|
|||
|
|
>
|
|||
|
|
<iframe
|
|||
|
|
src={url}
|
|||
|
|
title={title}
|
|||
|
|
className="iframe-player-content"
|
|||
|
|
style={{
|
|||
|
|
zoom: isFullscreen ? 1 : zoom
|
|||
|
|
}}
|
|||
|
|
allowFullScreen
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{/* 全屏按钮 */}
|
|||
|
|
<button
|
|||
|
|
className="iframe-player-fullscreen-btn"
|
|||
|
|
onClick={handleFullscreen}
|
|||
|
|
title={isFullscreen ? "退出全屏" : "全屏"}
|
|||
|
|
>
|
|||
|
|
{isFullscreen ? (
|
|||
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|||
|
|
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" />
|
|||
|
|
</svg>
|
|||
|
|
) : (
|
|||
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|||
|
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
|||
|
|
</svg>
|
|||
|
|
)}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default IframePlayer;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**IframePlayer.css**
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
.iframe-player-container {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background-color: #000;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.iframe-player-content {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.iframe-player-fullscreen-btn {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 16px;
|
|||
|
|
right: 16px;
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
border: none;
|
|||
|
|
background-color: rgba(0, 0, 0, 0.6);
|
|||
|
|
color: #fff;
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 20px;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.iframe-player-fullscreen-btn:hover {
|
|||
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 全屏模式下的样式 */
|
|||
|
|
.iframe-player-container:fullscreen {
|
|||
|
|
border-radius: 0;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.3 全屏iframe页面组件
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// FullscreenIframePage.jsx
|
|||
|
|
import React from 'react';
|
|||
|
|
import { IconArrowLeft } from '@arco-design/web-react/icon';
|
|||
|
|
import './FullscreenIframePage.css';
|
|||
|
|
|
|||
|
|
const FullscreenIframePage = ({
|
|||
|
|
url,
|
|||
|
|
title = '课程预览',
|
|||
|
|
onBack,
|
|||
|
|
zoom = 0.8
|
|||
|
|
}) => {
|
|||
|
|
return (
|
|||
|
|
<div className="fullscreen-iframe-wrapper">
|
|||
|
|
<div className="fullscreen-iframe-header">
|
|||
|
|
<button
|
|||
|
|
className="fullscreen-iframe-back-btn"
|
|||
|
|
onClick={onBack}
|
|||
|
|
>
|
|||
|
|
<IconArrowLeft style={{ marginRight: '8px' }} />
|
|||
|
|
返回
|
|||
|
|
</button>
|
|||
|
|
<span className="fullscreen-iframe-title">{title}</span>
|
|||
|
|
</div>
|
|||
|
|
<iframe
|
|||
|
|
src={url}
|
|||
|
|
className="fullscreen-iframe-content"
|
|||
|
|
title={title}
|
|||
|
|
style={{ zoom }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default FullscreenIframePage;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**FullscreenIframePage.css**
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
.fullscreen-iframe-wrapper {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
background-color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-iframe-header {
|
|||
|
|
height: 60px;
|
|||
|
|
padding: 0 20px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
border-bottom: 1px solid #e5e6eb;
|
|||
|
|
background-color: #fff;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-iframe-back-btn {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 8px 16px;
|
|||
|
|
background: linear-gradient(135deg, #2c7aff 0%, #4096ff 100%);
|
|||
|
|
color: #fff;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-iframe-back-btn:hover {
|
|||
|
|
background: linear-gradient(135deg, #4096ff 0%, #69b1ff 100%);
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 4px 8px rgba(44, 122, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-iframe-title {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1d2129;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-iframe-content {
|
|||
|
|
flex: 1;
|
|||
|
|
width: 100%;
|
|||
|
|
border: none;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 步骤 4: 场景集成
|
|||
|
|
|
|||
|
|
#### 4.1 课程直播场景集成
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// CourseLiveExample.jsx
|
|||
|
|
import React, { useState } from 'react';
|
|||
|
|
import PreviewBadge from './PreviewBadge';
|
|||
|
|
import IframePlayer from './IframePlayer';
|
|||
|
|
|
|||
|
|
const CourseLiveExample = () => {
|
|||
|
|
const [selectedCourse, setSelectedCourse] = useState(null);
|
|||
|
|
|
|||
|
|
// 示例课程数据
|
|||
|
|
const courses = [
|
|||
|
|
{
|
|||
|
|
id: 1,
|
|||
|
|
name: "展会主题与品牌定位",
|
|||
|
|
canPreview: true,
|
|||
|
|
previewUrl: "https://du9uay.github.io/zhanhui/"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 2,
|
|||
|
|
name: "展区规划与动线设计",
|
|||
|
|
canPreview: false
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="course-live-container">
|
|||
|
|
{/* 课程列表 */}
|
|||
|
|
<div className="course-list">
|
|||
|
|
{courses.map(course => (
|
|||
|
|
<div
|
|||
|
|
key={course.id}
|
|||
|
|
className="course-item"
|
|||
|
|
onClick={() => setSelectedCourse(course)}
|
|||
|
|
>
|
|||
|
|
<span>{course.name}</span>
|
|||
|
|
{course.canPreview && <PreviewBadge type="course" />}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 视频播放区 */}
|
|||
|
|
<div className="video-player">
|
|||
|
|
{selectedCourse ? (
|
|||
|
|
selectedCourse.canPreview && selectedCourse.previewUrl ? (
|
|||
|
|
<IframePlayer
|
|||
|
|
url={selectedCourse.previewUrl}
|
|||
|
|
title={selectedCourse.name}
|
|||
|
|
zoom={0.5}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<div className="locked-view">
|
|||
|
|
<img src="/lock-icon.png" alt="锁定" />
|
|||
|
|
<span>非学员无查看权限</span>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
) : (
|
|||
|
|
<div className="empty-view">请选择课程</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default CourseLiveExample;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.2 课后作业场景集成
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// HomeworkExample.jsx
|
|||
|
|
import React, { useState } from 'react';
|
|||
|
|
import PreviewBadge from './PreviewBadge';
|
|||
|
|
import FullscreenIframePage from './FullscreenIframePage';
|
|||
|
|
|
|||
|
|
const HomeworkExample = () => {
|
|||
|
|
const [showIframe, setShowIframe] = useState(false);
|
|||
|
|
const [selectedHomework, setSelectedHomework] = useState(null);
|
|||
|
|
|
|||
|
|
// 示例作业数据
|
|||
|
|
const homeworks = [
|
|||
|
|
{
|
|||
|
|
id: 1,
|
|||
|
|
name: "展会主题与品牌定位",
|
|||
|
|
imageUrl: "/homework-poster.jpg",
|
|||
|
|
isShowCase: true,
|
|||
|
|
previewUrl: "https://du9uay.github.io/zhanhui/#/course-test"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 2,
|
|||
|
|
name: "展区规划与动线设计",
|
|||
|
|
imageUrl: "/homework-poster2.jpg",
|
|||
|
|
isShowCase: false
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const handleClick = (homework) => {
|
|||
|
|
if (homework.isShowCase) {
|
|||
|
|
setSelectedHomework(homework);
|
|||
|
|
setShowIframe(true);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 显示全屏iframe
|
|||
|
|
if (showIframe && selectedHomework) {
|
|||
|
|
return (
|
|||
|
|
<FullscreenIframePage
|
|||
|
|
url={selectedHomework.previewUrl}
|
|||
|
|
title={selectedHomework.name}
|
|||
|
|
onBack={() => setShowIframe(false)}
|
|||
|
|
zoom={0.8}
|
|||
|
|
/>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 显示作业列表
|
|||
|
|
return (
|
|||
|
|
<div className="homework-container">
|
|||
|
|
{homeworks.map(homework => (
|
|||
|
|
<div key={homework.id} className="homework-card">
|
|||
|
|
<img src={homework.imageUrl} alt={homework.name} />
|
|||
|
|
<p>{homework.name}</p>
|
|||
|
|
<div className="homework-action">
|
|||
|
|
<button
|
|||
|
|
className={homework.isShowCase ? 'completed' : 'disabled'}
|
|||
|
|
onClick={() => handleClick(homework)}
|
|||
|
|
disabled={!homework.isShowCase}
|
|||
|
|
>
|
|||
|
|
已完成
|
|||
|
|
</button>
|
|||
|
|
{homework.isShowCase && <PreviewBadge type="homework" />}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default HomeworkExample;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 代码示例
|
|||
|
|
|
|||
|
|
### 完整示例:课程列表组件
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// CourseList.jsx - 完整实现
|
|||
|
|
import React, { useState } from 'react';
|
|||
|
|
import { Collapse, Timeline } from '@arco-design/web-react';
|
|||
|
|
import './CourseList.css';
|
|||
|
|
|
|||
|
|
const CourseList = ({ courses, onCourseClick }) => {
|
|||
|
|
const [activeKeys, setActiveKeys] = useState([]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="course-list-wrapper">
|
|||
|
|
<Collapse
|
|||
|
|
activeKey={activeKeys}
|
|||
|
|
onChange={setActiveKeys}
|
|||
|
|
>
|
|||
|
|
{courses.map((unit, index) => {
|
|||
|
|
// 检查单元是否包含可试看课程
|
|||
|
|
const hasPreviewCourse = unit.courses.some(c => c.canPreview);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Collapse.Item
|
|||
|
|
key={unit.id}
|
|||
|
|
name={String(index + 1)}
|
|||
|
|
header={
|
|||
|
|
<div className="unit-header">
|
|||
|
|
{unit.name}
|
|||
|
|
{hasPreviewCourse && (
|
|||
|
|
<span className="preview-badge-unit">可试看</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
className={hasPreviewCourse ? 'has-preview-unit' : ''}
|
|||
|
|
>
|
|||
|
|
<Timeline>
|
|||
|
|
{unit.courses.map(course => (
|
|||
|
|
<Timeline.Item key={course.id}>
|
|||
|
|
<div
|
|||
|
|
className={`course-item ${course.canPreview ? 'has-preview' : ''}`}
|
|||
|
|
onClick={() => onCourseClick(course)}
|
|||
|
|
>
|
|||
|
|
{course.canPreview && (
|
|||
|
|
<span className="preview-badge-course">可试看</span>
|
|||
|
|
)}
|
|||
|
|
<p>{course.name}</p>
|
|||
|
|
<div className="course-info">
|
|||
|
|
<span>{course.teacher}</span>
|
|||
|
|
<span>{course.date}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Timeline.Item>
|
|||
|
|
))}
|
|||
|
|
</Timeline>
|
|||
|
|
</Collapse.Item>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</Collapse>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default CourseList;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**CourseList.css**
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
.course-list-wrapper {
|
|||
|
|
width: 100%;
|
|||
|
|
background: #fff;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 单元标题样式 */
|
|||
|
|
.unit-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 包含可试看课程的单元样式 */
|
|||
|
|
.has-preview-unit .arco-collapse-item-header {
|
|||
|
|
background: linear-gradient(135deg, #f0f7ff 0%, #e8f3ff 100%);
|
|||
|
|
border: 1px solid #b3d4ff;
|
|||
|
|
box-shadow: 0 2px 8px rgba(64, 128, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 课程项样式 */
|
|||
|
|
.course-item {
|
|||
|
|
position: relative;
|
|||
|
|
padding: 10px;
|
|||
|
|
background: #f2f3f5;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.course-item:hover {
|
|||
|
|
background: #e8f3ff;
|
|||
|
|
transform: translateX(4px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.course-item.has-preview {
|
|||
|
|
border: 1px dashed #4080ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.course-item p {
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #1d2129;
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.course-info {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #86909c;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 样式规范
|
|||
|
|
|
|||
|
|
### 颜色规范
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* 可试看标签颜色 */
|
|||
|
|
--preview-badge-bg: #4080ff; /* 蓝色背景 */
|
|||
|
|
--preview-badge-color: #ffffff; /* 白色文字 */
|
|||
|
|
--preview-badge-homework: #ff8c00; /* 橙色(作业场景) */
|
|||
|
|
|
|||
|
|
/* 可试看单元背景 */
|
|||
|
|
--preview-unit-bg-start: #f0f7ff;
|
|||
|
|
--preview-unit-bg-end: #e8f3ff;
|
|||
|
|
--preview-unit-border: #b3d4ff;
|
|||
|
|
|
|||
|
|
/* 按钮状态 */
|
|||
|
|
--btn-completed-bg: linear-gradient(135deg, #2c7aff 0%, #4096ff 100%);
|
|||
|
|
--btn-disabled-bg: #f5f5f5;
|
|||
|
|
--btn-disabled-color: #c9cdd4;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 尺寸规范
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* 标签尺寸 */
|
|||
|
|
--badge-course-padding: 4px 12px;
|
|||
|
|
--badge-homework-padding: 2px 8px;
|
|||
|
|
--badge-unit-padding: 2px 8px;
|
|||
|
|
|
|||
|
|
--badge-font-size-normal: 12px;
|
|||
|
|
--badge-font-size-small: 11px;
|
|||
|
|
--badge-border-radius: 12px;
|
|||
|
|
|
|||
|
|
/* iframe容器 */
|
|||
|
|
--iframe-header-height: 60px;
|
|||
|
|
--iframe-fullscreen-btn-size: 40px;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 动画效果
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* 标签脉冲动画 */
|
|||
|
|
@keyframes badge-pulse {
|
|||
|
|
0%, 100% {
|
|||
|
|
transform: scale(1);
|
|||
|
|
box-shadow: 0 3px 8px rgba(64, 128, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
50% {
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
box-shadow: 0 4px 12px rgba(64, 128, 255, 0.5);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-badge-course {
|
|||
|
|
animation: badge-pulse 2s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 按钮悬停效果 */
|
|||
|
|
.completed-btn {
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.completed-btn:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 4px 8px rgba(44, 122, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 可试看配置表(数据库设计)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE preview_config (
|
|||
|
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
|||
|
|
resource_type VARCHAR(20) NOT NULL COMMENT '资源类型:course/homework',
|
|||
|
|
resource_id VARCHAR(50) NOT NULL COMMENT '资源ID',
|
|||
|
|
is_enabled BOOLEAN DEFAULT FALSE COMMENT '是否启用试看',
|
|||
|
|
preview_url VARCHAR(500) COMMENT '试看链接',
|
|||
|
|
zoom_level DECIMAL(3,2) DEFAULT 0.5 COMMENT '缩放比例',
|
|||
|
|
allow_fullscreen BOOLEAN DEFAULT TRUE COMMENT '是否允许全屏',
|
|||
|
|
expires_at DATETIME COMMENT '试看过期时间',
|
|||
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
UNIQUE KEY uk_resource (resource_type, resource_id)
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 配置示例数据
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO preview_config (resource_type, resource_id, is_enabled, preview_url, zoom_level) VALUES
|
|||
|
|
('course', 'course_001', TRUE, 'https://du9uay.github.io/zhanhui/', 0.5),
|
|||
|
|
('homework', 'homework_142', TRUE, 'https://du9uay.github.io/zhanhui/#/course-test', 0.8);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
### Q1: iframe 内容不显示或加载失败?
|
|||
|
|
|
|||
|
|
**可能原因:**
|
|||
|
|
- 目标网站设置了 `X-Frame-Options` 禁止被嵌套
|
|||
|
|
- HTTPS 网站嵌套 HTTP 内容被浏览器阻止
|
|||
|
|
- 网络问题或 URL 错误
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
```javascript
|
|||
|
|
// 1. 检查目标网站是否允许iframe嵌套
|
|||
|
|
// 2. 确保URL协议一致(都使用HTTPS)
|
|||
|
|
// 3. 添加错误处理和加载状态
|
|||
|
|
|
|||
|
|
const [loadError, setLoadError] = useState(false);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
|
|||
|
|
<iframe
|
|||
|
|
src={url}
|
|||
|
|
onLoad={() => setLoading(false)}
|
|||
|
|
onError={() => {
|
|||
|
|
setLoadError(true);
|
|||
|
|
setLoading(false);
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{loadError && <div className="error-message">内容加载失败</div>}
|
|||
|
|
{loading && <div className="loading">加载中...</div>}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Q2: 如何控制不同用户的试看权限?
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
```javascript
|
|||
|
|
// 在数据请求时根据用户权限返回不同的数据
|
|||
|
|
const fetchCourses = async (userId) => {
|
|||
|
|
const response = await api.get('/courses/list', {
|
|||
|
|
params: { userId }
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 后端根据用户VIP等级、购买记录等判断是否可试看
|
|||
|
|
return response.data;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 前端也可以做二次验证
|
|||
|
|
const canUserPreview = (course, userInfo) => {
|
|||
|
|
// VIP用户可以试看所有课程
|
|||
|
|
if (userInfo.isVIP) return true;
|
|||
|
|
|
|||
|
|
// 已购买用户可以观看完整内容
|
|||
|
|
if (course.isPurchased) return true;
|
|||
|
|
|
|||
|
|
// 其他用户只能试看标记为canPreview的课程
|
|||
|
|
return course.canPreview;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Q3: 如何限制试看时长?
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
```javascript
|
|||
|
|
// 添加试看时长限制
|
|||
|
|
const IframePlayerWithTimeLimit = ({ url, maxDuration = 300 }) => {
|
|||
|
|
const [remainingTime, setRemainingTime] = useState(maxDuration);
|
|||
|
|
const [expired, setExpired] = useState(false);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (remainingTime <= 0) {
|
|||
|
|
setExpired(true);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const timer = setInterval(() => {
|
|||
|
|
setRemainingTime(prev => prev - 1);
|
|||
|
|
}, 1000);
|
|||
|
|
|
|||
|
|
return () => clearInterval(timer);
|
|||
|
|
}, [remainingTime]);
|
|||
|
|
|
|||
|
|
if (expired) {
|
|||
|
|
return (
|
|||
|
|
<div className="trial-expired">
|
|||
|
|
<p>试看时间已结束</p>
|
|||
|
|
<button onClick={() => handlePurchase()}>立即购买完整课程</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div>
|
|||
|
|
<div className="time-remaining">
|
|||
|
|
剩余试看时间: {Math.floor(remainingTime / 60)}:{remainingTime % 60}
|
|||
|
|
</div>
|
|||
|
|
<iframe src={url} />
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Q4: 如何优化iframe加载性能?
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
```javascript
|
|||
|
|
// 1. 懒加载iframe
|
|||
|
|
const [shouldLoadIframe, setShouldLoadIframe] = useState(false);
|
|||
|
|
|
|||
|
|
<div onClick={() => setShouldLoadIframe(true)}>
|
|||
|
|
{shouldLoadIframe ? (
|
|||
|
|
<iframe src={url} />
|
|||
|
|
) : (
|
|||
|
|
<div className="preview-placeholder">
|
|||
|
|
<img src={posterUrl} />
|
|||
|
|
<button>点击播放</button>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
// 2. 预加载关键资源
|
|||
|
|
<link rel="preconnect" href="https://du9uay.github.io" />
|
|||
|
|
|
|||
|
|
// 3. 使用 loading="lazy" 属性(现代浏览器支持)
|
|||
|
|
<iframe src={url} loading="lazy" />
|
|||
|
|
|
|||
|
|
// 4. 添加缓存策略
|
|||
|
|
const getCachedPreviewUrl = (courseId) => {
|
|||
|
|
const cached = sessionStorage.getItem(`preview_${courseId}`);
|
|||
|
|
if (cached) return cached;
|
|||
|
|
|
|||
|
|
const url = fetchPreviewUrl(courseId);
|
|||
|
|
sessionStorage.setItem(`preview_${courseId}`, url);
|
|||
|
|
return url;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Q5: 如何追踪用户的试看行为?
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
```javascript
|
|||
|
|
// 添加试看行为追踪
|
|||
|
|
const trackPreviewBehavior = (eventType, data) => {
|
|||
|
|
api.post('/api/analytics/preview', {
|
|||
|
|
eventType, // 'start', 'end', 'fullscreen', 'exit'
|
|||
|
|
courseId: data.courseId,
|
|||
|
|
userId: data.userId,
|
|||
|
|
duration: data.duration,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 在组件中使用
|
|||
|
|
useEffect(() => {
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
|
|||
|
|
// 记录开始试看
|
|||
|
|
trackPreviewBehavior('start', { courseId, userId });
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
// 记录结束试看
|
|||
|
|
const duration = Math.floor((Date.now() - startTime) / 1000);
|
|||
|
|
trackPreviewBehavior('end', { courseId, userId, duration });
|
|||
|
|
};
|
|||
|
|
}, [courseId, userId]);
|
|||
|
|
|
|||
|
|
// 监听全屏事件
|
|||
|
|
const handleFullscreenChange = () => {
|
|||
|
|
if (document.fullscreenElement) {
|
|||
|
|
trackPreviewBehavior('fullscreen', { courseId, userId });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Q6: 如何实现试看内容的水印?
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
```jsx
|
|||
|
|
// 添加水印层覆盖在iframe上
|
|||
|
|
const IframeWithWatermark = ({ url, userInfo }) => {
|
|||
|
|
return (
|
|||
|
|
<div className="iframe-container">
|
|||
|
|
<iframe src={url} />
|
|||
|
|
<div className="watermark-layer">
|
|||
|
|
<span className="watermark-text">
|
|||
|
|
试看用户:{userInfo.name} | {userInfo.id}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**CSS**
|
|||
|
|
```css
|
|||
|
|
.iframe-container {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.watermark-layer {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
pointer-events: none;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.watermark-text {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
padding: 4px 12px;
|
|||
|
|
background: rgba(0, 0, 0, 0.5);
|
|||
|
|
color: rgba(255, 255, 255, 0.6);
|
|||
|
|
font-size: 12px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 最佳实践
|
|||
|
|
|
|||
|
|
### 1. 安全性考虑
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 验证预览URL的合法性
|
|||
|
|
const isValidPreviewUrl = (url) => {
|
|||
|
|
const allowedDomains = [
|
|||
|
|
'du9uay.github.io',
|
|||
|
|
'example.com'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const urlObj = new URL(url);
|
|||
|
|
return allowedDomains.some(domain => urlObj.hostname.includes(domain));
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 使用时验证
|
|||
|
|
if (course.canPreview && isValidPreviewUrl(course.previewUrl)) {
|
|||
|
|
// 显示iframe
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 用户体验优化
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 添加loading状态
|
|||
|
|
const [iframeLoading, setIframeLoading] = useState(true);
|
|||
|
|
|
|||
|
|
<div className="iframe-wrapper">
|
|||
|
|
{iframeLoading && (
|
|||
|
|
<div className="iframe-loading">
|
|||
|
|
<Spin tip="课程加载中..." />
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
<iframe
|
|||
|
|
src={url}
|
|||
|
|
onLoad={() => setIframeLoading(false)}
|
|||
|
|
style={{ opacity: iframeLoading ? 0 : 1 }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 响应式设计
|
|||
|
|
|
|||
|
|
```css
|
|||
|
|
/* 移动端适配 */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.iframe-player-container {
|
|||
|
|
border-radius: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.preview-badge-course {
|
|||
|
|
font-size: 10px;
|
|||
|
|
padding: 2px 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fullscreen-iframe-header {
|
|||
|
|
height: 50px;
|
|||
|
|
padding: 0 12px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 错误边界处理
|
|||
|
|
|
|||
|
|
```jsx
|
|||
|
|
// 错误边界组件
|
|||
|
|
class PreviewErrorBoundary extends React.Component {
|
|||
|
|
state = { hasError: false };
|
|||
|
|
|
|||
|
|
static getDerivedStateFromError(error) {
|
|||
|
|
return { hasError: true };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
componentDidCatch(error, errorInfo) {
|
|||
|
|
console.error('Preview error:', error, errorInfo);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
render() {
|
|||
|
|
if (this.state.hasError) {
|
|||
|
|
return (
|
|||
|
|
<div className="preview-error">
|
|||
|
|
<p>内容加载出错</p>
|
|||
|
|
<button onClick={() => window.location.reload()}>
|
|||
|
|
刷新重试
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return this.props.children;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用
|
|||
|
|
<PreviewErrorBoundary>
|
|||
|
|
<IframePlayer url={previewUrl} />
|
|||
|
|
</PreviewErrorBoundary>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 总结
|
|||
|
|
|
|||
|
|
通过以上文档,你可以快速在其他系统中复用可试看功能。核心要点:
|
|||
|
|
|
|||
|
|
1. **数据层**:添加 `canPreview`、`previewUrl` 等字段标记
|
|||
|
|
2. **组件层**:封装可复用的标签组件和播放器组件
|
|||
|
|
3. **样式层**:遵循统一的设计规范和颜色体系
|
|||
|
|
4. **安全性**:验证URL合法性、控制权限、追踪行为
|
|||
|
|
5. **用户体验**:loading状态、错误处理、响应式设计
|
|||
|
|
|
|||
|
|
如有问题,请参考常见问题章节或联系开发团队。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档版本**: v1.0
|
|||
|
|
**最后更新**: 2025-01-15
|
|||
|
|
**维护者**: 教务系统前端团队
|