123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765 |
- import { defineComponent, reactive, ref, shallowReactive } from 'vue'
- import styles from './index.module.less'
- import iconArrow1 from '../images/icon-arrow1.png'
- import iconArrow11 from '../images/icon-arrow1-1.png'
- import icon1 from '../images/icon-1.png'
- import icon2 from '../images/icon-2.png'
- import ArrowUp from '../images/arrow-up.png'
- import ArrowUpActive from '../images/arrow-up-active.png'
- import iconDownload from '../images/icon-download.png'
- import { Button, DatetimePicker, Popup, Toast } from 'vant'
- import Echats from './echats'
- import ColHeader from '@/components/col-header'
- import TheSticky from '@/components/the-sticky'
- import { formatterDatePicker } from '@/helpers/utils'
- import dayjs from 'dayjs'
- import request from '@/helpers/request'
- import { getTimeRange, TIME_TYPE } from '../home-statistics'
- import ColResult from '@/components/col-result'
- import { promisefiyPostMessage } from '@/helpers/native-message'
- import { useRouter } from 'vue-router'
- import { state } from '@/state'
- /** 秒转分 */
- export const formatSecToMin = (second: number) => {
- if (isNaN(second)) {
- return '0'
- }
- const mm = (Math.floor(second / 60) + Math.floor(second % 60) / 60).toFixed(2)
- return mm
- }
- /** 秒转时分秒 */
- export const formatSecToHMS = second => {
- const hours = Math.floor(second / 3600)
- .toString()
- .padStart(2, '0')
- const minutes = Math.floor((second % 3600) / 60)
- .toString()
- .padStart(2, '0')
- const seconds = Math.round(second % 60)
- .toString()
- .padStart(2, '0')
- return {
- all: hours + '时' + minutes + '分' + seconds + '秒',
- hours,
- minutes,
- seconds
- }
- }
- const catchKey = 'practice-statistics-detail-search'
- export default defineComponent({
- name: 'PracticeDetail',
- setup() {
- const router = useRouter()
- let catchSearch: any = sessionStorage.getItem(catchKey)
- catchSearch = catchSearch ? JSON.parse(catchSearch) : {}
- sessionStorage.removeItem(catchKey)
- const searchStatus = ref(false)
- const currentType = ref<TIME_TYPE>(
- catchSearch.currentType !== undefined ? catchSearch.currentType : 'MONTH'
- )
- const searchObj = reactive({
- tempSubjectId: catchSearch.subjectId || ('' as any),
- type:
- catchSearch.currentType !== undefined
- ? catchSearch.currentType
- : ('MONTH' as TIME_TYPE)
- })
- const timeRange =
- catchSearch.startTime && catchSearch.endTime
- ? {
- startTime: catchSearch.startTime,
- endTime: catchSearch.endTime
- }
- : getTimeRange(currentType.value)
- const forms = reactive({
- loading: false,
- dataShow: true,
- isScrollLeft: false,
- subjectId: catchSearch.subjectId || ('' as any), // 选择的声部
- subjectList: [] as any,
- startTimeStatus: false,
- startTimeClosedStatus: false,
- endTimeMinDate: new Date(timeRange?.startTime || ''),
- endTimeMaxDate: dayjs(new Date(timeRange?.startTime || ''))
- .add(1, 'year')
- .toDate(),
- endTimeStatus: false,
- endTimeClosedStatus: false,
- startTime: new Date(timeRange?.startTime || ''),
- startTimeStr: timeRange?.startTime || '',
- endTime: new Date(timeRange?.endTime || ''),
- endTimeStr: timeRange?.endTime || '',
- sortField: '' as 'totalPracticeTime' | 'averagePracticeTime' | '', // 排序字段
- sortType: '' as 'ASC' | 'DESC' | '' // 排序方式 ,ASC升序,DESC降序
- })
- // 练习统计
- const practiceSummary = shallowReactive({
- averagePracticeTime: '0',
- practiceCount: '0',
- totalPracticeTime: '0',
- totalTimes: {
- hours: '00',
- minutes: '00',
- seconds: '00'
- }
- })
- const obj = ref({
- students: [] as any,
- xAxisDataTime: [] as any,
- yAxisDataTime: [] as any,
- timeMaxCount: 5,
- timeCount: 0 as any,
- timeStr: '',
- xAxisDataCount: [] as any,
- yAxisDataCount: [] as any,
- countMaxCount: 5, // 默认横线
- countCount: 0,
- countStr: ''
- })
- // const searchText = computed(() => {
- // const template = {
- // MONTH: '本月',
- // THREE_MONTH: '近三个月',
- // HALF_YEAR: '近年半',
- // YEAR: '近一年'
- // }
- // return template[currentType.value]
- // })
- // 导出学生练习时长数据
- const onExport = async () => {
- try {
- const { data } = await request.post(
- '/api-teacher/home/exportStudentPractice',
- {
- data: {
- startTime: forms.startTimeStr,
- endTime: forms.endTimeStr,
- subjectId: forms.subjectId,
- sortField: forms.sortField,
- sortType: forms.sortType // 排序方式 ,ASC升序,DESC降序
- }
- }
- )
- const pathname = data || ''
- if (pathname) {
- const fileName = `练习详情${forms.startTimeStr}~${forms.endTimeStr}_${state.user.data?.userId}`
- // 发送消息通知移动端下载文件
- promisefiyPostMessage({
- api: 'downloadFile',
- content: {
- downloadUrl: pathname,
- fileName
- }
- })
- }
- } catch {
- //
- }
- }
- const getDetail = async () => {
- forms.loading = true
- try {
- const { data } = await request.post('/api-teacher/home/practice', {
- data: {
- startTime: forms.startTimeStr,
- endTime: forms.endTimeStr,
- subjectId: forms.subjectId
- }
- })
- const summary = data.practiceSummary || {}
- practiceSummary.averagePracticeTime = formatSecToMin(
- summary.averagePracticeTime || 0
- )
- practiceSummary.practiceCount = summary.practiceCount || 0
- practiceSummary.totalPracticeTime = summary.totalPracticeTime || 0
- practiceSummary.totalTimes = formatSecToHMS(
- summary.totalPracticeTime || 0
- )
- // 练习时长
- const practiceTimes = data.practiceTimes || []
- const xAxisDataTimes: string[] = []
- const practiceTimeList: any[] = []
- practiceTimes.forEach((item: any, index: number) => {
- xAxisDataTimes.push(item.date)
- practiceTimeList.push([
- index,
- formatSecToMin(item.practiceTime),
- item.practiceTime
- ])
- if (practiceTimes.length - 1 === index) {
- obj.value.timeCount = item.practiceTime
- obj.value.timeStr = item.date
- }
- })
- // 练习人数
- const practiceCounts = data.practiceCounts || []
- const xAxisDataCounts: string[] = []
- const countList: number[] = []
- let maxCount = 0 // 最大人数 - 用记设置练习人数分割线
- practiceCounts.forEach((item: any, index: number) => {
- xAxisDataCounts.push(item.date)
- countList.push(item.practiceTime)
- if (maxCount < (item.practiceTime || 0)) {
- maxCount = item.practiceTime
- }
- if (practiceCounts.length - 1 === index) {
- obj.value.countCount = item.practiceTime
- obj.value.countStr = item.date
- }
- })
- obj.value.xAxisDataTime = xAxisDataTimes
- obj.value.yAxisDataTime = practiceTimeList
- obj.value.xAxisDataCount = xAxisDataCounts
- obj.value.yAxisDataCount = countList
- // 最小数量为1
- obj.value.countMaxCount = maxCount >= 5 ? 5 : Math.max(maxCount, 1)
- } catch {
- //
- }
- forms.loading = false
- }
- // 用户列表数据
- const getStudentDetail = async () => {
- try {
- const { data } = await request.post(
- '/api-teacher/home/studentPractice',
- {
- data: {
- startTime: forms.startTimeStr,
- endTime: forms.endTimeStr,
- subjectId: forms.subjectId,
- sortField: forms.sortField,
- sortType: forms.sortType
- }
- }
- )
- // 学员练习时长
- const studentPracticeSummary = data || []
- const tempStudents: any = []
- studentPracticeSummary.forEach((item: any) => {
- const student = {
- avatar: item.avatar,
- averagePracticeTime: formatSecToHMS(item.averagePracticeTime || 0),
- practiceDays: item.practiceDays || 0,
- studentName: item.studentName,
- subjectName: item.subjectName,
- totalPracticeTime: formatSecToHMS(item.totalPracticeTime || 0),
- userId: item.userId
- }
- tempStudents.push(student)
- })
- obj.value.students = tempStudents
- forms.dataShow = tempStudents.length > 0 ? true : false
- } catch {
- //
- }
- }
- const getSubjectList = async () => {
- const { data } = await request.get(
- `/api-teacher/subject/subSubjectSelect?type=MUSIC`
- )
- if (Array.isArray(data)) {
- forms.subjectList = data
- }
- }
- getSubjectList()
- getDetail()
- getStudentDetail()
- const onChangeTime = (type: TIME_TYPE) => {
- if (searchObj.type === type) return
- searchObj.type = type
- resetTime(type)
- }
- // 格式化
- const resetTime = (type: TIME_TYPE) => {
- const timeRang = getTimeRange(type)
- forms.startTime = new Date(timeRang?.startTime || '')
- forms.startTimeStr = timeRang?.startTime || ''
- forms.endTimeMinDate = dayjs(timeRang?.startTime || '').toDate()
- forms.endTimeMaxDate = dayjs(timeRang?.startTime || '')
- .add(1, 'year')
- .toDate()
- forms.endTime = new Date(timeRang?.endTime || '')
- forms.endTimeStr = timeRang?.endTime || ''
- }
- // 重置
- const onConfirm = () => {
- if (!forms.startTimeStr || !forms.endTimeStr) {
- Toast('请选择时间范围')
- return
- }
- // timeRange.value = getTimeRange(currentType.value)
- searchStatus.value = false
- forms.subjectId = searchObj.tempSubjectId
- currentType.value = searchObj.type
- getDetail()
- getStudentDetail()
- }
- /** 排序 */
- const onSort = (
- field: 'totalPracticeTime' | 'averagePracticeTime' | ''
- ) => {
- if (!field) return
- if (forms.sortField !== field) {
- forms.sortType = ''
- }
- forms.sortField = field
- if (forms.sortType === 'ASC') {
- forms.sortType = ''
- forms.sortField = ''
- } else if (forms.sortType === 'DESC') {
- forms.sortType = 'ASC'
- } else {
- forms.sortType = 'DESC'
- }
- getStudentDetail()
- }
- /** 跳转详情 */
- const toDetail = (item: any) => {
- sessionStorage.setItem(
- catchKey,
- JSON.stringify({
- startTime: forms.startTimeStr,
- endTime: forms.endTimeStr,
- currentType: currentType.value,
- subjectId: forms.subjectId
- })
- )
- router.push({
- path: '/exercise-detail',
- query: {
- studentId: item.userId || ''
- }
- })
- }
- return () => (
- <div class={styles.practiceDetail}>
- <TheSticky position="top">
- <ColHeader background="transparent" border={false} />
- </TheSticky>
- <div class={styles.groupContainer}>
- <div class={styles.section}>
- <div
- class={[styles.filter, searchStatus.value && styles.active]}
- onClick={() => (searchStatus.value = true)}
- >
- <span>筛选</span>
- <img src={searchStatus.value ? iconArrow11 : iconArrow1} />
- </div>
- <div class={styles.title}>
- <span>总练习时长</span>
- </div>
- <div class={styles.sList}>
- <div class={styles.sItem}>
- <div class={styles.sTop}>
- <img src={icon1} />
- <span>总练习时长</span>
- </div>
- <div class={styles.sBottom}>
- <div class={styles.leaveTime}>
- <span class={styles.num}>
- {practiceSummary.totalTimes.hours}
- </span>
- <span class={styles.text}>小时</span>
- <span class={styles.num}>
- {practiceSummary.totalTimes.minutes}
- </span>
- <span class={styles.text}>分</span>
- <span class={styles.num}>
- {practiceSummary.totalTimes.seconds}
- </span>
- <span class={styles.text}>秒</span>
- </div>
- </div>
- </div>
- <div class={styles.sItem}>
- <div class={styles.sTop}>
- <img src={icon2} />
- <span>练习人数</span>
- </div>
- <div class={styles.sBottom}>
- <span class={styles.num}>
- {practiceSummary.practiceCount}
- </span>
- <span class={styles.text}>人</span>
- </div>
- </div>
- {/* <div class={styles.sItem}>
- <div class={styles.sTop}>
- <img src={icon1} />
- <span>平均练习时长</span>
- </div>
- <div class={styles.sBottom}>
- <span class={styles.num}>
- {practiceSummary.averagePracticeTime}
- </span>
- <span class={styles.text}>分钟</span>
- </div>
- </div> */}
- </div>
- </div>
- <div class={styles.section}>
- <div class={styles.title}>
- <span>练习时长</span>
- </div>
- <Echats
- obj={{
- xAxisData: obj.value.xAxisDataTime,
- yAxisData: obj.value.yAxisDataTime,
- count: obj.value.timeCount,
- time: obj.value.timeStr
- }}
- />
- </div>
- <div class={styles.section}>
- <div class={styles.title}>
- <span>练习人数</span>
- </div>
- <Echats
- type="NUM"
- obj={{
- xAxisData: obj.value.xAxisDataCount,
- yAxisData: obj.value.yAxisDataCount,
- count: obj.value.countCount,
- time: obj.value.countStr,
- countMaxCount: obj.value.countMaxCount
- }}
- />
- </div>
- <div class={styles.section}>
- <div class={styles.title}>
- <span>学员练习详情</span>
- <div class={styles.download} onClick={onExport}>
- <div>导出</div>
- <img src={iconDownload} />
- </div>
- </div>
- <div class={styles.scroll}>
- {forms.dataShow ? (
- <table class={[styles.dataTable]} style={{ width: '486px' }}>
- <colgroup>
- <col style="width: 88px;" />
- <col style="width: 105px;" />
- <col style="width: 106px;" />
- <col style="width: 72px;" />
- <col style="width: 106px;" />
- </colgroup>
- <thead>
- <tr>
- <th class={[styles.tdFixedLeft]}>学员</th>
- <th>乐器</th>
- <th>
- <div
- class={styles.filterSection}
- onClick={() => onSort('totalPracticeTime')}
- >
- 练习总时长
- <div class={styles.filters}>
- <img
- src={
- forms.sortField === 'totalPracticeTime' &&
- forms.sortType === 'ASC'
- ? ArrowUpActive
- : ArrowUp
- }
- class={styles.upArrow}
- />
- <img
- src={
- forms.sortField === 'totalPracticeTime' &&
- forms.sortType === 'DESC'
- ? ArrowUpActive
- : ArrowUp
- }
- class={styles.downArrow}
- />
- </div>
- </div>
- </th>
- <th>练习天数</th>
- <th>
- <div
- class={styles.filterSection}
- onClick={() => onSort('averagePracticeTime')}
- >
- 平均练习时长
- <div class={styles.filters}>
- <img
- src={
- forms.sortField === 'averagePracticeTime' &&
- forms.sortType === 'ASC'
- ? ArrowUpActive
- : ArrowUp
- }
- class={styles.upArrow}
- />
- <img
- src={
- forms.sortField === 'averagePracticeTime' &&
- forms.sortType === 'DESC'
- ? ArrowUpActive
- : ArrowUp
- }
- class={styles.downArrow}
- />
- <img />
- </div>
- </div>
- </th>
- </tr>
- </thead>
- <tbody>
- {obj.value.students.map((item: any) => (
- <tr onClick={() => toDetail(item)}>
- <td class={[styles.tdFixedLeft]}>
- <img class={styles.userImg} src={item.avatar} />
- <span>{item.studentName}</span>
- </td>
- <td>{item.subjectName}</td>
- <td>
- {item.totalPracticeTime.hours}小时
- {item.totalPracticeTime.minutes}分
- {item.totalPracticeTime.seconds}秒
- </td>
- <td>{item.practiceDays}</td>
- <td>
- {item.averagePracticeTime.hours}小时
- {item.averagePracticeTime.minutes}分
- {item.averagePracticeTime.seconds}秒
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- ) : (
- <ColResult
- classImgSize="SMALL"
- btnStatus={false}
- tips="暂无数据"
- />
- )}
- </div>
- </div>
- </div>
- <Popup
- v-model:show={searchStatus.value}
- closeable
- round
- position="bottom"
- class={styles.searchPopup}
- >
- <div class={styles.popupContainer}>
- <div class={styles.popupTitle}>筛选</div>
- <div class={styles.popupSearchList}>
- <div class={styles.popupSection}>
- <div class={styles.title}>
- <span>时间</span>
- </div>
- <div class={styles.timeCount}>
- <p
- onClick={() => onChangeTime('MONTH')}
- class={searchObj.type === 'MONTH' ? styles.active : ''}
- >
- 本月
- </p>
- <p
- onClick={() => onChangeTime('THREE_MONTH')}
- class={
- searchObj.type === 'THREE_MONTH' ? styles.active : ''
- }
- >
- 近三个月
- </p>
- <p
- onClick={() => onChangeTime('HALF_YEAR')}
- class={searchObj.type === 'HALF_YEAR' ? styles.active : ''}
- >
- 近半年
- </p>
- <p
- onClick={() => onChangeTime('YEAR')}
- class={searchObj.type === 'YEAR' ? styles.active : ''}
- >
- 近一年
- </p>
- </div>
- <div class={styles.timeRang}>
- <p
- class={[
- styles.timeInput,
- forms.startTimeStr && styles.hasValue
- ]}
- onClick={() => {
- forms.startTimeStatus = true
- forms.startTimeClosedStatus = true
- }}
- >
- {forms.startTimeStr || '起始时间'}
- </p>
- <p class={styles.timeUnit}></p>
- <p
- class={[
- styles.timeInput,
- forms.endTimeStr && styles.hasValue
- ]}
- onClick={() => {
- forms.endTimeStatus = true
- forms.endTimeClosedStatus = true
- }}
- >
- {forms.endTimeStr || '终止时间'}
- </p>
- </div>
- </div>
- <div class={styles.popupSection}>
- <div class={styles.title}>
- <span>声部</span>
- </div>
- <div class={[styles.timeCount, styles.timeSubject]}>
- <p
- class={searchObj.tempSubjectId === '' ? styles.active : ''}
- onClick={() => (searchObj.tempSubjectId = '')}
- >
- 全部
- </p>
- {forms.subjectList.map((item: any) => (
- <p
- class={
- searchObj.tempSubjectId === item.id ? styles.active : ''
- }
- onClick={() => {
- searchObj.tempSubjectId = item.id
- }}
- >
- {item.name}
- </p>
- ))}
- </div>
- </div>
- </div>
- <div class={styles.popupBottom}>
- <Button
- round
- block
- type="default"
- onClick={() => {
- searchObj.tempSubjectId = ''
- searchObj.type = 'MONTH'
- resetTime('MONTH')
- }}
- >
- 重置
- </Button>
- <Button round block type="primary" onClick={onConfirm}>
- 确认
- </Button>
- </div>
- </div>
- </Popup>
- {/* 开始日期 */}
- <Popup
- v-model:show={forms.startTimeStatus}
- position="bottom"
- round
- class={'popupBottomSearch'}
- onClosed={() => {
- forms.startTimeClosedStatus = false
- }}
- >
- {forms.startTimeClosedStatus && (
- <DatetimePicker
- v-model={forms.startTime}
- type="date"
- formatter={formatterDatePicker}
- onCancel={() => (forms.startTimeStatus = false)}
- onConfirm={(val: any) => {
- forms.startTime = val
- forms.startTimeStr = dayjs(val).format('YYYY-MM-DD')
- forms.startTimeStatus = false
- forms.endTimeMinDate = dayjs(val || new Date()).toDate()
- forms.endTimeMaxDate = dayjs(val || new Date())
- .add(1, 'year')
- .toDate()
- forms.endTime = val
- forms.endTimeStr = ''
- searchObj.type = '' as any
- }}
- />
- )}
- </Popup>
- {/* 结束日期 */}
- <Popup
- v-model:show={forms.endTimeStatus}
- position="bottom"
- round
- class={'popupBottomSearch'}
- onClosed={() => {
- forms.endTimeClosedStatus = false
- }}
- >
- {forms.endTimeClosedStatus && (
- <DatetimePicker
- v-model={forms.endTime}
- type="date"
- minDate={forms.endTimeMinDate}
- maxDate={forms.endTimeMaxDate}
- formatter={formatterDatePicker}
- onCancel={() => (forms.endTimeStatus = false)}
- onConfirm={(val: any) => {
- forms.endTime = val
- forms.endTimeStatus = false
- forms.endTimeStr = dayjs(val).format('YYYY-MM-DD')
- searchObj.type = '' as any
- }}
- />
- )}
- </Popup>
- </div>
- )
- }
- })
|