index.tsx 19 KB

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