main
king 1 year ago
parent aee74e0418
commit bfbf611931
  1. 19
      src/api/curriculum.ts
  2. 7
      src/api/manage.ts
  3. 4
      src/api/user.ts
  4. 14
      src/app.config.ts
  5. 8
      src/app.tsx
  6. 10
      src/components/video/type.ts
  7. 22
      src/components/video/video.tsx
  8. 4
      src/pages/business/categoryCur/categoryCur.tsx
  9. 9
      src/pages/business/userInfo/userInfo.module.scss
  10. 5
      src/pages/business/userInfo/userInfo.tsx
  11. 64
      src/pages/business/videoInfo/components/catalogue.tsx
  12. 168
      src/pages/business/videoInfo/components/course.tsx
  13. 38
      src/pages/business/videoInfo/components/hours.tsx
  14. 69
      src/pages/business/videoInfo/components/judge.tsx
  15. 87
      src/pages/business/videoInfo/components/multi.tsx
  16. 60
      src/pages/business/videoInfo/components/shortAnswer.tsx
  17. 41
      src/pages/business/videoInfo/components/studentRecord.tsx
  18. 26
      src/pages/business/videoInfo/videoInfo.scss
  19. 17
      src/pages/business/videoInfo/videoInfo.tsx
  20. 22
      src/pages/index/components/videoList.tsx
  21. 2
      src/pages/index/index.tsx
  22. 1
      src/pages/login/login.tsx
  23. 13
      src/pages/manage/addCur/addCur.tsx
  24. 16
      src/pages/manage/addStudent/addStudent.tsx
  25. 15
      src/pages/manage/depAdmin/depAdmin.tsx
  26. 4
      src/pages/manage/depCur/depCur.tsx
  27. 16
      src/pages/manage/studentAdmin/studentAdmin.tsx
  28. 19
      src/static/css/module.scss
  29. BIN
      src/static/tabs/home-selected.png
  30. BIN
      src/static/tabs/home-unselect.png
  31. BIN
      src/static/tabs/mine-selected.png
  32. BIN
      src/static/tabs/mine-unselect.png
  33. 19
      types/topic.d.ts

