新增视频播放状态

1. 外部控制视频播放 | 暂停
main
king 1 year ago
parent 6201f78c93
commit 1e23fff664
  1. 4
      .env
  2. 2
      src/app.scss
  3. 4
      src/components/topic/judge.tsx
  4. 35
      src/components/topic/multi.tsx
  5. 7
      src/components/topic/topic.scss
  6. 29
      src/components/video/video.tsx
  7. 40
      src/hooks/videoEvents.ts
  8. 8
      src/pages/business/userInfo/userInfo.module.scss
  9. 47
      src/pages/business/videoInfo/components/catalogue.tsx
  10. 28
      src/pages/business/videoInfo/components/course.tsx
  11. 18
      src/pages/business/videoInfo/components/hours.tsx
  12. 2
      src/pages/business/videoInfo/videoInfo.scss
  13. 12
      src/pages/business/videoInfo/videoInfo.tsx
  14. 2
      src/pages/manage/spotMeeting/spotMeeting.config.ts
  15. 2
      src/pages/my/components/header/service.tsx
  16. 5
      src/static/css/module.scss

@ -1,5 +1,5 @@
TARO_APP_API=https://yjx.dev.yaojiankang.top
#TARO_APP_API=https://playedu.yaojiankang.top
#TARO_APP_API=https://yjx.dev.yaojiankang.top
TARO_APP_API=https://playedu.yaojiankang.top
TARO_APP_LGOIN=true

