- 更新多个组件的功能优化 - 整理简历映射数据 - 优化视频播放和面试模拟相关组件 - 更新就业策略和公司职位页面 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
28 KiB
28 KiB
可试看功能实现文档
📋 目录
功能概述
功能描述
"可试看"功能允许特定课程在未购买或未解锁的情况下,通过iframe方式预览课程内容。该功能分为两个场景:
- 课程直播间场景:在课程列表中显示可试看标签,点击后在视频播放器区域内嵌显示课程内容
- 课后作业场景:在作业卡片上显示可试看标签,点击后全屏显示课程内容
核心特性
- ✅ 视觉标签:醒目的"可试看"标签提示
- ✅ 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 课程直播数据结构
// 课程对象结构
const courseObj = {
courseId: "course_001",
courseName: "展会主题与品牌定位",
teacherName: "张老师",
date: "2025-09-15",
unitName: "消费电子展品牌策划与执行",
// 可试看相关字段
canPreview: true, // 是否可试看
previewUrl: "https://example.com/preview" // 试看页面URL
};
1.2 课后作业数据结构
// 作业对象结构
const homeworkItem = {
id: 142,
name: "展会主题与品牌定位",
level: "completed",
imageUrl: "https://example.com/poster.jpg",
// 可试看相关字段
isShowCase: true // 是否可试看(作业场景)
};
步骤 2: 数据标记逻辑
2.1 在数据处理函数中添加标记
// 示例:处理课程数据时添加可试看标记
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 可试看标签组件
// 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
.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播放器组件
// 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
.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页面组件
// 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
.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 课程直播场景集成
// 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 课后作业场景集成
// 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;
代码示例
完整示例:课程列表组件
// 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
.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;
}
样式规范
颜色规范
/* 可试看标签颜色 */
--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;
尺寸规范
/* 标签尺寸 */
--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;
动画效果
/* 标签脉冲动画 */
@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);
}
可试看配置表(数据库设计)
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)
);
配置示例数据
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 错误
解决方案:
// 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: 如何控制不同用户的试看权限?
解决方案:
// 在数据请求时根据用户权限返回不同的数据
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: 如何限制试看时长?
解决方案:
// 添加试看时长限制
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加载性能?
解决方案:
// 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: 如何追踪用户的试看行为?
解决方案:
// 添加试看行为追踪
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: 如何实现试看内容的水印?
解决方案:
// 添加水印层覆盖在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
.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. 安全性考虑
// 验证预览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. 用户体验优化
// 添加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. 响应式设计
/* 移动端适配 */
@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. 错误边界处理
// 错误边界组件
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>
总结
通过以上文档,你可以快速在其他系统中复用可试看功能。核心要点:
- 数据层:添加
canPreview、previewUrl等字段标记 - 组件层:封装可复用的标签组件和播放器组件
- 样式层:遵循统一的设计规范和颜色体系
- 安全性:验证URL合法性、控制权限、追踪行为
- 用户体验:loading状态、错误处理、响应式设计
如有问题,请参考常见问题章节或联系开发团队。
文档版本: v1.0 最后更新: 2025-01-15 维护者: 教务系统前端团队