index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import { defineComponent, onMounted, onUnmounted, reactive, ref } from 'vue';
  2. import styles from './index.module.less';
  3. import MSticky from '@/components/m-sticky';
  4. import MHeader from '@/components/m-header';
  5. import { browser, formatterDatePicker } from '@/helpers/utils';
  6. import { useRouter } from 'vue-router';
  7. import { DatePicker, List, Popup, Tab, Tabs } from 'vant';
  8. import { postMessage } from '@/helpers/native-message';
  9. import { listenerMessage } from '@/helpers/native-message';
  10. import OFullRefresh from '@/components/m-full-refresh';
  11. import iconBird from './images/icon-bird.png';
  12. import iconPP from './images/icon-p-p.png';
  13. import iconNumber from './images/icon-number.png';
  14. import iconAlbum from './images/icon-album.png';
  15. import icon1 from './images/icon-1.png';
  16. import icon2 from './images/icon-2.png';
  17. import iconArrow from './images/icon-arrow.png';
  18. import iconArrowActive from './images/icon-arrow-active.png';
  19. import iconLl from './images/l-l.png';
  20. import iconLc from './images/l-c.png';
  21. import iconLr from './images/l-r.png';
  22. import iconPl from './images/p-l.png';
  23. import iconPc from './images/p-c.png';
  24. import iconPr from './images/p-r.png';
  25. import MSearch from '@/components/m-search';
  26. import TheTimeRange from '@/components/the-time-range';
  27. import request from '@/helpers/request';
  28. import DetailItem from './modals/detail-item';
  29. import MEmpty from '@/components/m-empty';
  30. import dayjs from 'dayjs';
  31. export default defineComponent({
  32. name: 'exercise-record-index',
  33. setup() {
  34. const router = useRouter();
  35. const tabsRef = ref();
  36. const state = reactive({
  37. tabActive: 'EVALUATION' as 'EVALUATION' | 'PRACTICE',
  38. isClick: false,
  39. showPopoverTime: false
  40. });
  41. // <Tab name="EVALUATION" title="评测记录"></Tab>
  42. // <Tab name="PRACTICE" title="练习记录"></Tab>
  43. const formEvaluation = reactive({
  44. startTime: '',
  45. endTime: '',
  46. musicSheetName: null,
  47. page: 1,
  48. rows: 20
  49. });
  50. const fromPractice = reactive({
  51. showData: true,
  52. showTime: false,
  53. currentDate: [dayjs().format('YYYY'), dayjs().format('MM')],
  54. practiceMonthName:
  55. dayjs().format('YYYY') + '年' + dayjs().format('MM') + '月',
  56. practiceDetail: {} as any,
  57. practiceList: [] as any
  58. });
  59. const refreshing = ref(false);
  60. const loading = ref(false);
  61. const finished = ref(false);
  62. const showContact = ref(true);
  63. const topWrapHeight = ref(0);
  64. const infoDetail = ref({
  65. evaluationNum: 0,
  66. userMusicNum: 0
  67. });
  68. const onRefresh = () => {
  69. finished.value = false;
  70. // 重新加载数据
  71. // 将 loading 设置为 true,表示处于加载状态
  72. loading.value = true;
  73. getList();
  74. };
  75. const list = ref([]);
  76. const getList = async () => {
  77. if (state.isClick) {
  78. return;
  79. }
  80. state.isClick = true;
  81. if (refreshing.value) {
  82. list.value = [];
  83. formEvaluation.page = 1;
  84. refreshing.value = false;
  85. }
  86. try {
  87. const res = await request.post(`/edu-app/musicPracticeRecord/page`, {
  88. data: { ...formEvaluation, feature: 'EVALUATION' }
  89. });
  90. if (list.value.length > 0 && res.data.current === 1) {
  91. return;
  92. }
  93. list.value = list.value.concat(res.data.rows || []);
  94. formEvaluation.page = res.data.current + 1;
  95. showContact.value = list.value.length > 0;
  96. loading.value = false;
  97. finished.value = res.data.current >= res.data.pages;
  98. } catch {
  99. showContact.value = false;
  100. finished.value = true;
  101. }
  102. state.isClick = false;
  103. };
  104. const getDetail = async () => {
  105. try {
  106. const { data } = await request.post(
  107. `/edu-app/musicPracticeRecord/studentStat`,
  108. {
  109. data: {
  110. startTime: formEvaluation.startTime,
  111. endTime: formEvaluation.endTime,
  112. musicSheetName: formEvaluation.musicSheetName
  113. }
  114. }
  115. );
  116. infoDetail.value = { ...data };
  117. } catch (e: any) {}
  118. };
  119. const onEvaluation = () => {
  120. getDetail();
  121. getList();
  122. };
  123. const tabResize = () => {
  124. tabsRef.value?.resize();
  125. };
  126. //
  127. const getPractice = async () => {
  128. try {
  129. const currentDate = fromPractice.currentDate.join('-');
  130. const { data } = await request.post(
  131. `/edu-app/musicPracticeRecord/studentTrainStat`,
  132. {
  133. data: {
  134. startTime: currentDate + '-01 00:00:00',
  135. endTime:
  136. dayjs(currentDate).endOf('month').format('YYYY-MM-DD') +
  137. ' 23:59:59'
  138. }
  139. }
  140. );
  141. const { studentTrainStatList, ...more } = data;
  142. fromPractice.showData = studentTrainStatList?.length > 0;
  143. fromPractice.practiceDetail = { ...more };
  144. const tempList: any = [];
  145. let maxTime = 0;
  146. studentTrainStatList?.forEach((item: any) => {
  147. if (item.practiceTimes > maxTime) {
  148. maxTime = item.practiceTimes;
  149. }
  150. });
  151. studentTrainStatList?.forEach((item: any) => {
  152. tempList.push({
  153. date: dayjs(item.practiceDate).format('MM/DD'),
  154. time: parseFloat((item.practiceTimes / 60).toFixed(2)),
  155. rate: Math.floor((item.practiceTimes / maxTime) * 100)
  156. });
  157. });
  158. fromPractice.practiceList = tempList || [];
  159. } catch (e: any) {}
  160. };
  161. // 我的作品
  162. const gotoMyWork = (pageTag = 'my_work') => {
  163. postMessage({
  164. api: 'open_app_page',
  165. content: {
  166. action: 'app',
  167. pageTag: pageTag,
  168. url: ''
  169. }
  170. });
  171. };
  172. onMounted(() => {
  173. window.addEventListener('resize', tabResize);
  174. listenerMessage('webViewOnResume', () => {
  175. tabResize();
  176. });
  177. onEvaluation();
  178. getPractice();
  179. });
  180. onUnmounted(() => {
  181. window.removeEventListener('resize', tabResize);
  182. });
  183. return () => (
  184. <div class={styles.exerciseContainer}>
  185. <MSticky
  186. position="top"
  187. onBarHeight={(height: number) => {
  188. topWrapHeight.value = height;
  189. }}>
  190. <MHeader border={false} background={'transparent'}>
  191. {{
  192. content: () => (
  193. <div class={styles.woringHeader}>
  194. <i
  195. onClick={() => {
  196. if (browser().isApp) {
  197. postMessage({
  198. api: 'back'
  199. });
  200. } else {
  201. router.back();
  202. }
  203. }}
  204. class={[
  205. 'van-badge__wrapper van-icon van-icon-arrow-left van-nav-bar__arrow',
  206. styles.leftArrow
  207. ]}></i>
  208. <Tabs
  209. ref={tabsRef}
  210. class={styles.tabSection}
  211. v-model:active={state.tabActive}
  212. shrink>
  213. <Tab name="EVALUATION" title="评测记录"></Tab>
  214. <Tab name="PRACTICE" title="练习记录"></Tab>
  215. </Tabs>
  216. </div>
  217. )
  218. }}
  219. </MHeader>
  220. </MSticky>
  221. <Tabs v-model:active={state.tabActive} class={styles.hideTabsHeader}>
  222. <Tab name="EVALUATION" title="评测记录">
  223. <div
  224. style={{
  225. overflow: 'hidden',
  226. height: `calc(100vh - ${topWrapHeight.value}px)`,
  227. display: 'flex',
  228. flexDirection: 'column'
  229. }}>
  230. <div class={[styles.cardSection, styles.EVALUATION_CARD]}>
  231. <img src={iconBird} class={styles.iconBird} />
  232. <div class={styles.scBox}>
  233. <img src={iconLl} class={styles.l1} />
  234. <img src={iconLc} class={styles.l2} />
  235. <img src={iconLr} class={styles.l3} />
  236. </div>
  237. <div class={styles.sCountSection}>
  238. <div class={styles.item}>
  239. <img src={iconNumber} class={styles.iconNumber} />
  240. <span class={styles.label}>评测次数</span>
  241. <span class={styles.value}>
  242. {infoDetail.value.evaluationNum}
  243. <i></i>
  244. </span>
  245. </div>
  246. <span class={styles.line}></span>
  247. <div class={styles.item} onClick={() => gotoMyWork()}>
  248. <img src={iconAlbum} class={styles.iconNumber} />
  249. <span class={styles.label}>作品数量</span>
  250. <span class={styles.value}>
  251. {infoDetail.value.userMusicNum}
  252. <i></i>
  253. </span>
  254. <img src={iconArrow} class={styles.iconArrow} />
  255. </div>
  256. </div>
  257. </div>
  258. <div class={styles.searchGroup}>
  259. <div class={[styles.section, styles.sectionSearch]}>
  260. <MSearch
  261. shape="round"
  262. inputBackground="white"
  263. background="transparent"
  264. placeholder="请输入曲目名称"
  265. onSearch={(val: any) => {
  266. formEvaluation.musicSheetName = val;
  267. refreshing.value = true;
  268. loading.value = true;
  269. onEvaluation();
  270. }}>
  271. {{
  272. left: () => (
  273. <div
  274. class={[
  275. styles.searchDropDown,
  276. state.showPopoverTime && styles.active
  277. ]}
  278. onClick={() => {
  279. state.showPopoverTime = true;
  280. }}>
  281. <span>筛选</span>
  282. <img
  283. class={styles.iconArrow}
  284. src={
  285. state.showPopoverTime
  286. ? iconArrowActive
  287. : iconArrow
  288. }
  289. />
  290. </div>
  291. )
  292. }}
  293. </MSearch>
  294. </div>
  295. </div>
  296. <div class={styles.listSection} style={{ flex: '1' }}>
  297. {showContact.value ? (
  298. <OFullRefresh
  299. v-model:modelValue={refreshing.value}
  300. onRefresh={onRefresh}
  301. style={
  302. {
  303. // minHeight: `calc(100vh - ${topWrapHeight.value}px)`
  304. }
  305. }>
  306. <List
  307. loading-text=" "
  308. loading={loading.value}
  309. finished={finished.value}
  310. finished-text=" "
  311. onLoad={getList}>
  312. {list.value.map((item: any) => (
  313. <DetailItem item={item} />
  314. ))}
  315. </List>
  316. </OFullRefresh>
  317. ) : (
  318. <MEmpty description="暂无内容" />
  319. )}
  320. </div>
  321. </div>
  322. </Tab>
  323. <Tab name="PRACTICE" title="练习记录">
  324. <div
  325. style={{
  326. overflow: 'hidden',
  327. height: `calc(100vh - ${topWrapHeight.value}px)`,
  328. display: 'flex',
  329. flexDirection: 'column'
  330. }}>
  331. <div class={[styles.cardSection, styles.EVALUATION_CARD]}>
  332. <img src={iconPP} class={styles.iconBirdPP} />
  333. <div class={styles.scBox}>
  334. <img src={iconPl} class={styles.l1} />
  335. <img src={iconPc} class={styles.l2} />
  336. <img src={iconPr} class={styles.l3} />
  337. </div>
  338. <div class={styles.sCountSection}>
  339. <div class={styles.item}>
  340. <img src={icon1} class={styles.iconNumber} />
  341. <span class={styles.label}>练习天数</span>
  342. <span class={styles.value}>
  343. {fromPractice.practiceDetail.practiceDays || 0}
  344. <i></i>
  345. </span>
  346. </div>
  347. <span class={styles.line}></span>
  348. <div class={styles.item} onClick={() => gotoMyWork()}>
  349. <img src={icon2} class={styles.iconNumber} />
  350. <span class={styles.label}>练习时长</span>
  351. <span class={styles.value}>
  352. {fromPractice.practiceDetail.practiceTimes
  353. ? Math.floor(
  354. fromPractice.practiceDetail.practiceTimes / 60
  355. )
  356. : 0}
  357. <i>分钟</i>
  358. </span>
  359. </div>
  360. </div>
  361. </div>
  362. <div class={styles.searchGroup}>
  363. <div class={[styles.section, styles.sectionSearch]}>
  364. <div
  365. class={[
  366. styles.practiceName,
  367. fromPractice.showTime && styles.active
  368. ]}
  369. onClick={() => {
  370. fromPractice.showTime = true;
  371. }}>
  372. {fromPractice.practiceMonthName}
  373. <img
  374. class={styles.iconArrow}
  375. src={fromPractice.showTime ? iconArrowActive : iconArrow}
  376. />
  377. </div>
  378. </div>
  379. </div>
  380. <div class={styles.listParent} style={{ flex: '1' }}>
  381. <div class={styles.listChild}>
  382. {fromPractice.showData ? (
  383. <div class={styles.practiceList}>
  384. {fromPractice.practiceList?.map((item: any) => (
  385. <div class={styles.practiceItem}>
  386. <span class={styles.time}>{item.date}</span>
  387. <div class={styles.lineBox}>
  388. <div class={styles.boxSection}>
  389. <div
  390. class={styles.box}
  391. style={{ width: item.rate + '%' }}></div>
  392. <p
  393. class={styles.long}
  394. style={{ left: item.rate + '%' }}>
  395. <span>{item.time}</span>
  396. 分钟
  397. </p>
  398. </div>
  399. </div>
  400. </div>
  401. ))}
  402. </div>
  403. ) : (
  404. <MEmpty description="暂无内容" />
  405. )}
  406. </div>
  407. </div>
  408. </div>
  409. </Tab>
  410. </Tabs>
  411. <TheTimeRange
  412. v-model:show={state.showPopoverTime}
  413. onConfirm={(val: any) => {
  414. formEvaluation.startTime = val.startTime
  415. ? val.startTime + ' 00:00:00'
  416. : '';
  417. formEvaluation.endTime = val.endTime
  418. ? val.endTime + ' 23:59:59'
  419. : '';
  420. state.showPopoverTime = false;
  421. refreshing.value = true;
  422. loading.value = true;
  423. onEvaluation();
  424. }}
  425. />
  426. <Popup
  427. v-model:show={fromPractice.showTime}
  428. position="bottom"
  429. round
  430. class={'popupBottomSearch'}>
  431. <DatePicker
  432. onCancel={() => {
  433. fromPractice.showTime = false;
  434. }}
  435. onConfirm={(val: any) => {
  436. fromPractice.showTime = false;
  437. getPractice();
  438. }}
  439. v-model={fromPractice.currentDate}
  440. formatter={formatterDatePicker}
  441. columnsType={['year', 'month']}
  442. />
  443. </Popup>
  444. </div>
  445. );
  446. }
  447. });