index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. import { PropType, Transition, defineComponent, ref } from 'vue';
  2. import styles from './index.module.less';
  3. import {
  4. ImageRenderToolbarProps,
  5. NButton,
  6. NCard,
  7. NImage,
  8. NModal,
  9. NSpace,
  10. NSpin,
  11. NTooltip,
  12. useMessage
  13. } from 'naive-ui';
  14. import iconImage from '@common/images/icon-image.png';
  15. import iconVideo from '@common/images/icon-video.png';
  16. import iconAudio from '@common/images/icon-audio.png';
  17. import iconMusic from '@common/images/icon-music.png';
  18. import iconPPT from '@common/images/icon-ppt.png';
  19. import iconOther from '@common/images/icon-other.png';
  20. import iconCollectDefault from '@common/images/icon-collect-default.png';
  21. import iconCollectActive from '@common/images/icon-collect-active.png';
  22. import iconDownload from '@common/images/icon-download.png';
  23. import TheNoticeBar from '../TheNoticeBar';
  24. import AudioPlayer from './audio-player';
  25. import VideoPlayer from './video-player';
  26. import { PageEnum } from '/src/enums/pageEnum';
  27. import { api_musicSheetDetail } from '/src/api/user';
  28. import JSZip, { file } from 'jszip';
  29. import { saveAs } from 'file-saver';
  30. // LISTEN:听音,RHYTHM:节奏,THEORY:乐理知识,MUSIC_WIKI:曲目 INSTRUMENT:乐器 MUSICIAN:音乐家)
  31. type itemType = {
  32. id: string | number;
  33. type:
  34. | 'IMG'
  35. | 'VIDEO'
  36. | 'SONG'
  37. | 'MUSIC'
  38. | 'PPT'
  39. | 'LISTEN'
  40. | 'RHYTHM'
  41. | 'THEORY'
  42. | 'MUSIC_WIKI'
  43. | 'INSTRUMENT'
  44. | 'MUSICIAN';
  45. coverImg: string;
  46. content?: string;
  47. title: string;
  48. isCollect: boolean;
  49. audioPlayTypeArray?: string[];
  50. isSelected: boolean; // 精选
  51. exist?: boolean; // 是否已经选
  52. };
  53. export default defineComponent({
  54. name: 'card-type',
  55. props: {
  56. // 是否是选中状态
  57. isActive: {
  58. type: Boolean,
  59. default: false
  60. },
  61. /** 是否可以拖拽 */
  62. draggable: {
  63. type: Boolean,
  64. default: false
  65. },
  66. // 是否可以收藏
  67. isCollect: {
  68. type: Boolean,
  69. default: true
  70. },
  71. // 是否显示收藏
  72. isShowCollect: {
  73. type: Boolean,
  74. default: true
  75. },
  76. // 是否显示添加按钮
  77. isShowAdd: {
  78. type: Boolean,
  79. default: false
  80. },
  81. // 是否禁用添加按钮
  82. isShowAddDisabled: {
  83. type: Boolean,
  84. default: false
  85. },
  86. // 鼠标移动上面的时候是否自动播放,或者可以点击
  87. disabledMouseHover: {
  88. type: Boolean,
  89. default: true
  90. },
  91. // 是否预览
  92. isPreview: {
  93. type: Boolean,
  94. default: true
  95. },
  96. item: {
  97. type: Object as PropType<itemType>,
  98. default: () => ({})
  99. },
  100. /** 是否下架 */
  101. offShelf: {
  102. type: Boolean,
  103. default: false
  104. },
  105. /** 是否可以下载 */
  106. isDownload: {
  107. type: Boolean,
  108. default: false
  109. },
  110. audioPlayTypeSize: {
  111. type: String as PropType<'default' | 'small'>,
  112. deafult: 'default'
  113. }
  114. },
  115. /**
  116. * @type {string} click 点击事件
  117. * @type {string} collect 收藏
  118. * @type {string} add 添加
  119. * @type {string} offShelf 下架
  120. */
  121. emits: ['click', 'collect', 'add', 'offShelf'],
  122. setup(props, { emit }) {
  123. const message = useMessage();
  124. const isAnimation = ref(false);
  125. const downloadStatus = ref(false);
  126. const formatType = (type: string) => {
  127. let typeImg = iconOther;
  128. switch (type) {
  129. case 'IMG':
  130. typeImg = iconImage;
  131. break;
  132. case 'VIDEO':
  133. typeImg = iconVideo;
  134. break;
  135. case 'SONG':
  136. typeImg = iconAudio;
  137. break;
  138. case 'MUSIC':
  139. typeImg = iconMusic;
  140. break;
  141. case 'PPT':
  142. typeImg = iconPPT;
  143. break;
  144. }
  145. return typeImg;
  146. };
  147. // 获取文件blob格式
  148. const getFileBlob = (url: string) => {
  149. return new Promise((resolve, reject) => {
  150. const request = new XMLHttpRequest();
  151. request.open('GET', url, true);
  152. request.responseType = 'blob';
  153. request.onload = (res: any) => {
  154. if (res.target.status == 200) {
  155. resolve(res.target.response);
  156. } else {
  157. reject(res);
  158. }
  159. };
  160. request.send();
  161. });
  162. };
  163. // 多个文件下载
  164. const downLoadMultiFile = (files: any, filesName: string) => {
  165. const zip = new JSZip();
  166. const result = [];
  167. for (const i in files) {
  168. const promise = getFileBlob(files[i].url).then((res: any) => {
  169. zip.file(files[i].name, res, { binary: true });
  170. });
  171. result.push(promise);
  172. }
  173. Promise.all(result)
  174. .then(() => {
  175. zip.generateAsync({ type: 'blob' }).then(res => {
  176. saveAs(
  177. res,
  178. filesName
  179. ? filesName + Date.now() + '.zip'
  180. : `文件夹${Date.now()}.zip`
  181. );
  182. });
  183. })
  184. .catch(() => {
  185. message.error('下载失败');
  186. });
  187. downloadStatus.value = false;
  188. };
  189. const downloadFile = (filename: string, fileUrl: string) => {
  190. // 发起Fetch请求
  191. fetch(fileUrl)
  192. .then(response => response.blob())
  193. .then(blob => {
  194. saveAs(blob, filename);
  195. setTimeout(() => {
  196. downloadStatus.value = false;
  197. }, 100);
  198. })
  199. .catch(() => {
  200. message.error('下载失败');
  201. });
  202. downloadStatus.value = false;
  203. };
  204. const getFileName = (url: any) => {
  205. // 使用正则表达式获取文件名
  206. const tempUrl = url.split('?');
  207. const fileNameRegex = /\/([^\\/]+)$/; // 匹配最后一个斜杠后的内容
  208. const match = tempUrl[0].match(fileNameRegex);
  209. if (match) {
  210. return match[1];
  211. } else {
  212. return '';
  213. }
  214. };
  215. const onDownload = async (e: MouseEvent) => {
  216. e.stopPropagation();
  217. e.preventDefault();
  218. const item = props.item;
  219. if (!item.content) {
  220. message.error('下载失败');
  221. return;
  222. }
  223. if (downloadStatus.value) return false;
  224. downloadStatus.value = true;
  225. const suffix: any = item.content?.split('.');
  226. const fileName = item.title + '.' + suffix[suffix?.length - 1];
  227. if (item.type === 'MUSIC') {
  228. const { data } = await api_musicSheetDetail(item.content);
  229. const urls = [];
  230. if (data.xmlFileUrl) {
  231. urls.push({
  232. url: data.xmlFileUrl,
  233. name: getFileName(data.xmlFileUrl)
  234. });
  235. }
  236. if (data.background && data.background.length > 0) {
  237. data.background.forEach((item: any) => {
  238. urls.push({
  239. url: item.audioFileUrl,
  240. name: getFileName(item.audioFileUrl)
  241. });
  242. });
  243. }
  244. downLoadMultiFile(urls, item.title);
  245. // setTimeout(() => {
  246. // downloadStatus.value = false;
  247. // }, 1000);
  248. } else {
  249. downloadFile(fileName, item.content);
  250. }
  251. };
  252. return () => (
  253. <div
  254. onClick={() => emit('click', props.item)}
  255. key={props.item.id}
  256. draggable={!props.draggable ? false : props.item.exist ? false : true}
  257. class={[
  258. styles['card-section'],
  259. 'card-section-container',
  260. !props.draggable ? '' : props.item.exist ? '' : styles.cardDrag
  261. ]}
  262. onMouseenter={() => {
  263. isAnimation.value = true;
  264. }}
  265. onMouseleave={() => {
  266. isAnimation.value = false;
  267. }}
  268. onDragstart={(e: any) => {
  269. e.dataTransfer.setData('text', JSON.stringify(props.item));
  270. }}>
  271. {/* 判断是否下架 */}
  272. {props.offShelf && (
  273. <div class={styles.offShelfBg}>
  274. <p class={styles.offShelfTips}>该资源已被下架</p>
  275. <NButton
  276. type="primary"
  277. class={styles.offShelfBtn}
  278. onClick={(e: MouseEvent) => {
  279. e.stopPropagation();
  280. emit('offShelf');
  281. }}>
  282. 确认
  283. </NButton>
  284. </div>
  285. )}
  286. <NCard
  287. class={[
  288. styles['card-section-content'],
  289. props.isShowAdd ? '' : styles.course,
  290. props.isActive ? styles.isActive : '',
  291. props.item.exist ? styles.showAddBtn : '' // 是否已添加
  292. ]}
  293. style={{ cursor: 'pointer' }}>
  294. {{
  295. cover: () => (
  296. <>
  297. {/* 图片 */}
  298. {props.item.type === 'IMG' && (
  299. <NImage
  300. class={[styles.cover, styles.image]}
  301. lazy
  302. previewDisabled={props.disabledMouseHover}
  303. objectFit="cover"
  304. src={props.item.coverImg}
  305. previewSrc={props.item.content}
  306. renderToolbar={({ nodes }: ImageRenderToolbarProps) => {
  307. return [
  308. nodes.prev,
  309. nodes.next,
  310. nodes.rotateCounterclockwise,
  311. nodes.rotateClockwise,
  312. nodes.resizeToOriginalSize,
  313. nodes.zoomOut,
  314. nodes.close
  315. ];
  316. }}
  317. />
  318. )}
  319. {/* 乐谱 */}
  320. {props.item.type === 'MUSIC' && (
  321. <>
  322. <NImage
  323. class={[styles.cover, styles.image]}
  324. lazy
  325. previewDisabled={true}
  326. objectFit="contain"
  327. src={props.item.coverImg}
  328. />
  329. <NSpace
  330. class={[
  331. styles.audioPlayTypeSection,
  332. props.audioPlayTypeSize === 'small'
  333. ? styles.audioPlayTypeSmall
  334. : ''
  335. ]}>
  336. {props.item.audioPlayTypeArray?.includes('PLAY') && (
  337. <NTooltip trigger="hover" showArrow={false}>
  338. {{
  339. trigger: () => (
  340. <span
  341. class={[
  342. styles.iconType,
  343. styles.iconPlay
  344. ]}></span>
  345. ),
  346. default: '演奏场景'
  347. }}
  348. </NTooltip>
  349. )}
  350. {props.item.audioPlayTypeArray?.includes('SING') && (
  351. <NTooltip trigger="hover" showArrow={false}>
  352. {{
  353. trigger: () => (
  354. <span
  355. class={[
  356. styles.iconType,
  357. styles.iconSing
  358. ]}></span>
  359. ),
  360. default: '演唱场景'
  361. }}
  362. </NTooltip>
  363. )}
  364. </NSpace>
  365. </>
  366. )}
  367. {/* 音频 */}
  368. {props.item.type === 'SONG' && (
  369. <AudioPlayer
  370. content={props.item.content}
  371. cover={props.item.coverImg}
  372. previewDisabled={props.disabledMouseHover}
  373. />
  374. )}
  375. {/* 视频 */}
  376. {props.item.type === 'VIDEO' && (
  377. <VideoPlayer
  378. cover={props.item.coverImg}
  379. content={props.item.content}
  380. previewDisabled={props.disabledMouseHover}
  381. />
  382. )}
  383. {/* ppt */}
  384. {props.item.type === 'PPT' && (
  385. <NImage
  386. class={[styles.cover, styles.image]}
  387. lazy
  388. previewDisabled={true}
  389. objectFit="cover"
  390. src={props.item.coverImg || PageEnum.PPT_DEFAULT_COVER}
  391. />
  392. )}
  393. {/* 节奏练习 */}
  394. {props.item.type === 'RHYTHM' && (
  395. <NImage
  396. class={[styles.cover, styles.image]}
  397. lazy
  398. previewDisabled={true}
  399. objectFit="cover"
  400. src={props.item.coverImg || PageEnum.RHYTHM_DEFAULT_COVER}
  401. />
  402. )}
  403. {/* 听音练习 */}
  404. {props.item.type === 'LISTEN' && (
  405. <NImage
  406. class={[styles.cover, styles.image]}
  407. lazy
  408. previewDisabled={true}
  409. objectFit="cover"
  410. src={props.item.coverImg}
  411. />
  412. )}
  413. {/* 乐理 */}
  414. {props.item.type === 'THEORY' && (
  415. <NImage
  416. class={[styles.cover, styles.image]}
  417. lazy
  418. previewDisabled={true}
  419. objectFit="cover"
  420. src={props.item.coverImg || PageEnum.THEORY_DEFAULT_COVER}
  421. />
  422. )}
  423. {/* 名曲 */}
  424. {props.item.type === 'MUSIC_WIKI' && (
  425. <NImage
  426. class={[styles.cover, styles.image]}
  427. lazy
  428. previewDisabled={true}
  429. objectFit="cover"
  430. src={props.item.coverImg || PageEnum.MUSIC_DEFAULT_COVER}
  431. />
  432. )}
  433. {/* 乐器 */}
  434. {props.item.type === 'INSTRUMENT' && (
  435. <NImage
  436. class={[styles.cover, styles.image]}
  437. lazy
  438. previewDisabled={true}
  439. objectFit="cover"
  440. src={
  441. props.item.coverImg || PageEnum.INSTRUMENT_DEFAULT_COVER
  442. }
  443. />
  444. )}
  445. {/* 音乐家 */}
  446. {props.item.type === 'MUSICIAN' && (
  447. <NImage
  448. class={[styles.cover, styles.image]}
  449. lazy
  450. previewDisabled={true}
  451. objectFit="cover"
  452. src={props.item.coverImg || PageEnum.MUSICIAN_DEFAULT_COVER}
  453. />
  454. )}
  455. </>
  456. ),
  457. footer: () => (
  458. <div class={styles.footer}>
  459. <div class={[styles.title, 'footerTitle']}>
  460. <NImage
  461. class={[styles.titleType]}
  462. src={formatType(props.item.type)}
  463. objectFit="cover"
  464. />
  465. <span class={[styles.titleContent, 'titleContent']}>
  466. <TheNoticeBar
  467. isAnimation={isAnimation.value}
  468. text={props.item.title}
  469. />
  470. </span>
  471. </div>
  472. {/* 收藏 */}
  473. <div class={styles.btnGroup}>
  474. {props.isDownload && (
  475. <div class={styles.btnItem} onClick={onDownload}>
  476. <NSpin show={downloadStatus.value} size={'small'}>
  477. <img
  478. src={iconDownload}
  479. key="3"
  480. class={[styles.iconCollect]}
  481. />
  482. </NSpin>
  483. </div>
  484. )}
  485. {props.isShowCollect && (
  486. <div
  487. class={[styles.iconCollect, styles.btnItem]}
  488. onClick={(e: MouseEvent) => {
  489. e.stopPropagation();
  490. e.preventDefault();
  491. // 判断是否可以收藏
  492. if (props.isCollect) {
  493. emit('collect', props.item);
  494. }
  495. }}>
  496. <Transition name="favitor" mode="out-in">
  497. {props.item.isCollect ? (
  498. <img
  499. src={iconCollectActive}
  500. key="1"
  501. class={[
  502. styles.iconCollect,
  503. props.isCollect ? styles.isCollect : ''
  504. ]}
  505. />
  506. ) : (
  507. <img
  508. src={iconCollectDefault}
  509. key="2"
  510. class={[
  511. styles.iconCollect,
  512. props.isCollect ? styles.isCollect : ''
  513. ]}
  514. />
  515. )}
  516. </Transition>
  517. </div>
  518. )}
  519. </div>
  520. {/* 精选 */}
  521. {props.item.isSelected && (
  522. <span class={styles.iconSelected}></span>
  523. )}
  524. {/* 添加按钮 */}
  525. {props.isShowAdd &&
  526. (props.item.exist ? (
  527. <NButton
  528. type="primary"
  529. class={[
  530. styles.addBtn,
  531. props.item.exist ? styles.addBtnDisabled : ''
  532. ]}
  533. disabled={props.item.exist || props.isShowAddDisabled}
  534. onClick={(e: MouseEvent) => {
  535. e.stopPropagation();
  536. e.preventDefault();
  537. emit('add', props.item);
  538. }}>
  539. {props.item.exist ? '已添加' : '添加'}
  540. </NButton>
  541. ) : (
  542. !props.isShowAddDisabled && (
  543. <NButton
  544. type="primary"
  545. class={[
  546. styles.addBtn,
  547. props.item.exist ? styles.addBtnDisabled : ''
  548. ]}
  549. disabled={props.item.exist || props.isShowAddDisabled}
  550. onClick={(e: MouseEvent) => {
  551. e.stopPropagation();
  552. e.preventDefault();
  553. emit('add', props.item);
  554. }}>
  555. {props.item.exist ? '已添加' : '添加'}
  556. </NButton>
  557. )
  558. ))}
  559. </div>
  560. )
  561. }}
  562. </NCard>
  563. </div>
  564. );
  565. }
  566. });