# 可试看功能实现文档
## 📋 目录
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 (
可试看
);
};
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 (
{/* 全屏按钮 */}
);
};
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 (
);
};
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 (
{/* 课程列表 */}
{courses.map(course => (
setSelectedCourse(course)}
>
{course.name}
{course.canPreview &&
}
))}
{/* 视频播放区 */}
{selectedCourse ? (
selectedCourse.canPreview && selectedCourse.previewUrl ? (
) : (
非学员无查看权限
)
) : (
请选择课程
)}
);
};
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 (
setShowIframe(false)}
zoom={0.8}
/>
);
}
// 显示作业列表
return (
{homeworks.map(homework => (
{homework.name}
{homework.isShowCase &&
}
))}
);
};
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 (
{courses.map((unit, index) => {
// 检查单元是否包含可试看课程
const hasPreviewCourse = unit.courses.some(c => c.canPreview);
return (
{unit.name}
{hasPreviewCourse && (
可试看
)}
}
className={hasPreviewCourse ? 'has-preview-unit' : ''}
>
{unit.courses.map(course => (
onCourseClick(course)}
>
{course.canPreview && (
可试看
)}
{course.name}
{course.teacher}
{course.date}
))}
);
})}
);
};
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);