Ver código fonte

添加功能

lex 1 ano atrás
pai
commit
9114d219a7
42 arquivos alterados com 2836 adições e 295 exclusões
  1. 22 0
      package-lock.json
  2. 2 0
      package.json
  3. 20 0
      src/api/user.ts
  4. 1 0
      src/components/card-preview/index.tsx
  5. 10 8
      src/components/card-type/index.module.less
  6. 29 10
      src/components/card-type/index.tsx
  7. 10 0
      src/components/upload-file/api.ts
  8. 175 0
      src/components/upload-file/copper.tsx
  9. 366 0
      src/components/upload-file/index.tsx
  10. 16 1
      src/enums/pageEnum.ts
  11. 0 81
      src/helpers/request.ts
  12. 66 47
      src/store/modules/catchData.ts
  13. 20 0
      src/styles/index.less
  14. 21 6
      src/utils/index.ts
  15. 1 1
      src/views/login/components/pwdLogin.tsx
  16. 43 0
      src/views/natural-resources/api.ts
  17. 8 0
      src/views/natural-resources/components/my-collect/index.module.less
  18. 39 8
      src/views/natural-resources/components/my-collect/index.tsx
  19. 45 21
      src/views/natural-resources/components/my-collect/search-group-resources.tsx
  20. 41 0
      src/views/natural-resources/components/my-resources /index.module.less
  21. 161 19
      src/views/natural-resources/components/my-resources /index.tsx
  22. 30 23
      src/views/natural-resources/components/my-resources /search-group-resources.tsx
  23. 154 0
      src/views/natural-resources/components/my-resources /upload-modal/index.module.less
  24. 328 3
      src/views/natural-resources/components/my-resources /upload-modal/index.tsx
  25. 419 0
      src/views/natural-resources/components/my-resources /upload-modal/upload-file.tsx
  26. 8 0
      src/views/natural-resources/components/share-resources/index.module.less
  27. 42 24
      src/views/natural-resources/components/share-resources/index.tsx
  28. 47 39
      src/views/natural-resources/components/share-resources/search-group-resources.tsx
  29. 21 0
      src/views/natural-resources/images/btn-add.svg
  30. 27 0
      src/views/natural-resources/images/btn-delete.svg
  31. 18 0
      src/views/natural-resources/images/btn-down.svg
  32. 23 0
      src/views/natural-resources/images/btn-remove.svg
  33. 18 0
      src/views/natural-resources/images/btn-up.svg
  34. BIN
      src/views/natural-resources/images/icon-menu.png
  35. 16 0
      src/views/natural-resources/images/icon-upload-add.svg
  36. 25 0
      src/views/natural-resources/images/icon-upload-delete.svg
  37. BIN
      src/views/natural-resources/images/icon-upload.png
  38. 20 0
      src/views/natural-resources/images/resource-checked.svg
  39. 15 0
      src/views/natural-resources/images/resource-default.svg
  40. 7 4
      src/views/natural-resources/index.tsx
  41. 155 0
      src/views/natural-resources/model/add-teaching/index.module.less
  42. 367 0
      src/views/natural-resources/model/add-teaching/index.tsx

+ 22 - 0
package-lock.json

@@ -10,9 +10,11 @@
       "license": "MIT",
       "dependencies": {
         "@vant/use": "^1.5.2",
+        "@vicons/ionicons5": "^0.12.0",
         "@vueuse/core": "^10.2.0",
         "animate.css": "^4.1.1",
         "clean-deep": "^3.4.0",
+        "cropperjs": "^1.5.13",
         "dayjs": "^1.11.7",
         "echarts": "^5.4.2",
         "lodash": "^4.17.21",
@@ -3018,6 +3020,11 @@
         "vue": "^3.0.0"
       }
     },
+    "node_modules/@vicons/ionicons5": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz",
+      "integrity": "sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA=="
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "4.2.3",
       "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
@@ -3997,6 +4004,11 @@
         "croact": "^1.0.4"
       }
     },
+    "node_modules/cropperjs": {
+      "version": "1.5.13",
+      "resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.5.13.tgz",
+      "integrity": "sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA=="
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -11034,6 +11046,11 @@
       "resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.5.2.tgz",
       "integrity": "sha512-CBK61iT568dCHUwFFsErGbW6/5tmrPnZJKGtcSy7Tjcrmws8Ku+YZo7IUFD9Xkj9MfSJ4pfhQ7pU2KouP5Cojg=="
     },
+    "@vicons/ionicons5": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmmirror.com/@vicons/ionicons5/-/ionicons5-0.12.0.tgz",
+      "integrity": "sha512-Iy1EUVRpX0WWxeu1VIReR1zsZLMc4fqpt223czR+Rpnrwu7pt46nbnC2ycO7ItI/uqDLJxnbcMC7FujKs9IfFA=="
+    },
     "@vitejs/plugin-vue": {
       "version": "4.2.3",
       "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
@@ -11857,6 +11874,11 @@
         "react-moveable": "~0.52.1"
       }
     },
+    "cropperjs": {
+      "version": "1.5.13",
+      "resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.5.13.tgz",
+      "integrity": "sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA=="
+    },
     "cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",

+ 2 - 0
package.json

@@ -23,9 +23,11 @@
   },
   "dependencies": {
     "@vant/use": "^1.5.2",
+    "@vicons/ionicons5": "^0.12.0",
     "@vueuse/core": "^10.2.0",
     "animate.css": "^4.1.1",
     "clean-deep": "^3.4.0",
+    "cropperjs": "^1.5.13",
     "dayjs": "^1.11.7",
     "echarts": "^5.4.2",
     "lodash": "^4.17.21",

+ 20 - 0
src/api/user.ts

@@ -18,3 +18,23 @@ export const userLogin = (params: any) => {
 export const getUserInfo = () => {
   return request.get('/edu-app/user/getUserInfo');
 };
+
+/**
+ * 获取声部列表
+ * returns subjects
+ */
+export const getSubjectList = (params: any) => {
+  return request.post('/edu-app/subject/page', {
+    data: params
+  });
+};
+
+/**
+ * 获取曲谱分类
+ * @returns musicSheetCategories
+ */
+export const getCategories = (params: any) => {
+  return request.post('/edu-app/musicSheetCategories/page', {
+    data: params
+  });
+};

+ 1 - 0
src/components/card-preview/index.tsx

@@ -33,6 +33,7 @@ export default defineComponent({
     watch(
       () => props.item,
       () => {
+        console.log(props.item, 'itme');
         item.value = props.item;
       }
     );

+ 10 - 8
src/components/card-type/index.module.less

@@ -87,13 +87,15 @@
   }
 
   // 收藏按钮
+
+
   .iconCollect {
     width: 34px;
     height: 34px;
-    background: url('../../common/images/icon-collect-default.png') no-repeat center;
-    background-size: contain;
+    // background: url('../../common/images/icon-collect-default.png') no-repeat center;
+    // background-size: contain;
     position: absolute;
-    right: 12px;
+    right: 0;
     transition: transform .2s ease;
 
     &:hover {
@@ -105,10 +107,10 @@
       cursor: pointer;
     }
 
-    &.isActive {
-      background: url('../../common/images/icon-collect-active.png') no-repeat center;
-      background-size: contain;
-    }
+  }
+
+  .iconDiv {
+    right: 12px;
   }
 
   // 精选
@@ -137,4 +139,4 @@
     z-index: 99;
     transition: all .3s ease-in-out;
   }
-}
+}

+ 29 - 10
src/components/card-type/index.tsx

@@ -1,10 +1,12 @@
-import { PropType, defineComponent, ref } from 'vue';
+import { PropType, Transition, defineComponent, ref } from 'vue';
 import styles from './index.module.less';
 import { NButton, NCard, NImage, NModal } from 'naive-ui';
 import iconImage from '@common/images/icon-image.png';
 import iconVideo from '@common/images/icon-video.png';
 import iconAudio from '@common/images/icon-audio.png';
 import iconMusic from '@common/images/icon-music.png';
+import iconCollectDefault from '@common/images/icon-collect-default.png';
+import iconCollectActive from '@common/images/icon-collect-active.png';
 import TheNoticeBar from '../TheNoticeBar';
 import AudioPlayer from './audio-player';
 import VideoPlayer from './video-player ';
@@ -146,21 +148,38 @@ export default defineComponent({
                 </div>
                 {/* 收藏 */}
                 {props.isShowCollect && (
-                  <i
+                  <div
+                    class={[styles.iconCollect, styles.iconDiv]}
                     onClick={(e: MouseEvent) => {
                       e.stopPropagation();
                       e.preventDefault();
                       // 判断是否可以收藏
                       if (props.isCollect) {
-                        console.log('Collect');
-                        emit('collect');
+                        emit('collect', props.item);
                       }
-                    }}
-                    class={[
-                      styles.iconCollect,
-                      props.isCollect ? styles.isCollect : '',
-                      props.item.isCollect ? styles.isActive : ''
-                    ]}></i>
+                    }}>
+                    <Transition name="favitor" mode="out-in">
+                      {props.item.isCollect ? (
+                        <img
+                          src={iconCollectActive}
+                          key="1"
+                          class={[
+                            styles.iconCollect,
+                            props.isCollect ? styles.isCollect : ''
+                          ]}
+                        />
+                      ) : (
+                        <img
+                          src={iconCollectDefault}
+                          key="2"
+                          class={[
+                            styles.iconCollect,
+                            props.isCollect ? styles.isCollect : ''
+                          ]}
+                        />
+                      )}
+                    </Transition>
+                  </div>
                 )}
 
                 {/* 精选 */}

+ 10 - 0
src/components/upload-file/api.ts

@@ -0,0 +1,10 @@
+import request from '@/utils/request';
+
+/**
+ * @description: 获取key
+ */
+export const policy = (params: object) => {
+  return request.post('/edu-app/open/getUploadSign', {
+    data: params
+  });
+};

+ 175 - 0
src/components/upload-file/copper.tsx

@@ -0,0 +1,175 @@
+import { NButton, NGi, NGrid, NSpace } from 'naive-ui';
+import { defineComponent, nextTick, reactive, ref } from 'vue';
+import Cropper from 'cropperjs';
+import 'cropperjs/dist/cropper.css';
+
+export default defineComponent({
+  name: 'copper-image',
+  emits: ['close', 'cropperNo', 'cropperOk'],
+  setup(props, { emit, expose }) {
+    const state = reactive({
+      visible: false,
+      img: null,
+      confirmLoading: false,
+      options: {
+        img: '', //裁剪图片的地址
+        autoCrop: true, //是否默认生成截图框
+        autoCropWidth: 180, //默认生成截图框宽度
+        autoCropHeight: 180, //默认生成截图框高度
+        fixedBox: true, //是否固定截图框大小 不允许改变
+        full: false,
+        enlarge: 1, // 是否按照截图框比例输出 默认为1
+        previewsCircle: true, //预览图是否是原圆形
+        centerBox: true,
+        outputType: 'png',
+        title: '修改头像',
+        name: null // 文件名称
+      },
+      previews: {},
+      url: {
+        upload: '/sys/common/saveToImgByStr'
+      },
+      myCropper: null as any
+    });
+    const imgCropper = ref();
+
+    const edit = (record: any) => {
+      const { options } = state;
+      state.visible = true;
+      state.options = Object.assign({}, options, record);
+
+      nextTick(() => {
+        initImgCropper();
+      });
+    };
+
+    const initImgCropper = () => {
+      state.myCropper = new Cropper(imgCropper.value, {
+        viewMode: 1, //定义裁剪器的视图模式。如果将viewMode设置为0,则裁剪框可以延伸到画布外部,而值为1、2或3将限制裁剪框的大小为画布的大小。viewMode为2或3会将画布限制为容器。请注意,如果画布和容器的比例相同,则2和3之间没有差别。
+        dragMode: 'move', //定义的拖动模式裁剪器.canvas和容器一样,2和3没有区别。move:移动画布 crop:创建新的裁剪框(默认) none:什么也不做
+        //定义裁剪框的固定纵横比。默认情况下,裁剪框为自由比率。
+        aspectRatio: state.options.autoCropWidth / state.options.autoCropHeight,
+        initialAspectRatio: 1,
+        autoCropArea: 1, //定义0到1之间的fA编号。定义自动裁剪区域大小(百分比)。默认0.8
+        cropBoxMovable: true, //允许通过拖动移动裁剪框。默认true
+        cropBoxResizable: false, //以通过拖动来调整裁剪框的大小 默认true
+        background: true, //显示容器的网格背景
+        movable: true, //移动图像
+        modal: true,
+        preview: '.before'
+      });
+    };
+
+    const onOperation = (type: string) => {
+      switch (type) {
+        case 'left':
+          state.myCropper.rotate(90);
+          break;
+        case 'right':
+          state.myCropper.rotate(-90);
+          break;
+        case 'zoomIn':
+          state.myCropper.zoom(0.1);
+          break;
+        case 'zoomOut':
+          state.myCropper.zoom(-0.1);
+          break;
+      }
+    };
+
+    const onSubmit = () => {
+      state.confirmLoading = true;
+      state.myCropper
+        .getCroppedCanvas({
+          imageSmoothingQuality: 'high'
+        })
+        .toBlob((blob: any) => {
+          emit('cropperOk', blob);
+          state.confirmLoading = false;
+        });
+    };
+
+    expose({
+      edit
+    });
+    return () => (
+      <div>
+        <NGrid cols={2} xGap={24} style={{ paddingTop: '12px' }}>
+          <NGi>
+            <div style="width: 100%; height: 300px">
+              <img
+                ref={imgCropper}
+                id="myImages"
+                src={state.options.img}
+                alt=""
+              />
+            </div>
+
+            <NSpace justify="center" style={{ paddingTop: '12px' }}>
+              <NButton
+                type="primary"
+                size="small"
+                onClick={() => onOperation('left')}>
+                逆时针旋转
+              </NButton>
+              <NButton
+                type="primary"
+                size="small"
+                onClick={() => onOperation('right')}>
+                顺时针旋转
+              </NButton>
+              <NButton
+                type="primary"
+                size="small"
+                onClick={() => onOperation('zoomIn')}>
+                放大
+              </NButton>
+              <NButton
+                type="primary"
+                size="small"
+                onClick={() => onOperation('zoomOut')}>
+                缩小
+              </NButton>
+            </NSpace>
+          </NGi>
+          <NGi>
+            {/* font-weight: 600; padding-bottom: 8px; display: inline-block; */}
+            <span
+              style={{
+                fontSize: '15px',
+                fontWeight: 600,
+                paddingBottom: '8px',
+                display: 'inline-block'
+              }}>
+              预览图片
+            </span>
+            <div
+              class="before"
+              style={{
+                width: state.options.autoCropWidth + 'px',
+                height: state.options.autoCropHeight + 'px',
+                overflow: 'hidden'
+              }}></div>
+          </NGi>
+        </NGrid>
+        <NSpace justify="end">
+          <NButton
+            type="default"
+            onClick={() => {
+              state.confirmLoading = false;
+              emit('close');
+              emit('cropperNo');
+            }}>
+            取消
+          </NButton>
+          <NButton
+            type="primary"
+            loading={state.confirmLoading}
+            onClick={onSubmit}>
+            确认
+          </NButton>
+        </NSpace>
+      </div>
+    );
+  }
+});

