Просмотр исходного кода

Merge branch 'iteration-20240924' into dev

lex-xin 9 месяцев назад
Родитель
Сommit
3aa2da4fff

+ 8 - 1
src/App.tsx

@@ -42,11 +42,18 @@ export default defineComponent({
       const appTheme = setting.appTheme;
       const lightenStr = lighten(setting.appTheme, 6);
 
+    //   errorColor: string;
+    // errorColorHover: string;
+    // errorColorPressed: string;
+    // errorColorSuppl: string;
       return {
         common: {
           primaryColor: appTheme,
           primaryColorHover: lightenStr,
-          primaryColorPressed: lightenStr
+          primaryColorPressed: lightenStr,
+          errorColor: '#F51C08',
+          errorColorHover: '#F51C08',
+          errorColorPressed: '#F51C08'
         },
         LoadingBar: {
           colorLoading: appTheme

+ 15 - 14
src/components/CSelect/index.tsx

@@ -32,20 +32,21 @@ export default defineComponent({
             onUpdate:show={(flag: boolean) => {
               isFocus.value = flag;
             }}
-            v-slots={{
-              arrow: () =>
-                isFocus.value ? (
-                  <NImage
-                    class={styles.arrow}
-                    previewDisabled
-                    src={activeArrow}></NImage>
-                ) : (
-                  <NImage
-                    class={styles.arrow}
-                    previewDisabled
-                    src={arrow}></NImage>
-                )
-            }}></NSelect>
+            // v-slots={{
+            //   arrow: () =>
+            //     isFocus.value ? (
+            //       <NImage
+            //         class={styles.arrow}
+            //         previewDisabled
+            //         src={activeArrow}></NImage>
+            //     ) : (
+            //       <NImage
+            //         class={styles.arrow}
+            //         previewDisabled
+            //         src={arrow}></NImage>
+            //     )
+            // }}
+            ></NSelect>
         </div>
       </>
     );

+ 3 - 2
src/components/card-type/index.module.less

@@ -57,10 +57,11 @@
   &.isError {
     :global {
       .n-card--bordered {
-        border: 1Px solid #d03050 !important;
+        border: 1Px solid #F51C08 !important;
     
         &:hover {
-          border: 1Px solid #d03050 !important;
+          border: 1Px solid #F51C08 !important;
+          box-shadow: 0 0 0 2px rgba(245, 28, 8, 0.2);
         }
       }
     }

+ 18 - 0
src/styles/index.less

@@ -123,6 +123,24 @@ body>.n-drawer-container-relative {
   --n-padding: 0 28px !important;
 }
 
+.searchDate, .searchDateDefault {
+  --n-padding: 0 28px !important;
+  background: linear-gradient( 312deg, #1B7AF8 0%, #3CBBFF 100%);
+  border-radius: 8px;
+  // line-height: 41px;
+  font-size: 18px !important;
+  font-weight: 600 !important;
+
+  .n-button__state-border, .n-button__border {
+    border: none !important;
+  }
+}
+
+.searchDateDefault {
+  background: #F1F2F6;
+  color: #1E2022;
+}
+
 // .n-data-table {
 //   border-radius: 10px 10px 0 0;
 //   overflow: hidden;

+ 39 - 0
src/utils/dateFormat.ts

@@ -104,6 +104,16 @@ export function formatTime(time: number) {
   return str;
 }
 
+export function getHours(time: number) {
+  const minutes = Math.floor(time / 60 / 60);
+  return minutes;
+}
+
+export function getLastMinutes(time: number) {
+  const minutes = Math.floor(time / 60);
+  return Math.floor(minutes % 60)
+}
+
 export function getMinutes(time: number) {
   const minutes = Math.floor(time / 60);
   return minutes;
@@ -113,3 +123,32 @@ export function getSecend(time: number) {
   const seconds = Math.floor(time % 60);
   return seconds;
 }
+
+
+// 秒转时分秒
+export function formateSeconds(endTime: string, pad = 2) {
+  let secondTime = parseInt(endTime) //将传入的秒的值转化为Number
+  let min = 0 // 初始化分
+  let h = 0 // 初始化小时
+  let result = ''
+  if (secondTime >= 60) {
+    //如果秒数大于等于60,将秒数转换成整数
+    min = parseInt(secondTime / 60 + '') //获取分钟,除以60取整数,得到整数分钟
+    secondTime = parseInt((secondTime % 60) + '') //获取秒数,秒数取佘,得到整数秒数
+    if (min >= 60) {
+      //如果分钟大于等于60,将分钟转换成小时
+      h = parseInt(min / 60 + '') //获取小时,获取分钟除以60,得到整数小时
+      min = parseInt((min % 60) + '') //获取小时后取佘的分,获取分钟除以60取佘的分
+    }
+  }
+  if (h) {
+    result = `${h.toString().padStart(pad, '0')}时${min.toString().padStart(pad, '0')}分${secondTime
+      .toString()
+      .padStart(pad, '0')}秒`
+  } else if (min) {
+    result = `${min.toString().padStart(pad, '0')}分${secondTime.toString().padStart(pad, '0')}秒`
+  } else {
+    result = `${secondTime.toString().padStart(pad, '0')}秒`
+  }
+  return result
+}

+ 34 - 0
src/utils/index.ts

@@ -787,4 +787,38 @@ export const getAuthForAdmin = () => {
   //   sessionStorage.removeItem('authLoadNum');
   //   storage.remove(ACCESS_TOKEN_ADMIN);
   // }
+}
+
+
+export function convertToChineseNumeral(num: number) {
+  if (num == 10) {
+    return '十'
+  } else if (num == 1) {
+    return '一'
+  }
+  const digits = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九']
+  const units = ['', '十', '百', '千', '万']
+  let result = ''
+  let numStr = num.toString()
+  for (let i = 0; i < numStr.length; i++) {
+    let digit = parseInt(numStr.charAt(i))
+    let unit = units[numStr.length - i - 1]
+    if (digit === 0) {
+      // 当前数字为0时不需要输出汉字,但需要考虑上一个数字是否为0,避免出现连续的零
+      if (result.charAt(result.length - 1) !== '零') {
+        result += '零'
+      }
+    } else {
+      result += digits[digit] + unit
+    }
+  }
+  // 对于一些特殊的数字,如10、100等,需要在最前面加上“一”
+  if (result.charAt(0) === '一') {
+    result = result.substr(1, result.length)
+  } else if (result.charAt(0) === '百') {
+    result = '一' + result
+  } else if (result.charAt(0) === '千') {
+    result = '一' + result
+  }
+  return result
 }

+ 9 - 0
src/views/classList/api.ts

@@ -169,6 +169,15 @@ export const getTrainingStatList = (params: any) => {
   });
 };
 
+/**
+ * @description: 后台练习统计列表
+ */
+export const api_practiceStatPage = (params: object) => {
+  return request.post('/edu-app/musicPracticeRecordStat/practiceStatPage', {
+      data: params
+    })
+}
+
 /***
  * 创建班级群聊
  */

+ 21 - 9
src/views/classList/components/testRecode.tsx

@@ -23,7 +23,10 @@ import {
   getNowDateAndSunday,
   getTimes,
   getMinutes,
-  getSecend
+  getSecend,
+  formateSeconds,
+  getHours,
+  getLastMinutes
 } from '/src/utils/dateFormat';
 import { getTestList, getTrainingStat } from '../api';
 import CDatePicker from '/src/components/CDatePicker';
@@ -238,12 +241,7 @@ export default defineComponent({
         return (
           <>
             {row.practiceDuration
-              ? getMinutes(row.practiceDuration) > 0
-                ? getMinutes(row.practiceDuration) +
-                  '分' +
-                  getSecend(row.practiceDuration) +
-                  '秒'
-                : getSecend(row.practiceDuration) + '秒'
+              ? formateSeconds(row.practiceDuration, 1)
               : 0 + '秒'}
           </>
         );
@@ -451,12 +449,26 @@ export default defineComponent({
             <NGi>
               <div class={styles.TrainDataItem}>
                 <p class={styles.TrainDataItemTitle}>
-                  {getMinutes(state.testInfo.practiceDurationAvg) > 0 ? (
+                  {getHours(state.testInfo.practiceDurationAvg) > 0 ? (
                     <div>
                       <span>
                         <NNumberAnimation
                           from={0}
-                          to={getMinutes(
+                          to={getHours(
+                            state.testInfo.practiceDurationAvg
+                          )}></NNumberAnimation>
+                      </span>
+                      <i style={{ width: '4px', display: 'inline-block' }}></i>
+                      时
+                      <i style={{ width: '4px', display: 'inline-block' }}></i>
+                    </div>
+                  ) : null}
+                  {getHours(state.testInfo.practiceDurationAvg) > 0 || getLastMinutes(state.testInfo.practiceDurationAvg) > 0 ? (
+                    <div>
+                      <span>
+                        <NNumberAnimation
+                          from={0}
+                          to={getLastMinutes(
                             state.testInfo.practiceDurationAvg
                           )}></NNumberAnimation>
                       </span>{' '}

+ 8 - 8
src/views/classList/index.module.less

@@ -433,12 +433,12 @@
   display: flex;
   flex-direction: row;
   align-items: center;
-  margin-bottom: 32px;
+  margin-bottom: 24px;
 
   .teacherHeader {
-    width: 100px;
-    height: 100px;
-    padding: 4px;
+    width: 70px;
+    height: 70px;
+    padding: 2px;
     border-radius: 99px;
     background: linear-gradient(228deg,
         rgba(2, 186, 255, 1),
@@ -455,13 +455,13 @@
       flex-direction: row;
       align-items: center;
       justify-content: center;
-      padding: 4px;
+      padding: 3px;
     }
   }
 
   .teacherHeaderImg {
-    width: 84px;
-    height: 84px;
+    width: 60px;
+    height: 60px;
     border-radius: 50%;
     overflow: hidden;
   }
@@ -472,7 +472,7 @@
       line-height: 30px;
       font-weight: 600;
       color: #131415;
-      margin-bottom: 12px;
+      margin-bottom: 6px;
     }
 
     p {

+ 7 - 0
src/views/data-module/api.tsx

@@ -24,6 +24,13 @@ export const getTestStat = (params: any) => {
   });
 };
 
+export const getPracticePageStat = (params: any) => {
+  return request.post('/edu-app/musicPracticeRecordStat/practicePageStat', {
+    data: params
+    // requestType: 'form'
+  });
+};
+
 
 export const getTrainingRanking = (params: any) => {
   return request.post('/edu-app/musicPracticeRecordStat/trainingRanking', {

+ 400 - 68
src/views/home/components/practiceData.tsx

@@ -1,15 +1,18 @@
-import { Ref, computed, defineComponent, onMounted, reactive, ref } from 'vue';
+import { Ref, computed, defineComponent,  reactive, ref } from 'vue';
 import styles from '../index2.module.less';
-import { NButton, NDataTable, NNumberAnimation } from 'naive-ui';
-import numeral from 'numeral';
+import { NButton, NDataTable, NNumberAnimation, NTooltip, useMessage } from 'naive-ui';
 import { useECharts } from '@/hooks/web/useECharts';
-import Pagination from '/src/components/pagination';
-import { getTestStat } from '@/views/data-module/api';
-import { getMinutes, getSecend, getTimes } from '/src/utils/dateFormat';
-import { useRoute, useRouter } from 'vue-router';
-import { getTrainingStatList } from '../../classList/api';
-import dayjs from 'dayjs';
+// import Pagination from '/src/components/pagination';
+import { getPracticePageStat, getTestStat } from '@/views/data-module/api';
+import { formateSeconds, getHours, getLastMinutes, getMinutes, getSecend, getTimes } from '/src/utils/dateFormat';
+import { api_practiceStatPage } from '../../classList/api';
 import TheEmpty from '/src/components/TheEmpty';
+import iconSortDefault from '@/common/images/icon-sort-default.png';
+import iconSortDesc from '@/common/images/icon-sort-desc.png';
+import iconSortAsc from '@/common/images/icon-sort-asc.png';
+import { convertToChineseNumeral } from '/src/utils';
+import { useRouter } from 'vue-router';
+import { setTabsCaches } from '/src/hooks/use-async';
 export default defineComponent({
   name: 'home-practiceData',
   props: {
@@ -19,12 +22,19 @@ export default defineComponent({
     }
   },
   setup(props, { expose }) {
+    const router = useRouter()
+    const message = useMessage()
     const chartRef = ref<HTMLDivElement | null>(null);
     const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
     const practiceFlag = ref(true);
     const payForm = reactive({
       height: '360px',
       width: '100%',
+      practiceDuration: 0,
+      evaluateUserCount: 0,
+      evaluateFrequency: 0,
+      publishUserCount: 0,
+      publishCount: 0,
       practiceUserCount: 0,
       paymentAmount: 0,
       practiceDurationAvg: 0,
@@ -41,54 +51,273 @@ export default defineComponent({
         rows: 10,
         pageTotal: 4
       },
+      searchForm: {
+        orderBy: null as any,
+        sort: null as any,
+      },
       tableList: [] as any,
       goCourseVisiable: false
     });
     const currentTimer = computed(() => {
       return props.timer;
     });
+
+    const toolTitleTips = (title: string, item: any) => {
+      return <NTooltip showArrow={false} placement="top-start">
+      {{
+        trigger: () => (
+          <div class={styles.cell}>
+            {title}
+            <img
+              class={styles.sortIcon}
+              src={
+                item.sortOrder === 'descend'
+                  ? iconSortDesc
+                  : item.sortOrder === 'ascend'
+                  ? iconSortAsc
+                  : iconSortDefault
+              }
+            />
+          </div>
+        ),
+        default:
+          item.sortOrder === 'descend'
+            ? '点击升序'
+            : item.sortOrder === 'ascend'
+            ? '取消排序'
+            : '点击降序'
+      }}
+    </NTooltip>
+    }
+
+    const practiceDurationRef = reactive({
+      title() {
+        return (
+          toolTitleTips('练习总时长', practiceDurationRef)
+        );
+      },
+      key: 'practiceDuration',
+      sorter: true,
+      sortOrder: false as any,
+      render(row: any) {
+        return <>{formateSeconds((row.practiceDuration as any) || 0)}</>
+      }
+    });
+
+    const practiceDaysRef = reactive({
+      title() {
+        return (
+          toolTitleTips('练习天数', practiceDaysRef)
+        );
+      },
+      key: 'practiceDays',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const practiceDurationAvgRef = reactive({
+      title() {
+        return (
+          toolTitleTips('平均练习时长', practiceDurationAvgRef)
+        );
+      },
+      key: 'practiceDurationAvg',
+      sorter: true,
+      sortOrder: false as any,
+      render(row: any) {
+        return <>{formateSeconds((row.practiceDurationAvg as any) || 0)}</>
+      }
+    });
+
+    const evaluateFrequencyRef = reactive({
+      title() {
+        return (
+          toolTitleTips('评测次数', evaluateFrequencyRef)
+        );
+      },
+      key: 'evaluateFrequency',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const publishCountRef = reactive({
+      title() {
+        return (
+          toolTitleTips('作品数量', publishCountRef)
+        );
+      },
+      key: 'publishCount',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const publishScoreRef = reactive({
+      title() {
+        return (
+          toolTitleTips('最新作品分数', publishScoreRef)
+        );
+      },
+      key: 'publishScore',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const publishTimeRef = reactive({
+      title() {
+        return (
+          toolTitleTips('最新作品时间', publishTimeRef)
+        );
+      },
+      key: 'publishTime',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const copyTo = (text: string) => {
+      const input = document.createElement('input');
+      input.value = text;
+      document.body.appendChild(input);
+      input.select();
+      input.setSelectionRange(0, input.value.length);
+      document.execCommand('Copy');
+      document.body.removeChild(input);
+      message.success('复制成功');
+    };
     const columns = () => {
       return [
         {
-          title: '日期',
-          key: 'date'
-        },
-        {
-          title: '练习人数',
-          key: 'practiceUserCount',
-          render(row: any) {
-            return <>{row.practiceUserCount}人</>;
+          title: '学生姓名',
+          key: 'studentName',
+          render: (row: any) => {
+            return (
+              <NTooltip showArrow={false} placement="top-start">
+                {{
+                  trigger: () => (
+                    <div
+                      style={{ userSelect: 'all', cursor: 'pointer' }}
+                      onClick={() => copyTo(row.studentName)}>
+                      {row.studentName}
+                    </div>
+                  ),
+                  default: '点击复制'
+                }}
+              </NTooltip>
+            );
           }
         },
         {
-          title: '平均每天练习时长',
-          key: 'practiceDuration',
+          title: '年级班级',
+          key: 'date',
           render(row: any) {
             return (
               <>
-                {' '}
-                <>
-                  {row.practiceDuration
-                    ? getMinutes(row.practiceDuration) > 0
-                      ? getMinutes(row.practiceDuration) +
-                        '分' +
-                        getSecend(row.practiceDuration) +
-                        '秒'
-                      : getSecend(row.practiceDuration) + '秒'
-                    : 0 + '分钟'}
-                </>
+                {row.currentGradeNum && row.currentClass
+                  ? convertToChineseNumeral(row.currentGradeNum) + '年级' + row.currentClass + '班'
+                  : ''}
               </>
-            );
+            )
           }
+        },
+        {
+          title: '乐器',
+          key: 'instrumentName'
+        },
+        practiceDurationRef,
+        practiceDaysRef,
+        practiceDurationAvgRef,
+        evaluateFrequencyRef,
+        {
+          title: '发布作品',
+          key: 'publishFlag',
+          render: (row: any) => row.publishFlag ? '是' : '否'
+        },
+        publishCountRef,
+        publishScoreRef,
+        publishTimeRef,
+        {
+          title: '操作',
+          key: 'titleImg',
+          render: (row: any) => (
+            <NButton
+              type="primary"
+              text
+              onClick={() => {
+                setTabsCaches('evaluatingRcode', 'tabName', {
+                  path: '/studentDetail'
+                });
+                router.push({
+                  path: '/studentDetail',
+                  query: { studentId: row.studentId, studentName: row.studentName }
+                });
+              }}
+            >
+              详情
+            </NButton>
+          )
         }
       ];
     };
-    const getList = async () => {
+
+    // 统计排序
+    const handleSorterChange = (sorter: any) => {
+      if (!sorter.order) {
+        state.searchForm.orderBy = '' as string
+        state.searchForm.sort = '' as string
+        practiceDurationRef.sortOrder = false
+        practiceDaysRef.sortOrder = false
+        practiceDurationAvgRef.sortOrder = false
+        evaluateFrequencyRef.sortOrder = false
+        publishCountRef.sortOrder = false
+        publishScoreRef.sortOrder = false
+        publishTimeRef.sortOrder = false
+      } else {
+        state.searchForm.orderBy = sorter.columnKey
+        practiceDurationRef.sortOrder = false
+        practiceDaysRef.sortOrder = false
+        practiceDurationAvgRef.sortOrder = false
+        evaluateFrequencyRef.sortOrder = false
+        publishCountRef.sortOrder = false
+        publishScoreRef.sortOrder = false
+        publishTimeRef.sortOrder = false
+        if (sorter.columnKey == 'practiceDuration') {
+          practiceDurationRef.sortOrder = sorter.order
+        }
+        if (sorter.columnKey == 'practiceDays') {
+          practiceDaysRef.sortOrder = sorter.order
+        }
+
+        if (sorter.columnKey == 'practiceDurationAvg') {
+          practiceDurationAvgRef.sortOrder = sorter.order
+        }
+
+        if (sorter.columnKey == 'evaluateFrequency') {
+          evaluateFrequencyRef.sortOrder = sorter.order
+        }
+
+        if (sorter.columnKey == 'publishCount') {
+          publishCountRef.sortOrder = sorter.order
+        }
+
+        if (sorter.columnKey == 'publishScore') {
+          publishScoreRef.sortOrder = sorter.order
+        }
+
+        if (sorter.columnKey == 'publishTime') {
+          publishTimeRef.sortOrder = sorter.order
+        }
+
+        state.searchForm.sort = sorter.order == 'ascend' ? 'asc' : 'desc'
+      }
+      getList2()
+    }
+
+    const getList2 = async () => {
       state.loading = true
       try {
-        const res = await getTrainingStatList({
+        const res = await api_practiceStatPage({
           page: 1,
           rows: 999,
+          ...state.searchForm,
           ...getTimes(
             currentTimer.value,
             ['startTime', 'endTime'],
@@ -96,6 +325,16 @@ export default defineComponent({
           )
         });
 
+        state.tableList = res.data.rows;
+      } catch (e) {
+        console.log(e);
+      }
+      state.loading = false
+    };
+
+    const getTestStatList = async () => {
+      state.loading = true
+      try {
         const res2 = await getTestStat({
           page: 1,
           rows: 999,
@@ -105,10 +344,7 @@ export default defineComponent({
             'YYYY-MM-DD'
           )
         });
-        state.tableList = res.data.rows;
 
-        payForm.practiceDurationAvg = res2.data.practiceDurationAvg;
-        payForm.practiceUserCount = res2.data.practiceUserCount;
         payForm.dateList = res2.data.trainingStatDetailList.map((item: any) => {
           return item.date;
         });
@@ -118,11 +354,43 @@ export default defineComponent({
         });
 
         setChart();
-      } catch (e) {
-        console.log(e);
+      } catch {
+        // 
       }
       state.loading = false
-    };
+    }
+
+    const getPracticePageStatList = async () => {
+      state.loading = true
+      try {
+        const {data} = await getPracticePageStat({
+          page: 1,
+          rows: 999,
+          ...getTimes(
+            currentTimer.value,
+            ['startTime', 'endTime'],
+            'YYYY-MM-DD'
+          )
+        });
+        payForm.practiceDuration = data.practiceDuration;
+        payForm.practiceDurationAvg = data.practiceDurationAvg;
+        payForm.practiceUserCount = data.practiceUserCount;
+        payForm.evaluateUserCount = data.evaluateUserCount
+        payForm.evaluateFrequency = data.evaluateFrequency
+        payForm.publishUserCount = data.publishUserCount
+        payForm.publishCount = data.publishCount
+      } catch {
+        // 
+      }
+      state.loading = false
+    }
+
+    const getList = async () => {
+      await getPracticePageStatList()
+      await getTestStatList()
+      await getList2()
+    }
+
     expose({ getList });
     const setChart = () => {
       setOptions({
@@ -176,33 +444,19 @@ export default defineComponent({
         },
         series: [
           {
-            // smooth: true,
             data: payForm.timeList,
-            symbolSize: 10,
-            type: 'line',
-            symbol: 'circle',
-            smooth: true,
-            // barWidth: '48px',
-            // label: {
-            //   // 柱图头部显示值
-            //   show: true,
-            //   position: 'top',
-            //   color: '#333',
-            //   fontSize: '12px',
-            //   fontWeight: 600
-            // },
+            type: 'bar',
+            barWidth: '48px',
 
             itemStyle: {
               normal: {
                 //这里设置柱形图圆角 [左上角,右上角,右下角,左下角]
                 barBorderRadius: [8, 8, 0, 0],
-                color: '#3583FA'
+                color: '#CDE5FF'
               },
               emphasis: {
+                focus: 'series',
                 color: '#3583FA' //hover时改变柱子颜色
-                // borderWidth: 4,
-                // borderColor: 'rgba(213, 233, 255,.4)',
-                // borderType: 'solid'
               }
             } as any
           }
@@ -223,14 +477,6 @@ export default defineComponent({
             return item;
           }
         }
-        // dataZoom: [
-        //   {
-        //     type: 'slider',
-        //     start: 5,
-        //     end: 100,
-        //     filterMode: 'empty'
-        //   }
-        // ]
       });
     };
 
@@ -256,12 +502,25 @@ export default defineComponent({
               </div>
               <div class={styles.TrainDataItem}>
                 <p class={styles.TrainDataItemTitle}>
-                  {getMinutes(payForm.practiceDurationAvg) > 0 ? (
+                  {getHours(payForm.practiceDurationAvg) > 0 ? (
+                      <div>
+                        <span>
+                          <NNumberAnimation
+                            from={0}
+                            to={getHours(
+                              payForm.practiceDurationAvg
+                            )}></NNumberAnimation>
+                        </span>
+                        时
+                      </div>
+                    ) : null}
+                    
+                  {getHours(payForm.practiceDurationAvg) > 0 || getLastMinutes(payForm.practiceDurationAvg) > 0 ? (
                     <div>
                       <span>
                         <NNumberAnimation
                           from={0}
-                          to={getMinutes(
+                          to={getLastMinutes(
                             payForm.practiceDurationAvg
                           )}></NNumberAnimation>
                       </span>
@@ -281,6 +540,78 @@ export default defineComponent({
                 </p>
                 <p class={styles.TrainDataItemsubTitle}>平均每天练习时长</p>
               </div>
+
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  {getHours(payForm.practiceDuration) > 0 ? (
+                      <div>
+                        <span>
+                          <NNumberAnimation
+                            from={0}
+                            to={getHours(
+                              payForm.practiceDuration
+                            )}></NNumberAnimation>
+                        </span>
+                        时
+                      </div>
+                    ) : null}
+                  {getHours(payForm.practiceDuration) > 0 || getLastMinutes(payForm.practiceDuration) > 0 ? (
+                    <div>
+                      <span>
+                        <NNumberAnimation
+                          from={0}
+                          to={getLastMinutes(
+                            payForm.practiceDuration
+                          )}></NNumberAnimation>
+                      </span>
+                      分
+                    </div>
+                  ) : null}
+                  <div>
+                    <span>
+                      <NNumberAnimation
+                        from={0}
+                        to={getSecend(
+                          payForm.practiceDuration
+                        )}></NNumberAnimation>
+                    </span>
+                    秒
+                  </div>
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>练习总时长</p>
+              </div>
+
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <div>
+                    <span>
+                      <NNumberAnimation
+                        from={0}
+                        to={payForm.evaluateUserCount}></NNumberAnimation>/
+                        <NNumberAnimation
+                        from={0}
+                        to={payForm.evaluateFrequency}></NNumberAnimation>
+                    </span>
+                  </div>
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>评测人数/次数</p>
+              </div>
+
+              <div class={styles.TrainDataItem}>
+                <p class={styles.TrainDataItemTitle}>
+                  <div>
+                    <span>
+                      <NNumberAnimation
+                        from={0}
+                        to={payForm.publishUserCount}></NNumberAnimation>/
+                        <NNumberAnimation
+                        from={0}
+                        to={payForm.publishCount}></NNumberAnimation>
+                    </span>
+                  </div>
+                </p>
+                <p class={styles.TrainDataItemsubTitle}>作品人数/数量</p>
+              </div>
             </div>
             <div class={styles.TrainDataTopRight}>
               {/* <div
@@ -306,7 +637,7 @@ export default defineComponent({
               ref={chartRef}
               style={{ height: payForm.height, width: payForm.width }}></div>
           </div>
-          <div class={styles.tableWrap}>
+          <div class={[styles.tableWrap, styles.noSort]}>
             <NDataTable
               v-slots={{
                 empty: () => <TheEmpty></TheEmpty>
@@ -314,6 +645,7 @@ export default defineComponent({
               class={styles.classTable}
               loading={state.loading}
               columns={columns()}
+              onUpdate:sorter={handleSorterChange}
               data={state.tableList}></NDataTable>
             {/* <Pagination
               v-model:page={state.pagination.page}

+ 4 - 14
src/views/home/components/practiceRanking.tsx

@@ -2,7 +2,7 @@ import { defineComponent, reactive, onMounted, computed, nextTick } from 'vue';
 import styles from '../index2.module.less';
 import { NDataTable, NTooltip } from 'naive-ui';
 import Pagination from '@/components/pagination';
-import { getMinutes, getSecend, getTimes } from '/src/utils/dateFormat';
+import { formateSeconds, getMinutes, getSecend, getTimes } from '/src/utils/dateFormat';
 import { getTestList } from '../../classList/api';
 import TheEmpty from '/src/components/TheEmpty';
 import iconSortDefault from '@/common/images/icon-sort-default.png';
@@ -196,12 +196,7 @@ export default defineComponent({
         return (
           <>
             {row.practiceDuration
-              ? getMinutes(row.practiceDuration) > 0
-                ? getMinutes(row.practiceDuration) +
-                  '分' +
-                  getSecend(row.practiceDuration) +
-                  '秒'
-                : getSecend(row.practiceDuration) + '秒'
+              ? formateSeconds(row.practiceDuration, 1)
               : 0}
           </>
         );
@@ -244,13 +239,8 @@ export default defineComponent({
       render(row: any) {
         return (
           <>
-            {row.practiceDurationAvg
-              ? getMinutes(row.practiceDurationAvg) > 0
-                ? getMinutes(row.practiceDurationAvg) +
-                  '分' +
-                  getSecend(row.practiceDurationAvg) +
-                  '秒'
-                : getSecend(row.practiceDurationAvg) + '秒'
+            {row.practiceDurationAvg ?
+               formateSeconds(row.practiceDuration, 1)
               : 0}
           </>
         );

+ 9 - 1
src/views/home/index2.module.less

@@ -905,6 +905,14 @@
   width: 514px;
 }
 
+.noSort {
+  :global {
+    .n-data-table-sorter {
+      display: none !important;
+    }
+  }
+}
+
 .cell {
   display: flex;
   align-items: center;
@@ -914,4 +922,4 @@
     width: 13px;
     height: 13px;
   }
-}
+}

+ 1 - 1
src/views/prepare-lessons/components/lesson-main/courseware-presets/index.tsx

@@ -789,7 +789,7 @@ export default defineComponent({
           v-model:show={forms.instrumentErrorVisiable}
           preset="card"
           class={['modalTitle', styles.removeVisiable1]}
-          title={'提示'}>
+          title={'温馨提示'}>
           <TheMessageDialog
             content={forms.instrumentErrorContent}
             contentDirection="left"

+ 59 - 16
src/views/prepare-lessons/components/lesson-main/courseware/addCourseware.tsx

@@ -149,7 +149,7 @@ export default defineComponent({
                 title: sub.bizInfo.name,
                 dataJson: sub.dataJson,
                 instrumentIds: sub.instrumentIds, // 素材编号
-                isError: checkCurrentIsInstrument(sub.instruments), // 是否异常 当前素材是否在选中的乐器里面
+                isError: checkCurrentIsInstrument(sub.instruments, sub.type), // 是否异常 当前素材是否在选中的乐器里面
                 // isCollect: !!sub.favoriteFlag,
                 isSelected: sub.source === 'PLATFORM' ? true : false,
                 audioPlayTypeArray: sub.audioPlayTypes
@@ -192,12 +192,17 @@ export default defineComponent({
     };
 
     /** 检测当前素材是否包含所选声部 */
-    const checkCurrentIsInstrument = (instruments: string) => {
+    const checkCurrentIsInstrument = (instruments: string, type: string) => {
       // 当前素材是否在选中的乐器里面
       let isError = false 
       if(forms.subjects.length <= 0) {
         return true
       }
+      // 过滤一些不做校验的素材
+      const checkType = ['IMG', 'VIDEO', 'SONG', 'MUSIC', 'PPT', 'LISTEN', 'MUSICIAN']
+      if(!checkType.includes(type)) {
+        return false
+      }
       forms.subjects.forEach((item: any) => {
         if(!instruments?.includes(item)) {
           isError = true
@@ -206,6 +211,15 @@ export default defineComponent({
       return isError
     }
 
+    /** 检测之后的提示 */
+    const checkCurrentInstrumentTip = (isError = false) => {
+      if(isError) {
+        message.error('您添加的资源与课件乐器不符')
+      } else {
+        message.success('添加成功');
+      }
+    }
+
     // 删除
     const onDelete = (j: number, index: number) => {
       const coursewareItem = forms.coursewareList[index];
@@ -397,7 +411,11 @@ export default defineComponent({
           materialList.forEach((m: any) => {
             forms.coursewareList[item.index || 0].list.push(m);
           });
-          message.success('添加成功');
+          // if(item.isError) {
+          //   message.error('您添加的资源与课件乐器不符')
+          // } else {
+          //   message.success('添加成功');
+          // }
         }
 
         timer = setTimeout(() => {
@@ -441,6 +459,10 @@ export default defineComponent({
         });
         forms.coursewareList[item.index || 0].list = array;
 
+        if(item.isError) {
+          message.error('您添加的资源与课件乐器不符')
+        }
+
         timer = setTimeout(() => {
           // 内容有更新 - 相关资源会刷新
           eventGlobal.emit('onCoursewareUpdate');
@@ -481,10 +503,10 @@ export default defineComponent({
           type: 'checkInstrument',
           loading: false,
           contentDirection: 'center',
-          title: '提示',
+          title: '温馨提示',
           content: '课件中含有不符合课件乐器的资源,是否继续保存?',
           cancelButtonText: '取消',
-          confirmButtonText: '保存',
+          confirmButtonText: '继续保存',
           index: 0
         };
       }
@@ -604,7 +626,7 @@ export default defineComponent({
         eventGlobal.emit('checkCoursewareForm', 'subject')
         return
       }
-      item.isError = checkCurrentIsInstrument(item.instrumentIds) // 是否异常
+      item.isError = checkCurrentIsInstrument(item.instrumentIds, item.type) // 是否异常
       if (forms.coursewareList.length <= 0) {
         // 添加到临时对象
         forms.addCoursewareItem = item;
@@ -624,6 +646,7 @@ export default defineComponent({
         forms.addCoursewareItem = item;
       } else {
         addCoursewareItem(item, point);
+        checkCurrentInstrumentTip(item.isError)
       }
     };
 
@@ -707,13 +730,21 @@ export default defineComponent({
     const onCourseWareSubjectChange = (subjects: any) => {
       forms.subjects = subjects
 
+      let isTips = false
       // 修改声部后重新检测状态
       forms.coursewareList.forEach((item: any) => {
         const childList = item.list || []
         childList.forEach((child: any) => {
-          child.isError = checkCurrentIsInstrument(child.instrumentIds)
+          child.isError = checkCurrentIsInstrument(child.instrumentIds, child.type)
+          if(child.isError) {
+            isTips = true
+          }
         })
       })
+
+      if(isTips) {
+        message.error('您添加的资源与课件乐器不符')
+      }
     }
 
     onMounted(async () => {
@@ -849,7 +880,7 @@ export default defineComponent({
                       if (dropItem.sourceForm === 'resource-item') {
                         if(forms.subjects.length <= 0) {
                           list.splice(evt.newDraggableIndex, 1)
-                          message.error('请先选择乐器')
+                          message.error('请先选择课件乐器')
                           eventGlobal.emit('checkCoursewareForm', 'subject')
                           return
                         }
@@ -863,7 +894,7 @@ export default defineComponent({
                             refFlag: dropItem.refFlag,
                             isCollect: dropItem.isCollect,
                             isSelected: dropItem.isSelected,
-                            isError: checkCurrentIsInstrument(dropItem.instrumentIds), // 是否异常
+                            isError: checkCurrentIsInstrument(dropItem.instrumentIds, dropItem.type), // 是否异常
                             content: dropItem.content,
                             audioPlayTypeArray: dropItem.audioPlayTypeArray,
                             removeFlag: false,
@@ -871,6 +902,7 @@ export default defineComponent({
                           },
                           evt.newDraggableIndex
                         );
+                        // checkCurrentInstrumentTip(checkCurrentIsInstrument(dropItem.instrumentIds, dropItem.type))
                       }
                     }}
                     onDrag={(event: any) => {
@@ -951,6 +983,11 @@ export default defineComponent({
                                 'handle'
                               ]}
                               onClick={() => {
+                                if(forms.subjects.length <= 0) {
+                                  message.error('请先选择课件乐器')
+                                  eventGlobal.emit('checkCoursewareForm', 'subject')
+                                  return
+                                }
                                 forms.addOtherSource = true;
                                 forms.addOtherIndex = index;
                               }}>
@@ -1015,14 +1052,17 @@ export default defineComponent({
             onClose={() => (forms.addCoursewareVisiable = false)}
             onConfirm={(selects: number[]) => {
               if (Array.isArray(selects) && selects.length > 0) {
+                
                 selects.forEach(select => {
                   addCoursewareItem({
                     ...forms.addCoursewareItem,
                     index: select
                   });
                 });
-
+                console.log(forms.addCoursewareItem, '----', forms.subjects)
                 forms.addCoursewareVisiable = false;
+               
+                checkCurrentInstrumentTip(forms.addCoursewareItem.isError)
               } else {
                 message.error('请选择需要添加的知识点');
               }
@@ -1078,20 +1118,23 @@ export default defineComponent({
             onClose={() => (forms.addOtherSource = false)}
             onComfirm={item => {
               if (Array.isArray(item)) {
-                console.log(item, 'item - item');
+                let isTips = false
                 item.forEach(async (child: any) => {
-                  await addCoursewareItem(
-                    { ...child, index: forms.addOtherIndex },
-                    null,
-                    true
-                  );
+                  child.isError = checkCurrentIsInstrument(child.instrumentIds, child.type)
+                  forms.coursewareList[forms.addOtherIndex || 0].list.push(child);
+                  if(child.isError) {
+                    isTips = true
+                  }
                 });
+                checkCurrentInstrumentTip(isTips)
               } else {
+                item.isError = checkCurrentIsInstrument(item.instrumentIds, item.type)
                 addCoursewareItem(
                   { ...item, index: forms.addOtherIndex },
                   null,
                   true
                 );
+                checkCurrentInstrumentTip(item.isError)
               }
             }}
           />

+ 2 - 2
src/views/prepare-lessons/components/lesson-main/train/assign-homework.tsx

@@ -584,11 +584,11 @@ export default defineComponent({
           style={
             props.from === 'class'
               ? {
-                  width: '640px',
+                  width: '780px',
                   ...assignHomeworkStuBoxDragData.styleDrag.value
                 }
               : {
-                  width: '640px'
+                  width: '780px'
                 }
           }
           preset="card"

+ 2 - 2
src/views/prepare-lessons/components/lesson-main/train/assign-student/index.tsx

@@ -224,7 +224,7 @@ export default defineComponent({
           <div class={styles.searchSection}>
             <div class={styles.searchSpace}>
               <NSelect
-                placeholder="全部班级"
+                placeholder="全部年级班级"
                 disabled={props.classGroupId ? true : false}
                 labelField="defaultLabel"
                 filterable
@@ -232,7 +232,7 @@ export default defineComponent({
                 v-model:value={state.searchFrom.classGroupId}
                 onUpdate:value={() => onSearch()}
                 options={
-                  [{ defaultLabel: '全部班级', value: '' }, ...props.classList] as any
+                  [{ defaultLabel: '全部年级班级', value: '' }, ...props.classList] as any
                 }
               />
               <NCascader

+ 2 - 0
src/views/prepare-lessons/model/add-other-source/index.tsx

@@ -320,6 +320,7 @@ export default defineComponent({
                 value.forEach((item: any) => {
                   temp.push({
                     materialId: item.materialId,
+                    instrumentIds: item.instrumentIds,
                     coverImg: item.coverImg,
                     dataJson: null,
                     title: item.title,
@@ -364,6 +365,7 @@ export default defineComponent({
                 const temp: any[] = [];
                 value.forEach((item: any) => {
                   temp.push({
+                    instrumentIds: item.instrumentIds,
                     materialId: item.id,
                     coverImg: item.coverImg,
                     dataJson: null,

+ 1 - 0
src/views/prepare-lessons/model/subject-sync/index.tsx

@@ -72,6 +72,7 @@ export default defineComponent({
           const tempCode = item.code ? item.code.split(',')[0] : '';
           subjectCode.push({
             materialId: item.id,
+            instrumentIds: item.id,
             coverImg: subjectImgs[tempCode] || subjectImgs.Panpipes,
             dataJson: null,
             title: item.name,

+ 9 - 0
src/views/studentList/api.ts

@@ -75,3 +75,12 @@ export const api_getCurrentGradeYear = (params: object) => {
     data: params
   });
 };
+
+/**
+ * 评测记录分页统计
+ */
+export const api_musicPracticeRecordPageStat = (params: object) => {
+  return request.post('/edu-app/musicPracticeRecord/pageStat', {
+    data: params
+  })
+}

+ 386 - 73
src/views/studentList/components/evaluationRecords.tsx

@@ -2,18 +2,22 @@ import {  defineComponent, onMounted, reactive, ref } from 'vue';
 import styles from '../index.module.less';
 import {
   NButton,
+  NCascader,
   NDataTable,
   NForm,
   NFormItem,
   NInput,
   NInputNumber,
   NModal,
+  NNumberAnimation,
   NSpace,
-  NTag
+  NTag,
+  NTooltip,
+  useMessage
 } from 'naive-ui';
 // import { useECharts } from '@/hooks/web/useECharts';
 import Pagination from '/src/components/pagination';
-import { getPracticeRecordList } from '../api';
+import { api_musicPracticeRecordPageStat, getPracticeRecordList } from '../api';
 import {
   getNowDateAndMonday,
   getNowDateAndSunday,
@@ -24,11 +28,18 @@ import CDatePicker from '/src/components/CDatePicker';
 import { useUserStore } from '/src/store/modules/users';
 import TheEmpty from '/src/components/TheEmpty';
 import { initCache, setCache } from '/src/hooks/use-async';
-import { iframeDislableKeyboard } from '/src/utils';
+import { checkUrlType, iframeDislableKeyboard } from '/src/utils';
 import { modalClickMask } from '/src/state';
 // import SearchInput from '/src/components/searchInput';
 import CSelect from '/src/components/CSelect';
 import { evaluateDifficultArray } from '/src/utils/searchArray';
+import { useCatchStore } from '/src/store/modules/catchData';
+import iconSortDefault from '@/common/images/icon-sort-default.png';
+import iconSortDesc from '@/common/images/icon-sort-desc.png';
+import iconSortAsc from '@/common/images/icon-sort-asc.png';
+import TheTooltip from '/src/components/TheTooltip';
+import CardPreview from '/src/components/card-preview';
+import { saveAs } from 'file-saver';
 export default defineComponent({
   name: 'student-practiceData',
   props: {
@@ -43,7 +54,9 @@ export default defineComponent({
   },
   setup(props) {
     const userStore = useUserStore();
-    const chartRef = ref<HTMLDivElement | null>(null);
+    const catchData = useCatchStore()
+    const message = useMessage()
+    // const chartRef = ref<HTMLDivElement | null>(null);
     // const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
     // const practiceFlag = ref(true);
     const payForm = reactive({
@@ -64,14 +77,18 @@ export default defineComponent({
         rows: 10,
         pageTotal: 4
       },
+      asc: null as any,
+      sortType: null as any,
       searchForm: {
         musicSheetName: '',
+        instrumentId: null,
         heardLevel: null, //
         userMusicFlag: null, // 是否生成作品
         minScore: null,
         maxScore: null,
         musicStartTime: []
       },
+      stat: {} as any,
       tableList: [] as any,
       goCourseVisiable: false
     });
@@ -79,17 +96,134 @@ export default defineComponent({
       getNowDateAndMonday(new Date().getTime()),
       getNowDateAndSunday(new Date().getTime())
     ]);
+    const previewShow = ref(false);
+    const previewItem = ref({
+      type: '',
+      content: '',
+      title: ''
+    });
+
+
+    const toolTitleTips = (title: string, item: any) => {
+      return <NTooltip showArrow={false} placement="top-start">
+      {{
+        trigger: () => (
+          <div class={styles.cell}>
+            {title}
+            <img
+              class={styles.sortIcon}
+              src={
+                item.sortOrder === 'descend'
+                  ? iconSortDesc
+                  : item.sortOrder === 'ascend'
+                  ? iconSortAsc
+                  : iconSortDefault
+              }
+            />
+          </div>
+        ),
+        default:
+          item.sortOrder === 'descend'
+            ? '点击升序'
+            : item.sortOrder === 'ascend'
+            ? '取消排序'
+            : '点击降序'
+      }}
+    </NTooltip>
+    }
+
+    const createTimeRef = reactive({
+      title() {
+        return (
+          toolTitleTips('评测时间', createTimeRef)
+        );
+      },
+      key: 'createTime',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const evaluationProgressRef = reactive({
+      title() {
+        return (
+          toolTitleTips('评测进度', evaluationProgressRef)
+        );
+      },
+      key: 'evaluationProgress',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const scoreRef = reactive({
+      title() {
+        return (
+          toolTitleTips('评测分数', scoreRef)
+        );
+      },
+      key: 'score',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const intonationRef = reactive({
+      title() {
+        return (
+          toolTitleTips('音准', intonationRef)
+        );
+      },
+      key: 'intonation',
+      sorter: true,
+      sortOrder: false as any,
+      render(row: any) {
+        return row.rhythmFlag ? '--' : row.intonation
+      }
+    });
+
+    const cadenceRef = reactive({
+      title() {
+        return (
+          toolTitleTips('节奏', cadenceRef)
+        );
+      },
+      key: 'cadence',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const integrityRef = reactive({
+      title() {
+        return (
+          toolTitleTips('完整度', integrityRef)
+        );
+      },
+      key: 'integrity',
+      sorter: true,
+      sortOrder: false as any
+    });
+
+    const userMusicTimeRef = reactive({
+      title() {
+        return (
+          toolTitleTips('发布时间', userMusicTimeRef)
+        );
+      },
+      key: 'userMusicTime',
+      sorter: true,
+      sortOrder: false as any
+    });
+
     const columns = () => {
       return [
-        {
-          title: '时间',
-          key: 'createTime'
-        },
+        createTimeRef,
         {
           title: '评测曲目',
           key: 'musicSheetName',
           render(row: any) {
-            return <span>{row.musicSheetName}</span>;
+            return <TheTooltip
+            maxWidth={200}
+            showContentWidth={300}
+            content={row.musicSheetName}
+          />;
           }
         },
         // 入门:BEGINNER/进阶:ADVANCED/大师:PERFORMER"
@@ -114,83 +248,168 @@ export default defineComponent({
           }
         },
         {
-          title: '评测分数',
-          key: 'score',
-          render(row: any) {
-            return <span>{row.score}</span>;
-          }
-        },
-        {
-          title: '音准',
-          key: 'intonation',
-          render(row: any) {
-            return <span>{row.rhythmFlag ? '--' : row.intonation}</span>;
-          }
-        },
-        {
-          title: '节奏',
-          key: 'cadence',
-          render(row: any) {
-            return <span>{row.rhythmFlag ? '--' : row.cadence}</span>;
-          }
-        },
-        {
-          title: '完整度',
-          key: 'integrity',
-          render(row: any) {
-            return <span>{row.integrity}</span>;
-          }
+          title: '乐器',
+          key: 'instrumentName'
         },
+        evaluationProgressRef,
+        scoreRef,
+        intonationRef,
+        cadenceRef,
+        integrityRef,
         {
-          title: '生成作品',
+          title: '发布作品',
           key: 'integrity',
           render(row: any) {
             return <span>{row.userMusicFlag ? '是' : '否'}</span>;
           }
         },
-        {
-          title: '生成时间',
-          key: 'userMusicTime',
-          render(row: any) {
-            return <span>{row.userMusicTime || '--'}</span>;
-          }
-        },
+        userMusicTimeRef,
         {
           title: '操作',
           key: 'id',
           render(row: any) {
             return (
-              <NButton
-                text
-                type="primary"
-                onClick={() => {
-                  gotoRecode(row);
-                }}>
-                评测报告
-              </NButton>
+              <NSpace>
+                <NButton
+                  text
+                  type="primary"
+                  onClick={() => {
+                    gotoRecode(row);
+                  }}>
+                  评测报告
+                </NButton>
+                {row.videoFilePath || row.recordFilePath ? <><NButton
+                  text
+                  type="primary"
+                  onClick={() => {
+                    gotoPreview(row);
+                  }}>
+                  预览作品
+                </NButton>
+                <NButton
+                  text
+                  type="primary"
+                  onClick={() => {
+                    gotoDownload(row);
+                  }}>
+                  下载作品
+                </NButton></> : ''}
+                
+              </NSpace>
             );
           }
         }
       ];
     };
+
+    // 统计排序
+    const handleSorterChange = (sroter: any) => {
+      if (!sroter.order) {
+        state.asc = null
+        state.sortType = null
+        evaluationProgressRef.sortOrder = false
+        scoreRef.sortOrder = false
+        createTimeRef.sortOrder = false
+        intonationRef.sortOrder = false
+        cadenceRef.sortOrder = false
+        integrityRef.sortOrder = false
+        userMusicTimeRef.sortOrder = false
+      } else {
+        // 1:综合得分,2:音准,3:节奏:4:完整度,5:评测时间  6:生成时间,7:评测进度
+        const template = {
+          score: 1,
+          intonation: 2,
+          cadence: 3,
+          integrity: 4,
+          createTime: 5,
+          userMusicTime: 6,
+          evaluationProgress: 7
+        } as any
+
+        state.sortType = template[sroter.columnKey]
+
+        evaluationProgressRef.sortOrder = false
+        scoreRef.sortOrder = false
+        createTimeRef.sortOrder = false
+        intonationRef.sortOrder = false
+        cadenceRef.sortOrder = false
+        integrityRef.sortOrder = false
+        userMusicTimeRef.sortOrder = false
+
+        if (sroter.columnKey == 'score') {
+          scoreRef.sortOrder = sroter.order
+        }
+        if (sroter.columnKey == 'createTime') {
+          createTimeRef.sortOrder = sroter.order
+        }
+        if (sroter.columnKey == 'intonation') {
+          intonationRef.sortOrder = sroter.order
+        }
+        if (sroter.columnKey == 'cadence') {
+          cadenceRef.sortOrder = sroter.order
+        }
+        if (sroter.columnKey == 'userMusicTime') {
+          userMusicTimeRef.sortOrder = sroter.order
+        }
+        if (sroter.columnKey == 'evaluationProgress') {
+          evaluationProgressRef.sortOrder = sroter.order
+        }
+
+        state.asc = sroter.order == 'ascend' ? true : false
+      }
+      getList()
+    }
+
     const getList = async () => {
-      const { musicStartTime, ...temp } = state.searchForm;
-      const res = await getPracticeRecordList({
-        userId: props.studentId,
-        ...state.pagination,
-        ...temp,
-        ...getTimes(
-          musicStartTime,
-          ['userMusicStartTime', 'userMusicEndTime'],
-          'YYYY-MM-DD'
-        ),
-        classGroupId: props.classGroupId,
-        feature: 'EVALUATION',
-        ...getTimes(timer.value, ['startTime', 'endTime'], 'YYYY-MM-DD')
-      });
-      state.tableList = res.data.rows;
-      state.pagination.pageTotal = res.data.total;
+      state.loading = true;
+      try {
+        const { musicStartTime, ...temp } = state.searchForm;
+        const res = await getPracticeRecordList({
+          userId: props.studentId,
+          ...state.pagination,
+          sortType: state.sortType,
+          asc: state.asc,
+          ...temp,
+
+          ...getTimes(
+            musicStartTime,
+            ['startTime', 'endTime'],
+            'YYYY-MM-DD'
+          ),
+          classGroupId: props.classGroupId,
+          ...getTimes(timer.value, ['startTime', 'endTime'], 'YYYY-MM-DD')
+        });
+        state.tableList = res.data.rows;
+        state.pagination.pageTotal = res.data.total;
+      } catch {
+
+      }
+      state.loading = false
     };
+
+    const getTrainingStat = async () => {
+      state.loading = true;
+      try {
+        const { musicStartTime, ...more } = state.searchForm
+        const { data } = await api_musicPracticeRecordPageStat({
+          userId: props.studentId,
+          page: state.pagination.page,
+          rows: state.pagination.rows,
+          sortType: state.sortType,
+          asc: state.asc,
+          ...more,
+          ...getTimes(timer, ['startTime', 'endTime'])
+        })
+
+        state.stat = {
+          evaluateFrequency: data.evaluateFrequency || 0,
+          publishCount: data.publishCount || 0
+        }
+      } catch {}
+      state.loading = false
+    }
+
+
     const gotoRecode = (row: any) => {
       const token = userStore.getToken;
       reportSrc.value =
@@ -200,8 +419,43 @@ export default defineComponent({
         }&platform=webTeacher&Authorization=${token}`;
       payForm.detailVisiable = true;
     };
+    const gotoPreview = (row: any) => {
+      let lookTitle = '';
+      if (row.videoFilePath) {
+        lookTitle = checkUrlType(row.videoFilePath);
+      } else {
+        lookTitle = checkUrlType(row.recordFilePath);
+      }
+      const lookUrl = row.videoFilePath || row.recordFilePath;
+      previewItem.value.content = lookUrl;
+      previewItem.value.title = row.musicSheetName;
+      if (lookTitle === 'video') {
+        previewItem.value.type = 'VIDEO';
+      } else if (lookTitle === 'audio') {
+        previewItem.value.type = 'SONG';
+      }
+      previewShow.value = true;
+    }
+    const gotoDownload = (row: any) => {
+      // 下载资源
+      const fileUrl = row.videoFilePath || row.recordFilePath;
+      const filename =
+        row.musicSheetName + '-' + row.userId;
+      const sfixx = fileUrl.substring(fileUrl.lastIndexOf('.'));
+      // 发起Fetch请求
+      fetch(fileUrl)
+        .then(response => response.blob())
+        .then(blob => {
+          saveAs(blob, (filename || new Date().getTime()) + sfixx);
+        })
+        .catch(() => {
+          message.error('下载失败');
+        });
+    
+    }
     const search = () => {
       state.pagination.page = 1;
+      getTrainingStat()
       getList();
       setCache({
         current: { timer: timer.value },
@@ -219,7 +473,8 @@ export default defineComponent({
         userMusicFlag: null, // 是否生成作品
         minScore: null,
         maxScore: null,
-        musicStartTime: []
+        musicStartTime: [],
+        instrumentId: null
       };
       search();
       setCache({
@@ -235,8 +490,11 @@ export default defineComponent({
       }
     });
     const iframeRef = ref();
-    onMounted(() => {
-      getList();
+    onMounted(async () => {
+      state.loading = true
+      await catchData.getSubjects();
+      await getTrainingStat()
+      await getList();
     });
     return () => (
       <>
@@ -299,6 +557,25 @@ export default defineComponent({
           </NFormItem>
 
           <NFormItem>
+            <NCascader
+              to="body"
+              placeholder="选择乐器"
+              options={[
+                { value: '', label: '全部乐器' },
+                ...catchData.getSubjectList
+              ]}
+              childrenField="instruments"
+              checkStrategy="child"
+              expandTrigger="hover"
+              showPath={false}
+              clearable
+              v-model:value={state.searchForm.instrumentId}
+              onUpdate:value={(val: any, option: any, pathValues: any) => {
+                console.log(val, option, pathValues);
+              }}
+            />
+          </NFormItem>
+          <NFormItem>
             <CSelect
               {...({
                 options: [
@@ -312,7 +589,7 @@ export default defineComponent({
               v-model:value={state.searchForm.userMusicFlag}></CSelect>
           </NFormItem>
 
-          <NFormItem>
+          {/* <NFormItem>
             <CDatePicker
               v-model:value={state.searchForm.musicStartTime}
               separator={'至'}
@@ -324,7 +601,7 @@ export default defineComponent({
                 clearable: true
               } as any)}
               timerValue={state.searchForm.musicStartTime}></CDatePicker>
-          </NFormItem>
+          </NFormItem> */}
 
           <NFormItem>
             <NSpace justify="end">
@@ -337,7 +614,35 @@ export default defineComponent({
             </NSpace>
           </NFormItem>
         </NForm>
-        <div class={styles.tableWrap}>
+        <div class={[styles.TrainDataTop, styles.TrainDataTopEvaluation]}>
+          <div class={styles.TrainDataTopLeft}>
+            <div class={styles.TrainDataItem}>
+              <p class={styles.TrainDataItemTitle}>
+                <div>
+                  <span>
+                    <NNumberAnimation
+                      from={0}
+                      to={state.stat.evaluateFrequency || 0}></NNumberAnimation>
+                  </span>
+                </div>
+              </p>
+              <p class={styles.TrainDataItemsubTitle}>评测次数</p>
+            </div>
+            <div class={styles.TrainDataItem}>
+              <p class={styles.TrainDataItemTitle}>
+                <div>
+                  <span>
+                    <NNumberAnimation
+                      from={0}
+                      to={state.stat.publishCount || 0}></NNumberAnimation>
+                  </span>
+                </div>
+              </p>
+              <p class={styles.TrainDataItemsubTitle}>作品数量</p>
+            </div>
+          </div>
+        </div>
+        <div class={[styles.tableWrap, styles.noSort]}>
           <NDataTable
             v-slots={{
               empty: () => <TheEmpty></TheEmpty>
@@ -345,6 +650,7 @@ export default defineComponent({
             class={styles.classTable}
             loading={state.loading}
             columns={columns()}
+            onUpdate:sorter={handleSorterChange}
             data={state.tableList}></NDataTable>
           <Pagination
             v-model:page={state.pagination.page}
@@ -352,6 +658,7 @@ export default defineComponent({
             v-model:pageTotal={state.pagination.pageTotal}
             onList={getList}
             sync
+            saveKey='studentDetail-evaluationRecords'
           />
         </div>
         <NModal
@@ -371,6 +678,12 @@ export default defineComponent({
               src={reportSrc.value}></iframe>
           </div>
         </NModal>
+
+        <CardPreview
+          v-model:show={previewShow.value}
+          item={previewItem.value}
+          isDownload={false}
+        />
       </>
     );
   }

+ 73 - 48
src/views/studentList/components/practiceData.tsx

@@ -18,7 +18,10 @@ import {
   getNowDateAndSunday,
   getTimes,
   getMinutes,
-  getSecend
+  getSecend,
+  getHours,
+  getLastMinutes,
+  formateSeconds
 } from '/src/utils/dateFormat';
 import CDatePicker from '/src/components/CDatePicker';
 import TheEmpty from '/src/components/TheEmpty';
@@ -51,6 +54,7 @@ export default defineComponent({
 
     const state = reactive({
       loading: false,
+      dayFlag: true,
       pagination: {
         page: 1,
         rows: 10,
@@ -78,12 +82,7 @@ export default defineComponent({
                 {' '}
                 <>
                   {row.practiceDuration
-                    ? getMinutes(row.practiceDuration) > 0
-                      ? getMinutes(row.practiceDuration) +
-                        '分' +
-                        getSecend(row.practiceDuration) +
-                        '秒'
-                      : getSecend(row.practiceDuration) + '秒'
+                    ? formateSeconds(row.practiceDuration, 1)
                     : 0 + '分钟'}
                 </>
               </>
@@ -97,6 +96,7 @@ export default defineComponent({
         const res = await getTrainingStatList({
           page: 1,
           rows: 999,
+          dayFlag: state.dayFlag,
           studentId: props.studentId,
           classGroupId: props.classGroupId,
           ...getTimes(timer.value, ['startTime', 'endTime'], 'YYYY-MM-DD')
@@ -163,18 +163,6 @@ export default defineComponent({
             type: 'bar',
             barWidth: '48px',
             stack: 'total',
-            // label: {
-            //   // 柱图头部显示值
-            //   formatter: (value: any) => {
-            //     console.log(value);
-            //     return getMinutes(value.value);
-            //   },
-            //   show: true,
-            //   position: 'top',
-            //   color: '#333',
-            //   fontSize: '12px',
-            //   fontWeight: 600
-            // },
 
             itemStyle: {
               normal: {
@@ -198,11 +186,7 @@ export default defineComponent({
             return [
               item[0].axisValueLabel,
               ...item.map((d: any) => {
-                let str;
-                getMinutes(d.value) > 0
-                  ? (str =
-                      getMinutes(d.value) + '分' + getSecend(d.value) + '秒')
-                  : (str = getSecend(d.value) + '秒');
+                let str = formateSeconds(d.value, 1)
                 return `<br/>${d.marker}<span style="margin-top:10px;margin-left:5px;font-size: 13px;font-weight: 500;
                   color: #131415;font-weight: 600;
                   margin-top:12px
@@ -228,6 +212,7 @@ export default defineComponent({
       try {
         const res = await getTrainingStat({
           studentId: props.studentId,
+          dayFlag: state.dayFlag,
           classGroupId: props.classGroupId,
           ...getTimes(timer.value, ['startTime', 'endTime'], 'YYYY-MM-DD')
         });
@@ -282,37 +267,63 @@ export default defineComponent({
     });
     return () => (
       <>
-        <NForm label-placement="left" inline>
-          <NFormItem>
-            <CDatePicker
-              v-model:value={timer.value}
-              separator={'至'}
-              type="daterange"
-              timerValue={timer.value}></CDatePicker>
-          </NFormItem>
+        <NSpace justify="space-between">
+          <NForm label-placement="left" inline>
+            <NFormItem>
+              <CDatePicker
+                v-model:value={timer.value}
+                separator={'至'}
+                type="daterange"
+                timerValue={timer.value}></CDatePicker>
+            </NFormItem>
 
-          <NFormItem>
-            <NSpace justify="end">
-              <NButton type="primary" class="searchBtn" onClick={search}>
-                搜索
-              </NButton>
-              <NButton type="primary" ghost class="resetBtn" onClick={onReset}>
-                重置
-              </NButton>
-            </NSpace>
-          </NFormItem>
-        </NForm>
-        <div class={styles.homeTrainData}>
+            <NFormItem>
+              <NSpace justify="end">
+                <NButton type="primary" class="searchBtn" onClick={search}>
+                  搜索
+                </NButton>
+                <NButton type="primary" ghost class="resetBtn" onClick={onReset}>
+                  重置
+                </NButton>
+              </NSpace>
+            </NFormItem>
+          </NForm>
+          <NSpace>
+            <NButton type="primary" class={state.dayFlag ? "searchDate" : "searchDateDefault"} onClick={() =>{
+              state.dayFlag = true;
+              search()
+            }}>按天</NButton>
+            <NButton type="primary" class={state.dayFlag ? "searchDateDefault" : "searchDate"} onClick={() =>{
+              state.dayFlag = false;
+              search()
+            }}>按月</NButton>
+          </NSpace>
+        </NSpace>
+        <div class={[styles.homeTrainData, styles.homeTrainDataPractice]}>
           <div class={styles.TrainDataTop}>
             <div class={styles.TrainDataTopLeft}>
               <div class={styles.TrainDataItem}>
                 <p class={styles.TrainDataItemTitle}>
-                  {getMinutes(payForm.practiceDurationTotal) > 0 ? (
+                {getHours(payForm.practiceDurationTotal) > 0 ? (
                     <div>
                       <span>
                         <NNumberAnimation
                           from={0}
-                          to={getMinutes(
+                          to={getHours(
+                            payForm.practiceDurationTotal
+                          )}></NNumberAnimation>
+                      </span>
+                      <i style={{ width: '4px', display: 'inline-block' }}></i>
+                      时
+                      <i style={{ width: '4px', display: 'inline-block' }}></i>
+                    </div>
+                  ) : null}
+                  {getHours(payForm.practiceDurationAvg) > 0 || getLastMinutes(payForm.practiceDurationTotal) > 0 ? (
+                    <div>
+                      <span>
+                        <NNumberAnimation
+                          from={0}
+                          to={getLastMinutes(
                             payForm.practiceDurationTotal
                           )}></NNumberAnimation>
                       </span>
@@ -336,12 +347,26 @@ export default defineComponent({
               </div>
               <div class={styles.TrainDataItem}>
                 <p class={styles.TrainDataItemTitle}>
-                  {getMinutes(payForm.practiceDurationAvg) > 0 ? (
+                {getHours(payForm.practiceDurationAvg) > 0 ? (
+                    <div>
+                      <span>
+                        <NNumberAnimation
+                          from={0}
+                          to={getHours(
+                            payForm.practiceDurationAvg
+                          )}></NNumberAnimation>
+                      </span>
+                      <i style={{ width: '4px', display: 'inline-block' }}></i>
+                      时
+                      <i style={{ width: '4px', display: 'inline-block' }}></i>
+                    </div>
+                  ) : null}
+                  {getHours(payForm.practiceDurationAvg) > 0 || getLastMinutes(payForm.practiceDurationAvg) > 0 ? (
                     <div>
                       <span>
                         <NNumberAnimation
                           from={0}
-                          to={getMinutes(
+                          to={getLastMinutes(
                             payForm.practiceDurationAvg
                           )}></NNumberAnimation>
                       </span>

+ 90 - 64
src/views/studentList/index.module.less

@@ -164,89 +164,96 @@
 .homeTrainData {
   margin-top: 40px;
 
-  .TrainDataTop {
-    margin-bottom: 40px;
+ &.homeTrainDataPractice {
+  margin-top: 20px;
+ } 
+}
+
+.TrainDataTop {
+  margin-bottom: 40px;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  &.TrainDataTopEvaluation {
+    margin-bottom: 24px;
+  }
+
+  .TrainDataTopLeft {
     display: flex;
     flex-direction: row;
     align-items: center;
-    justify-content: space-between;
 
-    .TrainDataTopLeft {
-      display: flex;
-      flex-direction: row;
-      align-items: center;
-
-      .TrainDataItem {
-        margin-right: 40px;
+    .TrainDataItem {
+      margin-right: 40px;
 
-        .TrainDataItemTitle {
-          display: flex;
-          flex-direction: row;
-          align-items: center;
-          text-align: center;
-          font-size: max(13px, 11Px);
-          font-weight: 400;
+      .TrainDataItemTitle {
+        display: flex;
+        flex-direction: row;
+        align-items: center;
+        text-align: center;
+        font-size: max(13px, 11Px);
+        font-weight: 400;
+        color: #131415;
+        line-height: 18px;
+
+        span {
+          font-family: 'DINA';
+          font-size: max(26px, 18Px);
+          font-weight: 600;
           color: #131415;
-          line-height: 18px;
-
-          span {
-            font-family: 'DINA';
-            font-size: max(26px, 18Px);
-            font-weight: 600;
-            color: #131415;
-            line-height: 28px;
-          }
+          line-height: 28px;
         }
+      }
 
-        .TrainDataItemsubTitle {
-          margin-top: 4px;
-          text-align: center;
-          font-size: max(14px, 11Px);
-          font-family: PingFangSC-Regular, PingFang SC;
-          font-weight: 400;
-          color: #777777;
-          line-height: 18px;
-        }
+      .TrainDataItemsubTitle {
+        margin-top: 4px;
+        text-align: center;
+        font-size: max(14px, 11Px);
+        font-family: PingFangSC-Regular, PingFang SC;
+        font-weight: 400;
+        color: #777777;
+        line-height: 18px;
       }
     }
+  }
 
-    .TrainDataTopRight {
+  .TrainDataTopRight {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+
+    .DataTopRightItem {
+      cursor: pointer;
       display: flex;
       flex-direction: row;
       align-items: center;
+      margin-left: 30px;
 
-      .DataTopRightItem {
-        cursor: pointer;
-        display: flex;
-        flex-direction: row;
-        align-items: center;
-        margin-left: 30px;
-
-        &:hover {
-          opacity: 0.8;
-        }
+      &:hover {
+        opacity: 0.8;
+      }
 
-        .DataTopRightDot {
-          width: 16px;
-          height: 16px;
-          background: #3583fa;
-          border-radius: 4px;
-          margin-right: 6px;
-        }
+      .DataTopRightDot {
+        width: 16px;
+        height: 16px;
+        background: #3583fa;
+        border-radius: 4px;
+        margin-right: 6px;
+      }
 
-        .DataTopRightDot.DataTopRightDotBlue {
-          background: #d5e9ff;
-        }
+      .DataTopRightDot.DataTopRightDotBlue {
+        background: #d5e9ff;
+      }
 
-        .DataTopRightDot.red {
-          background: #ff7aa7;
-        }
+      .DataTopRightDot.red {
+        background: #ff7aa7;
       }
+    }
 
-      .DataTopRightItem.DataTopRightItemDis {
-        .DataTopRightDot {
-          background: #f5f6fa;
-        }
+    .DataTopRightItem.DataTopRightItemDis {
+      .DataTopRightDot {
+        background: #f5f6fa;
       }
     }
   }
@@ -640,4 +647,23 @@
 
 .updateStudent {
   width: 580px;
-}
+}
+
+.noSort {
+  :global {
+    .n-data-table-sorter {
+      display: none !important;
+    }
+  }
+}
+
+.cell {
+  display: flex;
+  align-items: center;
+
+  .sortIcon {
+    margin-left: 7px;
+    width: 13px;
+    height: 13px;
+  }
+}