From a704da92409c2571fa11f7ab32407f7ad3fdaaee Mon Sep 17 00:00:00 2001 From: king <2229249788@qq.com> Date: Tue, 29 Aug 2023 14:46:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E5=BC=B9=E7=AA=97=E7=AD=94?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.tsx | 1 - src/components/image/image.tsx | 64 +++---- src/components/spinner/index.tsx | 171 ++++++++++++++++++ src/components/spinner/loading.svg | 13 ++ src/components/spinner/style.scss | 66 +++++++ src/components/topic/single.module.scss | 37 ++++ src/components/topic/single.tsx | 100 +++++++++- src/components/video/type.ts | 6 +- src/components/video/video.tsx | 23 +-- .../business/videoInfo/components/course.tsx | 107 ++--------- src/pages/business/videoInfo/videoInfo.scss | 4 - .../home/components/feature_recommended.tsx | 4 +- src/pages/preview/brand/list/list.tsx | 36 ++-- src/static/img/shard.png | Bin 0 -> 3486 bytes 14 files changed, 458 insertions(+), 174 deletions(-) create mode 100644 src/components/spinner/index.tsx create mode 100644 src/components/spinner/loading.svg create mode 100644 src/components/spinner/style.scss create mode 100644 src/components/topic/single.module.scss create mode 100644 src/static/img/shard.png diff --git a/src/app.tsx b/src/app.tsx index 14389ff..9e6d8c4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -32,7 +32,6 @@ function updateApp() { function App(props) { - // 可以使用所有的 React Hooks Taro.useLaunch(() => { updateApp() diff --git a/src/components/image/image.tsx b/src/components/image/image.tsx index d3591b8..1ca8d81 100644 --- a/src/components/image/image.tsx +++ b/src/components/image/image.tsx @@ -1,58 +1,58 @@ import {FC, useEffect, useState} from "react"; -import {Image, View} from "@tarojs/components"; -import emptyImg from '@/static/img/empty.png' +import {Image, ImageProps, View} from "@tarojs/components"; import {AtActivityIndicator} from "taro-ui"; +import shard from '@/static/img/shard.png' -interface Props { - src: string - mode?: "scaleToFill" | "aspectFit" | "aspectFill" | "widthFix" | "heightFix" | "top" | "bottom" | "center" | "left" | "right" | "top left" | "top right" | "bottom left" | "bottom right" | undefined - width:number - height:number +interface Props extends ImageProps { + width: number + height: number fallback?: string } -const Img: FC = ({src,mode = 'aspectFill',width,height,fallback = emptyImg}) => { - const [isError,setIsError] = useState(false) - const [loading,setLoading] = useState(true) +const Img: FC = ({src, mode = 'aspectFill', width, height, fallback = shard}) => { + const [isError, setIsError] = useState(false) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!src) { + setIsError(true) + setLoading(false) + } else { + setIsError(false) + setLoading(false) + } + }, [src]) - useEffect(()=>{ - console.log(src, !src) - if(!src){ - setIsError(true) - setLoading(false) - } else { - setIsError(false) - setLoading(false) - } - },[src]) // 图片加载失败 function onErrorHandler() { setLoading(false) setIsError(true) } - function onLoadHandler() { - setLoading(false) - setIsError(false) - } + + function onLoadHandler() { + setLoading(false) + setIsError(false) + } return ( - - { !isError && + + {!isError && } - { loading && - + { + loading && + } - { isError && !loading && - + { + isError && !loading && + } - ) } diff --git a/src/components/spinner/index.tsx b/src/components/spinner/index.tsx new file mode 100644 index 0000000..0ecfb5e --- /dev/null +++ b/src/components/spinner/index.tsx @@ -0,0 +1,171 @@ +import Taro from "@tarojs/taro"; +import { View, Image } from '@tarojs/components' +import { Component, ReactNode } from "react"; +import indicator from './loading.svg' +import './style.scss' + +// 动画状态 +type Status = + | 'dismissed' + | 'forward' + | 'reverse' + | 'completed' + +interface Props { + enable?: boolean + overlay?: boolean +} + +interface State { + status?: Status + background?: Record, + rotation?: Record, +} + +type StateSetter = (state: State) => void + +type Controller = { + setTick: (enabled: boolean | undefined) => void + clear: () => void +} + +function createController(setState: StateSetter): Controller { + const background = Taro.createAnimation({ duration: 600 }) + const rotation = Taro.createAnimation({ duration: 600 }) + let rotateTimer: ReturnType | undefined + let status: Status | undefined + + const notifyListener = () => setState({ + status, + background: background.export(), + rotation: rotation.export(), + }) + + const clearAnimation = (notify = true) => { + // 清空旋转动画定时器 + if (rotateTimer != null) { + clearTimeout(rotateTimer) + rotateTimer = undefined + } + + // 清空动画 + background.export() + rotation.export() + + // 通知 UI 刷新 + if (notify) { + notifyListener() + } + } + + const setAnimation = (opacity: number) => { + // 停止旋转动画 + if (rotateTimer != null) { + clearTimeout(rotateTimer) + } + + // 旋转动画定时器 + const rotate = () => { + rotation.opacity(opacity).rotate(360).step({ duration: 600 }) + notifyListener() + rotateTimer = setTimeout(rotate, 600) + } + + // 背景动画 + background.backgroundColor(`rgba(255,255,255,${opacity})`).step() + + // 启动旋转动画 + rotate() + } + + const onFinish = (opacity: number, nextStatus: Status) => { + const lockStatus = status + setTimeout(() => { + if (lockStatus === status) { + background.backgroundColor(`rgba(255,255,255,${opacity})`).step({ duration: 0 }) + if (nextStatus === 'dismissed') { + clearAnimation() + } + status = nextStatus + notifyListener() + } + }, 600) + } + + const setStatus = (newStatus: Status, opacity: number) => { + if (status !== newStatus) { + status = newStatus + setAnimation(opacity) + + if (status === 'reverse') { + onFinish(0, 'dismissed') + } else if (status === 'forward') { + onFinish(1, 'completed') + } + } + } + + const setTick = (enabled?: boolean) => { + if (enabled !== true) { + if (status === 'dismissed') { + clearAnimation() + } else if (status !== 'reverse') { + setStatus('reverse', 0) + } + } else if (status === 'completed') { + // ignore + } else if (status !== 'forward') { + setStatus('forward', 1) + } + } + + return { + setTick, + clear() { + clearAnimation(false) + }, + } +} + +export default class Spin extends Component { + constructor(props) { + super(props) + this.controller = createController(this.onAnimation) + this.state = {} + } + + controller: Controller + + onAnimation = (state: State) => { + this.setState((s) => ({...s, ...state})) + } + + componentDidMount(): void { + console.log(this.props.enable) + this.controller.setTick(this.props.enable) + } + + componentDidUpdate(): void { + this.controller.setTick(this.props.enable) + } + + componentWillUnmount(): void { + this.controller.clear() + } + + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly): boolean { + return nextProps.enable !== this.props.enable + || nextProps.overlay !== this.props.overlay + || nextState.status != this.state.status + } + + render(): ReactNode { + return ( + + + + + + ) + } +} diff --git a/src/components/spinner/loading.svg b/src/components/spinner/loading.svg new file mode 100644 index 0000000..6a23520 --- /dev/null +++ b/src/components/spinner/loading.svg @@ -0,0 +1,13 @@ + diff --git a/src/components/spinner/style.scss b/src/components/spinner/style.scss new file mode 100644 index 0000000..1ff6261 --- /dev/null +++ b/src/components/spinner/style.scss @@ -0,0 +1,66 @@ +.spinner-wrapper { + background-color: rgba( #fff, 1.0); + transition: background-color 1200ms ease-out; + + &.is-fixed { + z-index: 99999; + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + &.reverse, + &.dismissed { + background-color: rgba( #fff, 0.0); + } + + &.dismissed { + position: fixed; + width: 0; + height: 0; + left: -10000px; + top: -10000px; + right: auto; + bottom: auto; + overflow: hidden; + } +} + +.spinner-wrapper, +.spinner { + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.spinner { + padding: 10rpx; + opacity: 1; + transition: opacity 1200ms ease-out; + + &.reverse, + &.dismissed { + opacity: 0; + } +} + +.spinner-icon { + width: 38px; + height: 38px; + animation: spinner-rotation 600ms linear infinite; +} + +@keyframes spinner-rotation { + from { + transform: rotate(0); + } + + to { + transform: rotate(360deg); + } +} diff --git a/src/components/topic/single.module.scss b/src/components/topic/single.module.scss new file mode 100644 index 0000000..c794da4 --- /dev/null +++ b/src/components/topic/single.module.scss @@ -0,0 +1,37 @@ +.single-cover { + position: absolute; + bottom: 0; + top: 0; + margin: auto; + height: 220rpx; + right: 20rpx; + max-width: 50%; + min-width: 300rpx; + padding: 25rpx; + background: rgba(#000, .7); + border-radius: 20rpx; + line-height: 40rpx; + display: flex; + flex-direction: column; + justify-content: space-between; + + view:first-child { + font-weight: bold; + color: #fff; + } + + view:not(:first-child) { + background: #fff; + border-radius: 10rpx; + padding: 15rpx; + } +} + + +.correct{ + +} + +.mistake{ + +} diff --git a/src/components/topic/single.tsx b/src/components/topic/single.tsx index 9a2f92d..030abb7 100644 --- a/src/components/topic/single.tsx +++ b/src/components/topic/single.tsx @@ -1,9 +1,101 @@ -import {FC} from "react"; +import {FC, useEffect, useMemo, useState} from "react"; import {View} from "@tarojs/components"; +import Taro from "@tarojs/taro"; +import styles from './single.module.scss' -export const Single:FC = ()=>{ - return( - 1 +interface Props { + full: boolean + topic?: ShareSubject + examination: (result: boolean) => void +} + +type AnswerType = "true" | 'false' + +export const Single: FC = (props) => { + if (!props.topic) return (<>) + + let timer: NodeJS.Timeout + const [lastState, setLastState] = useState<0 | 1>(0) // 0为竖屏,1为横屏 + const [result, setResult] = useState(undefined) + let lastTime = Date.now() + Taro.startAccelerometer() + Taro.onAccelerometerChange((res) => { + const now = Date.now(); + if (now - lastTime < 500) return; + + lastTime = now; + let nowState; + const Roll = Math.atan2(-res.x, Math.sqrt(res.y * res.y + res.z * res.z)) * 57.3; + const Pitch = Math.atan2(res.y, res.z) * 57.3; + + // 横屏状态 + if (Roll > 50) { + if ((Pitch > -180 && Pitch < -60) || (Pitch > 130)) { + nowState = 1; + } else { + nowState = lastState; + } + + } else if ((Roll > 0 && Roll < 30) || (Roll < 0 && Roll > -30)) { + let absPitch = Math.abs(Pitch); + // 如果手机平躺,保持原状态不变,40容错率 + if ((absPitch > 140 || absPitch < 40)) { + nowState = lastState; + } else if (Pitch < 0) { + // 收集竖向正立的情况 + nowState = 0; + } else { + nowState = lastState; + } + } else { + nowState = lastState; + } + + // 状态变化时,触发 + if (nowState !== lastState) { + setLastState(nowState); + } + }) + + useEffect(() => { + // timer = setTimeout(() => { + // props.examination(false) + // }, 4000) + }, [props.topic]) + + const style: React.CSSProperties = useMemo(() => ({ + transform: lastState === 0 && props.full ? "rotate(-90deg)" : 'none' + }), [lastState]) + + function examination(result: AnswerType) { + clearTimeout(timer) + setResult(result) + + setTimeout(() => { + props.examination(props.topic?.right_value === result) + setResult(undefined) + }, 2000) + } + + function judgment(answer: AnswerType): string { + if (props.topic?.right_value === answer && result === answer) { + return styles.correct + } + return styles.mistake + } + + return ( + + {props.topic.question} + examination("true")}> + {props.topic.right_value} + + examination("false")}> + {props.topic.error_value} + + ) } diff --git a/src/components/video/type.ts b/src/components/video/type.ts index a0fc3fa..1c51c51 100644 --- a/src/components/video/type.ts +++ b/src/components/video/type.ts @@ -12,9 +12,11 @@ export interface HVideoOptions { /** 视频断点 */ breakpoint: number[] /** 进入断点 */ - onBreakpoint: (id: number) => void + onBreakpoint: (time: number) => void /** 视频播放结束 */ onEnded: () => void - setTime: (fn: (time: number) => void) => void + // setTime: (fn: (time: number) => void) => void + /** 全屏改变 */ + fullChange: (fullScreen: boolean) => void children?: ReactNode } diff --git a/src/components/video/video.tsx b/src/components/video/video.tsx index 69e9501..55ea43d 100644 --- a/src/components/video/video.tsx +++ b/src/components/video/video.tsx @@ -44,26 +44,17 @@ const HVideo: FC = (opt: HVideoOptions) => { /** 判断是否进入断点 */ opt.breakpoint.forEach(d => { if (time < d + deviation && time > d - deviation) { - video?.pause() - if (process.env.TARO_ENV === 'h5') { - try { - document?.exitFullscreen().then() - } catch (e) { - } - } - video?.exitFullScreen() opt.onBreakpoint(d) - return } }) } - opt.setTime((time?: number) => { - if (typeof time === 'number') { - video?.seek(time) - } - video?.play() - }) + // opt.setTime((time?: number) => { + // if (typeof time === 'number') { + // video?.seek(time) + // } + // video?.play() + // }) function onEnded() { if (currentTime + 1 > opt.duration) { @@ -118,11 +109,11 @@ const HVideo: FC = (opt: HVideoOptions) => { poster={opt?.poster || ''} src={opt.src} enableProgressGesture={opt.preview} - direction={90} onTimeUpdate={onTimeUpdate} onEnded={onEnded} onPlay={onPlay} onPause={onPause} + onFullScreenChange={(event) => opt.fullChange((event.target as any).fullScreen)} > {opt.children} diff --git a/src/pages/business/videoInfo/components/course.tsx b/src/pages/business/videoInfo/components/course.tsx index 53ba61b..4bf9128 100644 --- a/src/pages/business/videoInfo/components/course.tsx +++ b/src/pages/business/videoInfo/components/course.tsx @@ -1,14 +1,9 @@ -import {CoverView, ScrollView, Text, View} from "@tarojs/components"; import {FC, useEffect, useState} from "react"; import HVideo from "@/components/video/video"; import {curriculum, HourPlayData} from "@/api"; import {Profile} from '@/store' import Taro from "@tarojs/taro"; -import Judge from "@/components/topic/judge"; import unique_ident from "@/hooks/unique_ident"; -import MyButton from "@/components/button/MyButton"; -import {formatMinute} from "@/utils/time"; -import CustomPageContainer from "@/components/custom-page-container/custom-page-container"; import Single from "@/components/topic/single"; interface Props { @@ -19,25 +14,24 @@ interface Props { } -let seek: (time: number) => void const Course: FC = ({id, courseId, preview, curEnd}) => { - const [breakpoint, setBreakpoint] = useState([]) // 断点 - const [show, setShow] = useState(false) // 题 + const [breakpoints, setBreakpoints] = useState([]) // 断点 + const [isFull, setIsFull] = useState(false) + + const [examAll, setExamAll] = useState>([]) // 题库 const [data, setData] = useState(null) - const [examAll, setExamAll] = useState>([]) // 题库 const [time, setTime] = useState(0) // 进入断点的时间 - const [validate, setValidate] = useState(false) // 开启验证 - const [record, setRecord] = useState([]) // 考题记录 const [testId, setTestId] = useState(null) const {user} = Profile.useContainer() async function onEnded() { + // 学习记录 const startRecording = unique_ident.get() startRecording && await curriculum.curEnd(courseId, id, {...startRecording, duration: data?.duration!}) // 结束 unique_ident.remove() if (testId) { - if (preview) { // 预览 + if (preview) { Taro.showModal({ title: "是否前往考试", success({confirm}) { @@ -58,66 +52,34 @@ const Course: FC = ({id, courseId, preview, curEnd}) => { } } - /** 进入断点 */ - function onBreakpoint(breakpoint: number) { - setTime(breakpoint) - setShow(true) - } - async function getData() { unique_ident.put(id, Date.now()) const res = await curriculum.hourPlay(courseId, id) if (res) { setData(res) - setBreakpoint(res.timeList) + setBreakpoints(res.timeList) setExamAll(res.hourExamQuestions || []) setTestId(res?.hour_test?.id || null) } } useEffect(() => { - init() getData() }, [id]) - - function init(show = true) { - show && setShow(false) - setValidate(false) - setRecord([]) - setTime(0) - } - - - useEffect(() => { - if (!record.length) return; - const pass = record.every(d => d) - /** 考题正确 */ + function examination(result: boolean) { const {id: question_id, question_type} = examAll?.[time]?.[0] curriculum.answerRecord(id, { - is_pass: pass, + is_pass: result, user_id: user?.id!, time: time, question_type, question_id - }).then() - - /** 删除断点 */ - const old: number[] = JSON.parse(JSON.stringify(breakpoint)) - const index = old.indexOf(time) - old.splice(index, 1) - setBreakpoint(old) - - if (pass) { - seek(time) - init() - } - }, [record]) - - function videoSeek(fn: (time: number) => void) { - seek = fn + }) + setTime(0) } + return ( <> = ({id, courseId, preview, curEnd}) => { preview={preview} src={data?.url || ''} onEnded={onEnded} - breakpoint={breakpoint} - onBreakpoint={onBreakpoint} - setTime={videoSeek} + breakpoint={breakpoints} + onBreakpoint={(time) => setTime(time)} + fullChange={(fullScreen) => setIsFull(fullScreen)} > - {/**/} - {/* */} - {/**/} + - - - - - {formatMinute(time)}考题 - - { - examAll?.[time]?.slice(0, 1)?.map((d) => - - {d.question_type === 2 && - setRecord([isAnswer])} - />} - ) - } - - - - { - record.length > 0 - ? { - init(); - seek(time) - }}>关闭 - : setValidate(true)}>交卷 - } - - - - ) } diff --git a/src/pages/business/videoInfo/videoInfo.scss b/src/pages/business/videoInfo/videoInfo.scss index 7057f0c..da6c6f2 100644 --- a/src/pages/business/videoInfo/videoInfo.scss +++ b/src/pages/business/videoInfo/videoInfo.scss @@ -106,8 +106,4 @@ filter: saturate(0); } -.single-cover { - position: absolute; - color: #fff; -} diff --git a/src/pages/home/components/feature_recommended.tsx b/src/pages/home/components/feature_recommended.tsx index 2cd2fc9..f482f33 100644 --- a/src/pages/home/components/feature_recommended.tsx +++ b/src/pages/home/components/feature_recommended.tsx @@ -164,10 +164,8 @@ const FeatureRecommended: FC = (props) => { onClick={() => jump(d.detailsUrl + c.path, c.id, d.type)}> - + - {/**/} - diff --git a/src/pages/preview/brand/list/list.tsx b/src/pages/preview/brand/list/list.tsx index f873dc1..cf422a0 100644 --- a/src/pages/preview/brand/list/list.tsx +++ b/src/pages/preview/brand/list/list.tsx @@ -4,35 +4,26 @@ import {brandApi, BrandRecord} from "@/api"; import styles from './list.module.scss' import Taro, {useReachBottom} from "@tarojs/taro"; import Empty from "@/components/empty/empty"; -import {AtActivityIndicator} from "taro-ui"; - - +import Spinner from "@/components/spinner"; const BrandList: FC = () => { const [page, setPage] = useState(1) const [brands, setBrands] = useState([]) const [total, setTotal] = useState(0) const [text, setText] = useState('') + const [loading, setLoading] = useState(true) useEffect(() => { - Taro.showLoading({ - title: '加载中', - mask: true - }) - setTimeout(function () { - Taro.hideLoading() - }, 650) getData() }, [page]) const getData = useCallback(async () => { try { - setText('加载中...') const res = await brandApi.list(page, 10) - if(page === 1){ - if(res.list.length < 10) { + if (page === 1) { + if (res.list.length < 10) { setText('没有更多了~') - }else{ + } else { setText('上拉加载更多~') } } @@ -43,6 +34,7 @@ const BrandList: FC = () => { ]) } catch (e) { } + setLoading(false) }, [page]) @@ -53,32 +45,28 @@ const BrandList: FC = () => { useReachBottom(useCallback(() => { if (brands?.length < total) { setPage(page + 1) - }else{ + } else { setText('没有更多了~') } }, [total, brands])) return ( - + + { brands.length ? <> - { brands.map((d) => jumpInfo(d.id)} className={styles.box} key={d.id}> + {brands.map((d) => jumpInfo(d.id)} className={styles.box} key={d.id}> {d.name} {d.graphic_introduction} ) - } - { text === '加载中...' ? - - - : - {text} } - : + {text} + : } ) diff --git a/src/static/img/shard.png b/src/static/img/shard.png new file mode 100644 index 0000000000000000000000000000000000000000..2e16c7bd04080ccfe1394affc53e034bbc3c943b GIT binary patch literal 3486 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