+ 366 - 0
src/components/upload-file/index.tsx

@@ -0,0 +1,366 @@
+import { NButton, NModal, NUpload, UploadFileInfo, useMessage } from 'naive-ui';
+import { defineComponent, watch, PropType, reactive, ref } from 'vue';
+import { policy } from './api';
+import Copper from './copper';
+import axios from 'axios';
+
+export default defineComponent({
+  name: 'upload-file',
+  props: {
+    fileList: {
+      type: String,
+      default: ''
+    },
+    imageList: {
+      type: Array,
+      default: () => []
+    },
+    accept: {
+      // 支持类型
+      type: String,
+      default: '.jpg,.png,.jpeg,.gif'
+    },
+    listType: {
+      type: String as PropType<'image' | 'image-card'>,
+      default: 'image-card'
+    },
+    showType: {
+      type: String as PropType<'default' | 'custom'>,
+      default: 'default'
+    },
+    showFileList: {
+      type: Boolean,
+      default: true
+    },
+    // width: {
+    //   type: Number,
+    //   default: 96
+    // },
+    // height: {
+    //   type: Number,
+    //   default: 96
+    // },
+    text: {
+      type: String as PropType<string>,
+      default: '上传文件'
+    },
+    size: {
+      // 文件大小
+      type: Number as PropType<number>,
+      default: 5
+    },
+    max: {
+      type: Number as PropType<number>,
+      default: 1
+    },
+    multiple: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    disabled: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    tips: {
+      type: String as PropType<string>,
+      default: ''
+    },
+    bucketName: {
+      type: String,
+      default: 'gyt'
+    },
+    path: {
+      type: String,
+      default: ''
+    },
+    fileName: {
+      type: String,
+      default: ''
+    },
+    cropper: {
+      // 是否裁切, 只有图片才支持
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    options: {
+      type: Object,
+      default: () => {
+        return {
+          viewMode: 0,
+          autoCrop: true, //是否默认生成截图框
+          enlarge: 1, //  图片放大倍数
+          autoCropWidth: 200, //默认生成截图框宽度
+          autoCropHeight: 200, //默认生成截图框高度
+          fixedBox: false, //是否固定截图框大小 不允许改变
+          previewsCircle: true, //预览图是否是原图形
+          title: '上传图片'
+        };
+      }
+    }
+  },
+  // readFileInputEventAsArrayBuffer 只会在文件的时间回调
+  emits: [
+    'update:fileList',
+    'close',
+    'readFileInputEventAsArrayBuffer',
+    'remove'
+  ],
+  setup(props, { emit, expose, slots }) {
+    const ossUploadUrl = `https://${props.bucketName}.ks3-cn-beijing.ksyuncs.com/`;
+    const message = useMessage();
+    const visiable = ref<boolean>(false);
+    const btnLoading = ref<boolean>(false);
+    const tempFiileBuffer = ref();
+    const uploadRef = ref();
+    const state = reactive({
+      policy: '',
+      signature: '',
+      key: '',
+      KSSAccessKeyId: '',
+      acl: 'public-read',
+      name: ''
+    }) as any;
+
+    const fileListRef = ref<UploadFileInfo[]>([]);
+    const initFileList = () => {
+      if (props.fileList) {
+        console.log('downloadUrl', props.fileList);
+        const splitName = props.fileList.split('/');
+        fileListRef.value = [
+          {
+            id: new Date().getTime().toString(),
+            name: splitName[splitName.length - 1],
+            status: 'finished',
+            url: props.fileList
+          }
+        ];
+      } else if (Array.isArray(props.imageList)) {
+        const list: any = [];
+        props.imageList.forEach((n: any) => {
+          const splitName = n.split('/');
+          list.push({
+            id: Date.now().toString(),
+            name: splitName[splitName.length - 1],
+            status: 'finished',
+            url: n
+          });
+        });
+        fileListRef.value = list;
+      } else {
+        fileListRef.value = [];
+      }
+    };
+    initFileList();
+    watch(
+      () => props.imageList,
+      () => {
+        initFileList();
+      }
+    );
+    watch(
+      () => props.fileList,
+      () => {
+        console.log('list');
+        initFileList();
+      }
+    );
+    const handleClearFile = () => {
+      uploadRef.value?.clear();
+      console.log('清空', uploadRef.value);
+    };
+    expose({
+      handleClearFile
+    });
+
+    const CropperModal = ref();
+    const onBeforeUpload = async (options: any) => {
+      const file = options.file;
+      // 文件大小
+      let isLt2M = true;
+      if (props.size) {
+        isLt2M = file.file.size / 1024 / 1024 < props.size;
+        if (!isLt2M) {
+          message.error(`文件大小不能超过${props.size}M`);
+          return false;
+        }
+      }
+
+      if (!isLt2M) {
+        return isLt2M;
+      }
+      // 是否裁切
+      if (props.cropper) {
+        getBase64(file.file, (imageUrl: any) => {
+          const target = Object.assign({}, props.options, {
+            img: imageUrl,
+            name: file.file.name // 上传文件名
+          });
+          visiable.value = true;
+
+          setTimeout(() => {
+            CropperModal.value?.edit(target);
+            console.log(CropperModal.value, 'cropper');
+          }, 100);
+        });
+        return false;
+      }
+      try {
+        btnLoading.value = true;
+        console.log(props.path, file.file);
+        const name = file.file.name;
+        const suffix = name.slice(name.lastIndexOf('.'));
+        // const months = dayjs().format('MM')
+        const fileName = `${props.path}${
+          props.fileName || Date.now() + suffix
+        }`;
+        const obj = {
+          filename: fileName,
+          bucketName: props.bucketName,
+          postData: {
+            filename: fileName,
+            acl: 'public-read',
+            key: fileName,
+            unknowValueField: []
+          }
+        };
+        const { data } = await policy(obj);
+
+        state.policy = data.policy;
+        state.signature = data.signature;
+        state.key = fileName;
+        state.KSSAccessKeyId = data.kssAccessKeyId;
+        state.name = fileName;
+
+        tempFiileBuffer.value = file.file;
+      } catch {
+        //
+        // message.error('上传失败')
+        btnLoading.value = false;
+        return false;
+      }
+      return true;
+    };
+    const getBase64 = async (img: any, callback: any) => {
+      const reader = new FileReader();
+      reader.addEventListener('load', () => callback(reader.result));
+      reader.readAsDataURL(img);
+    };
+    const onFinish = (options: any) => {
+      const url = ossUploadUrl + state.key;
+      console.log(url, 'url');
+      emit('update:fileList', url);
+      emit('readFileInputEventAsArrayBuffer', tempFiileBuffer.value);
+      options.file.url = url;
+      visiable.value = false;
+      btnLoading.value = false;
+    };
+    const onRemove = async (options: any) => {
+      console.log('🚀 ~ options', options);
+      emit('update:fileList', '');
+      emit('remove');
+      btnLoading.value = false;
+    };
+
+    // 裁切失败
+    // const cropperNo = () => {}
+    // 裁切成功
+    const cropperOk = async (blob: any) => {
+      try {
+        // const months = dayjs().format('MM')
+        const fileName = `${props.path}${
+          props.fileName || new Date().getTime() + '.png'
+        }`;
+        const obj = {
+          filename: fileName,
+          bucketName: props.bucketName,
+          postData: {
+            filename: fileName,
+            acl: 'public-read',
+            key: fileName,
+            unknowValueField: []
+          }
+        };
+        const { data } = await policy(obj);
+
+        state.policy = data.policy;
+        state.signature = data.signature;
+        state.key = fileName;
+        state.KSSAccessKeyId = data.kssAccessKeyId;
+        state.name = fileName;
+
+        const formData = new FormData();
+        for (const key in state) {
+          formData.append(key, state[key]);
+        }
+        formData.append('file', blob);
+
+        await axios.post(ossUploadUrl, formData).then(() => {
+          const url = ossUploadUrl + state.key;
+          console.log(url, 'url');
+          const splitName = url.split('/');
+          fileListRef.value = [
+            {
+              id: new Date().getTime().toString(),
+              name: splitName[splitName.length - 1],
+              status: 'finished',
+              url: url
+            }
+          ];
+          emit('update:fileList', url);
+          visiable.value = false;
+        });
+      } catch {
+        //
+        // message.error('上传失败')
+        return false;
+      }
+    };
+    return () => (
+      <div>
+        <NUpload
+          ref={uploadRef}
+          action={ossUploadUrl}
+          data={state}
+          v-model:fileList={fileListRef.value}
+          listType={props.listType}
+          accept={props.accept}
+          multiple={props.multiple}
+          max={props.max}
+          disabled={props.disabled}
+          showFileList={props.showFileList}
+          showPreviewButton
+          onBeforeUpload={(options: any) => onBeforeUpload(options)}
+          onFinish={(options: any) => onFinish(options)}
+          onRemove={(options: any) => onRemove(options)}>
+          {props.showType === 'default' && props.listType === 'image' && (
+            <NButton loading={btnLoading.value} type="primary">
+              {props.text}
+            </NButton>
+          )}
+          {props.showType === 'custom' && slots.custom && slots.custom()}
+        </NUpload>
+        {props.tips && (
+          <p style="font-size: 13px; color: #666; padding-top: 4px;">
+            {props.tips}
+          </p>
+        )}
+
+        <NModal
+          v-model:show={visiable.value}
+          preset="dialog"
+          showIcon={false}
+          class={['modalTitle background']}
+          title="上传图片"
+          style={{ width: '800px' }}>
+          {/* @cropper-no="error" @cropper-ok="success" */}
+          <Copper
+            // ref="CropperModal"
+            ref={CropperModal}
+            onClose={() => (visiable.value = false)}
+            onCropperOk={cropperOk}
+          />
+        </NModal>
+      </div>
+    );
+  }
+});

+ 16 - 1
src/enums/pageEnum.ts

@@ -6,9 +6,24 @@ export enum PageEnum {
   REDIRECT = '/redirect',
   // REDIRECT_NAME = 'Redirect',
   // 首页
-  BASE_HOME = '/home'
+  BASE_HOME = '/home',
   // //首页跳转默认路由
   // BASE_HOME_REDIRECT = '/dashboard/console',
   // // 错误
   // ERROR_PAGE_NAME = 'ErrorPage'
+  SONG_DEFAULT_COVER = 'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687916228530.png'
+}
+
+export const NaturalType: { [_: string]: string } = {
+  IMG: '图片',
+  VIDEO: '视频',
+  SONG: '音频',
+  MUSIC: '乐谱'
+};
+
+export enum NaturalTypeEnum {
+  IMG = 'IMG',
+  VIDEO = 'VIDEO',
+  MUSIC = 'MUSIC',
+  SONG = 'SONG'
 }

+ 0 - 81
src/helpers/request.ts

@@ -1,81 +0,0 @@
-import { extend } from 'umi-request';
-import cleanDeep from 'clean-deep';
-import { setLogout, setLoginError } from '@/state';
-import { postMessage } from './native-message';
-
-export interface SearchInitParams {
-  rows?: string | number;
-  page?: string | number;
-}
-
-const request = extend({
-  // requestType: 'form',
-  hideLoading: true, // 默认都不显示加载
-  timeout: 20000,
-  timeoutMessage: '请求超时'
-});
-
-// 是否是初始化接口
-let initRequest = false;
-
-request.interceptors.request.use(
-  (url, options: any) => {
-    initRequest = options.initRequest || false;
-    const Authorization = sessionStorage.getItem('Authorization') || '';
-    const authHeaders: any = {};
-    if (
-      Authorization &&
-      ![
-        '/api-oauth/userlogin',
-        // `${state.platformApi}/user/getUserInfo`,
-        '/api-oauth/open/sendSms'
-      ].includes(url)
-    ) {
-      authHeaders.Authorization = Authorization;
-    }
-
-    return {
-      url,
-      options: {
-        ...options,
-        params: cleanDeep(options.params),
-        data: cleanDeep(options.data),
-        headers: {
-          ...options.headers,
-          ...authHeaders
-        }
-      }
-    };
-  },
-  { global: false }
-);
-
-request.interceptors.response.use(
-  async res => {
-    if (res.status > 299 || res.status < 200) {
-      const msg = '服务器错误,状态码' + res.status;
-      throw new Error(msg);
-    }
-    const data = await res.clone().json();
-    // 999 为特殊code码
-    if (data.code !== 200 && data.errCode !== 0 && data.code !== 999) {
-      const msg = data.msg || data.message || '处理失败,请重试';
-      if (initRequest) {
-        if (data.code === 403 || data.code === 5000) {
-          setLogout();
-        } else {
-          setLoginError();
-        }
-      }
-      console.log(data.code, '5104');
-      if (!(data.code === 403 || data.code === 5000)) {
-        // showToast(msg);
-      }
-      throw new Error(msg);
-    }
-    return res;
-  },
-  { global: false }
-);
-
-export default request;

+ 66 - 47
src/store/modules/catchData.ts

@@ -1,7 +1,6 @@
 import { defineStore } from 'pinia';
 import { store } from '@/store';
-import { storage } from '@/utils/storage';
-import { userLogin, getUserInfo } from '@/api/user';
+import { getSubjectList, getCategories } from '@/api/user';
 
 export const useCatchStore = defineStore('catch-store', {
   state: () => ({
@@ -13,66 +12,86 @@ export const useCatchStore = defineStore('catch-store', {
     getBookVersion(): any[] {
       return this.bookVersionList;
     },
-    getMusicType(): any[] {
+    getMusicCategories(): any[] {
       return this.musicTypeList;
     },
-    getSubjects(): any[] {
+    getAllMusicCategories(): any[] {
+      return [
+        {
+          name: '全部',
+          id: null
+        },
+        ...this.musicTypeList
+      ];
+    },
+    getSubjectList(): any[] {
       return this.subjectList;
+    },
+    getSubjectAllList(): any[] {
+      return [
+        {
+          name: '全部',
+          id: null
+        },
+        ...this.subjectList
+      ];
     }
   },
   actions: {
     setBookVersion(books: any[]) {
       this.bookVersionList = books;
     },
-    setMusicType(musics: any[]) {
+    setMusicCategories(musics: any[]) {
       this.musicTypeList = musics;
     },
     setSubjects(subjects: any[]) {
       this.subjectList = subjects;
-    }
-    //
-    // 登录
-    // async login(userInfo: any) {
-    //   try {
-    //     const { data } = await userLogin(userInfo);
-    //     console.log(data, 'data');
-    //     const userToken = data.token_type + ' ' + data.access_token;
-    //     const ex = 7 * 24 * 60 * 60 * 1000;
-    //     storage.set(ACCESS_TOKEN, userToken, ex);
-    //     // storage.get(IM_TOKEN, data.imToken);
-
-    //     this.setToken(userToken);
-    //     // this.setImToken(data.imToken);
-    //     return Promise.resolve();
-    //   } catch (e) {
-    //     return Promise.reject(e);
-    //   }
-    // },
+    },
+    /**
+     * 判断是否有声部数据,如不存在则获取声部列表
+     * @returns Promise
+     */
+    async getSubjects() {
+      try {
+        // 判断是否存在声部数据
+        if (this.getSubjectList && this.getSubjectList.length > 0) {
+          return Promise.resolve();
+        }
+        const { data } = await getSubjectList({
+          enableFlag: true,
+          delFlag: 0,
+          page: 1,
+          row: 999
+        });
 
-    // // 获取用户信息
-    // async getInfo() {
-    //   return new Promise((resolve, reject) => {
-    //     getUserInfo()
-    //       .then((res: any) => {
-    //         const result = res.data;
-    //         this.setUserInfo(result);
-    //         this.setAvatar(result.account.avatar);
-    //         this.setUsername(result.nickname);
-    //         resolve(true);
-    //       })
-    //       .catch((error: any) => {
-    //         reject(error);
-    //       });
-    //   });
-    // },
+        this.setSubjects(data.rows || []);
+        return Promise.resolve();
+      } catch (e) {
+        return Promise.reject(e);
+      }
+    },
+    /**
+     * 判断是否有教材分类数据,如不存在则获取教材分类列表
+     * @returns Promise
+     */
+    async getMusicSheetCategory() {
+      try {
+        // 判断是否存在声部数据
+        if (this.getMusicCategories && this.getMusicCategories.length > 0) {
+          return Promise.resolve();
+        }
+        const { data } = await getCategories({
+          enable: true,
+          page: 1,
+          row: 999
+        });
 
-    // // 登出
-    // async logout() {
-    //   this.setUserInfo('');
-    //   storage.remove(ACCESS_TOKEN);
-    //   storage.remove(CURRENT_USER);
-    //   return Promise.resolve('');
-    // }
+        this.setMusicCategories(data.rows || []);
+        return Promise.resolve();
+      } catch (e) {
+        return Promise.reject(e);
+      }
+    }
   }
 });
 

+ 20 - 0
src/styles/index.less

@@ -131,6 +131,7 @@ body {
 
     color: #333333;
     font-size: 14px;
+
     .n-button__content {
       font-size: 14px;
       color: #1677ff;
@@ -247,11 +248,13 @@ body {
     transform: rotate(0);
     transform: rotate(0);
   }
+
   10%,
   20% {
     transform: rotate(-6deg);
     transform: rotate(-6deg);
   }
+
   30%,
   50%,
   70%,
@@ -259,14 +262,31 @@ body {
     transform: rotate(6deg);
     transform: rotate(6deg);
   }
+
   40%,
   60%,
   80% {
     transform: rotate(-6deg);
     transform: rotate(-6deg);
   }
+
   100% {
     transform: rotate(0);
     transform: rotate(0);
   }
 }
+
+
+/* 列表动画 start */
+.list-move,
+/* 对移动中的元素应用的过渡 */
+.list-enter-active,
+.list-leave-active {
+  transition: all 0.5s ease;
+}
+
+.list-enter-from,
+.list-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}

+ 21 - 6
src/utils/index.ts

@@ -268,12 +268,27 @@ export function clearEmtryData(list: any[], key: string) {
 
 // 秒转分
 export const getSecondRPM = (second: number, type?: string) => {
-  if (isNaN(second)) return '00:00'
-  const mm = Math.floor(second / 60).toString().padStart(2, '0')
-  const dd = Math.floor(second % 60).toString().padStart(2, '0')
+  if (isNaN(second)) return '00:00';
+  const mm = Math.floor(second / 60)
+    .toString()
+    .padStart(2, '0');
+  const dd = Math.floor(second % 60)
+    .toString()
+    .padStart(2, '0');
   if (type === 'cn') {
-    return mm + '分' + dd + '秒'
+    return mm + '分' + dd + '秒';
   } else {
-    return mm + ':' + dd
+    return mm + ':' + dd;
   }
-}
+};
+
+/** 滚动到表单填写错误的地方 */
+export function scrollToErrorForm() {
+  const isError =
+    document.querySelector('.n-input--error-status') ||
+    document.querySelector('.n-base-selection--error-status');
+  isError?.scrollIntoView({
+    block: 'center',
+    behavior: 'smooth'
+  });
+}

+ 1 - 1
src/views/login/components/pwdLogin.tsx

@@ -134,7 +134,7 @@ export default defineComponent({
                 ),
                 suffix: () => (
                   <img
-                    src={showPwd.value ? closeEye : openEye}
+                    src={showPwd.value ? openEye : closeEye}
                     class={styles.pwdIcon}
                     alt=""
                     onClick={() => {

+ 43 - 0
src/views/natural-resources/api.ts

@@ -8,3 +8,46 @@ export const materialQueryPage = (params: any) => {
     data: params
   });
 };
+
+/**
+ * 资源 - 收藏/取消收藏
+ */
+export const favorite = (params: any) => {
+  return request.post('/edu-app/material/favorite', {
+    data: params
+  });
+};
+
+/** 音乐教材-新增 */
+export const api_lessonCoursewareSave = (params: any): Promise<any> => {
+  return request.post('/edu-app/lessonCourseware/save', {
+    data: params
+  });
+};
+
+/** 资源-新增 */
+export const materialSave = (params: any[]): Promise<any> => {
+  return request.post('/edu-app/material/save', {
+    data: params
+  });
+};
+
+/** 资源-修改 */
+export const materialUpdate = (params: any): Promise<any> => {
+  return request.post('/edu-app/material/update', {
+    data: params
+  });
+};
+/** 资源-批量修改 */
+export const materialUpdateAll = (params: any): Promise<any> => {
+  return request.post('/edu-app/material/updateAll', {
+    data: params
+  });
+};
+
+/** 资源-批量删除 */
+export const materialRemoveAll = (params: any): Promise<any> => {
+  return request.post('/edu-app/material/removeAll', {
+    data: params
+  });
+};

+ 8 - 0
src/views/natural-resources/components/my-collect/index.module.less

@@ -110,4 +110,12 @@
       }
     }
   }
+}
+
+.spaceSection {
+  width: 76%;
+
+  &>div {
+    line-height: var(--n-blank-height);
+  }
 }

+ 39 - 8
src/views/natural-resources/components/my-collect/index.tsx

@@ -1,11 +1,12 @@
 import { defineComponent, onMounted, reactive } from 'vue';
 import styles from './index.module.less';
-import CardType from '/src/components/card-type';
-import Pagination from '/src/components/pagination';
+import CardType from '@/components/card-type';
+import Pagination from '@/components/pagination';
 import SearchGroupResources from './search-group-resources';
-import { materialQueryPage } from '../../api';
-import { NEmpty, NSpin } from 'naive-ui';
-import TheEmpty from '/src/components/TheEmpty';
+import { favorite, materialQueryPage } from '../../api';
+import { NSpin } from 'naive-ui';
+import TheEmpty from '@/components/TheEmpty';
+import CardPreview from '@/components/card-preview';
 
 export default defineComponent({
   name: 'share-resources',
@@ -25,7 +26,9 @@ export default defineComponent({
         subjectId: null,
         sourceType: 4
       },
-      tableList: [] as any
+      tableList: [] as any,
+      show: false,
+      item: {} as any
     });
     const getList = async () => {
       try {
@@ -45,9 +48,23 @@ export default defineComponent({
     const onSearch = async (item: any) => {
       state.pagination.page = 1;
       state.searchGroup = Object.assign(state.searchGroup, item);
-      console.log(item, state.searchGroup);
       getList();
     };
+    // 收藏
+    const onCollect = async (item: any) => {
+      try {
+        await favorite({
+          materialId: item.id,
+          favoriteFlag: item.isCollect ? 0 : 1,
+          type: item.type
+        });
+        item.isCollect = !item.isCollect;
+
+        onSearch();
+      } catch {
+        //
+      }
+    };
 
     onMounted(() => {
       getList();
@@ -67,7 +84,18 @@ export default defineComponent({
                 isCollect: !!item.favoriteFlag,
                 isSelected: item.sourceFrom === 'PLATFORM' ? true : false
               };
-              return <CardType item={tmpItem} />;
+              return (
+                <CardType
+                  item={tmpItem}
+                  disabledMouseHover={false}
+                  onClick={(val: any) => {
+                    if (val.type === 'IMG') return;
+                    state.show = true;
+                    state.item = val;
+                  }}
+                  onCollect={(item: any) => onCollect(item)}
+                />
+              );
             })}
 
             {!state.loading && state.tableList.length <= 0 && (
@@ -82,6 +110,9 @@ export default defineComponent({
           v-model:pageTotal={state.pageTotal}
           onList={getList}
         />
+
+        {/* 弹窗查看 */}
+        <CardPreview v-model:show={state.show} item={state.item} />
       </>
     );
   }

+ 45 - 21
src/views/natural-resources/components/my-collect/search-group-resources.tsx

@@ -1,14 +1,15 @@
 import { defineComponent, onMounted, reactive, ref } from 'vue';
 import styles from './index.module.less';
 import { NButton, NForm, NFormItem, NSpace } from 'naive-ui';
-import iconAdd from '../../images/icon-add.svg';
 import TheSearch from '/src/components/TheSearch';
 import { resourceTypeArray } from '/src/utils/searchArray';
+import { useCatchStore } from '/src/store/modules/catchData';
 
 export default defineComponent({
   name: 'search-group',
   emits: ['search'],
   setup(props, { emit }) {
+    const catchStore = useCatchStore();
     const resourceList = ref([] as any[]);
     const forms = reactive({
       type: '', //
@@ -21,7 +22,7 @@ export default defineComponent({
       emit('search', forms);
     };
 
-    onMounted(() => {
+    onMounted(async () => {
       resourceList.value = [
         {
           label: '全部',
@@ -29,6 +30,11 @@ export default defineComponent({
         },
         ...resourceTypeArray
       ];
+
+      // 获取教材分类列表
+      await catchStore.getMusicSheetCategory();
+      // 获取声部列表
+      await catchStore.getSubjects();
     });
     return () => (
       <div class={styles.searchGroup}>
@@ -51,26 +57,44 @@ export default defineComponent({
           </NSpace>
         </div>
         <NForm labelAlign="left" labelPlacement="left">
+          {forms.type === 'MUSIC' && (
+            <NFormItem label="教材:">
+              <NSpace class={styles.spaceSection}>
+                {catchStore.getAllMusicCategories.map((music: any) => (
+                  <NButton
+                    secondary={forms.bookVersionId === music.id}
+                    quaternary={forms.bookVersionId !== music.id}
+                    strong
+                    focusable={false}
+                    type={
+                      forms.bookVersionId === music.id ? 'primary' : 'default'
+                    }
+                    onClick={() => {
+                      forms.bookVersionId = music.id;
+                      onSearch();
+                    }}>
+                    {music.name}
+                  </NButton>
+                ))}
+              </NSpace>
+            </NFormItem>
+          )}
           <NFormItem label="乐器:">
-            <NSpace>
-              <NButton secondary strong type="primary">
-                全部
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                竖笛
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                排萧
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                口风琴
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                陶笛
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                葫芦丝
-              </NButton>
+            <NSpace class={styles.spaceSection}>
+              {catchStore.getSubjectAllList.map((subject: any) => (
+                <NButton
+                  secondary={forms.subjectId === subject.id}
+                  quaternary={forms.subjectId !== subject.id}
+                  strong
+                  focusable={false}
+                  type={forms.subjectId === subject.id ? 'primary' : 'default'}
+                  onClick={() => {
+                    forms.subjectId = subject.id;
+                    onSearch();
+                  }}>
+                  {subject.name}
+                </NButton>
+              ))}
             </NSpace>
           </NFormItem>
 

+ 41 - 0
src/views/natural-resources/components/my-resources /index.module.less

@@ -93,6 +93,7 @@
     display: flex;
     justify-content: space-between;
     padding-bottom: 24px;
+    padding-top: 3px;
     border-bottom: 1px solid #F2F2F2;
     margin-bottom: 20px;
 
@@ -138,4 +139,44 @@
       }
     }
   }
+}
+
+.itemSection {
+  position: relative;
+
+  .itemBg {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    z-index: 99;
+    border-radius: 14px;
+
+    &.itemBgChecked {
+      background-color: rgba(0, 0, 0, .5);
+    }
+
+    .resourceDefault {
+      position: absolute;
+      top: 16px;
+      right: 16px;
+      width: 30px;
+      height: 30px;
+
+    }
+  }
+
+}
+
+.spaceSection {
+  width: 76%;
+
+  &>div {
+    line-height: var(--n-blank-height);
+  }
+}
+
+.attendClassModal {
+  width: 1100px;
 }

+ 161 - 19
src/views/natural-resources/components/my-resources /index.tsx

@@ -3,14 +3,19 @@ import styles from './index.module.less';
 import CardType from '/src/components/card-type';
 import Pagination from '/src/components/pagination';
 import SearchGroupResources from './search-group-resources';
-import { materialQueryPage } from '../../api';
-import { NModal, NSpin } from 'naive-ui';
+import { favorite, materialQueryPage, materialRemoveAll } from '../../api';
+import { NModal, NSpin, useDialog, useMessage } from 'naive-ui';
 import TheEmpty from '/src/components/TheEmpty';
 import UploadModal from './upload-modal';
+import CardPreview from '@/components/card-preview';
+import resourceDefault from '../../images/resource-default.svg';
+import resourceChecked from '../../images/resource-checked.svg';
 
 export default defineComponent({
   name: 'share-resources',
   setup() {
+    const message = useMessage();
+    const dialog = useDialog();
     const state = reactive({
       searchWord: '',
       loading: false,
@@ -27,7 +32,12 @@ export default defineComponent({
         sourceType: 3
       },
       tableList: [] as any,
-      uploadStatus: false
+      uploadStatus: false,
+      show: false,
+      item: {} as any,
+      editStatus: false, // 是否编辑
+      editList: [] as any, // TOD
+      editIds: [] as any // 编辑的
     });
     const getList = async () => {
       try {
@@ -38,19 +48,72 @@ export default defineComponent({
         });
         state.loading = false;
         state.pageTotal = Number(data.total);
-        state.tableList = data.rows || [];
+        const tempRows = data.rows || [];
+        const temp: any = [];
+        tempRows.forEach((row: any) => {
+          temp.push({
+            id: row.id,
+            coverImg: row.coverImg,
+            type: row.type,
+            title: row.name,
+            isCollect: !!row.favoriteFlag,
+            isSelected: row.sourceFrom === 'PLATFORM' ? true : false,
+            content: row.content,
+            subjectId: row.subjectIds,
+            enableFlag: row.enableFlag ? 1 : 0,
+            openFlag: row.openFlag
+          });
+        });
+        state.tableList = temp || [];
       } catch {
         state.loading = false;
       }
     };
 
+    // 收藏
+    const onCollect = async (item: any) => {
+      try {
+        await favorite({
+          materialId: item.id,
+          favoriteFlag: item.isCollect ? 0 : 1,
+          type: item.type
+        });
+        item.isCollect = !item.isCollect;
+      } catch {
+        //
+      }
+    };
+
     const onSearch = async (item: any) => {
       state.pagination.page = 1;
       state.searchGroup = Object.assign(state.searchGroup, item);
-      console.log(item, state.searchGroup);
       getList();
     };
 
+    // 批量删除
+    const onDelete = async () => {
+      try {
+        if (state.editIds.length <= 0) {
+          message.error('至少选择一条资源进行删除');
+          return;
+        }
+        dialog.warning({
+          title: '提示',
+          content: '你确定删除该资源?',
+          positiveText: '确定',
+          negativeText: '取消',
+          onPositiveClick: async () => {
+            await materialRemoveAll(state.editIds);
+            message.success('删除成功');
+            onSearch(state.searchGroup);
+            state.editIds = [];
+          }
+        });
+      } catch {
+        //
+      }
+    };
+
     onMounted(() => {
       getList();
     });
@@ -58,22 +121,90 @@ export default defineComponent({
       <>
         <SearchGroupResources
           onSearch={(item: any) => onSearch(item)}
-          onUpdate={() => (state.uploadStatus = true)}
+          onUpload={() => {
+            state.editList = [];
+            state.uploadStatus = true;
+          }}
+          onUpdate={() => {
+            // 修改
+            const list: any[] = [];
+            state.tableList.forEach((item: any) => {
+              if (state.editIds.indexOf(item.id) > -1) {
+                list.push(item);
+              }
+            });
+            state.editList = list || [];
+            if (state.editList.length <= 0) {
+              message.error('至少选择一条资源进行编辑');
+              return;
+            }
+            state.uploadStatus = true;
+          }}
+          onEdit={(status: boolean) => {
+            // 点击编辑
+            state.editStatus = status;
+            if (!state.editStatus) {
+              state.editIds = [];
+            }
+          }}
+          onSelectAll={(status: boolean) => {
+            // 全选
+            if (status) {
+              const tempIds: any[] = [];
+              state.tableList.forEach((item: any) => {
+                tempIds.push(item.id);
+              });
+              state.editIds = tempIds;
+            } else {
+              state.editIds = [];
+            }
+          }}
+          onDelete={onDelete}
         />
 
         <NSpin v-model:show={state.loading}>
           <div class={styles.list}>
-            {state.tableList.map((item: any) => {
-              const tmpItem = {
-                id: item.id,
-                coverImg: item.coverImg,
-                type: item.type,
-                title: item.name,
-                isCollect: !!item.favoriteFlag,
-                isSelected: item.sourceFrom === 'PLATFORM' ? true : false
-              };
-              return <CardType item={tmpItem} />;
-            })}
+            {state.tableList.map((item: any) => (
+              <div class={styles.itemSection}>
+                <CardType
+                  item={item}
+                  disabledMouseHover={false}
+                  onClick={(val: any) => {
+                    if (val.type === 'IMG') return;
+                    state.show = true;
+                    state.item = val;
+                  }}
+                  onCollect={(item: any) => onCollect(item)}
+                />
+                {/* 编辑模式 */}
+                {state.editStatus && (
+                  <div
+                    class={[
+                      styles.itemBg,
+                      state.editIds.includes(item.id)
+                        ? styles.itemBgChecked
+                        : ''
+                    ]}
+                    onClick={() => {
+                      const index = state.editIds.indexOf(item.id);
+                      if (index > -1) {
+                        state.editIds.splice(index, 1);
+                      } else {
+                        state.editIds.push(item.id);
+                      }
+                    }}>
+                    <img
+                      src={
+                        state.editIds.includes(item.id)
+                          ? resourceChecked
+                          : resourceDefault
+                      }
+                      class={styles.resourceDefault}
+                    />
+                  </div>
+                )}
+              </div>
+            ))}
 
             {!state.loading && state.tableList.length <= 0 && (
               <TheEmpty description="暂无资源" />
@@ -88,14 +219,25 @@ export default defineComponent({
           onList={getList}
         />
 
+        {/* 弹窗查看 */}
+        <CardPreview v-model:show={state.show} item={state.item} />
+
         <NModal
           v-model:show={state.uploadStatus}
           preset="card"
           showIcon={false}
           class={['modalTitle background', styles.attendClassModal]}
-          title={'选择班级'}
+          title={state.editStatus ? '修改资源' : '上传资源'}
           blockScroll={false}>
-          <UploadModal />
+          <UploadModal
+            onClose={() => (state.uploadStatus = false)}
+            onConfirm={() => {
+              state.editIds = [];
+              state.editList = [];
+              onSearch(state.searchGroup);
+            }}
+            list={state.editList}
+          />
         </NModal>
       </>
     );

+ 30 - 23
src/views/natural-resources/components/my-resources /search-group-resources.tsx

@@ -9,12 +9,14 @@ import iconPen from '../../images/icon-pen.svg';
 import iconDelete from '../../images/icon-delete.svg';
 import TheSearch from '/src/components/TheSearch';
 import { resourceTypeArray } from '/src/utils/searchArray';
+import { useCatchStore } from '/src/store/modules/catchData';
 
 export default defineComponent({
   name: 'search-group',
   emits: ['search', 'upload', 'edit', 'selectAll', 'delete', 'update'],
   setup(props, { emit }) {
     const resourceList = ref([] as any[]);
+    const catchStore = useCatchStore();
     const forms = reactive({
       type: '', //
       keyword: '',
@@ -30,7 +32,7 @@ export default defineComponent({
       emit('search', forms);
     };
 
-    onMounted(() => {
+    onMounted(async () => {
       resourceList.value = [
         {
           label: '全部',
@@ -38,6 +40,9 @@ export default defineComponent({
         },
         ...resourceTypeArray
       ];
+
+      // 获取声部列表
+      await catchStore.getSubjects();
     });
     return () => (
       <div class={styles.searchGroup}>
@@ -105,7 +110,10 @@ export default defineComponent({
                   class={styles.addTrain}
                   focusable={false}
                   strong
-                  onClick={() => (state.isEdit = false)}>
+                  onClick={() => {
+                    state.isEdit = false;
+                    emit('edit', state.isEdit);
+                  }}>
                   完成编辑
                 </NButton>
               </>
@@ -116,7 +124,7 @@ export default defineComponent({
                   class={styles.addTrain}
                   focusable={false}
                   strong
-                  onClick={() => emit('update')}>
+                  onClick={() => emit('upload')}>
                   <img src={iconUpload} class={styles.iconUpload} />
                   上传资源
                 </NButton>
@@ -125,7 +133,10 @@ export default defineComponent({
                   class={styles.addTrain}
                   focusable={false}
                   strong
-                  onClick={() => (state.isEdit = true)}>
+                  onClick={() => {
+                    state.isEdit = true;
+                    emit('edit', state.isEdit);
+                  }}>
                   <img src={iconEdit} class={styles.iconEdit} />
                   编辑资源
                 </NButton>
@@ -135,25 +146,21 @@ export default defineComponent({
         </div>
         <NForm labelAlign="left" labelPlacement="left">
           <NFormItem label="乐器:">
-            <NSpace>
-              <NButton secondary strong type="primary">
-                全部
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                竖笛
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                排萧
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                口风琴
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                陶笛
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                葫芦丝
-              </NButton>
+            <NSpace class={styles.spaceSection}>
+              {catchStore.getSubjectAllList.map((subject: any) => (
+                <NButton
+                  secondary={forms.subjectId === subject.id}
+                  quaternary={forms.subjectId !== subject.id}
+                  strong
+                  focusable={false}
+                  type={forms.subjectId === subject.id ? 'primary' : 'default'}
+                  onClick={() => {
+                    forms.subjectId = subject.id;
+                    onSearch();
+                  }}>
+                  {subject.name}
+                </NButton>
+              ))}
             </NSpace>
           </NFormItem>
 

+ 154 - 0
src/views/natural-resources/components/my-resources /upload-modal/index.module.less

@@ -0,0 +1,154 @@
+.uploadModal {
+  padding-top: 40px;
+}
+
+.formModal {
+  min-height: 45vh;
+  padding: 0 40px 0;
+
+  .formSpace {
+    gap: 40px 24px !important;
+  }
+
+  .previewModal {
+    position: relative;
+    width: 320px;
+    height: 180px;
+    margin-bottom: 12px;
+    border: 1px solid #DCE2F1;
+    border-radius: 10px;
+    overflow: hidden;
+
+    &:hover {
+      border-color: #198CFE;
+    }
+
+    .image {
+      width: 320px;
+      height: 180px;
+
+      img {
+        width: inherit;
+        height: inherit;
+      }
+    }
+
+    .titleType {
+      position: absolute;
+      top: 6px;
+      left: 6px;
+      width: 54px;
+      height: 26px;
+      z-index: 99;
+    }
+
+    .iconUploadDelete {
+      position: absolute;
+      top: 7px;
+      right: 7px;
+      width: 27px;
+      height: 27px;
+      z-index: 99;
+      cursor: pointer;
+    }
+
+    .commonType {
+      display: flex;
+      align-items: center;
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      font-size: 12px;
+      font-weight: 600;
+      color: #FFFFFF;
+      height: 43px;
+      background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%);
+      width: 100%;
+      border-radius: 0 0 10px 10px;
+      justify-content: flex-end;
+      padding-right: 12px;
+      z-index: 99;
+
+      :global {
+        .n-switch__rail {
+          background-color: #ccc;
+        }
+
+        .n-switch.n-switch--active .n-switch__rail {
+          background-color: var(--n-rail-color-active);
+        }
+      }
+    }
+  }
+
+  .formItem {
+    width: 320px;
+
+    :global {
+      .n-input {
+        margin-bottom: 12px;
+      }
+
+      .n-input,
+      .n-base-selection {
+        border-radius: 8px;
+
+        .n-input__input-el,
+        .n-input__input-el::placeholder {
+          font-size: 18px;
+
+        }
+      }
+
+      .n-form-item-feedback-wrapper {
+        min-height: 12px;
+      }
+
+      .n-upload-trigger+.n-upload-file-list {
+        margin-top: 12px;
+      }
+    }
+  }
+}
+
+.btnGroup {
+  padding: 32px 0;
+
+  :global {
+    .n-button {
+      height: 47px;
+      min-width: 156px;
+    }
+  }
+}
+
+
+
+.uploadFile {
+  margin-bottom: 12px;
+
+  .uploadBtn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-direction: column;
+    width: 320px;
+    height: 180px;
+    padding-top: 20px;
+    background: #F9FAFD;
+    border-radius: 10px;
+    border: 1px solid #DCE2F1;
+    font-size: 18px;
+    color: #9EADD9;
+
+    &:hover {
+      border-color: #198CFE;
+    }
+
+    .iconUploadAdd {
+      width: 50px;
+      height: 50px;
+      margin-bottom: 20px;
+    }
+  }
+}

+ 328 - 3
src/views/natural-resources/components/my-resources /upload-modal/index.tsx

@@ -1,8 +1,333 @@
-import { defineComponent } from 'vue';
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref
+} from 'vue';
+import styles from './index.module.less';
+import {
+  NButton,
+  NForm,
+  NFormItem,
+  NImage,
+  NInput,
+  NScrollbar,
+  NSelect,
+  NSpace,
+  NSwitch,
+  useMessage
+} from 'naive-ui';
+import UploadFile from './upload-file';
+import { useCatchStore } from '/src/store/modules/catchData';
+import iconImage from '@common/images/icon-image.png';
+import iconVideo from '@common/images/icon-video.png';
+import iconAudio from '@common/images/icon-audio.png';
+import iconMusic from '@common/images/icon-music.png';
+import iconUploadDelete from '../../../images/icon-upload-delete.svg';
+import { materialSave, materialUpdateAll } from '../../../api';
+import { NaturalTypeEnum, PageEnum } from '@/enums/pageEnum';
+import { scrollToErrorForm } from '/src/utils';
+
+// 判断链接后辍
+export const formatUrlType = (url: string) => {
+  if (url.indexOf('.mp3') > -1) {
+    return NaturalTypeEnum.SONG;
+  } else if (url.indexOf('.mp4') > -1) {
+    return NaturalTypeEnum.VIDEO;
+  } else {
+    return NaturalTypeEnum.IMG;
+  }
+};
 
 export default defineComponent({
   name: 'upload-modal',
-  setup() {
-    return () => <>上传</>;
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    }
+  },
+  emits: ['close', 'confirm'],
+  setup(props, { emit }) {
+    const catchStore = useCatchStore();
+    const formRef = ref();
+    const message = useMessage();
+
+    const uploadRef = ref();
+    const uploadForms = reactive({
+      list: [] as any[],
+      uploading: false,
+      uploadUrl: '',
+      name: '',
+      subjectIds: [] as any
+    });
+
+    // 判断类型
+    const formatType = (type: string) => {
+      let typeImg = iconImage;
+      switch (type) {
+        case 'IMG':
+          typeImg = iconImage;
+          break;
+        case 'VIDEO':
+          typeImg = iconVideo;
+          break;
+        case 'SONG':
+          typeImg = iconAudio;
+          break;
+        case 'MUSIC':
+          typeImg = iconMusic;
+          break;
+      }
+      return typeImg;
+    };
+
+    const onSubmit = async () => {
+      //
+      formRef.value?.validate(async (err: any) => {
+        if (err) {
+          nextTick(scrollToErrorForm);
+          return;
+        }
+        uploadForms.uploading = true;
+        try {
+          const body: any[] = [];
+          uploadForms.list.forEach(item => {
+            body.push({
+              subjectIds: item.subjectIds.join(','),
+              openFlag: item.openFlag,
+              coverImg: item.coverImg,
+              name: item.name,
+              type: item.type,
+              enableFlag: 1,
+              content: item.content,
+              id: item.id || null
+            });
+          });
+          if (isUpdate.value) {
+            await materialUpdateAll(body);
+          } else {
+            await materialSave(body);
+          }
+          uploadForms.list = [];
+          message.success('保存成功');
+          emit('close', true);
+          emit('confirm');
+        } catch {
+          //
+        }
+        uploadForms.uploading = false;
+      });
+    };
+
+    const handleRemove = (index: number) => {
+      uploadForms.list.splice(index, 1);
+    };
+
+    const getImgBase64 = (url: any) => {
+      return new Promise(function (resolve) {
+        let dataURL = '';
+        const video = document.createElement('video');
+        video.currentTime = 1; //指定帧数
+        video.setAttribute('crossOrigin', 'anonymous'); //处理跨域
+        video.setAttribute('src', url);
+        video.setAttribute('width', '100');
+        video.setAttribute('height', '100');
+
+        video.addEventListener('loadeddata', function () {
+          const canvas: any = document.createElement('canvas'),
+            width = video.width, //canvas的尺寸和图片一样
+            height = video.height;
+          canvas.width = width;
+          canvas.height = height;
+          canvas.getContext('2d').drawImage(video, 0, 0, width, height); //绘制canvas
+          dataURL = canvas.toDataURL('image/jpeg'); //转换为base64
+          resolve(dataURL);
+        });
+      });
+    };
+
+    const isUpdate = computed(() => (props.list.length > 0 ? true : false));
+
+    onMounted(async () => {
+      // console.log(props.list, 'props.list');
+      const list = props.list || [];
+      const temps: any[] = [];
+      list.forEach((item: any) => {
+        temps.push({
+          subjectIds: item.subjectId
+            ? item.subjectId
+                .split(',')
+                .map((subjectId: any) => Number(subjectId))
+            : [],
+          openFlag: item.openFlag,
+          coverImg: item.coverImg,
+          name: item.title,
+          type: item.type,
+          enableFlag: item.enableFlag,
+          content: item.content,
+          id: item.id
+        });
+      });
+      console.log(temps, 'temp', list);
+      uploadForms.list = temps || [];
+
+      await catchStore.getSubjects();
+    });
+    return () => (
+      <div class={styles.uploadModal}>
+        <NScrollbar style={{ 'max-height': '55vh' }}>
+          <NForm
+            ref={formRef}
+            labelPlacement="left"
+            labelWidth={120}
+            model={uploadForms}
+            class={styles.formModal}>
+            <NSpace class={styles.formSpace}>
+              {uploadForms.list.map((item: any, index: number) => (
+                <div class={styles.formItem} key={index}>
+                  <div class={styles.previewModal}>
+                    <NImage
+                      class={[styles.titleType]}
+                      src={formatType('MUSIC')}
+                      previewDisabled
+                      objectFit="cover"
+                    />
+                    {/* 编辑时不能删除 */}
+                    {!isUpdate.value && (
+                      <img
+                        class={[styles.iconUploadDelete]}
+                        src={iconUploadDelete}
+                        onClick={() => handleRemove(index)}
+                      />
+                    )}
+
+                    <NImage
+                      class={[styles.cover, styles.image]}
+                      lazy
+                      previewDisabled
+                      src={item.coverImg}
+                      objectFit="cover"
+                    />
+
+                    <div class={styles.commonType}>
+                      公开资源:
+                      <NSwitch size="small" v-model:value={item.openFlag} />
+                    </div>
+                  </div>
+                  <NFormItem
+                    showFeedback={false}
+                    path={`list.${index}.name`}
+                    rule={[
+                      {
+                        required: true,
+                        message: '请输入资源名称',
+                        trigger: ['input', 'blur']
+                      }
+                    ]}>
+                    <NInput
+                      v-model:value={item.name}
+                      placeholder="请输入资源名称"
+                      maxlength={25}
+                      clearable
+                    />
+                  </NFormItem>
+                  <NFormItem
+                    path={`list[${index}].subjectIds`}
+                    showFeedback={false}
+                    rule={[
+                      {
+                        required: true,
+                        message: '请选择素材可用乐器',
+                        trigger: 'change',
+                        type: 'array'
+                      }
+                    ]}>
+                    <NSelect
+                      v-model:value={item.subjectIds}
+                      placeholder="请选择素材可用乐器(可多选)"
+                      options={catchStore.getSubjectList}
+                      labelField="name"
+                      valueField="id"
+                      multiple
+                      maxTagCount={2}
+                      clearable
+                    />
+                  </NFormItem>
+                </div>
+              ))}
+
+              {!isUpdate.value && (
+                <div class={styles.formItem}>
+                  <UploadFile
+                    v-model:fileList={uploadForms.uploadUrl}
+                    accept=".jpg,jpeg,.png,audio/mp3,video/mp4"
+                    showFileList={false}
+                    ref={uploadRef}
+                    cropper
+                    options={{
+                      autoCropWidth: 320,
+                      autoCropHeight: 180,
+                      fixedBox: true
+                    }}
+                    onFinished={(val: any) => {
+                      uploadForms.list.push({
+                        subjectIds: uploadForms.subjectIds || [],
+                        openFlag: true,
+                        coverImg: val.coverImg,
+                        name: uploadForms.name || '',
+                        type: formatUrlType(val.content),
+                        enableFlag: 1,
+                        content: val.content
+                      });
+                      uploadForms.uploadUrl = '';
+                      uploadForms.name = '';
+                      uploadForms.subjectIds = [];
+                      uploadRef.value.handleClearFile();
+                    }}
+                  />
+                  <NFormItem showFeedback={false}>
+                    <NInput
+                      v-model:value={uploadForms.name}
+                      placeholder="请输入资源名称"
+                      maxlength={25}
+                      clearable
+                    />
+                  </NFormItem>
+                  <NFormItem showFeedback={false}>
+                    <NSelect
+                      v-model:value={uploadForms.subjectIds}
+                      placeholder="请选择素材可用乐器(可多选)"
+                      options={catchStore.getSubjectList}
+                      labelField="name"
+                      valueField="id"
+                      multiple
+                      maxTagCount={2}
+                      clearable
+                    />
+                  </NFormItem>
+                </div>
+              )}
+            </NSpace>
+          </NForm>
+        </NScrollbar>
+
+        <NSpace class={styles.btnGroup} justify="center">
+          <NButton round onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            round
+            type="primary"
+            loading={uploadForms.uploading}
+            disabled={uploadForms.list.length === 0}
+            onClick={onSubmit}>
+            确定
+          </NButton>
+        </NSpace>
+      </div>
+    );
   }
 });

+ 419 - 0
src/views/natural-resources/components/my-resources /upload-modal/upload-file.tsx

@@ -0,0 +1,419 @@
+import { NModal, NUpload, UploadFileInfo, useMessage } from 'naive-ui';
+import { defineComponent, watch, PropType, reactive, ref } from 'vue';
+import { policy } from '@/components/upload-file/api';
+import Copper from '@/components/upload-file/copper';
+import axios from 'axios';
+import styles from './index.module.less';
+import iconUploadAdd from '../../../images/icon-upload-add.svg';
+import { NaturalTypeEnum, PageEnum } from '@/enums/pageEnum';
+import { formatUrlType } from '.';
+
+/**
+ * 1. 图片上传可以进行裁剪
+ * 2. 视频上传可以选择某一帧做为封面
+ * 3. 音频只用限制某一种格式
+ * 4. 只支持单个上传,因为多个上传没有办法去处理,即有视频,图片等
+ */
+export default defineComponent({
+  name: 'upload-file',
+  props: {
+    fileList: {
+      type: String,
+      default: ''
+    },
+    imageList: {
+      type: Array,
+      default: () => []
+    },
+    accept: {
+      // 支持类型
+      type: String,
+      default: '.jpg,.png,.jpeg,.gif'
+    },
+    showType: {
+      type: String as PropType<'default' | 'custom'>,
+      default: 'default'
+    },
+    showFileList: {
+      type: Boolean,
+      default: true
+    },
+    max: {
+      type: Number as PropType<number>,
+      default: 1
+    },
+    multiple: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    disabled: {
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    bucketName: {
+      type: String,
+      default: 'gyt'
+    },
+    path: {
+      type: String,
+      default: ''
+    },
+    fileName: {
+      type: String,
+      default: ''
+    },
+    cropper: {
+      // 是否裁切, 只有图片才支持
+      type: Boolean as PropType<boolean>,
+      default: false
+    },
+    options: {
+      type: Object,
+      default: () => {
+        return {
+          viewMode: 0,
+          autoCrop: true, //是否默认生成截图框
+          enlarge: 1, //  图片放大倍数
+          autoCropWidth: 200, //默认生成截图框宽度
+          autoCropHeight: 200, //默认生成截图框高度
+          fixedBox: false, //是否固定截图框大小 不允许改变
+          previewsCircle: true, //预览图是否是原图形
+          title: '上传图片'
+        };
+      }
+    }
+  },
+  emits: [
+    'update:fileList',
+    'close',
+    'readFileInputEventAsArrayBuffer',
+    'remove',
+    'finished'
+  ],
+  setup(props, { emit, expose, slots }) {
+    const ossUploadUrl = `https://${props.bucketName}.ks3-cn-beijing.ksyuncs.com/`;
+    const message = useMessage();
+    const visiable = ref<boolean>(false);
+    const btnLoading = ref<boolean>(false);
+    const tempFiileBuffer = ref();
+    const uploadRef = ref();
+    const state = reactive({
+      policy: '',
+      signature: '',
+      key: '',
+      KSSAccessKeyId: '',
+      acl: 'public-read',
+      name: ''
+    }) as any;
+
+    const fileListRef = ref<UploadFileInfo[]>([]);
+    const initFileList = () => {
+      if (props.fileList) {
+        const splitName = props.fileList.split('/');
+        fileListRef.value = [
+          {
+            id: new Date().getTime().toString(),
+            name: splitName[splitName.length - 1],
+            status: 'finished',
+            url: props.fileList
+          }
+        ];
+      } else {
+        fileListRef.value = [];
+      }
+    };
+    initFileList();
+    watch(
+      () => props.imageList,
+      () => {
+        initFileList();
+      }
+    );
+    watch(
+      () => props.fileList,
+      () => {
+        // console.log('list');
+        initFileList();
+      }
+    );
+    const handleClearFile = () => {
+      console.log(uploadRef.value, 'uploadRef.value');
+      uploadRef.value?.clear();
+    };
+    expose({
+      handleClearFile
+    });
+
+    const getVideoMsg = (file: any) => {
+      return new Promise(resolve => {
+        let dataURL = '';
+        const videoElement = document.createElement('video');
+        videoElement.src = URL.createObjectURL(file);
+        videoElement.addEventListener('loadedmetadata', function () {
+          const canvas: any = document.createElement('canvas'),
+            width = videoElement.videoWidth, //canvas的尺寸和图片一样
+            height = videoElement.videoHeight;
+          canvas.width = width;
+          canvas.height = height;
+          canvas.getContext('2d').drawImage(videoElement, 0, 0, width, height); //绘制canvas
+          dataURL = canvas.toDataURL('image/jpeg'); //转换为base64
+          resolve(dataURL);
+          // resolve({
+          //   duration: videoElement.duration,
+          //   height: videoElement.videoHeight,
+          //   width: videoElement.videoWidth
+          // });
+        });
+      });
+    };
+
+    const CropperModal = ref();
+    const onBeforeUpload = async (options: any) => {
+      const file = options.file;
+      // 文件大小
+      let isLt2M = true;
+      console.log(file, 'file');
+
+      const type = file.type.includes('image')
+        ? NaturalTypeEnum.IMG
+        : file.type.includes('audio')
+        ? NaturalTypeEnum.SONG
+        : NaturalTypeEnum.VIDEO;
+
+      const size = type === 'IMG' ? 2 : type === 'SONG' ? 20 : 500;
+      if (size) {
+        isLt2M = file.file.size / 1024 / 1024 < size;
+        if (!isLt2M) {
+          message.error(`文件大小不能超过${size}M`);
+          return false;
+        }
+      }
+
+      if (!isLt2M) {
+        return isLt2M;
+      }
+      // 是否裁切
+      if (props.cropper && type === 'IMG') {
+        getBase64(file.file, (imageUrl: any) => {
+          const target = Object.assign({}, props.options, {
+            img: imageUrl,
+            name: file.file.name // 上传文件名
+          });
+          visiable.value = true;
+
+          setTimeout(() => {
+            CropperModal.value?.edit(target);
+            // console.log(CropperModal.value, 'cropper');
+          }, 100);
+        });
+        return false;
+      }
+
+      try {
+        btnLoading.value = true;
+        const name = file.file.name;
+        const suffix = name.slice(name.lastIndexOf('.'));
+        const fileName = `${props.path}${
+          props.fileName || Date.now() + suffix
+        }`;
+        const obj = {
+          filename: fileName,
+          bucketName: props.bucketName,
+          postData: {
+            filename: fileName,
+            acl: 'public-read',
+            key: fileName,
+            unknowValueField: []
+          }
+        };
+        const { data } = await policy(obj);
+
+        state.policy = data.policy;
+        state.signature = data.signature;
+        state.key = fileName;
+        state.KSSAccessKeyId = data.kssAccessKeyId;
+        state.name = fileName;
+
+        tempFiileBuffer.value = file.file;
+      } catch {
+        //
+        // message.error('上传失败')
+        btnLoading.value = false;
+        return false;
+      }
+      return true;
+    };
+    const getBase64 = async (img: any, callback: any) => {
+      const reader = new FileReader();
+      reader.addEventListener('load', () => callback(reader.result));
+      reader.readAsDataURL(img);
+    };
+    const onFinish = async (options: any) => {
+      const url = ossUploadUrl + state.key;
+      const type = formatUrlType(url);
+      let coverImg = '';
+      if (type === 'IMG') {
+        coverImg = url;
+      } else if (type === 'SONG') {
+        coverImg = PageEnum.SONG_DEFAULT_COVER;
+      } else if (type === 'VIDEO') {
+        // 获取视频封面图
+        await getVideoCoverImg();
+      }
+      emit('finished', {
+        cover: coverImg,
+        content: url
+      });
+      emit('update:fileList', url);
+      emit('readFileInputEventAsArrayBuffer', tempFiileBuffer.value);
+      options.file.url = url;
+      visiable.value = false;
+      btnLoading.value = false;
+    };
+
+    const getVideoCoverImg = async () => {
+      try {
+        btnLoading.value = true;
+        const imgBlob: any = await getVideoMsg(tempFiileBuffer.value);
+        const fileName = `${props.path}${Date.now() + '.png'}`;
+        const obj = {
+          filename: fileName,
+          bucketName: props.bucketName,
+          postData: {
+            filename: fileName,
+            acl: 'public-read',
+            key: fileName,
+            unknowValueField: []
+          }
+        };
+        const { data } = await policy(obj);
+
+        const fileParams = {
+          policy: data.policy,
+          signature: data.signature,
+          key: fileName,
+          KSSAccessKeyId: data.KSSAccessKeyId,
+          name: fileName
+        } as any;
+        const formData = new FormData();
+        for (const key in fileParams) {
+          formData.append(key, fileParams[key]);
+        }
+        formData.append('file', imgBlob);
+
+        await axios.post(ossUploadUrl, formData);
+
+        const url = ossUploadUrl + state.key;
+        console.log(url, 'url');
+        return url;
+      } catch {
+        //
+        // message.error('上传失败')
+        // btnLoading.value = false;
+        // return false;
+      } finally {
+        btnLoading.value = false;
+      }
+    };
+
+    const onRemove = async (options: any) => {
+      emit('update:fileList', '');
+      emit('remove');
+      btnLoading.value = false;
+    };
+
+    // 裁切失败
+    // const cropperNo = () => {}
+    // 裁切成功
+    const cropperOk = async (blob: any) => {
+      try {
+        const fileName = `${props.path}${
+          props.fileName || new Date().getTime() + '.png'
+        }`;
+        const obj = {
+          filename: fileName,
+          bucketName: props.bucketName,
+          postData: {
+            filename: fileName,
+            acl: 'public-read',
+            key: fileName,
+            unknowValueField: []
+          }
+        };
+        const { data } = await policy(obj);
+
+        state.policy = data.policy;
+        state.signature = data.signature;
+        state.key = fileName;
+        state.KSSAccessKeyId = data.kssAccessKeyId;
+        state.name = fileName;
+
+        const formData = new FormData();
+        for (const key in state) {
+          formData.append(key, state[key]);
+        }
+        formData.append('file', blob);
+
+        await axios.post(ossUploadUrl, formData).then(() => {
+          const url = ossUploadUrl + state.key;
+          console.log(url, 'url');
+          const splitName = url.split('/');
+          fileListRef.value = [
+            {
+              id: new Date().getTime().toString(),
+              name: splitName[splitName.length - 1],
+              status: 'finished',
+              url: url
+            }
+          ];
+          emit('update:fileList', url);
+          visiable.value = false;
+        });
+      } catch {
+        return false;
+      }
+    };
+
+    return () => (
+      <div class={styles.uploadFile}>
+        <NUpload
+          ref={uploadRef}
+          action={ossUploadUrl}
+          data={state}
+          v-model:fileList={fileListRef.value}
+          accept={props.accept}
+          multiple={props.multiple}
+          max={props.max}
+          disabled={props.disabled}
+          showFileList={props.showFileList}
+          showPreviewButton
+          onBeforeUpload={(options: any) => onBeforeUpload(options)}
+          onFinish={(options: any) => onFinish(options)}
+          onRemove={(options: any) => onRemove(options)}>
+          {props.showType === 'default' && (
+            <div class={styles.uploadBtn}>
+              <img src={iconUploadAdd} class={styles.iconUploadAdd} />
+              <p>上传</p>
+            </div>
+          )}
+          {props.showType === 'custom' && slots.custom && slots.custom()}
+        </NUpload>
+
+        <NModal
+          v-model:show={visiable.value}
+          preset="dialog"
+          showIcon={false}
+          class={['modalTitle background']}
+          title="上传图片"
+          style={{ width: '800px' }}>
+          {/* @cropper-no="error" @cropper-ok="success" */}
+          <Copper
+            ref={CropperModal}
+            onClose={() => (visiable.value = false)}
+            onCropperOk={cropperOk}
+          />
+        </NModal>
+      </div>
+    );
+  }
+});

+ 8 - 0
src/views/natural-resources/components/share-resources/index.module.less

@@ -115,3 +115,11 @@
 .teachingModal {
   width: 1100px;
 }
+
+.spaceSection {
+  width: 76%;
+
+  &>div {
+    line-height: var(--n-blank-height);
+  }
+}

+ 42 - 24
src/views/natural-resources/components/share-resources/index.tsx

@@ -3,10 +3,11 @@ import styles from './index.module.less';
 import CardType from '/src/components/card-type';
 import Pagination from '/src/components/pagination';
 import SearchGroupResources from './search-group-resources';
-import { materialQueryPage } from '../../api';
+import { favorite, materialQueryPage } from '../../api';
 import { NModal, NSpin } from 'naive-ui';
 import TheEmpty from '/src/components/TheEmpty';
 import CardPreview from '/src/components/card-preview';
+import AddTeaching from '../../model/add-teaching';
 
 export default defineComponent({
   name: 'share-resources',
@@ -40,7 +41,20 @@ export default defineComponent({
         });
         state.loading = false;
         state.pageTotal = Number(data.total);
-        state.tableList = data.rows || [];
+        const tempRows = data.rows || [];
+        const temp: any = [];
+        tempRows.forEach((row: any) => {
+          temp.push({
+            id: row.id,
+            coverImg: row.coverImg,
+            type: row.type,
+            title: row.name,
+            isCollect: !!row.favoriteFlag,
+            isSelected: row.sourceFrom === 'PLATFORM' ? true : false,
+            content: row.content
+          });
+        });
+        state.tableList = temp || [];
       } catch {
         state.loading = false;
       }
@@ -49,10 +63,23 @@ export default defineComponent({
     const onSearch = async (item: any) => {
       state.pagination.page = 1;
       state.searchGroup = Object.assign(state.searchGroup, item);
-      console.log(item, state.searchGroup);
       getList();
     };
 
+    // 收藏
+    const onCollect = async (item: any) => {
+      try {
+        await favorite({
+          materialId: item.id,
+          favoriteFlag: item.isCollect ? 0 : 1,
+          type: item.type
+        });
+        item.isCollect = !item.isCollect;
+      } catch {
+        //
+      }
+    };
+
     onMounted(() => {
       getList();
     });
@@ -65,26 +92,17 @@ export default defineComponent({
 
         <NSpin v-model:show={state.loading}>
           <div class={styles.list}>
-            {state.tableList.map((item: any) => {
-              const tmpItem = {
-                id: item.id,
-                coverImg: item.coverImg,
-                type: item.type,
-                title: item.name,
-                isCollect: !!item.favoriteFlag,
-                isSelected: item.sourceFrom === 'PLATFORM' ? true : false,
-                content: item.content
-              };
-              return (
-                <CardType
-                  item={tmpItem}
-                  onClick={(val: any) => {
-                    state.show = true;
-                    state.item = val;
-                  }}
-                />
-              );
-            })}
+            {state.tableList.map((item: any) => (
+              <CardType
+                item={item}
+                onClick={(val: any) => {
+                  if (val.type === 'IMG') return;
+                  state.show = true;
+                  state.item = val;
+                }}
+                onCollect={(item: any) => onCollect(item)}
+              />
+            ))}
 
             {!state.loading && state.tableList.length <= 0 && (
               <TheEmpty description="暂无共享资源" />
@@ -110,7 +128,7 @@ export default defineComponent({
           class={['modalTitle background', styles.teachingModal]}
           title={'自定义教材'}
           blockScroll={false}>
-          1212
+          <AddTeaching onClose={() => (state.teachingStatus = false)} />
         </NModal>
       </>
     );

+ 47 - 39
src/views/natural-resources/components/share-resources/search-group-resources.tsx

@@ -1,14 +1,16 @@
-import { defineComponent, onMounted, reactive, ref } from 'vue';
+import { defineComponent, onMounted, reactive } from 'vue';
 import styles from './index.module.less';
 import { NButton, NForm, NFormItem, NSpace } from 'naive-ui';
 import iconAdd from '../../images/icon-add.svg';
 import TheSearch from '/src/components/TheSearch';
 import { resourceTypeArray } from '/src/utils/searchArray';
+import { useCatchStore } from '/src/store/modules/catchData';
 
 export default defineComponent({
   name: 'search-group',
   emits: ['search', 'add'],
   setup(props, { emit }) {
+    const catchStore = useCatchStore();
     const forms = reactive({
       type: 'MUSIC', //
       keyword: '',
@@ -20,6 +22,12 @@ export default defineComponent({
       emit('search', forms);
     };
 
+    onMounted(async () => {
+      // 获取教材分类列表
+      await catchStore.getMusicSheetCategory();
+      // 获取声部列表
+      await catchStore.getSubjects();
+    });
     return () => (
       <div class={styles.searchGroup}>
         <div class={styles.searchCatatory}>
@@ -51,45 +59,45 @@ export default defineComponent({
           </NButton>
         </div>
         <NForm labelAlign="left" labelPlacement="left">
-          <NFormItem label="教材:">
-            <NSpace>
-              <NButton secondary strong type="primary" focusable={false}>
-                全部
-              </NButton>
-              <NButton quaternary focusable={false}>
-                人教版
-              </NButton>
-              <NButton quaternary focusable={false}>
-                声部训练
-              </NButton>
-              <NButton quaternary focusable={false}>
-                小曲目
-              </NButton>
-              <NButton quaternary focusable={false}>
-                考级曲目
-              </NButton>
-            </NSpace>
-          </NFormItem>
+          {forms.type === 'MUSIC' && (
+            <NFormItem label="教材:">
+              <NSpace class={styles.spaceSection}>
+                {catchStore.getAllMusicCategories.map((music: any) => (
+                  <NButton
+                    secondary={forms.bookVersionId === music.id}
+                    quaternary={forms.bookVersionId !== music.id}
+                    strong
+                    focusable={false}
+                    type={
+                      forms.bookVersionId === music.id ? 'primary' : 'default'
+                    }
+                    onClick={() => {
+                      forms.bookVersionId = music.id;
+                      onSearch();
+                    }}>
+                    {music.name}
+                  </NButton>
+                ))}
+              </NSpace>
+            </NFormItem>
+          )}
+
           <NFormItem label="乐器:">
-            <NSpace>
-              <NButton secondary strong type="primary">
-                全部
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                竖笛
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                排萧
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                口风琴
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                陶笛
-              </NButton>
-              <NButton quaternary focusable={false} type="default">
-                葫芦丝
-              </NButton>
+            <NSpace class={styles.spaceSection}>
+              {catchStore.getSubjectAllList.map((subject: any) => (
+                <NButton
+                  secondary={forms.subjectId === subject.id}
+                  quaternary={forms.subjectId !== subject.id}
+                  strong
+                  focusable={false}
+                  type={forms.subjectId === subject.id ? 'primary' : 'default'}
+                  onClick={() => {
+                    forms.subjectId = subject.id;
+                    onSearch();
+                  }}>
+                  {subject.name}
+                </NButton>
+              ))}
             </NSpace>
           </NFormItem>
 

+ 21 - 0
src/views/natural-resources/images/btn-add.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 4</title>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="37、资源-自定义教材(已填写)备份" transform="translate(-1148.000000, -647.000000)">
+            <g id="编组-5" transform="translate(410.000000, 74.000000)">
+                <g id="编组-13" transform="translate(66.000000, 528.000000)">
+                    <g id="编组-12" transform="translate(332.000000, 44.000000)">
+                        <g id="编组-4" transform="translate(340.000000, 1.000000)">
+                            <circle id="椭圆形" fill="#E0F0FF" cx="25" cy="25" r="25"></circle>
+                            <g id="编组-3" transform="translate(14.000000, 14.000000)" fill="#198CFE">
+                                <rect id="矩形" x="0" y="9.5" width="22" height="3" rx="1.5"></rect>
+                                <rect id="矩形" transform="translate(11.000000, 11.000000) rotate(-270.000000) translate(-11.000000, -11.000000) " x="-3.63797881e-12" y="9.5" width="22" height="3" rx="1.5"></rect>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 27 - 0
src/views/natural-resources/images/btn-delete.svg

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>删除</title>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="36、资源-自定义教材(未填写)" transform="translate(-1236.000000, -646.000000)">
+            <g id="编组-5" transform="translate(410.000000, 74.000000)">
+                <g id="编组-13" transform="translate(40.000000, 527.000000)">
+                    <g id="编组-12" transform="translate(352.000000, 44.000000)">
+                        <g id="删除" transform="translate(434.000000, 1.000000)">
+                            <rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="0" y="0" width="50" height="50"></rect>
+                            <g id="编组-6">
+                                <path d="M0,25 C0,38.8071187 11.1928813,50 25,50 C38.8071187,50 50,38.8071187 50,25 C50,11.1928813 38.8071187,0 25,0 C11.1928813,0 0,11.1928813 0,25 Z" id="路径" fill="#FDEBED" fill-rule="nonzero"></path>
+                                <g id="编组-11" transform="translate(13.500000, 12.000000)" fill="#FF4B5F">
+                                    <g id="编组-10">
+                                        <rect id="矩形" x="0" y="3" width="23" height="3" rx="1"></rect>
+                                        <path d="M7.5,0 L15.5,0 C16.3284271,-1.52179594e-16 17,0.671572875 17,1.5 L17,3 L17,3 L6,3 L6,1.5 C6,0.671572875 6.67157288,1.52179594e-16 7.5,0 Z" id="矩形备份"></path>
+                                    </g>
+                                    <path d="M19,7.5 C20.1045695,7.5 21,8.3954305 21,9.5 L21,23.5 C21,24.6045695 20.1045695,25.5 19,25.5 L4,25.5 C2.8954305,25.5 2,24.6045695 2,23.5 L2,9.5 C2,8.3954305 2.8954305,7.5 4,7.5 L19,7.5 Z M8.5,12.5 C7.94771525,12.5 7.5,12.9477153 7.5,13.5 L7.5,13.5 L7.5,19.5 C7.5,20.0522847 7.94771525,20.5 8.5,20.5 C9.05228475,20.5 9.5,20.0522847 9.5,19.5 L9.5,19.5 L9.5,13.5 C9.5,12.9477153 9.05228475,12.5 8.5,12.5 Z M14.5,12.5 C13.9477153,12.5 13.5,12.9477153 13.5,13.5 L13.5,13.5 L13.5,19.5 C13.5,20.0522847 13.9477153,20.5 14.5,20.5 C15.0522847,20.5 15.5,20.0522847 15.5,19.5 L15.5,19.5 L15.5,13.5 C15.5,12.9477153 15.0522847,12.5 14.5,12.5 Z" id="形状结合"></path>
+                                </g>
+                            </g>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 18 - 0
src/views/natural-resources/images/btn-down.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 8</title>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="37、资源-自定义教材(已填写)备份" transform="translate(-1394.000000, -647.000000)">
+            <g id="编组-5" transform="translate(410.000000, 74.000000)">
+                <g id="编组-13" transform="translate(66.000000, 528.000000)">
+                    <g id="编组-12" transform="translate(332.000000, 44.000000)">
+                        <g id="编组-8" transform="translate(586.000000, 1.000000)">
+                            <circle id="椭圆形备份-3" fill="#E0F0FF" cx="25" cy="25" r="25"></circle>
+                            <path d="M26.3448191,19.8339249 L34.0278003,31.9649479 C34.3233015,32.4315286 34.184614,33.049318 33.7180334,33.3448191 C33.5579828,33.4461844 33.3724308,33.5 33.1829813,33.5 L17.8170187,33.5 C17.264734,33.5 16.8170187,33.0522847 16.8170187,32.5 C16.8170187,32.3105505 16.8708343,32.1249985 16.9721997,31.9649479 L24.6551809,19.8339249 C24.950682,19.3673442 25.5684714,19.2286568 26.0350521,19.5241579 C26.1599336,19.6032495 26.2657275,19.7090434 26.3448191,19.8339249 Z" id="三角形备份" fill="#198CFE" transform="translate(25.500000, 26.000000) rotate(-180.000000) translate(-25.500000, -26.000000) "></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 23 - 0
src/views/natural-resources/images/btn-remove.svg

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="44px" height="44px" viewBox="0 0 44 44" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <defs>
+        <linearGradient x1="55.6886253%" y1="7.14706072e-14%" x2="50%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" stop-opacity="0.522426792" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="37、资源-自定义教材(已填写)备份" transform="translate(-1437.000000, -565.000000)">
+            <g id="编组-5" transform="translate(410.000000, 74.000000)">
+                <g id="删除" transform="translate(1027.000000, 491.000000)">
+                    <circle id="椭圆形" stroke="url(#linearGradient-1)" stroke-width="1.62962963" fill="#EA4132" stroke-linecap="round" stroke-linejoin="round" cx="22" cy="22" r="21.1851852"></circle>
+                    <g id="编组-6" transform="translate(22.000000, 22.000000) rotate(-315.000000) translate(-22.000000, -22.000000) translate(11.407407, 11.407407)" fill="#FFFFFF">
+                        <rect id="矩形" x="8.96296296" y="0" width="3.25925926" height="21.1851852" rx="1.62962963"></rect>
+                        <rect id="矩形" transform="translate(10.592593, 10.592593) rotate(-270.000000) translate(-10.592593, -10.592593) " x="8.96296296" y="2.96427903e-12" width="3.25925926" height="21.1851852" rx="1.62962963"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 18 - 0
src/views/natural-resources/images/btn-up.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 7</title>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="37、资源-自定义教材(已填写)备份" transform="translate(-1312.000000, -647.000000)">
+            <g id="编组-5" transform="translate(410.000000, 74.000000)">
+                <g id="编组-13" transform="translate(66.000000, 528.000000)">
+                    <g id="编组-12" transform="translate(332.000000, 44.000000)">
+                        <g id="编组-7" transform="translate(504.000000, 1.000000)">
+                            <circle id="椭圆形备份-2" fill="#E0F0FF" cx="25" cy="25" r="25"></circle>
+                            <path d="M26.3448191,18.3339249 L34.0278003,30.4649479 C34.3233015,30.9315286 34.184614,31.549318 33.7180334,31.8448191 C33.5579828,31.9461844 33.3724308,32 33.1829813,32 L17.8170187,32 C17.264734,32 16.8170187,31.5522847 16.8170187,31 C16.8170187,30.8105505 16.8708343,30.6249985 16.9721997,30.4649479 L24.6551809,18.3339249 C24.950682,17.8673442 25.5684714,17.7286568 26.0350521,18.0241579 C26.1599336,18.1032495 26.2657275,18.2090434 26.3448191,18.3339249 Z" id="三角形" fill="#198CFE"></path>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/views/natural-resources/images/icon-menu.png


+ 16 - 0
src/views/natural-resources/images/icon-upload-add.svg

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="50px" height="50px" viewBox="0 0 50 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组</title>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="41、我的资源-上传素材中" transform="translate(-1273.000000, -650.000000)" fill="#9EADD9">
+            <g id="编组-5" transform="translate(410.000000, 135.000000)">
+                <g id="编组-3备份" transform="translate(728.000000, 468.000000)">
+                    <g id="编组" transform="translate(135.000000, 47.000000)">
+                        <rect id="矩形" x="0" y="21" width="50" height="8" rx="4"></rect>
+                        <rect id="矩形" transform="translate(25.000000, 25.000000) rotate(-270.000000) translate(-25.000000, -25.000000) " x="-5.45696821e-12" y="21" width="50" height="8" rx="4"></rect>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 25 - 0
src/views/natural-resources/images/icon-upload-delete.svg

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="27px" height="27px" viewBox="0 0 27 27" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <defs>
+        <linearGradient x1="55.6886253%" y1="7.14706072e-14%" x2="50%" y2="100%" id="linearGradient-1">
+            <stop stop-color="#FFFFFF" offset="0%"></stop>
+            <stop stop-color="#FFFFFF" stop-opacity="0.522426792" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="所有页面" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="41、我的资源-上传素材中" transform="translate(-1080.000000, -252.000000)">
+            <g id="编组-5" transform="translate(410.000000, 135.000000)">
+                <g id="编组-3" transform="translate(384.000000, 110.000000)">
+                    <g id="删除" transform="translate(286.000000, 7.000000)">
+                        <circle id="椭圆形" stroke="url(#linearGradient-1)" fill="#EA4132" stroke-linecap="round" stroke-linejoin="round" cx="13.5" cy="13.5" r="13"></circle>
+                        <g id="编组-6" transform="translate(13.500000, 13.500000) rotate(-315.000000) translate(-13.500000, -13.500000) translate(7.000000, 7.000000)" fill="#FFFFFF">
+                            <rect id="矩形" x="5.5" y="0" width="2" height="13" rx="1"></rect>
+                            <rect id="矩形" transform="translate(6.500000, 6.500000) rotate(-270.000000) translate(-6.500000, -6.500000) " x="5.5" y="1.8189894e-12" width="2" height="13" rx="1"></rect>
+                        </g>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/views/natural-resources/images/icon-upload.png


+ 20 - 0
src/views/natural-resources/images/resource-checked.svg

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="39、我的资源-编辑资源" transform="translate(-952.000000, -356.000000)">
+            <g id="编组-7" transform="translate(164.000000, 340.000000)">
+                <g id="矩形备份" transform="translate(429.000000, 0.000000)">
+                    <g id="资源/编辑资源/未勾选" transform="translate(359.000000, 16.000000)">
+                        <g id="save"></g>
+                        <rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="0" y="0" width="30" height="30"></rect>
+                        <g id="表格/勾选/选中" fill="#198CFE" stroke="#FFFFFF">
+                            <rect id="Rectangle-271" x="0.5" y="0.5" width="29" height="29" rx="14.5"></rect>
+                        </g>
+                        <path d="M22.069225,9.85125 L14.171725,20.80125 C13.695475,21.465 12.709225,21.465 12.232975,20.80125 L7.55672495,14.32125 C7.41422495,14.1225 7.55672495,13.845 7.80047495,13.845 L9.55922495,13.845 C9.94172495,13.845 10.305475,14.02875 10.530475,14.34375 L13.200475,18.04875 L19.095475,9.87375 C19.320475,9.5625 19.680475,9.375 20.066725,9.375 L21.825475,9.375 C22.069225,9.375 22.211725,9.6525 22.069225,9.85125 Z" id="Path" fill="#FFFFFF"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/views/natural-resources/images/resource-default.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" fill-opacity="0.3">
+        <g id="39、我的资源-编辑资源" transform="translate(-1381.000000, -356.000000)" fill="#000000" stroke="#FFFFFF">
+            <g id="编组-7" transform="translate(164.000000, 340.000000)">
+                <g id="矩形备份-2" transform="translate(858.000000, 0.000000)">
+                    <g id="资源/编辑资源/未勾选" transform="translate(359.000000, 16.000000)">
+                        <circle id="椭圆形" cx="15" cy="15" r="14.5"></circle>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 7 - 4
src/views/natural-resources/index.tsx

@@ -10,7 +10,7 @@ export default defineComponent({
     return () => (
       <div class={styles.listWrap}>
         <NTabs
-          defaultValue="shareResources"
+          defaultValue="myResources"
           paneClass={styles.paneTitle}
           justifyContent="center"
           animated
@@ -18,19 +18,22 @@ export default defineComponent({
           <NTabPane
             name="shareResources"
             tab="共享资源"
-            displayDirective="show:lazy">
+            // displayDirective="show:lazy"
+          >
             <ShareResources />
           </NTabPane>
           <NTabPane
             name="myResources"
             tab="我的资源"
-            displayDirective="show:lazy">
+            // displayDirective="show:lazy"
+          >
             <MyResources />
           </NTabPane>
           <NTabPane
             name="myCollect"
             tab="我的收藏"
-            displayDirective="show:lazy">
+            // displayDirective="show:lazy"
+          >
             <MyCollect />
           </NTabPane>
         </NTabs>

+ 155 - 0
src/views/natural-resources/model/add-teaching/index.module.less

@@ -0,0 +1,155 @@
+.container {
+  min-height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  :global {
+
+    .n-base-selection--error-status,
+    .n-input--error-status {
+      animation: errorRotate 0.3s ease 3;
+    }
+  }
+}
+
+.closeBtn {
+  position: absolute;
+  top: -15px;
+  right: -10px;
+
+  img {
+    width: 44px;
+    height: 44px;
+  }
+}
+
+.topForms {
+  padding: 40px 40px 0;
+  display: flex;
+  align-items: center;
+
+  :global {
+    .n-upload {
+      width: 210px !important;
+    }
+
+    .n-upload-trigger.n-upload-trigger--image-card {
+      width: 210px !important;
+      height: 297px !important;
+    }
+
+    .n-upload-file.n-upload-file--image-card-type {
+      width: 210px !important;
+      height: 297px !important;
+      border-radius: 12px;
+    }
+
+    .n-upload-dragger {
+      border-radius: 12px;
+      background: #F9FAFD;
+      border: 2px solid #DCE2F1;
+
+      &:hover {
+        border: 2px solid #198CFE;
+      }
+    }
+  }
+
+  .uploadContent {
+    .iconUpload {
+      width: 60px;
+      height: 60px;
+    }
+
+    p {
+      padding-top: 32px;
+      font-size: 18px;
+      color: #9EADD9;
+      line-height: 25px;
+    }
+  }
+
+  .topFormInput {
+    margin-left: 60px;
+  }
+}
+
+.menuTitle {
+  display: flex;
+  align-items: center;
+  padding: 20px 40px;
+  font-size: 18px;
+  font-weight: 600;
+  color: #131415;
+  // border-bottom: 1px solid #EDEEF3;
+
+  .iconMenu {
+    margin-right: 8px;
+    width: 24px;
+    height: 24px;
+  }
+}
+
+.lessonItem {
+  border-radius: 20px;
+  background: #F7F9FF;
+  position: relative;
+  margin: 0 40px 24px;
+  padding: 24px 26px 0;
+  --n-border: 1px solid #289bff !important;
+
+  :global {
+    .n-input {
+      width: 320px;
+    }
+  }
+
+  .btnGroupAll {
+    gap: 8px 24px !important;
+  }
+
+  .btnImg {
+    width: 50px;
+    height: 50px;
+  }
+}
+
+.line {
+  background-color: #EDEEF3;
+  width: calc(100% - 80px);
+  margin: 0 auto;
+  height: 1px;
+}
+
+//
+.addUnitBtn {
+
+  height: 52px;
+  border-radius: 12px;
+  margin: 24px 40px 0;
+
+  width: calc(100% - 80px) !important;
+  background: #E8F4FF;
+
+  &:not(.n-button--disabled):hover,
+  &:not(.n-button--disabled):active,
+  &:not(.n-input--disabled).n-input--focus {
+    background: #E8F4FF;
+  }
+
+  img {
+    width: 16px;
+    height: 16px;
+  }
+}
+
+.btnGroup {
+  padding: 40px 0;
+
+  :global {
+    .n-button {
+      height: 47px;
+      min-width: 156px;
+    }
+  }
+}

+ 367 - 0
src/views/natural-resources/model/add-teaching/index.tsx

@@ -0,0 +1,367 @@
+import {
+  NButton,
+  NForm,
+  NFormItem,
+  NInput,
+  NScrollbar,
+  NSelect,
+  NSpace,
+  useMessage
+} from 'naive-ui';
+import { TransitionGroup, defineComponent, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import UploadFile from '@/components/upload-file';
+import { nextTick } from 'vue';
+import { scrollToErrorForm } from '@/utils';
+import { api_lessonCoursewareSave } from '../../api';
+import iconUpload from '../../images/icon-upload.png';
+import iconAdd from '../../images/icon-add.svg';
+import iconMenu from '../../images/icon-menu.png';
+import btnAdd from '../../images/btn-add.svg';
+import btnDelete from '../../images/btn-delete.svg';
+import btnUp from '../../images/btn-up.svg';
+import btnDown from '../../images/btn-down.svg';
+import btnRemove from '../../images/btn-remove.svg';
+export const BOOK_DATA = {
+  grades: [
+    { label: '一年级', value: 1 },
+    { label: '二年级', value: 2 },
+    { label: '三年级', value: 3 },
+    { label: '四年级', value: 4 },
+    { label: '五年级', value: 5 },
+    { label: '六年级', value: 6 },
+    { label: '七年级', value: 7 },
+    { label: '八年级', value: 8 },
+    { label: '九年级', value: 9 }
+  ],
+  bookTypes: [
+    { label: '上册', value: 'LAST' },
+    { label: '下册', value: 'NEXT' }
+  ]
+};
+
+/** 添加单元 */
+const createLesson = () => {
+  return {
+    key: 'item' + Date.now(),
+    name: '', // 单元名称
+    lessonTargetDesc: '', // 课时目标描述
+    knowledgeList: [
+      {
+        key: Date.now() + '' + 0,
+        name: '' // 章节名称
+      }
+    ] // 章节信息
+  };
+};
+const initState = () => ({
+  id: null, // 教材id
+  name: '',
+  currentGradeNum: null,
+  bookType: null, // 上下册
+  coverImg: '', // 封面
+  enableFlag: true, // 状态
+  type: 'COURSEWARE', // 教材类型:COURSEWARE,THEORY,可用值:COURSEWARE,THEORY
+  lessonList: [createLesson()] // 单元列表
+});
+
+export default defineComponent({
+  name: 'addNatural',
+  emits: ['close'],
+  setup(props, { emit }) {
+    const message = useMessage();
+    const data = reactive({
+      uploading: false
+    });
+    const formRef = ref();
+    const form = reactive(initState());
+    const handleSave = () => {
+      formRef.value?.validate((err: any) => {
+        if (err) {
+          nextTick(scrollToErrorForm);
+          return;
+        }
+        handleSubmit();
+      });
+    };
+    const handleSubmit = async () => {
+      data.uploading = true;
+      try {
+        await api_lessonCoursewareSave(form);
+        Object.assign(form, initState());
+        message.success('添加成功');
+        emit('close', true);
+      } catch {
+        //
+      }
+      data.uploading = false;
+    };
+    return () => (
+      <div class={styles.container}>
+        <NScrollbar style={{ 'max-height': '55vh' }}>
+          <NForm
+            ref={formRef}
+            labelPlacement="left"
+            labelWidth={120}
+            model={form}>
+            <div class={styles.topForms}>
+              <NFormItem
+                path="coverImg"
+                rule={[
+                  {
+                    required: true,
+                    message: '请上传教材封面',
+                    trigger: ['change']
+                  }
+                ]}>
+                <UploadFile
+                  cropper
+                  // tips="建议尺寸:210*297, 文件大小: 5MB以内;"
+                  v-model:fileList={form.coverImg}
+                  showType="custom"
+                  size={2}
+                  accept=".jpg,jpeg,.png"
+                  options={{
+                    autoCropWidth: 210,
+                    autoCropHeight: 297,
+                    fixedBox: true
+                  }}>
+                  {{
+                    custom: () => (
+                      <div class={styles.uploadContent}>
+                        <img src={iconUpload} class={styles.iconUpload} />
+                        <p>请上传教材封面</p>
+                      </div>
+                    )
+                  }}
+                </UploadFile>
+              </NFormItem>
+              <div class={styles.topFormInput}>
+                <NFormItem
+                  style={{ minWidth: '360px' }}
+                  path="name"
+                  rule={[
+                    {
+                      required: true,
+                      message: '请输入教材名称',
+                      trigger: ['blur', 'change']
+                    }
+                  ]}>
+                  <NInput
+                    placeholder="请输入教材名称"
+                    maxlength={25}
+                    v-model:value={form.name}
+                    clearable></NInput>
+                </NFormItem>
+                <NFormItem
+                  path="currentGradeNum"
+                  rule={{
+                    required: true,
+                    message: '请选择年级',
+                    trigger: 'change',
+                    type: 'number'
+                  }}>
+                  <NSelect
+                    style={{ minWidth: '360px' }}
+                    placeholder="请选择年级"
+                    options={BOOK_DATA.grades}
+                    v-model:value={form.currentGradeNum}
+                    clearable
+                    filterable
+                  />
+                </NFormItem>
+                <NFormItem
+                  path="bookType"
+                  style={{ width: '360px' }}
+                  rule={{
+                    required: true,
+                    message: '请选择册别',
+                    trigger: 'change'
+                  }}>
+                  <NSelect
+                    placeholder="请选择册别"
+                    options={BOOK_DATA.bookTypes}
+                    v-model:value={form.bookType}
+                    clearable
+                  />
+                </NFormItem>
+              </div>
+            </div>
+
+            <div class={styles.menuTitle}>
+              <img src={iconMenu} class={styles.iconMenu} />
+              目录
+            </div>
+            <TransitionGroup name="list" tag="div">
+              {form.lessonList.map((item, index) => {
+                return (
+                  <NSpace
+                    class={styles.lessonItem}
+                    wrap={false}
+                    wrapItem={false}
+                    align="start"
+                    key={item.key}>
+                    <NFormItem
+                      label="单元名称"
+                      labelPlacement="top"
+                      path={`lessonList[${index}].name`}
+                      rule={{
+                        required: true,
+                        message: '填写单元名称',
+                        trigger: ['blur', 'change']
+                      }}>
+                      <NInput
+                        placeholder="填写单元名称"
+                        maxlength={25}
+                        v-model:value={item.name}
+                        clearable></NInput>
+                    </NFormItem>
+                    <TransitionGroup name="list" tag="div">
+                      {item.knowledgeList.map((know, knowIndex) => {
+                        return (
+                          <NFormItem
+                            style={{
+                              '--n-label-height': knowIndex === 0 ? '26px' : '0'
+                            }}
+                            labelPlacement="top"
+                            label={knowIndex === 0 ? '章节名称' : ''}
+                            key={know.key}
+                            path={`lessonList[${index}].knowledgeList[${knowIndex}].name`}
+                            rule={{
+                              required: true,
+                              message: '填写章节名称',
+                              trigger: ['blur', 'change']
+                            }}>
+                            <NSpace
+                              wrap={false}
+                              align="center"
+                              class={styles.btnGroupAll}
+                              wrapItem={false}>
+                              <NInput
+                                maxlength={25}
+                                placeholder="填写章节名称"
+                                v-model:value={know.name}
+                                clearable></NInput>
+                              <NButton
+                                quaternary
+                                circle
+                                onClick={() => {
+                                  item.knowledgeList.splice(knowIndex + 1, 0, {
+                                    name: '',
+                                    key: Date.now() + '' + knowIndex
+                                  });
+                                }}>
+                                {{
+                                  icon: () => (
+                                    <img src={btnAdd} class={styles.btnImg} />
+                                  )
+                                }}
+                              </NButton>
+                              <NButton
+                                quaternary
+                                circle
+                                disabled={item.knowledgeList.length < 2}
+                                onClick={() => {
+                                  item.knowledgeList.splice(knowIndex, 1);
+                                }}>
+                                {{
+                                  icon: () => (
+                                    <img
+                                      src={btnDelete}
+                                      class={styles.btnImg}
+                                    />
+                                  )
+                                }}
+                              </NButton>
+                              <NButton
+                                quaternary
+                                circle
+                                disabled={knowIndex === 0}
+                                onClick={() => {
+                                  if (knowIndex === 0) return;
+                                  const tmp = item.knowledgeList[knowIndex - 1];
+                                  item.knowledgeList[knowIndex - 1] =
+                                    item.knowledgeList[knowIndex];
+                                  item.knowledgeList[knowIndex] = tmp;
+                                }}>
+                                {{
+                                  icon: () => (
+                                    <img src={btnUp} class={styles.btnImg} />
+                                  )
+                                }}
+                              </NButton>
+                              <NButton
+                                quaternary
+                                circle
+                                disabled={
+                                  knowIndex === item.knowledgeList.length - 1
+                                }
+                                onClick={() => {
+                                  if (
+                                    knowIndex ===
+                                    item.knowledgeList.length - 1
+                                  )
+                                    return;
+                                  const tmp = item.knowledgeList[knowIndex + 1];
+                                  item.knowledgeList[knowIndex + 1] =
+                                    item.knowledgeList[knowIndex];
+                                  item.knowledgeList[knowIndex] = tmp;
+                                }}>
+                                {{
+                                  icon: () => (
+                                    <img src={btnDown} class={styles.btnImg} />
+                                  )
+                                }}
+                              </NButton>
+                            </NSpace>
+                          </NFormItem>
+                        );
+                      })}
+                    </TransitionGroup>
+                    <NButton
+                      class={styles.closeBtn}
+                      secondary
+                      circle
+                      size="small"
+                      disabled={form.lessonList.length < 2}
+                      onClick={() => {
+                        form.lessonList.splice(index, 1);
+                      }}>
+                      <img src={btnRemove} />
+                    </NButton>
+                  </NSpace>
+                );
+              })}
+            </TransitionGroup>
+            <div class={styles.line}></div>
+            <NButton
+              block
+              class={styles.addUnitBtn}
+              ghost
+              color="#198CFE"
+              onClick={() => {
+                form.lessonList.push(createLesson());
+              }}>
+              {{
+                icon: () => <img src={iconAdd} />,
+                default: () => '新增单元'
+              }}
+            </NButton>
+          </NForm>
+        </NScrollbar>
+        <NSpace class={styles.btnGroup} justify="center">
+          <NButton round onClick={() => emit('close')}>
+            取消
+          </NButton>
+          <NButton
+            round
+            loading={data.uploading}
+            type="primary"
+            onClick={() => handleSave()}>
+            保存
+          </NButton>
+        </NSpace>
+      </div>
+    );
+  }
+});