Files
jiaowu-test/可试看功能实现文档.md
KQL 63f8cf2e7d chore: 迁移项目到新仓库并整理代码
- 更新多个组件的功能优化
- 整理简历映射数据
- 优化视频播放和面试模拟相关组件
- 更新就业策略和公司职位页面

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 18:42:25 +08:00

1181 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 可试看功能实现文档
## 📋 目录
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
**维护者**: 教务系统前端团队