Browse Source

添加工作

lex 1 year ago
parent
commit
64e9592b6e
48 changed files with 2547 additions and 9227 deletions
  1. 1 9153
      package-lock.json
  2. 2 0
      package.json
  3. BIN
      src/common/images/icon-upload.png
  4. BIN
      src/common/images/logo.png
  5. 48 35
      src/components/m-uploader/index.tsx
  6. 25 38
      src/components/m-uploader/inside.tsx
  7. 235 0
      src/helpers/oss-file-upload.ts
  8. 16 0
      src/helpers/utils.ts
  9. 7 1
      src/router/router-root.ts
  10. 14 0
      src/router/routes-common.ts
  11. 2 0
      src/shims-vue.d.ts
  12. 105 0
      src/views/creation/api.ts
  13. 88 0
      src/views/creation/edit/index.module.less
  14. 107 0
      src/views/creation/edit/index.tsx
  15. BIN
      src/views/creation/images/audio-banner-bg.png
  16. BIN
      src/views/creation/images/audio-bg.png
  17. BIN
      src/views/creation/images/audio-pan.png
  18. BIN
      src/views/creation/images/audio-point.png
  19. BIN
      src/views/creation/images/audio-shadow.png
  20. BIN
      src/views/creation/images/audio-zhen.png
  21. BIN
      src/views/creation/images/icon-delete.png
  22. BIN
      src/views/creation/images/icon-download.png
  23. BIN
      src/views/creation/images/icon-member.png
  24. BIN
      src/views/creation/images/icon-pause.png
  25. BIN
      src/views/creation/images/icon-play.png
  26. BIN
      src/views/creation/images/icon-share.png
  27. BIN
      src/views/creation/images/icon-z.png
  28. BIN
      src/views/creation/images/icon-zan-active.png
  29. BIN
      src/views/creation/images/icon-zan.png
  30. 358 0
      src/views/creation/index-share.tsx
  31. 490 0
      src/views/creation/index.module.less
  32. 415 0
      src/views/creation/index.tsx
  33. BIN
      src/views/creation/login-model/images/icon-close.png
  34. BIN
      src/views/creation/login-model/images/icon-password.png
  35. BIN
      src/views/creation/login-model/images/icon-phone.png
  36. BIN
      src/views/creation/login-model/images/login-bg.png
  37. 62 0
      src/views/creation/login-model/index.module.less
  38. 190 0
      src/views/creation/login-model/index.tsx
  39. BIN
      src/views/creation/share-model/images/icon-download.png
  40. BIN
      src/views/creation/share-model/images/icon-friend-ring.png
  41. BIN
      src/views/creation/share-model/images/icon-friend.png
  42. BIN
      src/views/creation/share-model/images/icon-link.png
  43. BIN
      src/views/creation/share-model/images/icon-logo.png
  44. BIN
      src/views/creation/share-model/images/icon-wechat.png
  45. BIN
      src/views/creation/share-model/images/music-bg.png
  46. BIN
      src/views/creation/share-model/images/share-bg.png
  47. 193 0
      src/views/creation/share-model/index.module.less
  48. 189 0
      src/views/creation/share-model/index.tsx

File diff suppressed because it is too large
+ 1 - 9153
package-lock.json


+ 2 - 0
package.json

@@ -27,6 +27,7 @@
     "@vant/use": "^1.5.1",
     "@vueuse/core": "^10.1.2",
     "clean-deep": "^3.4.0",
+    "cos-js-sdk-v5": "^1.4.21",
     "dayjs": "^1.11.7",
     "echarts": "^5.4.2",
     "eventemitter3": "^5.0.1",
@@ -50,6 +51,7 @@
     "vue-router": "^4.1.6",
     "vue3-lottie": "^2.7.0",
     "vuedraggable": "^4.1.0",
