提交 8f787db2 作者: ZhangLingKun

功能:课程详情

上级 4b706df3
流水线 #8083 已通过 于阶段
in 5 分 34 秒
// 分页通用接口
import { AxiosRequestConfig } from 'axios';
export interface PaginationProps {
pageSize: number;
pageNo: number;
......@@ -39,17 +41,23 @@ export interface ResponseType<D> {
// 通用接口封装函数(分页) 建议用这个
export interface InterListFunction<D extends object, T> {
(req: D & Partial<PaginationProps>): Promise<ResponseListType<T>>;
(
req: D & Partial<PaginationProps>,
config?: AxiosRequestConfig,
): Promise<ResponseListType<T>>;
}
// 通用接口封装函数(不分页) 建议用这个
export interface InterFunction<D extends object, T> {
(req?: D): Promise<ResponseType<T>>;
(req?: D, config?: AxiosRequestConfig): Promise<ResponseType<T>>;
}
// 通用接口封装函数(分页了,但又没有分页) 建议用这个
export interface InterItemFunction<D extends object, T> {
(req: D & Partial<PaginationProps>): Promise<ResponseItemType<T>>;
(
req: D & Partial<PaginationProps>,
config?: AxiosRequestConfig,
): Promise<ResponseItemType<T>>;
}
// 返回类型封装
......
......@@ -15,6 +15,6 @@ export class CourseAPI {
request.post('/release/curriculum/queryCurriculumInfoList', params);
// 课程详情
static getCourseDetail: CourseDetailType = (params) =>
request.get('/release/curriculum/curriculumDetails', { params });
static getCourseDetail: CourseDetailType = (params, config) =>
request.get('/release/curriculum/curriculumDetails', { params, ...config });
}
......@@ -12,7 +12,6 @@ const service = axios.create({
service.interceptors.request.use(
(config: any) => {
const token = Cookies.get('SHAREFLY-WEB-TOKEN');
// console.log('token ==========>', config.headers);
if (token) {
// eslint-disable-next-line no-param-reassign
config.headers.token = token;
......
......@@ -150,7 +150,7 @@ const HeaderView: React.FC<{
onOk: () => {
dispatch(setUserInfo(null));
dispatch(setSystem({ token: undefined }));
Cookies.remove('SHAREFLY-TOKEN');
Cookies.remove('SHAREFLY-WEB-TOKEN');
},
});
}}
......
import React from 'react';
import styled from 'styled-components';
import { InterDataType } from '@/api/interface';
import { CourseDetailType } from '@/api/interface/course';
import { CourseAPI } from '@/api/modules/course';
import BreadcrumbView from '@/components/breadcrumb';
import LayoutView from '@/components/layout';
import CourseMenuView from '@/pages/course/detail/comp/_courseMenu';
import { wrapper } from '@/store';
// 课程类型
type DetailType = InterDataType<CourseDetailType>;
// 每次加载页面都会执行
export const getServerSideProps = wrapper.getServerSideProps(
(_store) => async (ctx) => {
// 课程id
const id = String(ctx.params?.id);
// 课程详情
let courseDetail: DetailType | undefined;
// 获取课程详情
const getCourseDetail = async () => {
const res = await CourseAPI.getCourseDetail(
{
id,
},
{ headers: { token: ctx.req.cookies['SHAREFLY-WEB-TOKEN'] } },
);
if (res && res.code === '200') {
courseDetail = res.result;
// console.log('服务器渲染 ====>', ctx.req.cookies['SHAREFLY-WEB-TOKEN']);
}
};
// 依次获取接口数据
await (async () => {
await getCourseDetail();
})();
return { props: { courseDetail } };
},
);
// 课程类型列表
const courseAttributeList: { label: string; value: number; color: string }[] = [
{ label: '免费', value: 0, color: '#f99a0b' },
{ label: '积分', value: 1, color: '#00AD53' },
{ label: '付费', value: 2, color: '#FF4600' },
];
// 组件
const CourseDetailView: React.FC<{
courseDetail: DetailType;
}> = ({ courseDetail }) => {
// 获取当前课程属性
const getCourseAttribute = () => {
return courseAttributeList.find(
(i) => i.value === courseDetail?.courseAttribute,
);
};
return (
<LayoutView>
<CourseDetailWrap>
{/* 面包屑 */}
<BreadcrumbView />
{/* 课程详情 */}
<div className="course-head">
{/* 课程视频 */}
<div className="course-video">
{courseDetail?.buy || courseDetail?.courseAttribute === 0 ? (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
className="media"
src={courseDetail?.videoUrl}
controls={true}
/>
) : (
<img
className="media"
src={
courseDetail.surfaceUrl
? `${courseDetail?.surfaceUrl}?x-oss-process=image/quality,Q_50`
: `${courseDetail?.videoUrl}?x-oss-process=video/snapshot,t_1000,m_fast`
}
alt={courseDetail?.curriculumName}
/>
)}
{!courseDetail?.buy && courseDetail?.courseAttribute !== 0 ? (
<div className="flex-center shadow">
<div className="text">
{courseDetail?.courseAttribute === 1
? '兑换课程后可立即观看'
: '购买课程后可立即观看'}
</div>
</div>
) : undefined}
</div>
{/* 课程目录 */}
<div className="course-menu scroll-view">
<div className="menu-title">目录</div>
{/* 目录组件 */}
<CourseMenuView detail={courseDetail} />
</div>
</div>
{/* 课程介绍 */}
<div className="course-content">
<div className="content-title">
<div className="title">{courseDetail?.curriculumName}</div>
<div
className="tag"
style={{ background: getCourseAttribute()?.color }}
>
{getCourseAttribute()?.label}
</div>
</div>
{!!courseDetail?.requireAmout && (
<div className="content-price">
<span className="label"></span>
<span className="num">{courseDetail?.requireAmout}</span>
</div>
)}
{!!courseDetail?.requireIntegral && (
<div className="content-price">
<span className="num">{courseDetail?.requireIntegral}</span>
<span className="label">积分</span>
</div>
)}
<div className="content-desc">
<div className="title">课程简介</div>
<div className="desc">{courseDetail?.curriculumDesc}</div>
</div>
<div className="content-desc">
<div className="title">课程图文</div>
{courseDetail?.detailContent && (
<div
className="richText"
dangerouslySetInnerHTML={{
__html: courseDetail?.detailContent,
}}
/>
)}
</div>
</div>
</CourseDetailWrap>
</LayoutView>
);
};
export default CourseDetailView;
// 样式
const CourseDetailWrap = styled.div`
position: relative;
max-width: 1190px;
box-sizing: border-box;
padding: 2rem 0 0 0;
margin: 0 auto;
.course-head {
position: relative;
width: 100%;
height: 26rem;
display: flex;
flex-wrap: nowrap;
.course-video {
position: relative;
width: calc(100% / 3 * 2);
height: 100%;
.media {
width: 100%;
height: 100%;
}
.shadow {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.5);
.text {
position: relative;
height: 1.75rem;
line-height: 1.75rem;
background: #ff4600;
border-radius: 0.25rem;
color: #ffffff;
box-sizing: border-box;
padding: 0 0.75rem;
cursor: pointer;
&:hover {
filter: brightness(0.9);
}
}
}
}
.course-menu {
width: calc(100% / 3 * 1 - 2rem);
height: 100%;
margin-left: 2rem;
background: #000c17;
box-sizing: border-box;
padding: 0.67rem 0.67rem 0.67rem 1rem;
overflow: hidden;
overflow-y: scroll;
.menu-title {
position: relative;
width: 2rem;
color: #c3c3c3;
font-size: 16px;
text-align: center;
margin-bottom: 1rem;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
background: #247bfe;
}
}
}
}
.course-content {
position: relative;
width: calc(100% / 3 * 2);
box-sizing: border-box;
padding: 0.5rem 0 0 0;
.content-title {
position: relative;
width: 100%;
display: flex;
align-items: baseline;
justify-content: flex-start;
flex-wrap: nowrap;
.title {
max-width: calc(100% - 2.25rem - 0.5rem);
font-size: 20px;
color: #333333;
font-weight: bold;
margin-right: 0.5rem;
}
.tag {
min-width: 2.25rem;
box-sizing: border-box;
font-size: 12px;
background: #f99a0b;
border-radius: 0.25rem;
text-align: center;
color: #ffffff;
padding: 0 0.25rem;
transform: scale(0.8);
}
}
.content-price {
font-weight: 500;
color: #ff3300;
.num {
font-size: 24px;
//line-height: 35px;
margin-right: 8rpx;
}
.label {
font-size: 16px;
line-height: 28rpx;
font-weight: bold;
}
}
.content-desc {
position: relative;
box-sizing: border-box;
width: 100%;
margin-bottom: 1rem;
.title {
font-size: 16px;
color: #333333;
font-weight: bold;
margin-bottom: 0.5rem;
}
.desc {
color: #666666;
}
.richText {
width: 100%;
img {
width: 100%;
}
}
}
}
`;
import React, { useEffect, useState } from 'react';
import { Menu, MenuProps } from 'antd';
import { useRouter } from 'next/router';
import { InterDataType, InterListType } from '@/api/interface';
import {
CourseDetailType,
QueryCurriculumInfoListType,
SelectCurriculumClassifyType,
} from '@/api/interface/course';
import { CourseAPI } from '@/api/modules/course';
// 详情类型
type DetailType = InterDataType<CourseDetailType>;
// 分类类型
type TabType = Array<
InterDataType<SelectCurriculumClassifyType>[0] & {
children?: InterDataType<SelectCurriculumClassifyType>;
}
>;
// 列表类型
type ListType = InterListType<QueryCurriculumInfoListType>;
const CourseMenuView: React.FC<{ detail: DetailType }> = ({ detail }) => {
// 路由钩子
const router = useRouter();
// 分类列表
const [tabList, setTabList] = useState<TabType>([]);
// 课程列表
const [courseList, setCourseList] = useState<ListType>([]);
// 获取课程分类
const getCurriculumClassify = async () => {
const res = await CourseAPI.selectCurriculumClassify();
if (res && res.code === '200') {
setTabList(
res.result
?.filter((i) => !i.twoCourseId)
?.map((i) => {
const children = res.result?.filter(
(k) => k.oneCourseId === i.oneCourseId && k.twoCourseId,
);
return {
...i,
children: children?.length ? children : undefined,
};
}),
);
}
};
// 获取课程列表
const getCurriculumInfoList = async () => {
const res = await CourseAPI.getCourseVideoList({
pageNo: 1,
pageSize: 999,
oneCourseId: detail?.oneCourseId,
twoCourseId: detail?.twoCourseId,
});
if (res && res.code === '200') {
setCourseList(res.result.list || []);
}
};
// 获取当前的分类信息
const getCurrentMenu = (): MenuProps['items'] => {
const main = tabList?.find((i) => i.oneCourseId === detail?.oneCourseId);
const second = main?.children?.find(
(i) => i.twoCourseId === detail?.twoCourseId,
);
return [
{
label: String(second?.name),
key: String(second?.oneCourseId),
children: courseList?.map((i) => ({
label: i.curriculumName,
key: String(i.id),
})),
},
];
};
// 切换课程
const handleSelect: MenuProps['onSelect'] = (e) => {
router.push(`/course/detail/${e.key}`).then();
};
// 组件挂载
useEffect(() => {
if (!detail?.id) return;
Promise.all([getCurriculumClassify(), getCurriculumInfoList()]).then();
}, [detail]);
// 视图组件
return (
<div className="relative w-full">
<Menu
onSelect={handleSelect}
style={{ width: '100%' }}
mode="inline"
items={getCurrentMenu()}
theme="dark"
defaultOpenKeys={[String(detail?.oneCourseId)]}
defaultSelectedKeys={[String(detail?.id)]}
/>
</div>
);
};
export default CourseMenuView;
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论