|
@@ -0,0 +1,468 @@
|
|
|
+import { defineComponent, onMounted, onUnmounted, reactive, ref } from 'vue';
|
|
|
+import styles from './index.module.less';
|
|
|
+import MSticky from '@/components/m-sticky';
|
|
|
+import MHeader from '@/components/m-header';
|
|
|
+import { browser, formatterDatePicker } from '@/helpers/utils';
|
|
|
+import { useRouter } from 'vue-router';
|
|
|
+import { DatePicker, List, Popup, Tab, Tabs } from 'vant';
|
|
|
+import { listenerMessage } from '@/helpers/native-message';
|
|
|
+import OFullRefresh from '@/components/m-full-refresh';
|
|
|
+import iconBird from './images/icon-bird.png';
|
|
|
+import iconPP from './images/icon-p-p.png';
|
|
|
+import iconNumber from './images/icon-number.png';
|
|
|
+import iconAlbum from './images/icon-album.png';
|
|
|
+import icon1 from './images/icon-1.png';
|
|
|
+import icon2 from './images/icon-2.png';
|
|
|
+import iconArrow from './images/icon-arrow.png';
|
|
|
+import iconArrowActive from './images/icon-arrow-active.png';
|
|
|
+import iconLl from './images/l-l.png';
|
|
|
+import iconLc from './images/l-c.png';
|
|
|
+import iconLr from './images/l-r.png';
|
|
|
+import iconPl from './images/p-l.png';
|
|
|
+import iconPc from './images/p-c.png';
|
|
|
+import iconPr from './images/p-r.png';
|
|
|
+import MSearch from '@/components/m-search';
|
|
|
+import TheTimeRange from '@/components/the-time-range';
|
|
|
+import request from '@/helpers/request';
|
|
|
+import DetailItem from './modals/detail-item';
|
|
|
+import MEmpty from '@/components/m-empty';
|
|
|
+import dayjs from 'dayjs';
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: 'exercise-record-index',
|
|
|
+ setup() {
|
|
|
+ const router = useRouter();
|
|
|
+ const tabsRef = ref();
|
|
|
+ const state = reactive({
|
|
|
+ tabActive: 'EVALUATION' as 'EVALUATION' | 'PRACTICE',
|
|
|
+ isClick: false,
|
|
|
+ showPopoverTime: false
|
|
|
+ });
|
|
|
+ // <Tab name="EVALUATION" title="评测记录"></Tab>
|
|
|
+ // <Tab name="PRACTICE" title="练习记录"></Tab>
|
|
|
+ const formEvaluation = reactive({
|
|
|
+ startTime: '',
|
|
|
+ endTime: '',
|
|
|
+ musicSheetName: null,
|
|
|
+ page: 1,
|
|
|
+ rows: 20
|
|
|
+ });
|
|
|
+
|
|
|
+ const fromPractice = reactive({
|
|
|
+ showData: true,
|
|
|
+ showTime: false,
|
|
|
+ currentDate: [dayjs().format('YYYY'), dayjs().format('MM')],
|
|
|
+ practiceMonthName:
|
|
|
+ dayjs().format('YYYY') + '年' + dayjs().format('MM') + '月',
|
|
|
+ practiceDetail: {} as any,
|
|
|
+ practiceList: [] as any
|
|
|
+ });
|
|
|
+ const refreshing = ref(false);
|
|
|
+ const loading = ref(false);
|
|
|
+ const finished = ref(false);
|
|
|
+ const showContact = ref(true);
|
|
|
+ const topWrapHeight = ref(0);
|
|
|
+ const infoDetail = ref({
|
|
|
+ evaluationNum: 0,
|
|
|
+ userMusicNum: 0
|
|
|
+ });
|
|
|
+ const onRefresh = () => {
|
|
|
+ finished.value = false;
|
|
|
+ // 重新加载数据
|
|
|
+ // 将 loading 设置为 true,表示处于加载状态
|
|
|
+ loading.value = true;
|
|
|
+ getList();
|
|
|
+ };
|
|
|
+ const list = ref([]);
|
|
|
+ const getList = async () => {
|
|
|
+ if (state.isClick) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ state.isClick = true;
|
|
|
+ if (refreshing.value) {
|
|
|
+ list.value = [];
|
|
|
+ formEvaluation.page = 1;
|
|
|
+ refreshing.value = false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await request.post(`/edu-app/musicPracticeRecord/page`, {
|
|
|
+ data: { ...formEvaluation, feature: 'EVALUATION' }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (list.value.length > 0 && res.data.current === 1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ list.value = list.value.concat(res.data.rows || []);
|
|
|
+ formEvaluation.page = res.data.current + 1;
|
|
|
+ showContact.value = list.value.length > 0;
|
|
|
+ loading.value = false;
|
|
|
+ finished.value = res.data.current >= res.data.pages;
|
|
|
+ } catch {
|
|
|
+ showContact.value = false;
|
|
|
+ finished.value = true;
|
|
|
+ }
|
|
|
+ state.isClick = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getDetail = async () => {
|
|
|
+ try {
|
|
|
+ const { data } = await request.post(
|
|
|
+ `/edu-app/musicPracticeRecord/studentStat`,
|
|
|
+ {
|
|
|
+ data: {
|
|
|
+ startTime: formEvaluation.startTime,
|
|
|
+ endTime: formEvaluation.endTime,
|
|
|
+ musicSheetName: formEvaluation.musicSheetName
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ infoDetail.value = { ...data };
|
|
|
+ } catch (e: any) {}
|
|
|
+ };
|
|
|
+
|
|
|
+ const onEvaluation = () => {
|
|
|
+ getDetail();
|
|
|
+ getList();
|
|
|
+ };
|
|
|
+
|
|
|
+ const tabResize = () => {
|
|
|
+ tabsRef.value?.resize();
|
|
|
+ };
|
|
|
+ //
|
|
|
+ const getPractice = async () => {
|
|
|
+ try {
|
|
|
+ const currentDate = fromPractice.currentDate.join('-');
|
|
|
+ const { data } = await request.post(
|
|
|
+ `/edu-app/musicPracticeRecord/studentTrainStat`,
|
|
|
+ {
|
|
|
+ data: {
|
|
|
+ startTime: currentDate + '-01 00:00:00',
|
|
|
+ endTime:
|
|
|
+ dayjs(currentDate).endOf('month').format('YYYY-MM-DD') +
|
|
|
+ ' 23:59:59'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const { studentTrainStatList, ...more } = data;
|
|
|
+ fromPractice.showData = studentTrainStatList?.length > 0;
|
|
|
+ fromPractice.practiceDetail = { ...more };
|
|
|
+
|
|
|
+ const tempList: any = [];
|
|
|
+ let maxTime = 0;
|
|
|
+ studentTrainStatList?.forEach((item: any) => {
|
|
|
+ if (item.practiceTimes > maxTime) {
|
|
|
+ maxTime = item.practiceTimes;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ studentTrainStatList?.forEach((item: any) => {
|
|
|
+ tempList.push({
|
|
|
+ date: dayjs(item.practiceDate).format('MM/DD'),
|
|
|
+ time: parseFloat((item.practiceTimes / 60).toFixed(2)),
|
|
|
+ rate: Math.floor((item.practiceTimes / maxTime) * 100)
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ fromPractice.practiceList = tempList || [];
|
|
|
+ } catch (e: any) {}
|
|
|
+ };
|
|
|
+
|
|
|
+ // 我的作品
|
|
|
+ const gotoMyWork = (pageTag = 'my_work') => {
|
|
|
+ postMessage({
|
|
|
+ api: 'open_app_page',
|
|
|
+ content: {
|
|
|
+ action: 'app',
|
|
|
+ pageTag: pageTag,
|
|
|
+ url: ''
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ window.addEventListener('resize', tabResize);
|
|
|
+ listenerMessage('webViewOnResume', () => {
|
|
|
+ tabResize();
|
|
|
+ });
|
|
|
+ onEvaluation();
|
|
|
+
|
|
|
+ getPractice();
|
|
|
+ });
|
|
|
+
|
|
|
+ onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', tabResize);
|
|
|
+ });
|
|
|
+ return () => (
|
|
|
+ <div class={styles.exerciseContainer}>
|
|
|
+ <MSticky
|
|
|
+ position="top"
|
|
|
+ onBarHeight={(height: number) => {
|
|
|
+ topWrapHeight.value = height;
|
|
|
+ }}>
|
|
|
+ <MHeader border={false} background={'transparent'}>
|
|
|
+ {{
|
|
|
+ content: () => (
|
|
|
+ <div class={styles.woringHeader}>
|
|
|
+ <i
|
|
|
+ onClick={() => {
|
|
|
+ if (browser().isApp) {
|
|
|
+ postMessage({
|
|
|
+ api: 'back'
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ router.back();
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ class={[
|
|
|
+ 'van-badge__wrapper van-icon van-icon-arrow-left van-nav-bar__arrow',
|
|
|
+ styles.leftArrow
|
|
|
+ ]}></i>
|
|
|
+ <Tabs
|
|
|
+ ref={tabsRef}
|
|
|
+ class={styles.tabSection}
|
|
|
+ v-model:active={state.tabActive}
|
|
|
+ shrink>
|
|
|
+ <Tab name="EVALUATION" title="评测记录"></Tab>
|
|
|
+ <Tab name="PRACTICE" title="练习记录"></Tab>
|
|
|
+ </Tabs>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }}
|
|
|
+ </MHeader>
|
|
|
+ </MSticky>
|
|
|
+
|
|
|
+ <Tabs v-model:active={state.tabActive} class={styles.hideTabsHeader}>
|
|
|
+ <Tab name="EVALUATION" title="评测记录">
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ overflow: 'hidden',
|
|
|
+ height: `calc(100vh - ${topWrapHeight.value}px)`,
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column'
|
|
|
+ }}>
|
|
|
+ <div class={[styles.cardSection, styles.EVALUATION_CARD]}>
|
|
|
+ <img src={iconBird} class={styles.iconBird} />
|
|
|
+
|
|
|
+ <div class={styles.scBox}>
|
|
|
+ <img src={iconLl} class={styles.l1} />
|
|
|
+ <img src={iconLc} class={styles.l2} />
|
|
|
+ <img src={iconLr} class={styles.l3} />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class={styles.sCountSection}>
|
|
|
+ <div class={styles.item}>
|
|
|
+ <img src={iconNumber} class={styles.iconNumber} />
|
|
|
+ <span class={styles.label}>评测次数</span>
|
|
|
+ <span class={styles.value}>
|
|
|
+ {infoDetail.value.evaluationNum}
|
|
|
+ <i>次</i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <span class={styles.line}></span>
|
|
|
+ <div class={styles.item} onClick={() => gotoMyWork()}>
|
|
|
+ <img src={iconAlbum} class={styles.iconNumber} />
|
|
|
+ <span class={styles.label}>作品数量</span>
|
|
|
+ <span class={styles.value}>
|
|
|
+ {infoDetail.value.userMusicNum}
|
|
|
+ <i>首</i>
|
|
|
+ </span>
|
|
|
+ <img src={iconArrow} class={styles.iconArrow} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.searchGroup}>
|
|
|
+ <div class={[styles.section, styles.sectionSearch]}>
|
|
|
+ <MSearch
|
|
|
+ shape="round"
|
|
|
+ inputBackground="white"
|
|
|
+ background="transparent"
|
|
|
+ placeholder="请输入曲目名称"
|
|
|
+ onSearch={(val: any) => {
|
|
|
+ formEvaluation.musicSheetName = val;
|
|
|
+ refreshing.value = true;
|
|
|
+ loading.value = true;
|
|
|
+ onEvaluation();
|
|
|
+ }}>
|
|
|
+ {{
|
|
|
+ left: () => (
|
|
|
+ <div
|
|
|
+ class={[
|
|
|
+ styles.searchDropDown,
|
|
|
+ state.showPopoverTime && styles.active
|
|
|
+ ]}
|
|
|
+ onClick={() => {
|
|
|
+ state.showPopoverTime = true;
|
|
|
+ }}>
|
|
|
+ <span>筛选</span>
|
|
|
+ <img
|
|
|
+ class={styles.iconArrow}
|
|
|
+ src={
|
|
|
+ state.showPopoverTime
|
|
|
+ ? iconArrowActive
|
|
|
+ : iconArrow
|
|
|
+ }
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }}
|
|
|
+ </MSearch>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.listSection} style={{ flex: '1' }}>
|
|
|
+ {showContact.value ? (
|
|
|
+ <OFullRefresh
|
|
|
+ v-model:modelValue={refreshing.value}
|
|
|
+ onRefresh={onRefresh}
|
|
|
+ style={
|
|
|
+ {
|
|
|
+ // minHeight: `calc(100vh - ${topWrapHeight.value}px)`
|
|
|
+ }
|
|
|
+ }>
|
|
|
+ <List
|
|
|
+ loading-text=" "
|
|
|
+ loading={loading.value}
|
|
|
+ finished={finished.value}
|
|
|
+ finished-text=" "
|
|
|
+ onLoad={getList}>
|
|
|
+ {list.value.map((item: any) => (
|
|
|
+ <DetailItem item={item} />
|
|
|
+ ))}
|
|
|
+ </List>
|
|
|
+ </OFullRefresh>
|
|
|
+ ) : (
|
|
|
+ <MEmpty description="暂无内容" />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Tab>
|
|
|
+ <Tab name="PRACTICE" title="练习记录">
|
|
|
+ <div
|
|
|
+ style={{
|
|
|
+ overflow: 'hidden',
|
|
|
+ height: `calc(100vh - ${topWrapHeight.value}px)`,
|
|
|
+ display: 'flex',
|
|
|
+ flexDirection: 'column'
|
|
|
+ }}>
|
|
|
+ <div class={[styles.cardSection, styles.EVALUATION_CARD]}>
|
|
|
+ <img src={iconPP} class={styles.iconBirdPP} />
|
|
|
+
|
|
|
+ <div class={styles.scBox}>
|
|
|
+ <img src={iconPl} class={styles.l1} />
|
|
|
+ <img src={iconPc} class={styles.l2} />
|
|
|
+ <img src={iconPr} class={styles.l3} />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class={styles.sCountSection}>
|
|
|
+ <div class={styles.item}>
|
|
|
+ <img src={icon1} class={styles.iconNumber} />
|
|
|
+ <span class={styles.label}>练习天数</span>
|
|
|
+ <span class={styles.value}>
|
|
|
+ {fromPractice.practiceDetail.practiceDays || 0}
|
|
|
+ <i>天</i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <span class={styles.line}></span>
|
|
|
+ <div class={styles.item} onClick={() => gotoMyWork()}>
|
|
|
+ <img src={icon2} class={styles.iconNumber} />
|
|
|
+ <span class={styles.label}>练习时长</span>
|
|
|
+ <span class={styles.value}>
|
|
|
+ {fromPractice.practiceDetail.practiceTimes
|
|
|
+ ? Math.floor(
|
|
|
+ fromPractice.practiceDetail.practiceTimes / 60
|
|
|
+ )
|
|
|
+ : 0}
|
|
|
+ <i>分钟</i>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class={styles.searchGroup}>
|
|
|
+ <div class={[styles.section, styles.sectionSearch]}>
|
|
|
+ <div
|
|
|
+ class={[
|
|
|
+ styles.practiceName,
|
|
|
+ fromPractice.showTime && styles.active
|
|
|
+ ]}
|
|
|
+ onClick={() => {
|
|
|
+ fromPractice.showTime = true;
|
|
|
+ }}>
|
|
|
+ {fromPractice.practiceMonthName}
|
|
|
+ <img
|
|
|
+ class={styles.iconArrow}
|
|
|
+ src={fromPractice.showTime ? iconArrowActive : iconArrow}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class={styles.listParent} style={{ flex: '1' }}>
|
|
|
+ <div class={styles.listChild}>
|
|
|
+ {fromPractice.showData ? (
|
|
|
+ <div class={styles.practiceList}>
|
|
|
+ {fromPractice.practiceList?.map((item: any) => (
|
|
|
+ <div class={styles.practiceItem}>
|
|
|
+ <span class={styles.time}>{item.date}</span>
|
|
|
+ <div class={styles.lineBox}>
|
|
|
+ <div class={styles.boxSection}>
|
|
|
+ <div
|
|
|
+ class={styles.box}
|
|
|
+ style={{ width: item.rate + '%' }}></div>
|
|
|
+ <p
|
|
|
+ class={styles.long}
|
|
|
+ style={{ left: item.rate + '%' }}>
|
|
|
+ <span>{item.time}</span>
|
|
|
+ 分钟
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <MEmpty description="暂无内容" />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Tab>
|
|
|
+ </Tabs>
|
|
|
+
|
|
|
+ <TheTimeRange
|
|
|
+ v-model:show={state.showPopoverTime}
|
|
|
+ onConfirm={(val: any) => {
|
|
|
+ formEvaluation.startTime = val.startTime
|
|
|
+ ? val.startTime + ' 00:00:00'
|
|
|
+ : '';
|
|
|
+ formEvaluation.endTime = val.endTime
|
|
|
+ ? val.endTime + ' 23:59:59'
|
|
|
+ : '';
|
|
|
+ state.showPopoverTime = false;
|
|
|
+ refreshing.value = true;
|
|
|
+ loading.value = true;
|
|
|
+ onEvaluation();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+
|
|
|
+ <Popup
|
|
|
+ v-model:show={fromPractice.showTime}
|
|
|
+ position="bottom"
|
|
|
+ round
|
|
|
+ class={'popupBottomSearch'}>
|
|
|
+ <DatePicker
|
|
|
+ onCancel={() => {
|
|
|
+ fromPractice.showTime = false;
|
|
|
+ }}
|
|
|
+ onConfirm={(val: any) => {
|
|
|
+ fromPractice.showTime = false;
|
|
|
+ getPractice();
|
|
|
+ }}
|
|
|
+ v-model={fromPractice.currentDate}
|
|
|
+ formatter={formatterDatePicker}
|
|
|
+ columnsType={['year', 'month']}
|
|
|
+ />
|
|
|
+ </Popup>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+});
|