@ -23,6 +23,12 @@ export interface HourPlayData {
extension: string
/** 地址 */
url: string
hourExamQuestions: {
count: Record<number, number>
data: Record<number, (ShareSubject | Multi)[]>
}
/** 断点 */
timeList: number[]
}
export interface Course {
@ -32,6 +38,12 @@ export interface Course {
stats: CueStats
}
interface AnswerRecord {
is_pass: boolean
user_id: number
question_position: number
}
export const curriculum = {
use() {
return request<Manage[]>('/api/v1/user/all', 'GET')
@ -60,7 +72,10 @@ export const curriculum = {
curEnd(courseId: number, id: number, duration: number) {
return request(`/api/v1/course/${courseId}/hour/${id}/record`, "POST", {duration})
},
categoryCur(categoryId:number,companyId:number){
return request<Curriculum[]>(`/api/v1/category/${categoryId}/${companyId}`,"GET")
categoryCur(categoryId: number, companyId: number, type: string) {
return request<Curriculum[]>(`/api/v1/category/${categoryId}/${companyId}/${type}`, "GET")
},
answerRecord(id: number, data: AnswerRecord) {
return request<{ time: number }>(`/api/v1/test/video/record/${id}`, "POST", data)
}
}

@ -1,7 +1,7 @@
import {request} from "@/api/request";
export interface Student {
company_id:number
company_id: number
avatar: string
dep_ids: number[]
email: string
@ -64,6 +64,7 @@ export interface CurLearningRecord {
total: number
}
export const ManageApi = {
/** 添加学员 */
addUser(data: Student) {
@ -111,5 +112,9 @@ export const ManageApi = {
},
buy(data_list: number[]) {
return request(`/api/v1/course/buy?data_list=${data_list}`, "POST")
},
/** 学员学习记录 */
userRecord(curId: number) {
return request<CurLearningRecord>(`/api/v1/course/${curId}/user/index?page=1&size=10000`, "GET")
}
}

@ -56,7 +56,7 @@ export const userApi = {
code(catch_key: string) {
return request<{ code: Code }>(`/api/v1/auth/login/code`, "POST", {openid: catch_key})
},
learningRecord(courseId = 0) {
return request<LearningRecord>(`/api/v1/user/record/course`, "GET", {courseId})
learningRecord(courseId?: number) {
return request<LearningRecord>(`/api/v1/user/record/course`, "GET", courseId ? {courseId} : {})
}
}

@ -12,8 +12,18 @@ export default defineAppConfig({
},
tabBar: {
list: [
{text: '课题', pagePath: 'pages/index/index'},
{text: "我的", pagePath: 'pages/my/my'}
{
text: '课题',
pagePath: 'pages/index/index',
iconPath: "static/tabs/home-unselect.png",
selectedIconPath: "static/tabs/home-selected.png",
},
{
text: "我的",
pagePath: 'pages/my/my',
iconPath: "static/tabs/mine-unselect.png",
selectedIconPath: "static/tabs/mine-selected.png",
}
]
},
preloadRule: {

@ -1,4 +1,3 @@
import {useEffect} from 'react'
import Taro, {useDidShow, useDidHide} from '@tarojs/taro'
import './app.scss'
import {CustomWrapper} from "@tarojs/components";
@ -32,13 +31,14 @@ function updateApp() {
function App(props) {
// 可以使用所有的 React Hooks
useEffect(() => {
Taro.useLaunch(()=>{
updateApp()
const token = JSON.parse(Taro.getStorageSync('profile') || '{}')?.token
if (!token) {
Taro.switchTab({url: '/pages/login/login'})
Taro.reLaunch({url: '/pages/login/login'})
}
}, [])
})
Taro.getSystemInfo({

@ -1,9 +1,3 @@
interface Breakpoint {
id: number
/** 秒 */
time: number
}
export interface HVideoOptions {
/** 视频时长s */
duration: number
@ -14,10 +8,12 @@ export interface HVideoOptions {
/** 视频封面 */
poster?: string
/** 视频断点 */
breakpoint: Breakpoint[]
breakpoint: number[]
/** 进入断点 */
onBreakpoint: (id: number) => void
/** 视频播放结束 */
onEnded: () => void
setTime: (fn:(time:number)=>void) => void
}

@ -8,12 +8,12 @@ const deviation: number = 0.5
const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
const {user} = Profile.useContainer()
const video = Taro.createVideoContext('video')
const video = Taro.createVideoContext('myVideo')
const [currentTime, setCurrentTime] = useState(0)
function onTimeUpdate(event: BaseEventOrig<VideoProps.onTimeUpdateEventDetail>) {
if (opt.preview) return;
if (user?.role_type === 2) return;
// if (user?.role_type === 2) return;
const time = event.detail.currentTime
/** 前进回退 */
if (currentTime + deviation < time) {
@ -24,16 +24,22 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
/** 判断是否进入断点 */
opt.breakpoint.forEach(d => {
if (time < d.time + deviation && time > d.time - deviation) {
opt.onBreakpoint(d.id)
if (time < d + deviation && time > d - deviation) {
video.pause()
video.seek(d.time - deviation)
video.seek(d - deviation)
opt.onBreakpoint(d)
return
}
})
}
opt.setTime((time: number) => {
video.stop()
video.seek(time)
})
function onEnded() {
if (opt.preview) return;
if (user?.role_type === 2) return;
if (currentTime + 1 > opt.duration) {
opt.onEnded()
@ -44,18 +50,16 @@ const HVideo: FC<HVideoOptions> = (opt: HVideoOptions) => {
}
return (
<>
<Video
id='video'
id={'myVideo'}
style={'width:100%'}
poster={opt.poster}
poster={opt?.poster || ''}
src={opt.src}
enableProgressGesture={false}
direction={90}
onTimeUpdate={onTimeUpdate}
onEnded={onEnded}
/>
</>
)
}

@ -7,12 +7,12 @@ import VideoCover from "@/components/videoCover/videoCover";
const Index: FC = () => {
const {company} = Profile.useContainer()
const {categoryId} = getCurrentInstance()?.router?.params as { categoryId: number }
const {categoryId,type} = getCurrentInstance()?.router?.params as { categoryId: number,type:string }
const [data, setData] = useState<Curriculum[]>([])
async function getData() {
try {
const res = await curriculum.categoryCur(categoryId!, company?.id!)
const res = await curriculum.categoryCur(categoryId!, company?.id!,type)
setData(res)
} catch (e) {
}

@ -9,14 +9,7 @@
padding: 10px 0;
}
.button{
width: 690rpx;
line-height: 76rpx;
background: #45D4A8;
border-radius: 40rpx;
color: #fff;
font-size: 32rpx;
}
.buttonFixed{

@ -47,11 +47,12 @@ const List = () => {
<PopPut title='头像' image={avatar} chevron no_border/>
<PopPut title='手机号' content={user?.phone_number} chevron no_border/>
<PopPut title='昵称' content={user?.name} isProp show={show} no_border>
<View className='h-6 pt-4 px-3'>
<View className='py-4 px-3'>
<View className='text-center font-weigh mb-3'></View>
<Input className='input'
placeholder='请输入昵称'
onInput={(event) => setName(event.detail.value)}
cursorSpacing={110}
value={name}
/>
<View className='text-muted my-3 font-24'>4-20_-</View>
@ -61,7 +62,7 @@ const List = () => {
<PopPut title='解绑微信' onClick={unbind} no_border/>
</View>
<Button className={`${styles.button} ${styles.buttonFixed}`} onClick={empty}>退</Button>
<Button className={`button ${styles.buttonFixed}`} onClick={empty}>退</Button>
</>
)
}

@ -5,15 +5,18 @@ import {Profile} from '@/store'
import {CourseDepData} from "@/api";
import Collapse from "@/components/collapse/collapse";
import Hours from "@/pages/business/videoInfo/components/hours";
import Taro from "@tarojs/taro";
import StudentRecord from "@/pages/business/videoInfo/components/studentRecord";
interface Props {
data: CourseDepData | null
setPlayId: (id: number) => void
setHors: (is_complete: boolean, id: number) => void
id: number
}
const Catalogue: FC<Props> = ({data, setPlayId}: Props) => {
const Catalogue: FC<Props> = ({data, setHors, id}) => {
const {user} = Profile.useContainer()
const [current, setCurrent] = useState(0)
const [current, setCurrent] = useState(1)
const [tabList, setTabList] = useState<TabList[]>([
{title: '介绍', value: 0},
{title: '目录', value: 1},
@ -24,7 +27,7 @@ const Catalogue: FC<Props> = ({data, setPlayId}: Props) => {
if (user?.role_type && user?.role_type > 0) {
setTabList([
...tabList,
// {title: '部门进度', value: 3}
{title: '学员进度', value: 3}
])
}
}, [])
@ -33,7 +36,7 @@ const Catalogue: FC<Props> = ({data, setPlayId}: Props) => {
setCurrent(tab?.value as number)
}
function getHors(chapter_id: number): Hour[] | null {
function getHors(chapter_id?: number): Hour[] | null {
for (const d of Object.values(data?.hours || {})) {
if (d[0].chapter_id === chapter_id) {
return d
@ -42,8 +45,30 @@ const Catalogue: FC<Props> = ({data, setPlayId}: Props) => {
return null
}
function complete(id: number): boolean {
return !!data?.learn_hour_records?.[id]?.is_finished
function chapters_complete(chapter: Chapters): Boolean {
const hors = getHors(chapter.id)
if (hors) {
for (const hor of hors) {
if (!data?.learn_hour_records?.[hor.id]?.is_finished) {
return false
}
}
return true
}
return false
}
function upChaptersOver(is_complete: boolean, id: number, index: number) {
if (!index) {
setHors(is_complete, id)
} else {
const ok = chapters_complete((Object.values(data?.chapters || {})?.[index - 1]))
if (!ok) {
Taro.showModal({title: '上一个章节还未完成'})
return
}
setHors(is_complete, id)
}
}
return (
@ -55,30 +80,17 @@ const Catalogue: FC<Props> = ({data, setPlayId}: Props) => {
<View className='font-weight mb-2'></View>
{data?.chapters.length ? Object.values(data?.chapters || {}).map((d, index) => <View>
<Collapse title={`${index + 1}.${d.name}`}>
<>
{getHors(d.id)?.map((hor, index) => <Hours
id={hor.id}
index={index}
title={hor.title}
duration={hor.duration}
complete={complete}
click={() => setPlayId(d.id)}
<Hours
click={(is_complete, id) => upChaptersOver(is_complete, id, index)}
learn_hour_records={data.learn_hour_records}
data={getHors(d.id)}
/>
)}
</>
</Collapse>
</View>)
: data?.hours?.[0].map((hor, index) => <Hours
id={hor.id}
index={index}
title={hor.title}
duration={hor.duration}
complete={complete}
click={() => setPlayId(hor.id)}
/>
)}
: <Hours data={data?.hours?.[0]} click={setHors} learn_hour_records={data?.learn_hour_records}/>}
</View>}
{current === 2 && <View className='text-center'></View>}
{current === 3 && <StudentRecord id={id}/>}
</View>
</View>
)

@ -1,9 +1,12 @@
import {CustomWrapper, PageContainer, ScrollView} from "@tarojs/components";
import {Button, PageContainer, ScrollView, Swiper, SwiperItem, 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 Multi from "@/pages/business/videoInfo/components/multi";
import Judge from "@/pages/business/videoInfo/components/judge";
import ShortAnswer from "@/pages/business/videoInfo/components/shortAnswer";
interface Props {
id: number,
@ -12,10 +15,20 @@ interface Props {
curEnd: () => void
}
let seek: (time: number) => void
let record: boolean[] = [] //答案记录
const Course: FC<Props> = ({id, courseId, preview, curEnd}: Props) => {
const [breakpoint, setBreakpoint] = useState<any[]>([])
const [show, setShow] = useState(false)
const [breakpoint, setBreakpoint] = useState<number[]>([]) // 断点
const [show, setShow] = useState(false) // 题
const [data, setData] = useState<HourPlayData | null>(null)
const [examAll, setExamAll] = useState<Record<number, (ShareSubject | Multi)[]>>([])
const [index, setIndex] = useState(0)
const [time, setTime] = useState<number>(0) // 进入断点的时间
const [validate, setValidate] = useState(false) // 开启验证
const [newRecord, setNewRecord] = useState<boolean[]>([])
const [count, setCount] = useState<Record<number, number> | null>(null)
const [frequency, setFrequency] = useState<number>(1) // 次数
const {user} = Profile.useContainer()
async function onEnded() {
try {
@ -30,8 +43,12 @@ const Course: FC<Props> = ({id, courseId, preview, curEnd}: Props) => {
}
}
function onBreakpoint(id: number) {
setBreakpoint(breakpoint.filter(d => d.id != id))
/** 进入断点 */
function onBreakpoint(breakpoint: number) {
if (breakpoint !== time) {
setFrequency(count?.[breakpoint] || 1)
}
setTime(breakpoint)
setShow(true)
}
@ -39,6 +56,9 @@ const Course: FC<Props> = ({id, courseId, preview, curEnd}: Props) => {
const res = await curriculum.hourPlay(courseId, id)
if (res) {
setData(res)
setBreakpoint(res.timeList)
setExamAll(res.hourExamQuestions.data)
setCount(res.hourExamQuestions.count)
}
}
@ -46,35 +66,147 @@ const Course: FC<Props> = ({id, courseId, preview, curEnd}: Props) => {
getData()
}, [id])
/** 统计答案 */
function onAnswer(isAnswer: boolean) {
record.push(isAnswer)
setNewRecord(record)
}
function init() {
record = []
setTime(0)
setShow(false)
setIndex(0)
setValidate(false)
setNewRecord([])
}
/** 再来一次 */
function onceMore() {
const oldTime: number = time
init()
setTimeout(() => {
setShow(true)
setTime(oldTime)
})
}
/** 重新看 */
function again() {
if (frequency === 0) {
init()
curriculum.answerRecord(id, {
is_pass: false,
user_id: user?.id!,
question_position: time
}).then(res => {
seek(res.time)
})
}
}
useEffect(() => {
if (!newRecord.length) return;
setFrequency(frequency - 1)
const pass = newRecord.every(d => d)
if (pass) {
curriculum.answerRecord(id, {
is_pass: pass,
user_id: user?.id!,
question_position: time
}).then(() => {
const old: number[] = JSON.parse(JSON.stringify(breakpoint))
const index = old.indexOf(time)
old.splice(index, 1)
setBreakpoint(old)
Taro.showToast({title: '全部通过'})
})
init()
} else {
Taro.showToast({title: '错误', icon: 'error'})
}
}, [newRecord])
function videoSeek(fn: (time: number) => void) {
seek = fn
}
return (
<CustomWrapper>
<Profile.Provider>
{data && <HVideo
duration={data?.duration}
<>
<HVideo
duration={data?.duration || 0}
preview={!!preview}
src={data?.url || ''}
onEnded={onEnded}
breakpoint={breakpoint}
onBreakpoint={onBreakpoint}
/>}
setTime={videoSeek}
/>
<view>
<PageContainer
show={show}
position='bottom'
round
onAfterLeave={again}
>
<ScrollView
style='height:70vh'
scrollY
scrollTop={0}
scrollWithAnimation
>
<Swiper style={{height: "70vh"}} snapToEdge current={index}>
{examAll?.[time]?.map((d, index) =>
<SwiperItem>
<ScrollView style='height:70vh' scrollY>
{d.question_type === 1 &&
<Multi
frequency={frequency}
data={d as Multi}
onAnswer={onAnswer}
onUpAndDown={(index) => setIndex(index)}
index={index}
validate={validate}
/>}
{d.question_type === 2 &&
<Judge
frequency={frequency}
onUpAndDown={(index) => setIndex(index)}
index={index}
data={d as ShareSubject}
validate={validate}
onAnswer={onAnswer}
/>}
{d.question_type === 3 &&
<ShortAnswer
frequency={frequency}
data={d as ShareSubject}
onAnswer={onAnswer}
onUpAndDown={(index) => setIndex(index)}
index={index}
validate={validate}
/>}
</ScrollView>
</SwiperItem>
)}
<SwiperItem>
<ScrollView style='height:70vh' scrollY>
<View className='mt-10 ml-4'>
<View className='mt-3'>{newRecord.filter(d => d).length}</View>
<View className='mt-3'>{newRecord.filter(d => !d).length}</View>
<View className='mt-3'>{frequency}</View>
</View>
{!validate && <Button className='button mt-10' onClick={() => setValidate(true)}></Button>}
{frequency > 0 && validate && <Button className='button mt-10' onClick={onceMore}></Button>}
{frequency === 0 && validate && <Button className='button mt-10' onClick={again}></Button>}
</ScrollView>
</SwiperItem>
</Swiper>
<View className='statistics'>
<View>{examAll?.[time]?.length}</View>
</View>
</PageContainer>
</view>
</Profile.Provider>
</CustomWrapper>
</>
)
}
export default Course

@ -4,28 +4,38 @@ import {Image, View} from "@tarojs/components";
import playOk from "@/static/img/play-ok.png";
import play from "@/static/img/play.png";
import {formatMinute} from "@/utils/time";
import Taro from "@tarojs/taro";
interface Props {
id: number
index: number
title: string
duration: number
click: (id: number) => void
complete: (id: number) => boolean
data?: Hour[] | null
learn_hour_records?: LearnHourRecords
click: (is_complete: boolean, id: number) => void
}
const Hours: FC<Props> = (opt: Props) => {
const Hours: FC<Props> = ({data, click, learn_hour_records}: Props) => {
const complete = (id: number) => !!learn_hour_records?.[id]?.is_finished
function onClick(id: number, is_complete: boolean, upId?: number) {
if (upId && !complete(upId)) {
Taro.showModal({title: '请播放上一个课程'})
return
}
click(is_complete, id)
}
return (
<>
<View
className={'hor' + ` ${opt.complete(opt.id) ? 'complete' : null}`}
onClick={() => opt.click(opt.id)}>
<Image src={opt.complete(opt.id) ? playOk : play} mode='aspectFit'/>
{
data?.map((d, index) => <View
className={'hor' + ` ${complete(d.id) ? 'complete' : null}`}
onClick={() => onClick(d.id, complete(d.id), data?.[index - 1]?.id)}>
<Image src={complete(d.id) ? playOk : play} mode='aspectFit'/>
<View>
<View>{opt.index + 1}.{opt.title}</View>
<View className='font-26'>{formatMinute(opt.duration)}</View>
</View>
<View>{index + 1}.{d.title}</View>
<View className='font-26'>{formatMinute(d.duration)}</View>
</View>
</View>)
}
</>
)
}

@ -0,0 +1,69 @@
import {FC, useEffect, useState} from "react";
import {Button, Radio, RadioGroup, Text, View} from "@tarojs/components";
interface Props {
data: ShareSubject
onAnswer: (isAnswer: boolean) => void
onUpAndDown: (index: number) => void
index: number
validate: boolean
frequency:number
}
const Judge: FC<Props> = ({data, onAnswer, onUpAndDown, index, validate,frequency}) => {
const [rightAnswer, setRightAnswer] = useState<string | null>(null) //答案
const rightKey = data.right_answer ? 'correct' : 'error' // 正确答案数组
const [error, setError] = useState(false)
function onChange(e) {
const value = e.detail.value
setRightAnswer(value)
}
useEffect(() => {
if (validate) {
onAnswer(rightAnswer === rightKey)
setError(rightAnswer !== rightKey)
}
}, [validate])
return (
<View className='topic'>
<View className='mb-3'>
<Text>{data.question}</Text>
</View>
<RadioGroup onChange={onChange}>
<Radio
value={'correct'}
className='option'
color={validate ? 'correct' !== rightKey ? 'red' : undefined : undefined}
disabled={validate ? 'correct' !== rightAnswer : false}
>
</Radio>
<Radio
value={'error'}
color={validate ? 'error' !== rightKey ? 'red' : undefined : undefined}
disabled={validate ? 'error' !== rightAnswer : false}
className='option'>
</Radio>
</RadioGroup>
<View className='upAndDown'>
{index > 0 && <Button className='button' onClick={() => onUpAndDown(index - 1)}></Button>}
<Button className='button' onClick={() => onUpAndDown(index + 1)}></Button>
</View>
{error && frequency === 0 && <View className='mt-3'>
<View className='font-weight mb-3'></View>
<View>{data.analysis}</View>
</View>}
</View>
)
}
export default Judge

@ -0,0 +1,87 @@
import {FC, useEffect, useState} from "react";
import {Button, Checkbox, CheckboxGroup, Radio, RadioGroup, Text, View} from "@tarojs/components";
interface Props {
data: Multi
onAnswer: (isAnswer: boolean) => void
onUpAndDown: (index: number) => void
index: number
validate: boolean
frequency:number
}
const Multi: FC<Props> = ({data, onAnswer, onUpAndDown, index, validate,frequency}) => {
const [rightAnswer, setRightAnswer] = useState<string[]>([]) //答案
const rightKey = data?.right_answer?.split(',') || [] // 正确答案数组
const [error, setError] = useState(false)
const answers = [
{value: "A", title: data.answerA},
{value: "B", title: data.answerB},
{value: "C", title: data.answerC},
{value: "D", title: data.answerD},
]
function onChange(e) {
const value = e.detail.value
if (data.type) {
setRightAnswer(value)
} else {
setRightAnswer([value])
}
}
useEffect(() => {
if (validate) {
const isAnswer = rightKey.toString() === rightAnswer.toString()
onAnswer(isAnswer)
setError(!isAnswer)
}
}, [validate])
return (
<View className='topic'>
<View className='mb-3'>
<Text>{data.question}</Text>
</View>
<View>
{!data.type ? <CheckboxGroup onChange={onChange}>
{answers.map(d => <Checkbox
value={d.value}
className='option'
disabled={validate}
>
{d.title}
</Checkbox>)}
</CheckboxGroup>
: <RadioGroup onChange={onChange}>
{answers.map(d => <Radio
value={d.value}
className='option'
color={validate ? rightKey.includes(d.value) ? '#45d4a8' : 'red' : '#45d4a8'}
disabled={validate ? !rightAnswer.includes(d.value) : false}
>
{d.title}
</Radio>)}
</RadioGroup>
}
</View>
<View className='upAndDown'>
{index > 0 && <Button className='button' onClick={() => onUpAndDown(index - 1)}></Button>}
<Button onClick={() => onUpAndDown(index + 1)} className='button'></Button>
</View>
{error && frequency == 0 && <View className='mt-3'>
<View className='font-weight mb-3'></View>
<View>{data.analysis}</View>
</View>}
</View>
)
}
export default Multi

@ -0,0 +1,60 @@
import {Button, Text, Textarea, View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import Taro from "@tarojs/taro";
interface Props {
data: ShareSubject
onAnswer: (isAnswer: boolean) => void
onUpAndDown: (index: number) => void
index: number
validate: boolean
frequency: number
}
const ShortAnswer: FC<Props> = ({data, onAnswer, onUpAndDown, index, validate, frequency}) => {
const [value, setValue] = useState<string>('')
function onBlur() {
if (value.length < 3) {
Taro.showToast({title: '最少3个字', icon: 'error'})
}
}
useEffect(() => {
if (validate) {
if (value.length < 3) {
onAnswer(false)
} else {
onAnswer(true)
}
}
}, [validate])
return (
<View className='topic'>
<View className='mb-3'>
<Text>{data.question}</Text>
</View>
<Textarea
placeholder='请输入答案'
adjustPosition
className='Textarea'
onBlur={onBlur}
onInput={(e) => setValue((e.target as HTMLTextAreaElement).value)}/>
<View className='upAndDown'>
{index > 0 && <Button className='button' onClick={() => onUpAndDown(index - 1)}></Button>}
<Button className='button' onClick={() => onUpAndDown(index + 1)}></Button>
</View>
<View>3</View>
{frequency === 0 && <View className='mt-3'>
<View className='font-weight mb-3'></View>
<View>{data.analysis}</View>
</View>}
</View>
)
}
export default ShortAnswer

@ -0,0 +1,41 @@
import {View} from "@tarojs/components";
import {FC, useEffect, useState} from "react";
import {CurLearningRecord, ManageApi} from "@/api/manage";
import PopPut from "@/components/popPut/popPut";
interface Props {
id: number
}
const StudentRecord: FC<Props> = ({id}) => {
const [data, setData] = useState<CurLearningRecord | null>(null)
async function getDate() {
try {
const res = await ManageApi.userRecord(id)
setData(res)
} catch (e) {
}
}
function record(id: number): string {
return `${data?.user_course_records?.[id]?.finished_count || 0}/${data?.course.class_hour || 0}`
}
useEffect(() => {
getDate()
}, [id])
return (
<View>
{data?.data?.map(d => <PopPut
leftImage={d.avatar}
title={d.name}
content={record(d.id)}
chevron
/>)}
</View>
)
}
export default StudentRecord

@ -56,3 +56,29 @@
.complete {
color: #45D4A8;
}
.topic{
padding: 20px;
box-sizing: border-box;
.option{
display: block;
margin-bottom: 10px;
}
}
.statistics{
padding: 40px 20px;
display: flex;
justify-content: space-around;
}
.upAndDown{
display: flex;
justify-content: space-around;
margin-top: 30px;
button{
width: 40%;
}
}

@ -11,6 +11,7 @@ const VideoInfo: FC = () => {
const {id, depId} = getCurrentInstance()?.router?.params as { id: number, depId: number | null }
const [data, setData] = useState<CourseDepData | null>(null)
const [playId, setPlayId] = useState<number | null>(null)
const [preview, setPreview] = useState(false)
async function getData() {
const res = await curriculum.courseDep(id, depId)
@ -19,6 +20,11 @@ const VideoInfo: FC = () => {
}
}
function setHors(is_complete: boolean, id: number) {
setPreview(is_complete)
setPlayId(id)
}
useEffect(() => {
getData()
}, [])
@ -27,11 +33,9 @@ const VideoInfo: FC = () => {
<Profile.Provider>
<View className='content'>
<View className='content-video'>
{
playId ?
<Course id={playId} courseId={id} curEnd={getData}/>
: <Image src={data?.course.thumb || ''} className='image' mode='scaleToFill'/>
}
{playId ?
<Course id={playId} courseId={id} curEnd={getData} preview={preview}/>
: <Image src={data?.course.thumb || ''} className='image' mode='scaleToFill'/>}
</View>
<View className='header'>
@ -41,12 +45,11 @@ const VideoInfo: FC = () => {
</View>
<View className='font-weight font-40 my-4'>{data?.course.title}</View>
<View className='text-muted font-26'>
{/*<Text className='mr-3'>时长:32:10</Text>*/}
<Text>{data?.learn_record?.finished_count || 0}/{data?.course.class_hour}</Text>
</View>
</View>
<Catalogue data={data} setPlayId={(id) => setPlayId(id)}/>
<Catalogue data={data} setHors={setHors} id={id}/>
</View>
</Profile.Provider>
)

@ -48,22 +48,34 @@ export const VideoList: FC<Props> = ({categoryId}: Props) => {
function rateOfLearning(id: number, class_hour: number): JSX.Element {
switch (categoryId) {
case "is_finished":
case "is_required":
case "is_not_required":
const find = records.find(d => d.course_id === id)
const find = records.find(d => d?.course_id === id)
if (find) {
return (<View>{`${class_hour}节/已学${find.finished_count}`}</View>)
}
return (<View>{`${class_hour}节/已学0节`}</View>)
case "is_not_finished":
return (<View>{`${class_hour}节/已学0节`}</View>)
case "is_required":
case "is_finished":
return (<View>{`${class_hour}节/已学${class_hour}`}</View>)
}
}
function jumpCategoryCur(id: number) {
Taro.navigateTo({url: '/pages/business/categoryCur/categoryCur?categoryId=' + id})
let type = 1
switch (categoryId) {
case "is_not_required":
type = 2
break
case "is_not_finished":
type = 4
break
case "is_finished":
type = 3
break
}
Taro.navigateTo({url: `/pages/business/categoryCur/categoryCur?categoryId=${id}&type=${type}`})
}
useReachBottom(() => {
@ -81,7 +93,7 @@ export const VideoList: FC<Props> = ({categoryId}: Props) => {
{data?.slice(0, index).map(d => (
<>{
d.courses?.[categoryId].length ? <View>
<View className='flex justify-between align-center my-1' onClick={() => jumpCategoryCur(d.id)}>
<View className='flex justify-between align-center my-1 mx-2' onClick={() => jumpCategoryCur(d.id)}>
<Text className='font-weight'>{d.name}</Text>
<Text className='font-20'></Text>
</View>

@ -30,7 +30,7 @@ const Index: FC = () => {
<Search/>
<Tabs tabList={category} onChange={tabChange} current={categoryId}/>
<VideoList categoryId={categoryId}/>
<View className='text-center text-muted mt-3'>- -</View>
<View className='text-center text-muted my-3'>- -</View>
</View>
</Profile.Provider>
)

@ -50,6 +50,7 @@ const Bing: FC<BingProps> = ({code, catch_key}: BingProps) => {
Taro.switchTab({url: '/pages/index/index'})
}
} catch (e) {
refreshCode()
}
Taro.hideLoading()
setLoading(false)

@ -59,11 +59,10 @@ const AddCur = () => {
}
}
const Add: FC<AddProps> = ({cur_id, name, index}: AddProps) => {
const Add: FC<AddProps> = ({cur_id, name, index}) => {
function addCur() {
function required() {
Taro.showModal({
title: '课程是否为必修',
title: "请选择" + name + '课程类型',
cancelText: '选修',
confirmText: '必修',
async success({confirm}) {
@ -87,14 +86,6 @@ const AddCur = () => {
})
}
Taro.showModal({
title: '确定添加' + name,
success({confirm}) {
confirm && required()
}
})
}
return (
<View className='text-center mt-1' onClick={addCur}></View>
)

@ -62,13 +62,18 @@ const AddStudent = () => {
}
function changeDepIds(item: Department) {
const ids = JSON.parse(JSON.stringify(depIds))
const ids: number[] = JSON.parse(JSON.stringify(depIds))
const index = depIds.indexOf(item.id)
if (index === -1) {
ids.push(item.id)
} else {
ids.splice(index, 1)
}
if (ids.length < 1) {
Taro.showToast({title: '最少一个部门'})
return
}
setDepIds(ids)
}
@ -103,7 +108,10 @@ const AddStudent = () => {
<View className='item'>
<View></View>
<Input placeholder='请输入手机号' name='phone_number' value={userInfo?.phone_number}
<Input
placeholder='请输入手机号'
name='phone_number'
value={userInfo?.phone_number}
onInput={(event) => setUerInfo({...userInfo, phone_number: event.detail.value} as Student)}/>
</View>
@ -112,9 +120,7 @@ const AddStudent = () => {
<View></View>
<View className='flex align-center' onClick={() => setShow(true)}>
<View>
{
depIds.length ? formatDep() : '请选择'
}
{depIds.length ? formatDep() : '请选择'}
</View>
<Icon name='chevron-right'/>
</View>

@ -70,7 +70,7 @@ const ChangeData: FC<ChangeDataProps> = ({putCompany, getDeps}: ChangeDataProps)
onInput={(event) => setSort(Number(event.detail.value))}/>
</View>
<Button className='mt-3' formType='submit' onClick={submit} disabled={disable}></Button>
<Button className='mt-3 button' formType='submit' onClick={submit} disabled={disable}></Button>
</Form>
</View>
)
@ -118,7 +118,12 @@ const DepAdmin: FC = () => {
function managesSheet(item: Manage) {
Taro.showActionSheet({
itemList: ['查看部门课程', '修改', '详情', '删除'],
itemList: [
'查看部门课程',
'修改',
// '详情',
'删除'
],
success({tapIndex}) {
switch (tapIndex) {
case 0:
@ -127,10 +132,10 @@ const DepAdmin: FC = () => {
case 1:
showPop(item)
break
// case 2:
// Taro.navigateTo({url: `/pages/manage/depAdmin/depAdmin?id=${item.id}&name=${item.name}`})
// break
case 2:
Taro.navigateTo({url: `/pages/manage/depAdmin/depAdmin?id=${item.id}&name=${item.name}`})
break
case 3:
del(item.name, item.id)
break
}

@ -73,7 +73,7 @@ const DepCur: FC = () => {
}
function jumpAddCur() {
Taro.navigateTo({url: "/pages/manage/addCur/addCur?id="+id})
Taro.navigateTo({url: "/pages/manage/addCur/addCur?id=" + id})
}
Taro.useDidShow(() => getData())
@ -95,8 +95,8 @@ const DepCur: FC = () => {
content={d.title}
/>)}
</View>
<Button className='add' onClick={jumpAddCur}></Button>
<View className='text-center p-3'>- -</View>
<Button className='button mt-3' onClick={jumpAddCur}></Button>
</CustomWrapper>
)
}

@ -13,6 +13,20 @@ interface RoleTypeProps {
getData: () => Promise<void>
}
interface DelProps {
onClick: () => void
id: number
}
const Del: FC<DelProps> = ({onClick, id}: DelProps) => {
const {user} = Profile.useContainer()
return (
<>
{user?.id !== id && <View className='del' onClick={onClick}></View>}
</>
)
}
const RoleType: FC<RoleTypeProps> = ({id, role_type, getData}: RoleTypeProps) => {
const {user} = Profile.useContainer()
@ -101,7 +115,7 @@ const studentAdmin = () => {
style={`background:${['#73c057', '#c94f4f'][d.is_lock]}`}>{['正常', '警用'][d.is_lock]}</View>
<Text> {d.id}</Text>
</View>
<View className='del' onClick={() => del(d.id)}></View>
{d.role_type === 0 && <Del id={d.id} onClick={() => del(d.id)}/>}
</View>
<View className='p-2 flex info justify-between'>
<View>

@ -50,5 +50,24 @@ page {
Image {
width: 68px;
height: 68px;
background: #ddd;
}
}
.Textarea{
width: 100%;
border-radius: 10px;
overflow: hidden;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ddd;
}
.button{
width: 690rpx;
line-height: 76rpx;
background: #45D4A8;
border-radius: 10rpx;
color: #fff;
font-size: 32rpx;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

19
types/topic.d.ts vendored

@ -0,0 +1,19 @@
/** 判断题 & 简答题 */
interface ShareSubject {
question_type: 1 | 2 | 3
question: string //课题题目
score: number //课题分数
right_answer?: string // 正确答案
analysis: string //课题解析
course_id: number
hour_id: number
}
/** 选择题 */
interface Multi extends ShareSubject {
type: boolean //是否多选
answerA: string
answerB: string
answerC: string
answerD: string
}
Loading…
Cancel
Save