+    "wavesurfer.js": "^7.4.2",
     "webpack-merge": "^5.9.0"
   },
   "devDependencies": {

BIN
src/common/images/icon-upload.png


BIN
src/common/images/logo.png


+ 48 - 35
src/components/m-uploader/index.tsx

@@ -16,6 +16,7 @@ import iconUploader from '@common/images/icon-upload.png';
 import iconVideoDefault from '@common/images/icon-video-c.png';
 import request from '@/helpers/request';
 import { getOssUploadUrl } from '@/state';
+import { getUploadSign, onOnlyFileUpload } from '@/helpers/oss-file-upload';
 
 export default defineComponent({
   name: 'col-upload',
@@ -164,23 +165,34 @@ export default defineComponent({
       // 上传文件
       try {
         // 获取签名
-        const signUrl = '/api-web/getUploadSign';
+        // const signUrl = '/api-web/getUploadSign';
         const tempName = file.name || '';
-        const fileName =
-          this.path + '/' + (tempName && tempName.replace(/ /gi, '_'));
+        const fileName = this.path
+          ? this.path + '/' + (tempName && tempName.replace(/ /gi, '_'))
+          : tempName && tempName.replace(/ /gi, '_');
         const key = new Date().getTime() + fileName;
         console.log(file);
 
-        const res = await request.post(signUrl, {
-          data: {
-            filename: fileName,
-            bucketName: this.bucket,
-            postData: {
-              filename: fileName,
-              acl: 'public-read',
-              key: key,
-              unknowValueField: []
-            }
+        // const res = await request.post(signUrl, {
+        //   data: {
+        //     filename: fileName,
+        //     bucketName: this.bucket,
+        //     postData: {
+        //       filename: fileName,
+        //       acl: 'public-read',
+        //       key: key,
+        //       unknowValueField: []
+        //     }
+        //   }
+        // });
+        const { data } = await getUploadSign({
+          filename: key,
+          bucketName: this.bucket,
+          postData: {
+            filename: key,
+            acl: 'public-read',
+            key: key,
+            unknowValueField: []
           }
         });
         showLoadingToast({
@@ -190,24 +202,29 @@ export default defineComponent({
           duration: 0
         });
         const obj = {
-          policy: res.data.policy,
-          signature: res.data.signature,
+          policy: data.policy,
+          signature: data.signature,
           key: key,
-          KSSAccessKeyId: res.data.kssAccessKeyId,
+          KSSAccessKeyId: data.kssAccessKeyId,
           acl: 'public-read',
-          name: fileName
+          name: key,
+          file
         } as any;
-        const formData = new FormData();
-        for (const key in obj) {
-          formData.append(key, obj[key]);
-        }
-        formData.append('file', file, fileName);
-        await umiRequest(getOssUploadUrl(this.bucket), {
-          method: 'POST',
-          data: formData
-        });
-        console.log(getOssUploadUrl(this.bucket) + key);
-        const uploadUrl = getOssUploadUrl(this.bucket) + key;
+        // const formData = new FormData();
+        // for (const key in obj) {
+        //   formData.append(key, obj[key]);
+        // }
+        // formData.append('file', file, fileName);
+        // await umiRequest(getOssUploadUrl(this.bucket), {
+        //   method: 'POST',
+        //   data: formData
+        // });
+        // console.log(getOssUploadUrl(this.bucket) + key);
+        // const uploadUrl = getOssUploadUrl(this.bucket) + key;
+        const uploadUrl = await onOnlyFileUpload(
+          getOssUploadUrl(this.bucket),
+          obj
+        );
         closeToast();
         // 判断是否是多选
         if (this.maxCount > 1) {
@@ -241,11 +258,7 @@ export default defineComponent({
               )}
               <div class={['van-uploader__upload']}>
                 {this.uploadType === 'IMAGE' ? (
-                  <Image
-                    src={item + '@base@tag=imgScale&w=200'}
-                    class={styles.previewImg}
-                    fit="cover"
-                  />
+                  <Image src={item} class={styles.previewImg} fit="cover" />
                 ) : (
                   <video
                     ref="videoUpload"
@@ -294,7 +307,7 @@ export default defineComponent({
                           fit="cover"
                           position="center"
                           class={styles.uploadImg}
-                          src={item + '@base@tag=imgScale&w=200'}
+                          src={item}
                         />
                       ) : (
                         <video
@@ -358,7 +371,7 @@ export default defineComponent({
                         fit="cover"
                         position="center"
                         class={styles.uploadImg}
-                        src={item + '@base@tag=imgScale&w=200'}
+                        src={item}
                       />
                     ) : (
                       <video

+ 25 - 38
src/components/m-uploader/inside.tsx

@@ -16,10 +16,8 @@ import iconUploader from '@common/images/icon-upload.png';
 import iconVideoDefault from '@common/images/icon-video-c.png';
 import request from '@/helpers/request';
 import { getOssUploadUrl } from '@/state';
+import { getUploadSign, onOnlyFileUpload } from '@/helpers/oss-file-upload';
 
-/**
- * 此组件暂时只支持ks3,如果想要支持tencent需要二次开发
- */
 export default defineComponent({
   name: 'col-upload',
   props: {
@@ -166,23 +164,22 @@ export default defineComponent({
       // 上传文件
       try {
         // 获取签名
-        const signUrl = '/api-web/getUploadSign';
+        // const signUrl = '/api-web/getUploadSign';
         const tempName = file.name || '';
-        const fileName =
-          this.path + '/' + (tempName && tempName.replace(/ /gi, '_'));
+        const fileName = this.path
+          ? this.path + '/' + (tempName && tempName.replace(/ /gi, '_'))
+          : tempName && tempName.replace(/ /gi, '_');
         const key = new Date().getTime() + fileName;
         console.log(file);
 
-        const res = await request.post(signUrl, {
-          data: {
-            filename: fileName,
-            bucketName: this.bucket,
-            postData: {
-              filename: fileName,
-              acl: 'public-read',
-              key: key,
-              unknowValueField: []
-            }
+        const { data } = await getUploadSign({
+          filename: key,
+          bucketName: this.bucket,
+          postData: {
+            filename: key,
+            acl: 'public-read',
+            key: key,
+            unknowValueField: []
           }
         });
         showLoadingToast({
@@ -192,24 +189,18 @@ export default defineComponent({
           duration: 0
         });
         const obj = {
-          policy: res.data.policy,
-          signature: res.data.signature,
+          policy: data.policy,
+          signature: data.signature,
           key: key,
-          KSSAccessKeyId: res.data.kssAccessKeyId,
+          KSSAccessKeyId: data.kssAccessKeyId,
           acl: 'public-read',
-          name: fileName
+          name: key,
+          file
         } as any;
-        const formData = new FormData();
-        for (const key in obj) {
-          formData.append(key, obj[key]);
-        }
-        formData.append('file', file, fileName);
-        await umiRequest(getOssUploadUrl(this.bucket), {
-          method: 'POST',
-          data: formData
-        });
-        console.log(getOssUploadUrl(this.bucket) + key);
-        const uploadUrl = getOssUploadUrl(this.bucket) + key;
+        const uploadUrl = await onOnlyFileUpload(
+          getOssUploadUrl(this.bucket),
+          obj
+        );
         closeToast();
         // 判断是否是多选
         if (this.maxCount > 1) {
@@ -243,11 +234,7 @@ export default defineComponent({
               )}
               <div class={['van-uploader__upload']}>
                 {this.uploadType === 'IMAGE' ? (
-                  <Image
-                    src={item + '@base@tag=imgScale&w=200'}
-                    class={styles.previewImg}
-                    fit="cover"
-                  />
+                  <Image src={item} class={styles.previewImg} fit="cover" />
                 ) : (
                   <video
                     ref="videoUpload"
@@ -296,7 +283,7 @@ export default defineComponent({
                           fit="cover"
                           position="center"
                           class={styles.uploadImg}
-                          src={item + '@base@tag=imgScale&w=200'}
+                          src={item}
                         />
                       ) : (
                         <video
@@ -360,7 +347,7 @@ export default defineComponent({
                         fit="cover"
                         position="center"
                         class={styles.uploadImg}
-                        src={item + '@base@tag=imgScale&w=200'}
+                        src={item}
                       />
                     ) : (
                       <video

+ 235 - 0
src/helpers/oss-file-upload.ts

@@ -0,0 +1,235 @@
+import request from './request';
+// import axios from 'axios'
+import umiRequest from 'umi-request';
+import COS from 'cos-js-sdk-v5';
+import { state } from '@/state';
+export const ossSwitch = 'tencent' as 'ks3' | 'tencent'; // 上传文件服务商
+const tencentBucket = 'daya-online-1303457149';
+
+/**
+ * 管乐团 ktqy/
+ * 酷乐秀 klx/
+ * 课堂乐器 ktqy/
+ * 管乐迷 gym/
+ */
+
+// 定义一个cos 对象
+/**
+ * 获取上传文件签名
+ * @param params 上传对应参数
+ * { filename: fileName,
+     bucketName: props.bucketName,
+     postData: {
+      filename: fileName,
+      acl: 'public-read',
+      key: fileName,
+      unknowValueField: []
+    }}
+ * @param oss 服务商 ks3 tencent
+ * @returns ”{'signatur'':'',''kssAccessKeyI'':'',''policy': '' }“
+ */
+export const getUploadSign = async (params: any) => {
+  const { bucketName, filename, postData } = params;
+  const ossType = ossSwitch;
+  let bucket = bucketName;
+  let file = filename;
+  // const key = postData.key;
+  let tempPostData: any = {};
+  if (ossType === 'tencent') {
+    bucket = tencentBucket;
+    file = 'ktqy/' + filename;
+
+    tempPostData = {
+      key: 'ktqy/' + postData.key
+    };
+  } else {
+    tempPostData = postData;
+  }
+  return request.post('/edu-app/open/getUploadSign', {
+    data: {
+      postData: tempPostData,
+      pluginName: ossType,
+      bucketName: bucket,
+      filename: file
+    },
+    params: { pluginName: ossType }
+  });
+};
+
+/**
+ * 使用组件上传时,调用方法
+ * @param param0
+ */
+export const onFileUpload = ({
+  file,
+  action,
+  data,
+  onProgress,
+  onFinish,
+  onError
+}: any) => {
+  if (ossSwitch === 'ks3') {
+    const fileParams = {
+      policy: data.policy,
+      signature: data.signature,
+      key: data.key,
+      acl: 'public-read',
+      KSSAccessKeyId: data.KSSAccessKeyId,
+      name: data.name
+    } as any;
+    const formData = new FormData();
+    for (const key in fileParams) {
+      formData.append(key, fileParams[key]);
+    }
+    formData.append('file', data.file as File);
+    umiRequest(action as string, {
+      method: 'POST',
+      data: formData
+    })
+      .then(() => {
+        file.url = action + data.key;
+        onFinish();
+      })
+      .catch(error => {
+        onError(error);
+      });
+  } else {
+    const cos = new COS({
+      Domain: 'https://oss.dayaedu.com',
+      Protocol: 'https',
+      // getAuthorization 必选参数
+      getAuthorization: async (options, callback: any) => {
+        callback({ Authorization: data.signature });
+      }
+    });
+    cos
+      .uploadFile({
+        Bucket: tencentBucket /* 填写自己的 bucket,必须字段 */,
+        Region: 'ap-nanjing' /* 存储桶所在地域,必须字段 */,
+        Key: `ktqy/${data.name}`,
+        /* 存储在桶里的对象键(例如:1.jpg,a/b/test.txt,图片.jpg)支持中文,必须字段 */
+        Body: data.file.file, // 上传文件对象
+        SliceSize:
+          1024 *
+          1024 *
+          500 /* 触发分块上传的阈值,超过5MB使用分块上传,小于5MB使用简单上传。可自行设置,非必须 */,
+        onProgress: function (progressData) {
+          onProgress({ percent: Math.ceil((progressData.percent || 0) * 100) });
+        }
+      })
+      .then((res: any) => {
+        // file.url = 'https://' + res.Location;
+        if (res.Location?.indexOf('http') >= 0) {
+          file.url = res.Location;
+        } else {
+          file.url = 'https://' + res.Location;
+        }
+        onFinish();
+      })
+      .catch(error => {
+        console.log(error, 'error');
+        onError();
+      });
+  }
+};
+
+export const onOnlyFileUpload = async (action: string, params: any) => {
+  if (ossSwitch === 'ks3') {
+    const fileParams = {
+      policy: params.policy,
+      signature: params.signature,
+      key: params.key,
+      acl: 'public-read',
+      KSSAccessKeyId: params.KSSAccessKeyId,
+      name: params.name
+    } as any;
+    const formData = new FormData();
+    for (const key in fileParams) {
+      formData.append(key, fileParams[key]);
+    }
+    formData.append('file', params.file as File);
+    let file = '';
+    let errorObj: any = null;
+    // await axios
+    //   .post(action as string, formData, {
+    //     // onUploadProgress: ({ progress }) => {
+    //     //   console.log(progress);
+    //     //   onProgress({ percent: Math.ceil((progress || 0) * 100) });
+    //     // }
+    //   })
+    //   .then(() => {
+    //     file = action + params.key
+    //   })
+    //   .catch((error) => {
+    //     // onError(error);
+    //     errorObj = error
+    //     // throw new Error(error);
+    //   })
+    await umiRequest(action as string, {
+      method: 'POST',
+      data: formData
+    })
+      .then(() => {
+        file = action + params.key;
+      })
+      .catch(error => {
+        errorObj = error;
+      });
+    if (file) {
+      return file;
+    } else {
+      throw new Error(errorObj);
+    }
+    return file;
+  } else {
+    let file = '';
+    let errorObj: any = null;
+    console.log(params, 'params');
+    const cos = new COS({
+      Domain: 'https://oss.dayaedu.com',
+      // getAuthorization 必选参数
+      getAuthorization: async (options, callback: any) => {
+        callback({ Authorization: params.signature });
+      }
+    });
+
+    // http://daya-online-1303457149.cos.ap-nanjing.myqcloud.com/ktqy
+    // http://daya-online-1303457149.cos.ap-nanjing.myqcloud.com/ktqy
+    await cos
+      .uploadFile({
+        Bucket: tencentBucket /* 填写自己的 bucket,必须字段 */,
+        Region: 'ap-nanjing' /* 存储桶所在地域,必须字段 */,
+        Key: `ktqy/${params.name}`,
+        /* 存储在桶里的对象键(例如:1.jpg,a/b/test.txt,图片.jpg)支持中文,必须字段 */
+        Body: params.file, // 上传文件对象
+        SliceSize:
+          1024 *
+          1024 *
+          500 /* 触发分块上传的阈值,超过5MB使用分块上传,小于5MB使用简单上传。可自行设置,非必须 */
+        // onProgress: function (progressData) {
+        //   onProgress({ percent: Math.ceil((progressData.percent || 0) * 100) });
+        // }
+      })
+      .then((res: any) => {
+        // file.url = 'https://' + res.Location;
+        // file = 'https://' + res.Location;
+        if (res.Location?.indexOf('http') >= 0) {
+          file = res.Location;
+        } else {
+          file = 'https://' + res.Location;
+        }
+        // onFinish();
+      })
+      .catch(error => {
+        // console.log(error, 'error');
+        // onError();
+        // throw new Error(error);
+        errorObj = error;
+      });
+    if (file) {
+      return file;
+    } else {
+      throw new Error(errorObj);
+    }
+  }
+};

+ 16 - 0
src/helpers/utils.ts

@@ -170,6 +170,22 @@ export const getWeekCh = (week: number, type = 0) => {
   return type ? template2[week] : template[week];
 };
 
+export const getGradeCh = (grade: number) => {
+  const template = [
+    '一年级',
+    '二年级',
+    '三年级',
+    '四年级',
+    '五年级',
+    '六年级',
+    '七年级',
+    '八年级',
+    '九年级'
+  ];
+
+  return template[grade];
+};
+
 export const numberFormat = (num: number, type?: string) => {
   if (type === 'percent') {
     return numeral(num).format('0.0%');

+ 7 - 1
src/router/router-root.ts

@@ -191,7 +191,13 @@ export default [
       title: '学校报名详情'
     }
   },
-
+  {
+    path: '/shareCreation',
+    component: () => import('@/views/creation/index-share'),
+    meta: {
+      title: '作品详情'
+    }
+  },
   {
     path: '/:pathMatch(.*)*',
     component: () => import('@/views/404'),

+ 14 - 0
src/router/routes-common.ts

@@ -170,6 +170,20 @@ export default [
         meta: {
           title: '课件播放'
         }
+      },
+      {
+        path: '/creation',
+        component: () => import('@/views/creation/index'),
+        meta: {
+          title: '作品详情'
+        }
+      },
+      {
+        path: '/creation-edit',
+        component: () => import('@/views/creation/edit/index'),
+        meta: {
+          title: '作品详情'
+        }
       }
     ]
   },

+ 2 - 0
src/shims-vue.d.ts

@@ -15,3 +15,5 @@ declare global {
   }
 }
 declare module 'tcplayer.js';
+
+declare module 'qrcode';

+ 105 - 0
src/views/creation/api.ts

@@ -0,0 +1,105 @@
+// userMusic/detail/1698984095609106434
+import request from '@/helpers/request';
+
+/** 详情 */
+export const api_userMusicDetail = (params: any): Promise<any> => {
+  return request.get(`/edu-app/userMusic/detail/${params}`);
+};
+
+/** 开放详情 */
+export const api_openUserMusicDetail = (params: any): Promise<any> => {
+  return request.get(`/edu-app/open/userMusic/detail/${params}`);
+};
+
+/**  点赞分页 */
+export const api_userMusicStarPage = (params: any): Promise<any> => {
+  return request.post(`/edu-app/userMusicStar/page`, {
+    data: params
+  });
+};
+
+/**  分享推荐作品 */
+export const api_openUserMusicPage = (params: any): Promise<any> => {
+  return request.post(`/edu-app/open/userMusic/page`, {
+    data: params
+  });
+};
+
+/** 点赞 */
+export const api_userMusicStar = (params: any): Promise<any> => {
+  return request.post('/edu-app/userMusicStar/star', {
+    data: params
+  });
+};
+
+/** 保存草稿/发布作品 */
+export const api_userMusicSave = (params: any): Promise<any> => {
+  return request.post('/edu-app/userMusic/save', {
+    data: params
+  });
+};
+
+/** 删除 */
+export const api_userMusicRemove = (params: any): Promise<any> => {
+  return request.get('/edu-app/userMusic/remove', {
+    params
+  });
+};
+
+// POST http://127.0.0.1:7093/userMusic/remove?id=1698984095609106434
+// Content-Type: application/json
+// Authorization: {{student}}
+// ### 点赞/取消点赞
+
+// POST http://127.0.0.1:7093/userMusicStar/star
+// Authorization: {{student}}
+// Content-Type: application/json
+
+// {
+//   "userMusicId": "1698984095609106434",
+//   "star": true
+// }
+// ### 点赞分页
+
+// POST http://127.0.0.1:7093/userMusicStar/page
+// Authorization: {{student}}
+// Content-Type: application/json
+
+// {
+//   "userMusicId": "1698984095609106434"
+// }
+
+// ### 分页查询
+
+// POST http://127.0.0.1:7093/userMusic/page
+// Content-Type: application/json
+// Authorization: {{student}}
+
+// {
+//   "keyword": "",
+//   "type": "FORMAL"
+// }
+
+// ###保存草稿/发布作品
+
+// POST http://127.0.0.1:7093/userMusic/save
+// Content-Type: application/json
+// Authorization: {{student}}
+
+// {
+//   "id": "1698984095609106434",
+//   "musicPracticeRecordId": 1698983103032250369,
+//   "type": "FORMAL",
+//   "img": "",
+//   "videoUrl": "www.baidu.com",
+//   "jsonConfig": "{\"test\": \"test\"}"
+// }### 删除作品
+
+// POST http://127.0.0.1:7093/userMusic/remove?id=1698984095609106434
+// Content-Type: application/json
+// Authorization: {{student}}
+// ### 详情/分享
+
+// GET http://127.0.0.1:7093/userMusic/detail/1698984095609106434
+// Content-Type: application/json
+// Authorization: {{student}}

+ 88 - 0
src/views/creation/edit/index.module.less

@@ -0,0 +1,88 @@
+.section {
+  margin: 12px 13px;
+  background: #FFFFFF;
+  border-radius: 10px;
+  overflow: hidden;
+  font-size: 16px;
+
+
+}
+
+.sectionFile {
+  padding: 12px;
+  display: flex;
+
+  :global {
+    .van-uploader {
+      --upload-file-size: 62px;
+    }
+
+    .van-uploader__upload {
+      margin: 0;
+    }
+  }
+
+  .muploader {
+    position: relative;
+    z-index: 9;
+  }
+
+  .uploadImg {
+    position: relative;
+    border-radius: 6px;
+    margin-right: 16px;
+
+    .tip {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      right: 0;
+      z-index: 10;
+      background: #000000;
+      opacity: 0.37;
+      font-size: 13px;
+      color: #FFFFFF;
+      line-height: 18px;
+      text-align: center;
+      border-radius: 0 0 6px 6px;
+      pointer-events: none;
+    }
+
+    &::before {
+      content: '';
+      background: url('../images/audio-pan.png') no-repeat center;
+      background-size: contain;
+      position: absolute;
+      top: 0;
+      right: -6px;
+      z-index: 1;
+      width: 60px;
+      height: 60px;
+    }
+  }
+
+  .musicDetail {
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+
+    .musicName {
+      font-size: 16px;
+      font-weight: 600;
+      color: #131415;
+      line-height: 22px;
+    }
+
+    .username {
+      padding-top: 4px;
+      font-size: 14px;
+      color: #777777;
+      line-height: 20px;
+    }
+  }
+}
+
+.btnGroup {
+  margin: 12px 24px;
+
+}

+ 107 - 0
src/views/creation/edit/index.tsx

@@ -0,0 +1,107 @@
+import MHeader from '@/components/m-header';
+import MSticky from '@/components/m-sticky';
+import { defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less';
+import { Button, Field, showToast } from 'vant';
+import MUploader from '@/components/m-uploader';
+import { api_userMusicDetail, api_userMusicSave } from '../api';
+import { useRoute, useRouter } from 'vue-router';
+
+export default defineComponent({
+  name: 'creation-edit',
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    const state = reactive({
+      id: route.query.id,
+      musicDetail: {} as any,
+      desc: '',
+      img: [] as any
+    });
+
+    const onSubmit = async () => {
+      try {
+        // {
+        //   "id": "1698984095609106434",
+        //   "musicPracticeRecordId": 1698983103032250369,
+        //   "type": "FORMAL",
+        //   "img": "",
+        //   "videoUrl": "www.baidu.com",
+        //   "jsonConfig": "{\"test\": \"test\"}"
+        // }
+        if (!state.desc) {
+          showToast('请输入详情');
+          return;
+        }
+
+        await api_userMusicSave({
+          id: state.id,
+          img: state.img.join(','),
+          desc: state.desc,
+          musicPracticeRecordId: state.musicDetail.musicPracticeRecordId,
+          type: 'FORMAL'
+        });
+
+        router.back();
+      } catch {
+        //
+      }
+    };
+    onMounted(async () => {
+      try {
+        const { data } = await api_userMusicDetail(state.id);
+        state.musicDetail = data;
+        state.desc = data.desc;
+        state.img = data.img ? [data.img] : [];
+      } catch {
+        //
+      }
+    });
+    return () => (
+      <div>
+        <MSticky position="top">
+          <MHeader border={false} />
+        </MSticky>
+
+        <div class={styles.section}>
+          <Field
+            rows={4}
+            autosize
+            type="textarea"
+            maxlength={150}
+            placeholder="我发布了一首演奏作品,快来听听吧~"
+            showWordLimit
+            v-model={state.desc}
+          />
+        </div>
+
+        <div class={[styles.section, styles.sectionFile]}>
+          <div class={styles.uploadImg}>
+            <MUploader
+              class={styles.muploader}
+              // native
+              deletable={false}
+              v-model:modelValue={state.img}
+            />
+            <div class={styles.tip}>选封面</div>
+          </div>
+          <div class={styles.musicDetail}>
+            <p class={styles.musicName}>{state.musicDetail.musicSheetName}</p>
+            <p class={styles.username}>{state.musicDetail.username}</p>
+          </div>
+        </div>
+
+        <div class={styles.btnGroup}>
+          <Button
+            type="primary"
+            round
+            block
+            color="linear-gradient(73deg, #5BECFF 0%, #259CFE 100%)"
+            onClick={onSubmit}>
+            {state.musicDetail.type === 'FORMAL' ? '保存' : '发布'}
+          </Button>
+        </div>
+      </div>
+    );
+  }
+});

BIN
src/views/creation/images/audio-banner-bg.png


BIN
src/views/creation/images/audio-bg.png


BIN
src/views/creation/images/audio-pan.png


BIN
src/views/creation/images/audio-point.png


BIN
src/views/creation/images/audio-shadow.png


BIN
src/views/creation/images/audio-zhen.png


BIN
src/views/creation/images/icon-delete.png


BIN
src/views/creation/images/icon-download.png


BIN
src/views/creation/images/icon-member.png


BIN
src/views/creation/images/icon-pause.png


BIN
src/views/creation/images/icon-play.png


BIN
src/views/creation/images/icon-share.png


BIN
src/views/creation/images/icon-z.png


BIN
src/views/creation/images/icon-zan-active.png


BIN
src/views/creation/images/icon-zan.png


+ 358 - 0
src/views/creation/index-share.tsx

@@ -0,0 +1,358 @@
+import { defineComponent, onMounted, reactive } from 'vue';
+import WaveSurfer from 'wavesurfer.js';
+import styles from './index.module.less';
+import MSticky from '@/components/m-sticky';
+import MHeader from '@/components/m-header';
+import { Cell, Image, List, Popup, Slider } from 'vant';
+import iconMember from './images/icon-member.png';
+import iconZan from './images/icon-zan.png';
+import iconZanActive from './images/icon-zan-active.png';
+import iconZ from './images/icon-z.png';
+import iconPlay from './images/icon-play.png';
+import iconPause from './images/icon-pause.png';
+import { browser, getGradeCh, getSecondRPM } from '@/helpers/utils';
+import { useRoute, useRouter } from 'vue-router';
+import {
+  api_openUserMusicDetail,
+  api_openUserMusicPage,
+  api_userMusicStar
+} from './api';
+import MEmpty from '@/components/m-empty';
+import { nextTick } from 'process';
+import MVideo from '@/components/m-video';
+import LoginModel from './login-model';
+import { removeAuth } from '../student-register/layout/utils';
+import { setLogout } from '@/state';
+
+export default defineComponent({
+  name: 'creation-detail',
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    const audioId = 'a' + +Date.now() + Math.floor(Math.random() * 100);
+
+    const state = reactive({
+      id: route.query.id,
+      loginTag: false, // 是否登录标识
+      loginStatus: true,
+      playType: '' as 'Audio' | 'Video' | '', // 播放类型
+      musicDetail: {} as any,
+      timer: null as any,
+      paused: true,
+      currentTime: 0,
+      duration: 0.1,
+      loop: false,
+      dragStatus: false, // 是否开始拖动
+      isClick: false,
+      list: [] as any,
+      listState: {
+        dataShow: true, // 判断是否有数据
+        loading: false,
+        finished: false
+      },
+      params: {
+        page: 1,
+        rows: 20
+      }
+    });
+    const audioDom = new Audio();
+    audioDom.controls = true;
+    audioDom.style.width = '100%';
+    audioDom.className = styles.audio;
+
+    /** 改变播放时间 */
+    const handleChangeTime = (val: number) => {
+      state.currentTime = val;
+      clearTimeout(state.timer);
+      state.timer = setTimeout(() => {
+        audioDom.currentTime = val;
+        state.timer = null;
+      }, 60);
+    };
+
+    // 切换音频播放
+    const onToggleAudio = (e: any) => {
+      e.stopPropagation();
+      if (audioDom.paused) {
+        audioDom.play();
+      } else {
+        audioDom.pause();
+      }
+
+      state.paused = audioDom.paused;
+    };
+
+    // 点赞
+    const onStarChange = async () => {
+      // 是否登录
+      if (!state.loginTag) {
+        state.loginStatus = true;
+        return;
+      }
+      try {
+        const { data } = await api_userMusicStar({
+          userMusicId: '1698984095609106434',
+          star: true
+        });
+      } catch {
+        //
+      }
+    };
+
+    // 获取列表
+    const getList = async () => {
+      try {
+        if (state.isClick) return;
+        state.isClick = true;
+        const res = await api_openUserMusicPage({
+          type: 'FORMAL',
+          exclusionId: state.id,
+          ...state.params
+        });
+        state.listState.loading = false;
+        const result = res.data || {};
+        // 处理重复请求数据
+        if (state.list.length > 0 && result.current === 1) {
+          return;
+        }
+        state.list = state.list.concat(result.rows || []);
+        state.listState.finished = result.current >= result.pages;
+        state.params.page = result.current + 1;
+        state.listState.dataShow = state.list.length > 0;
+        state.isClick = false;
+      } catch {
+        state.listState.dataShow = false;
+        state.listState.finished = true;
+        state.isClick = false;
+      }
+    };
+
+    const initAudio = () => {
+      const wavesurfer = WaveSurfer.create({
+        container: document.querySelector(`#${audioId}`) as HTMLElement,
+        waveColor: '#fff',
+        progressColor: '#2FA1FD',
+        url: state.musicDetail.videoUrl,
+        cursorWidth: 0,
+        height: 35,
+        width: 'auto',
+        normalize: true,
+        // Set a bar width
+        barWidth: 2,
+        // Optionally, specify the spacing between bars
+        barGap: 2,
+        // And the bar radius
+        barRadius: 4,
+        barHeight: 0.6,
+        autoScroll: true,
+        /** If autoScroll is enabled, keep the cursor in the center of the waveform during playback */
+        autoCenter: true,
+        hideScrollbar: false,
+        media: audioDom
+      });
+
+      wavesurfer.once('interaction', () => {
+        // wavesurfer.play();
+      });
+      wavesurfer.once('ready', () => {
+        state.paused = audioDom.paused;
+        state.duration = audioDom.duration;
+      });
+
+      wavesurfer.on('finish', () => {
+        state.paused = true;
+      });
+
+      // 播放时监听
+      audioDom.addEventListener('timeupdate', () => {
+        state.currentTime = audioDom.currentTime;
+      });
+    };
+
+    onMounted(async () => {
+      // 判断是否登录
+      if (!state.loginTag) {
+        removeAuth();
+        setLogout();
+      }
+
+      try {
+        const { data } = await api_openUserMusicDetail(state.id);
+        state.musicDetail = data;
+        getList();
+        // 判断是视频还是音频
+        if (data.videoFilePath) {
+          state.playType = 'Video';
+        } else {
+          state.playType = 'Audio';
+          // 初始化
+          nextTick(() => {
+            initAudio();
+          });
+        }
+      } catch {
+        //
+      }
+    });
+    return () => (
+      <div class={styles.creation}>
+        <MSticky position="top">
+          <MHeader
+            border={false}
+            isBack={route.query.platformType != 'ANALYSIS'}
+          />
+        </MSticky>
+        <div class={styles.playSection}>
+          {state.playType === 'Video' && (
+            <MVideo
+              src={state.musicDetail.videoUrl}
+              poster={state.musicDetail.img}
+            />
+          )}
+          {state.playType === 'Audio' && (
+            <div class={styles.audioSection}>
+              <div class={styles.audioContainer}>
+                <div
+                  id={audioId}
+                  onClick={(e: MouseEvent) => {
+                    e.stopPropagation();
+                  }}></div>
+              </div>
+
+              <div class={styles.audioBox}>
+                <div class={styles.audioPan}>
+                  <Image class={styles.audioImg} src={state.musicDetail.img} />
+                </div>
+                <i class={styles.audioPoint}></i>
+                <i class={styles.audioZhen}></i>
+              </div>
+              <div
+                class={[styles.controls]}
+                onClick={(e: Event) => {
+                  e.stopPropagation();
+                }}
+                onTouchmove={(e: TouchEvent) => {
+                  // emit('close');
+                }}>
+                <div class={styles.actions}>
+                  <div class={styles.actionBtn} onClick={onToggleAudio}>
+                    <img src={state.paused ? iconPlay : iconPause} />
+                  </div>
+                </div>
+                <div class={[styles.slider]}>
+                  <Slider
+                    step={0.01}
+                    class={styles.timeProgress}
+                    v-model={state.currentTime}
+                    max={state.duration}
+                    onUpdate:modelValue={val => {
+                      handleChangeTime(val);
+                    }}
+                    onDragStart={() => {
+                      state.dragStatus = true;
+                      console.log('onDragStart');
+                    }}
+                    onDragEnd={() => {
+                      state.dragStatus = false;
+                      console.log('onDragEnd');
+                    }}
+                  />
+                </div>
+                <div class={styles.time}>
+                  <div>{getSecondRPM(state.currentTime)}</div>
+                  <span>/</span>
+                  <div>{getSecondRPM(state.duration)}</div>
+                </div>
+              </div>
+            </div>
+          )}
+        </div>
+
+        <Cell class={styles.userSection} center>
+          {{
+            icon: () => (
+              <Image class={styles.userLogo} src={state.musicDetail.avatar} />
+            ),
+            title: () => (
+              <div class={styles.userInfo}>
+                <p class={styles.name}>
+                  {state.musicDetail.username}
+                  {state.musicDetail.vipFlag && (
+                    <img src={iconMember} class={styles.iconMember} />
+                  )}
+                </p>
+                <p class={styles.sub}>
+                  {state.musicDetail.subjectName}{' '}
+                  {getGradeCh(state.musicDetail.currentGradeNum)}
+                </p>
+              </div>
+            ),
+            value: () => (
+              <div class={styles.zan} onClick={onStarChange}>
+                <img src={iconZanActive} class={styles.iconZan} />
+                {state.musicDetail.likeNum}
+              </div>
+            )
+          }}
+        </Cell>
+
+        <div class={styles.musicSection}>
+          <div class={styles.musicName}>
+            <span class={styles.musicTag}>曲目名称</span>
+            {state.musicDetail.musicSheetName}
+          </div>
+          {state.musicDetail.desc && (
+            <div class={styles.musicDesc}>{state.musicDetail.desc}</div>
+          )}
+        </div>
+
+        <div class={styles.likeSection}>
+          <div class={styles.likeTitle}>推荐作品</div>
+
+          {state.listState.dataShow ? (
+            <List
+              finished={state.listState.finished}
+              finishedText=" "
+              class={[styles.container, styles.containerInformation]}
+              onLoad={getList}
+              immediateCheck={false}>
+              <div class={styles.cellGroup}>
+                {state.list.map((item: any) => (
+                  <div class={styles.cell}>
+                    <div class={styles.cellImg}>
+                      <Image class={styles.cellImage} src={item.img} />
+
+                      <div class={styles.iconZan}>{item.likeNum}</div>
+                    </div>
+                    <div class={[styles.cellTitle, 'van-ellipsis']}>
+                      {item.musicSheetName}
+                    </div>
+                    <div class={styles.users}>
+                      <Image src={item.avatar} class={styles.userImg} />
+                      <span class={styles.name}>{item.username}</span>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </List>
+          ) : (
+            <MEmpty description="暂无数据" />
+          )}
+        </div>
+
+        <Popup
+          v-model:show={state.loginStatus}
+          style={{ background: 'transparent', overflow: 'inherit' }}>
+          <LoginModel
+            onClose={() => (state.loginStatus = false)}
+            onConfirm={async (val: boolean) => {
+              state.loginStatus = val;
+              const { data } = await api_openUserMusicDetail(state.id);
+              state.musicDetail = data;
+            }}
+          />
+        </Popup>
+      </div>
+    );
+  }
+});

+ 490 - 0
src/views/creation/index.module.less

@@ -0,0 +1,490 @@
+.playSection {
+  min-height: 175px;
+}
+
+.audioSection {
+  position: relative;
+  background: url('./images/audio-banner-bg.png') no-repeat top center;
+  background-size: contain;
+  height: 175px;
+
+  .audioContainer {
+    position: absolute;
+    top: 60px;
+    width: 196px;
+    height: 35px;
+    left: 85px;
+  }
+
+
+  .audioBox {
+    position: absolute;
+    left: 50%;
+    transform: translate(-50%, 50%);
+    z-index: 2;
+    width: 74px;
+    height: 75px;
+    background: url('./images/audio-bg.png') no-repeat center;
+    background-size: contain;
+
+    // &::after {
+    //   content: '';
+    //   width: 134px;
+    //   height: 73px;
+    //   position: absolute;
+    //   left: 50%;
+    //   transform: translate(-50%, 50%);
+    //   background: url('./images/audio-shadow.png') no-repeat center;
+    //   background-size: contain;
+    //   z-index: -1;
+    // }
+    .audioPan {
+      position: absolute;
+      left: 8px;
+      top: 6px;
+      z-index: 8;
+      width: 59px;
+      height: 60px;
+      background: url('./images/audio-pan.png') no-repeat center;
+      background-size: contain;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .audioImg {
+      width: 32px;
+      height: 32px;
+      border-radius: 50%;
+      overflow: hidden;
+    }
+
+    .audioPoint {
+      position: absolute;
+      z-index: 9;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%);
+      width: 8px;
+      height: 8px;
+      background: url('./images/audio-point.png') no-repeat center;
+      background-size: contain;
+    }
+
+    .audioZhen {
+      position: absolute;
+      z-index: 9;
+      right: -4px;
+      top: -33px;
+      width: 26px;
+      height: 87px;
+      background: url('./images/audio-zhen.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+
+}
+
+.controls {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  height: 44px;
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  flex-direction: row;
+  transition: all 0.5s;
+  padding: 0 12px;
+
+  &>div {
+    display: flex;
+    align-items: center;
+  }
+
+  &.hide {
+    transform: translateY(100%);
+  }
+
+
+
+  .actionBtn {
+    line-height: 0;
+
+    img {
+      width: 14px;
+      height: 14px;
+      margin-bottom: -2px;
+    }
+  }
+
+  .time {
+    display: flex;
+    justify-content: space-between;
+    flex: 1;
+    min-width: 86px;
+    font-size: 14px;
+    font-weight: 500;
+    color: #131415;
+    line-height: 20px;
+
+    span {
+      font-size: 12px;
+      padding: 0 2px;
+    }
+  }
+
+  .slider {
+    width: 100%;
+    margin: 0 12px;
+    --van-slider-bar-height: 4px;
+    --van-slider-button-width: 13px !important;
+    --van-slider-button-height: 13px !important;
+    --van-slider-inactive-background: #fff;
+    --van-slider-active-background: #269EFE !important;
+
+    :global {
+
+      .van-loading {
+        width: 100%;
+        height: 100%;
+      }
+
+    }
+  }
+
+}
+
+.userSection {
+  padding: 15px 12px;
+  background-color: transparent;
+
+  .userLogo {
+    width: 44px;
+    height: 44px;
+    border: 1px solid #FFFFFF;
+    margin-right: 10px;
+    border-radius: 50%;
+  }
+
+  .userInfo {
+    .name {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333333;
+      line-height: 22px;
+    }
+
+    .sub {
+      padding-top: 2px;
+      font-size: 12px;
+      color: #777777;
+      line-height: 17px;
+    }
+
+    .iconMember {
+      margin-left: 6px;
+      width: 14px;
+      height: 14px;
+    }
+  }
+
+  .zan {
+    background: #F7EEEE;
+    border-radius: 13px;
+    font-size: 14px;
+    color: #FF6A6A;
+    line-height: 20px;
+    padding: 4px 9px 3px;
+    display: inline-flex;
+    align-items: center;
+
+    .iconZan {
+      width: 18px;
+      height: 18px;
+    }
+  }
+}
+
+.musicSection {
+  margin: 0 13px 12px;
+  padding: 14px 12px;
+  background: #FFFFFF;
+  border-radius: 10px;
+
+  .musicName {
+    font-size: 15px;
+    font-weight: 500;
+    color: #333333;
+    line-height: 21px;
+    display: flex;
+    align-items: center;
+
+    .musicTag {
+      margin-right: 6px;
+      padding: 1px 6px;
+      font-size: 12px;
+      color: #FF7B31;
+      line-height: 17px;
+      background: rgba(255, 166, 115, 0.07);
+      border-radius: 9px;
+      border: 1px solid #FFBF9A;
+      font-weight: 400;
+    }
+  }
+
+  .musicDesc {
+    padding-top: 8px;
+    font-size: 14px;
+    color: #777777;
+    line-height: 20px;
+  }
+}
+
+.likeSection {
+  margin: 0 13px 12px;
+  background: #FFFFFF;
+  border-radius: 10px;
+  padding: 10px 12px;
+
+  .likeTitle {
+    display: flex;
+    align-items: center;
+    font-size: 17px;
+    font-weight: 600;
+    color: #333333;
+    line-height: 24px;
+    padding-bottom: 8px;
+
+    &::before {
+      display: inline-block;
+      content: '';
+      width: 4px;
+      height: 14px;
+      border-radius: 2px;
+      background: linear-gradient(to bottom, #5BECFF, #259CFE);
+      margin-right: 6px;
+    }
+  }
+}
+
+.likeItem {
+  padding: 16px 0;
+
+  .userLogo {
+    border-radius: 50%;
+    overflow: hidden;
+    width: 42px;
+    height: 42px;
+    margin-right: 7px;
+  }
+
+  .userInfo {
+    .name {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333333;
+      line-height: 22px;
+    }
+
+    .sub {
+      padding-top: 2px;
+      font-size: 13px;
+      color: #777777;
+      line-height: 18px;
+    }
+  }
+
+  .time {
+    font-size: 13px;
+    color: #777777;
+    line-height: 18px;
+  }
+}
+
+
+.bottomSection {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background-color: #fff;
+  padding: 15px 12px;
+
+  .bottomShare {
+    display: flex;
+    align-items: center;
+
+    p {
+      padding: 0 15px;
+      text-align: center;
+      line-height: 0;
+
+      &:first-child {
+        padding-left: 5px;
+      }
+    }
+
+    img {
+      width: 18px;
+      height: 18px;
+    }
+
+    span {
+      padding-top: 8px;
+      font-size: 12px;
+      color: #333333;
+      line-height: 17px;
+      display: block;
+    }
+  }
+
+  .btnEdit {
+    font-size: 14px;
+    font-weight: 500;
+    background: linear-gradient(135deg, #19F1E1 0%, #0094FF 100%);
+    color: #FFFFFF;
+    line-height: 22px;
+    min-width: 80px;
+    height: 30px;
+    border: none;
+  }
+}
+
+.popupContainer {
+  width: 80%;
+
+
+  .popupContent {
+    padding: 29px 0 25px;
+    text-align: center;
+    font-size: 18px;
+    font-weight: 500;
+    color: #333333;
+    line-height: 25px;
+  }
+
+  .popupBtnGroup {
+    text-align: center;
+    margin-bottom: 22px;
+
+    :global {
+      .van-button {
+        height: 40px;
+        font-size: 16px;
+        font-weight: 400 !important;
+        line-height: 22px;
+        min-width: 122px;
+
+        &:last-child {
+          margin-left: 10px;
+          background: linear-gradient(to right, #5BECFF, #259CFE);
+          border: none;
+        }
+      }
+    }
+  }
+}
+
+.cellGroup {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.cell {
+  // display: flex;
+  // flex-direction: column;
+  width: 96px;
+  margin-right: 18px;
+  margin-bottom: 18px;
+
+  &:nth-child(3n + 3) {
+    margin-right: 0;
+  }
+
+  .cellImg {
+    position: relative;
+
+    &::before {
+      content: '';
+      position: absolute;
+      right: -8px;
+      top: 3px;
+      z-index: 8;
+      width: 84px;
+      height: 84px;
+      background: url('./images/audio-pan.png') no-repeat center;
+      background-size: contain;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+
+    .iconZan {
+      position: absolute;
+      bottom: 4px;
+      left: 4px;
+      z-index: 10;
+      padding: 3px;
+      background: rgba(67, 67, 67, 0.3);
+      border-radius: 8px;
+      backdrop-filter: blur(4px);
+
+      font-size: 9px;
+      font-weight: 500;
+      color: #FFFFFF;
+      line-height: 13px;
+      display: flex;
+      align-items: center;
+
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 12px;
+        height: 12px;
+        background: url('./images/icon-z.png') no-repeat center;
+        background-size: contain;
+      }
+    }
+  }
+
+  .cellImage {
+    position: relative;
+    width: 88px;
+    height: 88px;
+    border-radius: 12px;
+    z-index: 9;
+
+    :global {
+      img {
+        border-radius: 12px;
+      }
+    }
+  }
+
+  .cellTitle {
+    font-size: 13px;
+    color: #131415;
+    line-height: 18px;
+    margin: 8px 0 6px;
+  }
+
+  .users {
+    display: flex;
+    align-items: center;
+
+    .userImg {
+      width: 20px;
+      height: 20px;
+      border-radius: 50%;
+      overflow: hidden;
+      margin-right: 4px;
+    }
+
+    .name {
+      font-size: 12px;
+      color: #333333;
+      line-height: 14px;
+    }
+  }
+}

+ 415 - 0
src/views/creation/index.tsx

@@ -0,0 +1,415 @@
+import { defineComponent, onMounted, reactive } from 'vue';
+import WaveSurfer from 'wavesurfer.js';
+import styles from './index.module.less';
+import MSticky from '@/components/m-sticky';
+import MHeader from '@/components/m-header';
+import { Button, Cell, Image, List, Popup, Slider, showToast } from 'vant';
+import iconDownload from './images/icon-download.png';
+import iconShare from './images/icon-share.png';
+import iconDelete from './images/icon-delete.png';
+import iconMember from './images/icon-member.png';
+import iconZan from './images/icon-zan.png';
+import iconZanActive from './images/icon-zan-active.png';
+import iconPlay from './images/icon-play.png';
+import iconPause from './images/icon-pause.png';
+import { postMessage } from '@/helpers/native-message';
+import { browser, getGradeCh, getSecondRPM } from '@/helpers/utils';
+import { useRoute, useRouter } from 'vue-router';
+import {
+  api_userMusicDetail,
+  api_userMusicRemove,
+  api_userMusicStarPage
+} from './api';
+import MEmpty from '@/components/m-empty';
+import dayjs from 'dayjs';
+import { nextTick } from 'process';
+import MVideo from '@/components/m-video';
+import ShareModel from './share-model';
+
+export default defineComponent({
+  name: 'creation-detail',
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    const audioId = 'a' + +Date.now() + Math.floor(Math.random() * 100);
+
+    const state = reactive({
+      id: route.query.id,
+      deleteStatus: false,
+      shareStatus: false,
+      playType: '' as 'Audio' | 'Video' | '', // 播放类型
+      musicDetail: {} as any,
+      timer: null as any,
+      paused: true,
+      currentTime: 0,
+      duration: 0.1,
+      loop: false,
+      dragStatus: false, // 是否开始拖动
+      isClick: false,
+      list: [] as any,
+      listState: {
+        dataShow: true, // 判断是否有数据
+        loading: false,
+        finished: false
+      },
+      params: {
+        page: 1,
+        rows: 20
+      }
+    });
+    const audioDom = new Audio();
+    audioDom.controls = true;
+    audioDom.style.width = '100%';
+    audioDom.className = styles.audio;
+
+    /** 改变播放时间 */
+    const handleChangeTime = (val: number) => {
+      state.currentTime = val;
+      clearTimeout(state.timer);
+      state.timer = setTimeout(() => {
+        // audioRef.value.currentTime = val;
+        audioDom.currentTime = val;
+        state.timer = null;
+      }, 60);
+    };
+
+    // 切换音频播放
+    const onToggleAudio = (e: any) => {
+      e.stopPropagation();
+      if (audioDom.paused) {
+        audioDom.play();
+      } else {
+        audioDom.pause();
+      }
+
+      state.paused = audioDom.paused;
+    };
+
+    // 获取列表
+    const getStarList = async () => {
+      try {
+        if (state.isClick) return;
+        state.isClick = true;
+        const res = await api_userMusicStarPage({
+          userMusicId: state.id,
+          ...state.params
+        });
+        state.listState.loading = false;
+        const result = res.data || {};
+        // 处理重复请求数据
+        if (state.list.length > 0 && result.current === 1) {
+          return;
+        }
+        state.list = state.list.concat(result.rows || []);
+        state.listState.finished = result.current >= result.pages;
+        state.params.page = result.current + 1;
+        state.listState.dataShow = state.list.length > 0;
+        state.isClick = false;
+      } catch {
+        state.listState.dataShow = false;
+        state.listState.finished = true;
+        state.isClick = false;
+      }
+    };
+
+    const initAudio = () => {
+      const wavesurfer = WaveSurfer.create({
+        container: document.querySelector(`#${audioId}`) as HTMLElement,
+        waveColor: '#fff',
+        progressColor: '#2FA1FD',
+        url: state.musicDetail.videoUrl,
+        cursorWidth: 0,
+        height: 35,
+        width: 'auto',
+        normalize: true,
+        // Set a bar width
+        barWidth: 2,
+        // Optionally, specify the spacing between bars
+        barGap: 2,
+        // And the bar radius
+        barRadius: 4,
+        barHeight: 0.6,
+        autoScroll: true,
+        /** If autoScroll is enabled, keep the cursor in the center of the waveform during playback */
+        autoCenter: true,
+        hideScrollbar: false,
+        media: audioDom
+      });
+
+      wavesurfer.once('interaction', () => {
+        // wavesurfer.play();
+      });
+      wavesurfer.once('ready', () => {
+        state.paused = audioDom.paused;
+        state.duration = audioDom.duration;
+      });
+
+      wavesurfer.on('finish', () => {
+        state.paused = true;
+      });
+
+      // 播放时监听
+      audioDom.addEventListener('timeupdate', () => {
+        state.currentTime = audioDom.currentTime;
+      });
+    };
+
+    // 删除作品
+    const onDelete = async () => {
+      try {
+        await api_userMusicRemove({ id: state.id });
+
+        setTimeout(() => {
+          showToast('删除成功');
+        }, 100);
+
+        setTimeout(() => {
+          if (browser().isApp) {
+            postMessage({
+              api: 'goBack'
+            });
+          } else {
+            router.back();
+          }
+        }, 1200);
+      } catch {
+        //
+      }
+    };
+
+    // 下载
+    const onDownload = () => {
+      //
+    };
+
+    onMounted(async () => {
+      try {
+        const { data } = await api_userMusicDetail(state.id);
+        state.musicDetail = data;
+        getStarList();
+        // 判断是视频还是音频
+        if (data.videoFilePath) {
+          state.playType = 'Video';
+        } else {
+          state.playType = 'Audio';
+          // 初始化
+          nextTick(() => {
+            initAudio();
+          });
+        }
+      } catch {
+        //
+      }
+    });
+    return () => (
+      <div class={styles.creation}>
+        <MSticky position="top">
+          <MHeader
+            border={false}
+            isBack={route.query.platformType != 'ANALYSIS'}
+          />
+        </MSticky>
+        <div class={styles.playSection}>
+          {state.playType === 'Video' && (
+            <MVideo
+              src={state.musicDetail.videoUrl}
+              poster={state.musicDetail.img}
+            />
+          )}
+          {state.playType === 'Audio' && (
+            <div class={styles.audioSection}>
+              <div class={styles.audioContainer}>
+                <div
+                  id={audioId}
+                  onClick={(e: MouseEvent) => {
+                    e.stopPropagation();
+                  }}></div>
+              </div>
+
+              <div class={styles.audioBox}>
+                <div class={styles.audioPan}>
+                  <Image class={styles.audioImg} src={state.musicDetail.img} />
+                </div>
+                <i class={styles.audioPoint}></i>
+                <i class={styles.audioZhen}></i>
+              </div>
+              <div
+                class={[styles.controls]}
+                onClick={(e: Event) => {
+                  e.stopPropagation();
+                }}
+                onTouchmove={(e: TouchEvent) => {
+                  // emit('close');
+                }}>
+                <div class={styles.actions}>
+                  <div class={styles.actionBtn} onClick={onToggleAudio}>
+                    <img src={state.paused ? iconPlay : iconPause} />
+                  </div>
+                </div>
+                <div class={[styles.slider]}>
+                  <Slider
+                    step={0.01}
+                    class={styles.timeProgress}
+                    v-model={state.currentTime}
+                    max={state.duration}
+                    onUpdate:modelValue={val => {
+                      handleChangeTime(val);
+                    }}
+                    onDragStart={() => {
+                      state.dragStatus = true;
+                      console.log('onDragStart');
+                    }}
+                    onDragEnd={() => {
+                      state.dragStatus = false;
+                      console.log('onDragEnd');
+                    }}
+                  />
+                </div>
+                <div class={styles.time}>
+                  <div>{getSecondRPM(state.currentTime)}</div>
+                  <span>/</span>
+                  <div>{getSecondRPM(state.duration)}</div>
+                </div>
+              </div>
+            </div>
+          )}
+        </div>
+
+        <Cell class={styles.userSection} center>
+          {{
+            icon: () => (
+              <Image class={styles.userLogo} src={state.musicDetail.avatar} />
+            ),
+            title: () => (
+              <div class={styles.userInfo}>
+                <p class={styles.name}>
+                  {state.musicDetail.username}
+                  {state.musicDetail.vipFlag && (
+                    <img src={iconMember} class={styles.iconMember} />
+                  )}
+                </p>
+                <p class={styles.sub}>
+                  {state.musicDetail.subjectName}{' '}
+                  {getGradeCh(state.musicDetail.currentGradeNum)}
+                </p>
+              </div>
+            ),
+            value: () => (
+              <div class={styles.zan}>
+                <img src={iconZanActive} class={styles.iconZan} />
+                {state.musicDetail.likeNum}
+              </div>
+            )
+          }}
+        </Cell>
+
+        <div class={styles.musicSection}>
+          <div class={styles.musicName}>
+            <span class={styles.musicTag}>曲目名称</span>
+            {state.musicDetail.musicSheetName}
+          </div>
+          {state.musicDetail.desc && (
+            <div class={styles.musicDesc}>{state.musicDetail.desc}</div>
+          )}
+        </div>
+
+        <div class={styles.likeSection}>
+          <div class={styles.likeTitle}>点赞记录</div>
+
+          {state.listState.dataShow ? (
+            <List
+              finished={state.listState.finished}
+              finishedText=" "
+              class={[styles.container, styles.containerInformation]}
+              onLoad={getStarList}
+              immediateCheck={false}>
+              {state.list.map((item: any) => (
+                <Cell class={styles.likeItem}>
+                  {{
+                    icon: () => (
+                      <Image src={item.userAvatar} class={styles.userLogo} />
+                    ),
+                    title: () => (
+                      <div class={styles.userInfo}>
+                        <p class={styles.name}>{item.userName}</p>
+                        <p class={styles.sub}>
+                          {item.subjectName} {getGradeCh(item.currentGradeNum)}
+                        </p>
+                      </div>
+                    ),
+                    value: () => (
+                      <div class={styles.time}>
+                        {dayjs(item.createTime).format('YYYY-MM-DD HH:mm')}
+                      </div>
+                    )
+                  }}
+                </Cell>
+              ))}
+            </List>
+          ) : (
+            <MEmpty description="暂无数据" />
+          )}
+        </div>
+
+        <MSticky position="bottom">
+          <div class={styles.bottomSection}>
+            <div class={styles.bottomShare}>
+              <p onClick={onDownload}>
+                <img src={iconDownload} />
+                <span>下载</span>
+              </p>
+              <p onClick={() => (state.shareStatus = true)}>
+                <img src={iconShare} />
+                <span>分享</span>
+              </p>
+              <p onClick={() => (state.deleteStatus = true)}>
+                <img src={iconDelete} />
+                <span>删除</span>
+              </p>
+            </div>
+            <Button
+              round
+              class={styles.btnEdit}
+              type="primary"
+              onClick={() => {
+                router.push({
+                  path: '/creation-edit',
+                  query: {
+                    id: state.id
+                  }
+                });
+              }}>
+              编辑
+            </Button>
+          </div>
+        </MSticky>
+
+        <Popup
+          v-model:show={state.deleteStatus}
+          round
+          class={styles.popupContainer}>
+          <p class={styles.popupContent}>确定是否删除</p>
+          <div class={styles.popupBtnGroup}>
+            <Button round onClick={() => (state.deleteStatus = false)}>
+              取消
+            </Button>
+            <Button round type="primary" onClick={onDelete}>
+              确定
+            </Button>
+          </div>
+        </Popup>
+
+        <Popup
+          position="bottom"
+          v-model:show={state.shareStatus}
+          safeAreaInsetBottom
+          style={{ background: 'transparent' }}>
+          <ShareModel
+            musicDetail={state.musicDetail}
+            onClose={() => (state.shareStatus = false)}
+          />
+        </Popup>
+      </div>
+    );
+  }
+});

BIN
src/views/creation/login-model/images/icon-close.png


BIN
src/views/creation/login-model/images/icon-password.png


BIN
src/views/creation/login-model/images/icon-phone.png


BIN
src/views/creation/login-model/images/login-bg.png


+ 62 - 0
src/views/creation/login-model/index.module.less

@@ -0,0 +1,62 @@
+.loginModel {
+  background: url('./images/login-bg.png') no-repeat center;
+  background-size: contain;
+  width: 306px;
+  height: 370px;
+
+  .iconClose {
+    width: 24px;
+    height: 24px;
+    position: absolute;
+    top: 30px;
+    right: -12px;
+    background: url('./images/icon-close.png') no-repeat center;
+    background-size: contain;
+    z-index: 1;
+  }
+}
+
+.loginSection {
+  padding: 126px 16px 0;
+}
+
+.fieldSection {
+  padding-bottom: 18px;
+
+  :global {
+    .van-field {
+      background: #F2F4F8;
+      border-radius: 24px;
+      padding: 11px 16px;
+      margin-bottom: 12px;
+    }
+
+    .van-field__left-icon {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  .icon {
+    width: 20px;
+    height: 20px;
+  }
+
+  .codeText {
+    font-size: 14px;
+    color: #198CFE;
+  }
+}
+
+.btnGroup {
+  .btnText {
+    display: block;
+    padding-top: 16px;
+    text-align: center;
+    font-size: 16px;
+    font-weight: 500;
+    color: #496AAC;
+    line-height: 22px;
+    text-align: center;
+  }
+}

+ 190 - 0
src/views/creation/login-model/index.tsx

@@ -0,0 +1,190 @@
+import { Teleport, defineComponent, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import { Button, CountDown, Field, showToast } from 'vant';
+import iconPhone from './images/icon-phone.png';
+import iconPassword from './images/icon-password.png';
+import { checkPhone } from '@/helpers/utils';
+import request from '@/helpers/request';
+import { storage } from '@/helpers/storage';
+import { ACCESS_TOKEN } from '@/store/mutation-types';
+import { setLogin, state } from '@/state';
+import MImgCode from '@/components/m-img-code';
+import { nextTick } from 'process';
+
+type loginType = 'PWD' | 'SMS';
+export default defineComponent({
+  name: 'login-model',
+  props: {
+    /** 是否支持短信验证码注册 */
+    isRegister: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['close', 'confirm'],
+  setup(props, { emit }) {
+    const countDownRef = ref();
+    const state = reactive({
+      loginType: 'SMS' as loginType,
+      imgCodeStatus: false,
+      username: '',
+      password: '',
+      smsCode: '',
+      countDownStatus: true,
+      countDownTime: 1000 * 120 // 倒计时时间
+    });
+
+    const onLogin = async () => {
+      try {
+        if (!checkPhone(state.username)) {
+          return showToast('请输入正确的手机号码');
+        }
+        const forms: any = {
+          username: state.username,
+          client_id: 'cooleshow-student',
+          client_secret: 'cooleshow-student',
+          password: state.loginType === 'PWD' ? state.password : state.smsCode,
+          grant_type: 'password',
+          loginType: state.loginType === 'PWD' ? 'PASSWORD' : 'SMS',
+          autoRegister: props.isRegister
+        };
+
+        const { data } = await request.post('/edu-app/userlogin', {
+          requestType: 'form',
+          data: {
+            ...forms
+          }
+        });
+        storage.set(ACCESS_TOKEN, data.token_type + ' ' + data.access_token);
+
+        const userCash = await request.get('/edu-app/user/getUserInfo', {
+          initRequest: true // 初始化接口
+        });
+        setLogin(userCash.data);
+
+        emit('confirm', true);
+      } catch (e: any) {
+        //
+        console.log(e);
+      }
+    };
+
+    const onSendCode = () => {
+      // 发送验证码
+      if (!checkPhone(state.username)) {
+        return showToast('请输入正确的手机号码');
+      }
+      state.imgCodeStatus = true;
+    };
+
+    const onCodeSend = () => {
+      state.countDownStatus = false;
+      nextTick(() => {
+        console.log(countDownRef.value, 'countDownRef.value');
+        countDownRef.value.start();
+      });
+    };
+    const onFinished = () => {
+      state.countDownStatus = true;
+      countDownRef.value.reset();
+    };
+    return () => (
+      <div class={styles.loginModel}>
+        <i class={styles.iconClose} onClick={() => emit('close')}></i>
+        <div class={styles.loginSection}>
+          <div class={styles.fieldSection}>
+            <Field
+              v-model={state.username}
+              placeholder="请输入手机号"
+              autocomplete="off"
+              border={false}
+              type="tel"
+              maxlength={11}>
+              {{
+                'left-icon': () => <img src={iconPhone} class={styles.icon} />
+              }}
+            </Field>
+
+            {state.loginType === 'PWD' ? (
+              <Field
+                v-model={state.password}
+                placeholder="请输入密码"
+                autocomplete="off"
+                border={false}
+                type="password">
+                {{
+                  'left-icon': () => (
+                    <img src={iconPassword} class={styles.icon} />
+                  )
+                }}
+              </Field>
+            ) : (
+              <Field
+                v-model={state.smsCode}
+                placeholder="请输入验证码"
+                autocomplete="off"
+                maxlength={6}
+                type="tel"
+                border={false}>
+                {{
+                  'left-icon': () => (
+                    <img src={iconPassword} class={styles.icon} />
+                  ),
+                  button: () =>
+                    state.countDownStatus ? (
+                      <span class={styles.codeText} onClick={onSendCode}>
+                        获取验证码
+                      </span>
+                    ) : (
+                      <CountDown
+                        ref={(el: any) => (countDownRef.value = el)}
+                        auto-start={false}
+                        time={state.countDownTime}
+                        onFinish={onFinished}
+                        format="ss秒"
+                      />
+                    )
+                }}
+              </Field>
+            )}
+          </div>
+
+          <div class={styles.btnGroup}>
+            <Button
+              round
+              block
+              color="linear-gradient(90deg, #44C9FF 0%, #259CFE 100%)"
+              onClick={onLogin}>
+              登录
+            </Button>
+
+            <span
+              class={styles.btnText}
+              onClick={() => {
+                if (state.loginType === 'PWD') {
+                  state.loginType = 'SMS';
+                } else {
+                  state.loginType = 'PWD';
+                }
+              }}>
+              {state.loginType === 'PWD' ? '验证码登录' : '密码登录'}
+            </span>
+          </div>
+        </div>
+
+        {state.imgCodeStatus ? (
+          <Teleport to={'body'}>
+            <MImgCode
+              v-model:value={state.imgCodeStatus}
+              phone={state.username}
+              onClose={() => {
+                state.imgCodeStatus = false;
+              }}
+              onSendCode={onCodeSend}
+            />
+          </Teleport>
+        ) : null}
+      </div>
+    );
+  }
+});

BIN
src/views/creation/share-model/images/icon-download.png


BIN
src/views/creation/share-model/images/icon-friend-ring.png


BIN
src/views/creation/share-model/images/icon-friend.png


BIN
src/views/creation/share-model/images/icon-link.png


BIN
src/views/creation/share-model/images/icon-logo.png


BIN
src/views/creation/share-model/images/icon-wechat.png


BIN
src/views/creation/share-model/images/music-bg.png


BIN
src/views/creation/share-model/images/share-bg.png


+ 193 - 0
src/views/creation/share-model/index.module.less

@@ -0,0 +1,193 @@
+.shareContent {
+  position: relative;
+  width: 335px;
+  height: 355px;
+  margin: 0 auto 76px;
+
+  .shareBg {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 1;
+    width: 335px;
+    height: 355px;
+  }
+
+  .share_content__title {
+    position: relative;
+    z-index: 3;
+    padding-top: 54px;
+    padding-left: 14px;
+    color: #FFFFFF;
+    text-shadow: 0px 2px 4px #3681FF;
+    width: 170px;
+
+    .large {
+      font-size: 19px;
+      font-weight: 600;
+      line-height: 26px;
+    }
+
+    .small {
+      padding-top: 6px;
+      font-size: 14px;
+      line-height: 20px;
+    }
+  }
+}
+
+.sectionFile {
+  position: relative;
+  z-index: 3;
+  margin: 25px 15px 14px;
+  padding: 12px;
+  display: flex;
+  background: rgba(255, 255, 255, 0.79);
+  border-radius: 10px;
+
+  .muploader {
+    position: relative;
+    z-index: 9;
+    width: 56px;
+    height: 56px;
+    object-fit: cover;
+    border-radius: 6px;
+  }
+
+  .uploadImg {
+    position: relative;
+    border-radius: 6px;
+    margin-right: 16px;
+
+
+    .audioPan {
+      position: absolute;
+      top: 0;
+      right: -6px;
+      z-index: 1;
+      width: 56px;
+      height: 56px;
+    }
+
+
+  }
+
+  .musicDetail {
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+
+    .musicName {
+      font-size: 16px;
+      font-weight: 600;
+      color: #131415;
+      line-height: 22px;
+      max-width: 200px;
+    }
+
+    .username {
+      padding-top: 4px;
+      font-size: 13px;
+      color: #aaa;
+      line-height: 20px;
+    }
+  }
+}
+
+.downloadSection {
+  display: flex;
+  align-items: center;
+  margin: 0 20px;
+  position: relative;
+  z-index: 3;
+
+  .qrcode {
+    position: relative;
+    width: 84px;
+    height: 84px;
+    border-radius: 4px;
+    // opacity: 0.53;
+    border: 1px solid rgba(37, 156, 254, 1);
+    // border-image: linear-gradient(130deg, rgba(37, 156, 254, 1), rgba(68, 201, 255, 1)) 1 1;
+    flex-shrink: 0;
+    overflow: hidden;
+
+    .qrcodeCanvas {
+      width: 100% !important;
+      height: 100% !important;
+    }
+
+    .qrcodeLogo {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      z-index: 10;
+      margin-left: -8px;
+      margin-top: -8px;
+      width: 16px;
+      height: 16px;
+      border-radius: 4px;
+    }
+  }
+
+  .qrtips {
+    margin-left: 10px;
+    padding-left: 9px;
+    border-left: 1px dashed #D8D8D8;
+
+    .tip {
+      font-size: 12px;
+      color: #777777;
+      line-height: 17px;
+    }
+
+    .iconLogo {
+      width: 102px;
+      height: 20px;
+      margin: 6px 0 4px;
+    }
+
+    .downTip {
+      font-size: 12px;
+      font-weight: 500;
+      color: #333333;
+      line-height: 17px;
+    }
+  }
+}
+
+
+.shareBottom {
+  position: relative;
+  background-color: #fff;
+  border-radius: 12px 12px 0px 0px;
+
+  .iconClose {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    font-size: 16px;
+    color: #999999;
+  }
+}
+
+.share__header {
+  padding: 12px 15px 8px;
+  font-size: 16px;
+  color: #333333;
+  line-height: 22px;
+}
+
+.gridSection {
+  --van-grid-item-icon-size: 44px;
+  --van-grid-item-text-font-size: 12px;
+  --van-grid-item-text-color: #646566;
+  --van-grid-item-content-padding: 16px 12px;
+}
+
+.btn {
+  font-size: 16px;
+  color: #AAAAAA;
+  line-height: 55px;
+  text-align: center;
+}

+ 189 - 0
src/views/creation/share-model/index.tsx

@@ -0,0 +1,189 @@
+import { defineComponent, onMounted, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import {
+  Button,
+  Grid,
+  GridItem,
+  Icon,
+  Image,
+  closeToast,
+  showFailToast,
+  showLoadingToast,
+  showSuccessToast,
+  showToast
+} from 'vant';
+import iconDownload from './images/icon-download.png';
+import iconFirend from './images/icon-friend.png';
+import iconWeChat from './images/icon-wechat.png';
+import iconFriendRing from './images/icon-friend-ring.png';
+// import iconLink from './images/icon-link.png';
+import iconLogo from './images/icon-logo.png';
+import shareBg from './images/share-bg.png';
+import audioPan from '../images/audio-pan.png';
+import smallLogo from '@common/images/logo.png';
+import musicBg from './images/music-bg.png';
+import QRCode from 'qrcode';
+import { promisefiyPostMessage } from '@/helpers/native-message';
+import html2canvas from 'html2canvas';
+
+export default defineComponent({
+  name: 'sahre-model',
+  props: {
+    musicDetail: {
+      type: Object,
+      default: () => {}
+    }
+  },
+  emits: ['close'],
+  setup(props, { emit }) {
+    const canvasRef = ref();
+    const state = reactive({
+      saveLoading: false,
+      image: null as any
+    });
+    const saveImg = async () => {
+      showLoadingToast({
+        message: '图片生成中...',
+        forbidClick: true
+      });
+      setTimeout(() => {
+        state.saveLoading = false;
+      }, 100);
+      const res = await promisefiyPostMessage({
+        api: 'savePicture',
+        content: {
+          base64: state.image
+        }
+      });
+      if (res?.content?.status === 'success') {
+        showSuccessToast('保存成功');
+      } else {
+        showFailToast('保存失败');
+      }
+    };
+
+    const onSavePath = () => {
+      // 判断是否在保存中...
+      if (state.saveLoading) {
+        return;
+      }
+      state.saveLoading = true;
+      // 判断是否已经生成图片
+      if (state.image) {
+        saveImg();
+      } else {
+        const container: any = document.getElementById('shareContent');
+        html2canvas(container, {
+          allowTaint: true,
+          useCORS: true,
+          backgroundColor: null
+        })
+          .then(async canvas => {
+            const url = canvas.toDataURL('image/png');
+            state.image = url;
+            saveImg();
+          })
+          .catch(() => {
+            closeToast();
+            state.saveLoading = false;
+          });
+      }
+    };
+
+    onMounted(() => {
+      const canvas = canvasRef.value;
+      QRCode.toCanvas(
+        canvas,
+        location.origin +
+          location.pathname +
+          '#/shareCreation?id=' +
+          props.musicDetail.id,
+        {
+          margin: 1
+        },
+        (error: any) => {
+          if (error) console.log(error);
+          console.log('success');
+        }
+      );
+    });
+
+    return () => (
+      <div class={styles.shareModel}>
+        <div class={styles.shareContent} id="shareContent">
+          <img src={shareBg} class={styles.shareBg} />
+          <div class={styles.share_content__title}>
+            <p class={styles.large}>我在音乐数字课堂发布了演奏作品</p>
+            <p class={styles.small}>赶快扫码看看吧!</p>
+          </div>
+
+          <div class={[styles.share_music__container, styles.sectionFile]}>
+            <div class={styles.uploadImg}>
+              <img
+                src={audioPan}
+                class={styles.audioPan}
+                crossorigin="anonymous"
+              />
+              <img
+                src={
+                  props.musicDetail.img
+                    ? props.musicDetail.img + '?t' + new Date().getTime()
+                    : musicBg
+                }
+                class={styles.muploader}
+                crossorigin="anonymous"
+              />
+            </div>
+            <div class={styles.musicDetail}>
+              <p class={[styles.musicName, 'van-ellipsis']}>
+                {props.musicDetail.musicSheetName}
+              </p>
+              <p class={styles.username}>
+                演奏者:{props.musicDetail.username}
+              </p>
+            </div>
+          </div>
+
+          <div class={styles.downloadSection}>
+            <div class={styles.qrcode}>
+              <canvas ref={canvasRef} class={styles.qrcodeCanvas}></canvas>
+              <img src={smallLogo} class={styles.qrcodeLogo} />
+            </div>
+            <div class={styles.qrtips}>
+              <p class={styles.tip}>
+                温馨提示:保存图片到相册或长按识别二维码进入查看喔~
+              </p>
+              <img src={iconLogo} class={styles.iconLogo} />
+              <p class={styles.downTip}>扫码下载音乐数字课堂App</p>
+            </div>
+          </div>
+        </div>
+
+        <div class={styles.shareBottom}>
+          <Icon
+            name="cross"
+            class={styles.iconClose}
+            onClick={() => emit('close')}
+          />
+          <div class={styles.share__header}>海报已生成!快来分享吧!</div>
+          <Grid columnNum={4} border={false} class={styles.gridSection}>
+            <GridItem
+              icon={iconDownload}
+              text="保存本地"
+              onClick={onSavePath}></GridItem>
+            <GridItem icon={iconFirend} text="群聊"></GridItem>
+            <GridItem icon={iconWeChat} text="微信好友"></GridItem>
+            <GridItem icon={iconFriendRing} text="朋友圈"></GridItem>
+            {/* <GridItem icon={iconLink} text="复制链接"></GridItem> */}
+          </Grid>
+
+          <div
+            class={[styles.btn, 'van-hairline--top']}
+            onClick={() => emit('close')}>
+            取消
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

Some files were not shown because too many files changed in this diff