@ -255,7 +255,7 @@
.text-hover-primary { color: #0056b3;}
.text-secondary {color: #6c757d;}
.text-hover-secondary { color: #494f54;}
.text-success {color: #28a745;}
.text-success {color: #45D4A8;}
.text-hover-success{color: #19692c;}
.text-info { color: #17a2b8;}
.text-hover-info {color: #0f6674;}

@ -43,14 +43,14 @@ const Judge: FC<Props> = ({data, onAnswer, validate, frequency}) => {
color={validate ? 'correct' !== rightKey ? 'red' : '#45d4a8' : '#45d4a8'}
disabled={validate ? 'correct' !== rightAnswer : false}
>
<Text></Text>
</Radio>
<Radio
value={'error'}
color={validate ? 'error' !== rightKey ? 'red' : '#45d4a8' : '#45d4a8'}
disabled={validate ? 'error' !== rightAnswer : false}
className='option'>
<Text></Text>
</Radio>
</RadioGroup>

@ -1,4 +1,4 @@
import {FC, useEffect, useState} from "react";
import {FC, useCallback, useEffect, useState} from "react";
import {Checkbox, CheckboxGroup, Radio, RadioGroup, Text, View} from "@tarojs/components";
import './topic.scss'
@ -22,21 +22,38 @@ const Multi: FC<Props> = ({data, onAnswer, validate, frequency}) => {
{value: "D", title: data.answerD},
]
function onChange(e) {
function RadioChange(e) {
const value = e.detail.value
if (data.type) {
setRightAnswer(value)
} else {
setRightAnswer([value])
}
const changeCheckbox = useCallback((value: string) => {
const index = rightAnswer.indexOf(value)
if (index === -1) {
setRightAnswer([...rightAnswer, value])
} else {
const oldRightAnswer: string[] = JSON.parse(JSON.stringify(rightAnswer))
oldRightAnswer.splice(index, 1)
setRightAnswer(oldRightAnswer)
}
}, [rightAnswer])
useEffect(() => {
if (validate) {
const isAnswer = rightKey.toString() === rightAnswer.toString()
if (rightAnswer.length !== rightKey.length) {
onAnswer(false)
setError(false)
} else {
const isAnswer = rightAnswer.reduce((pre, cut) => {
if (!pre) {
return false
}
return rightKey.indexOf(cut) !== -1
}, true)
onAnswer(isAnswer)
setError(!isAnswer)
}
}
}, [validate])
return (
@ -47,17 +64,19 @@ const Multi: FC<Props> = ({data, onAnswer, validate, frequency}) => {
</View>
<View>
{data.type ? <CheckboxGroup onChange={onChange}>
{data.type ? <CheckboxGroup>
{answers.map(d => <Checkbox
onClick={() => changeCheckbox(d.value)}
key={d.value}
value={d.value}
className='option'
disabled={validate}
checked={rightAnswer.includes(d.value)}
>
<Text className='title'>{d.value}{d.title}</Text>
</Checkbox>)}
</CheckboxGroup>
: <RadioGroup onChange={onChange}>
: <RadioGroup onChange={RadioChange}>
{answers.map(d => <Radio
key={d.value}
value={d.value}

@ -16,7 +16,8 @@
.option,
.weui-cells_checkbox {
display: block;
display: flex;
align-items: center;
margin-bottom: 10px;
line-height: 2;
@ -26,6 +27,10 @@
}
}
.taro-checkbox_checked {
top: 0 !important;
}
.statistics {
border-top: 1px solid #F5F8F7;
height: 60rpx;

@ -3,9 +3,9 @@ import {HVideoOptions} from "@/components/video/type";
import Taro from "@tarojs/taro";
import {FC, useState} from "react";
import unique_ident from "@/hooks/unique_ident";
import videoEvents from "@/hooks/videoEvents";
const deviation: number = 1
const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
let video: Taro.VideoContext
@ -13,8 +13,17 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
try {
video = Taro.createVideoContext('myVideo')
} catch (e) {
videoEvents.onSetVideoState(({name}) => {
switch (name) {
case "pause":
video?.pause()
break
case "play":
video?.play()
break
}
})
} catch (e) {}
function onTimeUpdate(event: BaseEventOrig<VideoProps.onTimeUpdateEventDetail>) {
// if (opt.preview) return;
@ -38,8 +47,10 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
})
}
opt.setTime((time: number) => {
opt.setTime((time?: number) => {
if (typeof time === 'number') {
video?.seek(time)
}
video?.play()
})
@ -53,11 +64,20 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
}
}
function onPlay() {
videoEvents.videoState('play')
}
function onPause() {
videoEvents.videoState('pause')
}
Taro.useDidHide(() => {
video?.pause()
unique_ident.put(Number(currentTime.toFixed(2)), Date.now())
})
Taro.useDidShow(() => {
if (!video) {
video = Taro.createVideoContext('myVideo')
@ -67,6 +87,7 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
}
})
return (
<Video
id={'myVideo'}
@ -78,6 +99,8 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
direction={90}
onTimeUpdate={onTimeUpdate}
onEnded={onEnded}
onPlay={onPlay}
onPause={onPause}
/>
)
}

@ -0,0 +1,40 @@
import Taro from "@tarojs/taro";
const KEY = "VIDEO_EVENT"
const SET_KEY = 'SET_VIDEO_EVENT'
type StateChangeName = 'play' | 'pause'
/** 视频状态播改变播放 */
function videoState(name: StateChangeName) {
Taro.eventCenter.trigger(KEY, {name})
}
/** 接受状态播放改变 */
function onVideoState(fn: ({name: StateChangeName}) => void) {
Taro.eventCenter.once(KEY, fn)
}
/** 设置视频状态 */
function setVideoState(name: StateChangeName) {
Taro.eventCenter.trigger(SET_KEY, {name})
}
/** 接受状态改变 */
function onSetVideoState(fn: (data: { name: StateChangeName }) => void) {
Taro.eventCenter.on(SET_KEY, fn)
}
function videoOff() {
Taro.eventCenter.off(KEY)
Taro.eventCenter.off(SET_KEY)
}
export default {
videoState,
onVideoState,
onSetVideoState,
setVideoState,
videoOff
}

@ -1,17 +1,17 @@
.box {
width: 690rpx;
height: 412rpx;
background: #FFF;
background: #fff;
border-radius: 20rpx;
margin: auto;
overflow: hidden;
margin-top: 20px;
padding: 10px 0;
padding: 20px 0;
}
.buttonFixed {
position: fixed;
bottom: 100px;
bottom: env(safe-area-inset-bottom);
margin-bottom: 30rpx;
left: 0;
width: 100%;
padding: 0 30rpx;

@ -9,6 +9,7 @@ import CustomPageContainer from "@/components/custom-page-container/custom-page-
import MyButton from "@/components/button/MyButton";
import curRecord from '@/static/img/curRecord.png'
import hourRecord from '@/static/img/hourRecord.png'
import videoEvents from "@/hooks/videoEvents";
interface Props {
data: CourseDepData | null
@ -24,9 +25,27 @@ const tabList = [
]
const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
const Catalogue: FC<Props> = ({data, setHors, id, playId,}) => {
const [current, setCurrent] = useState(1)
const [show, setShow] = useState(false)
const [playing, setPlaying] = useState(false)
videoEvents.onVideoState(({name}) => {
switch (name) {
case "pause":
setPlaying(false)
break
case 'play':
setPlaying(true)
break
}
})
function onPause() {
videoEvents.setVideoState('pause')
}
function jumCurHistory() {
if (playId) {
@ -76,22 +95,30 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
function learning() {
const flats: Hour[] = Object.values(data?.hours || {}).flat(Infinity) as Hour[]
if ((data?.learn_hour_records?.length || undefined) == data?.learn_record?.hour_count && flats.length) {
if (flats[0].id === playId) {
videoEvents.setVideoState('play')
} else {
setHors(true, flats[0].id)
}
return
}
if (data?.learn_hour_records.length) {
const lastTimeId = data.learn_hour_records[data.learn_hour_records.length - 1].id
for (const [index, flat] of flats.entries()) {
if (flat.id === lastTimeId) {
const next = flats[index + 1]
if (next) {
if (next.id === playId) {
videoEvents.setVideoState('play')
} else {
setHors(true, next.id)
}
}
}
}
} else {
Taro.showToast({title: "无播放视频", icon: 'error'})
}
@ -106,23 +133,33 @@ const Catalogue: FC<Props> = ({data, setHors, id, playId}) => {
{current === 0 && <View className='short_desc'>{data?.course.short_desc || data?.course.title}</View>}
{current === 1 && <View>
<View className='my-2'></View>
{data?.chapters.length ? Object.values(data?.chapters || {}).map((d, index) => <View>
{data?.chapters.length
? Object.values(data?.chapters || {}).map((d, index) => <View>
<Collapse title={`${index + 1}.${d.name}`}>
<Hours
playId={playId}
click={(is_complete, id) => upChaptersOver(is_complete, id, index)}
learn_hour_records={data.learn_hour_records}
data={getHors(d.id)}
/>
</Collapse>
</View>)
: <Hours data={data?.hours?.[0]} click={setHors} learn_hour_records={data?.learn_hour_records}/>}
: <Hours
playId={playId}
data={data?.hours?.[0]}
click={setHors}
learn_hour_records={data?.learn_hour_records}/>}
</View>}
{current === 2 && <View className='text-center'></View>}
</View>
</View>
<View className='Videobutton'>
<MyButton onClick={learning}></MyButton>
{
playing ? <MyButton onClick={onPause}></MyButton>
: <MyButton onClick={learning}></MyButton>
}
<View className='px-3' onClick={() => setShow(true)}>...</View>
</View>

@ -243,29 +243,31 @@ const Course: FC<Props> = ({id, courseId, preview, curEnd}: Props) => {
)}
</Swiper>
<View>
<View className='statistics'>
<View>{frequency}</View>
{
(index + 1) === examAll?.[time]?.length && <View>
{!validate && <MyButton width={150} fillet onClick={() => setValidate(true)}></MyButton>}
{frequency > 0 && validate && <MyButton width={150} fillet onClick={onceMore}></MyButton>}
{frequency === 0 && validate && <MyButton width={150} fillet onClick={again}></MyButton>}
</View>
}
</View>
<View className='upAndDown'>
<View>
{index !== 0 && <MyButton size='mini' width={150} onClick={() => setIndex(index - 1)}></MyButton>}
{index !== 0 && <MyButton type='default' size='mini' width={150}
onClick={() => setIndex(index - 1)}></MyButton>}
</View>
<View>
{
(index + 1) !== examAll?.[time]?.length
&& <MyButton size='mini' width={150} onClick={() => setIndex(index + 1)}></MyButton>
&&
<MyButton size='mini' width={150} type='default' onClick={() => setIndex(index + 1)}></MyButton>
}
</View>
</View>
<View className='statistics'>
<View>{frequency}</View>
{
(index + 1) === examAll?.[time]?.length && <View>
{!validate && <MyButton width={150} fillet onClick={() => setValidate(true)}></MyButton>}
{frequency > 0 && validate && <MyButton width={150} fillet onClick={onceMore}></MyButton>}
{frequency === 0 && validate && <MyButton width={150} fillet onClick={again}></MyButton>}
</View>
}
</View>
</View>
</CustomPageContainer>
</View>

@ -1,6 +1,6 @@
import {FC} from "react";
import '../videoInfo.scss'
import {Image, View} from "@tarojs/components";
import {Image, Text, View} from "@tarojs/components";
import playOk from "@/static/img/play-ok.png";
import play from "@/static/img/play.png";
import {formatMinute} from "@/utils/time";
@ -9,6 +9,7 @@ import {curriculum} from "@/api";
import lock from '@/static/img/lock.png'
interface Props {
playId: number | null
data?: Hour[] | null
learn_hour_records?: LearnHourRecords[]
click: (is_complete: boolean, id: number) => void
@ -21,7 +22,7 @@ async function jumTest(hour: Hour) {
})
}
const Hours: FC<Props> = ({data, click, learn_hour_records}) => {
const Hours: FC<Props> = ({data, click, learn_hour_records, playId}) => {
const complete = (id: number): number | undefined => {
const find = learn_hour_records?.find(d => d.id === id)
if (find) {
@ -53,7 +54,7 @@ const Hours: FC<Props> = ({data, click, learn_hour_records}) => {
}
if (upId && complete(upId) !== 1) {
Taro.showToast({title: '禁止播放', icon: 'none'})
Taro.showToast({title: '锁定中', icon: 'none'})
return
}
@ -63,14 +64,15 @@ const Hours: FC<Props> = ({data, click, learn_hour_records}) => {
return (
<>
{data?.map((d, index) =>
<>
<View className={'hor' + ` ${complete(d.id) ? 'complete' : undefined}`}
key={index}
onClick={() => onClick(d.id, complete(d.id), d, data?.[index - 1]?.id)}>
<Image src={complete(d.id) ? playOk : play} mode="scaleToFill" className='image'/>
<Image src={(complete(d.id) || playId === d.id) ? playOk : play} mode="scaleToFill" className='image'/>
<View className='title'>
<View className='text'>
<View>{index + 1}.{d.title}</View>
<View className='font-26 text-muted'>{formatMinute(d.duration)}</View>
<View style={{wordBreak: 'break-all'}}>{playId === d.id && <Text className='text-center text-success'></Text>}{index + 1}. {d.title}</View>
<View className='font-26 text-muted mt-1'>{formatMinute(d.duration)}</View>
{complete(d.id) === 0 && <View className='font-26 text-danger'></View>}
</View>
{
@ -79,7 +81,9 @@ const Hours: FC<Props> = ({data, click, learn_hour_records}) => {
&& <Image className='lock' src={lock} mode='aspectFit'/>
}
</View>
</View>)}
</View>
</>)}
</>
)
}

@ -43,7 +43,7 @@
.image {
width: 40rpx;
height: 40rpx;
margin-top: 10rpx;
margin-top: 8rpx;
}

@ -8,13 +8,14 @@ import Taro from "@tarojs/taro";
import eventsIndex from "@/hooks/eventsIndex";
import {formatMinute} from "@/utils/time";
import unique_ident from "@/hooks/unique_ident";
import videoEvents from "@/hooks/videoEvents";
const VideoInfo: FC = () => {
const {id, depId} = Taro.getCurrentInstance()?.router?.params as any
const [data, setData] = useState<CourseDepData | null>(null)
const [playId, setPlayId] = useState<number | null>(null)
const [preview, setPreview] = useState(false)
const [playing, setPlaying] = useState(false)
const [preview, setPreview] = useState(false) // 预览
const [playing, setPlaying] = useState(false) // 学习中
const getData = useCallback(async (playing: boolean) => {
const res = await curriculum.courseDep(id, depId)
@ -102,6 +103,7 @@ const VideoInfo: FC = () => {
Taro.useUnload(() => {
unique_ident.del()
videoEvents.videoOff()
})
return (
<>
@ -123,7 +125,11 @@ const VideoInfo: FC = () => {
<Text>{((data?.learn_hour_records.length || 0) / (data?.course.class_hour || 1) * 100).toFixed(0)}%</Text>
</View>
</View>
<Catalogue data={data} setHors={setHors} id={id} playId={playId}/>
<Catalogue
data={data}
setHors={setHors}
id={id}
playId={playId}/>
</View>
</>
)

@ -1,4 +1,4 @@
export default definePageConfig({
navigationBarTitleText: '见面会',
navigationBarTitleText: '现场会',
enableShareAppMessage: true
})

@ -27,7 +27,7 @@ const Service = () => {
if ([1, 2].includes(user?.role_type || 0)) {
oldList.unshift(...[
{title: '部门管理', src: dep, router: '/pages/manage/depAdmin/depAdmin'},
{title: '课程市场', src: buy, router: '/pages/manage/curriculum/curriculum'},
// {title: '课程市场', src: buy, router: '/pages/manage/curriculum/curriculum'},
{title: '现场会', src: buy, router: '/pages/manage/spotMeeting/spotMeeting'},
])
setList(oldList)

@ -5,7 +5,6 @@ page,
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
//min-height: 100vh;
}
body {
@ -45,16 +44,14 @@ taro-button-core::after {
.card {
height: 100px;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #F5F8F7;
padding: 0 20rpx;
padding: 10px 20rpx;
font-size: 30rpx;
&-content {
font-size: 27rpx;
color: #8c8c8c;

Loading…
Cancel
Save