index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  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.rotateCounterclockwise,
  315. nodes.rotateClockwise,
  316. nodes.resizeToOriginalSize,
  317. nodes.zoomOut,
  318. nodes.zoomIn,
  319. nodes.close
  320. ];
  321. }}
  322. />
  323. )}
  324. {/* 乐谱 */}
  325. {props.item.type === 'MUSIC' && (
  326. <>
  327. <NImage
  328. class={[styles.cover, styles.image]}
  329. lazy
  330. previewDisabled={true}
  331. objectFit="contain"
  332. src={props.item.coverImg}
  333. />
  334. <NSpace
  335. class={[
  336. styles.audioPlayTypeSection,
  337. props.audioPlayTypeSize === 'small'
  338. ? styles.audioPlayTypeSmall
  339. : ''
  340. ]}>
  341. {props.item.audioPlayTypeArray?.includes('SING') && (
  342. <NTooltip trigger="hover" showArrow={false}>
  343. {{
  344. trigger: () => (
  345. <span
  346. class={[
  347. styles.iconType,
  348. styles.iconSing
  349. ]}></span>
  350. ),
  351. default: '演唱场景'
  352. }}
  353. </NTooltip>
  354. )}
  355. {props.item.audioPlayTypeArray?.includes('PLAY') && (
  356. <NTooltip trigger="hover" showArrow={false}>
  357. {{
  358. trigger: () => (
  359. <span
  360. class={[
  361. styles.iconType,
  362. styles.iconPlay
  363. ]}></span>
  364. ),
  365. default: '演奏场景'
  366. }}
  367. </NTooltip>
  368. )}
  369. </NSpace>
  370. </>
  371. )}
  372. {/* 音频 */}
  373. {props.item.type === 'SONG' && (
  374. <AudioPlayer
  375. content={props.item.content}
  376. cover={props.item.coverImg}
  377. previewDisabled={props.disabledMouseHover}
  378. />
  379. )}
  380. {/* 视频 */}
  381. {props.item.type === 'VIDEO' && (
  382. <VideoPlayer
  383. cover={props.item.coverImg}
  384. content={props.item.content}
  385. previewDisabled={props.disabledMouseHover}
  386. />
  387. )}
  388. {/* ppt */}
  389. {props.item.type === 'PPT' && (
  390. <NImage
  391. class={[styles.cover, styles.image]}
  392. lazy
  393. previewDisabled={true}
  394. objectFit="cover"
  395. src={props.item.coverImg || PageEnum.PPT_DEFAULT_COVER}
  396. />
  397. )}
  398. {/* 节奏练习 */}
  399. {props.item.type === 'RHYTHM' && (
  400. <NImage
  401. class={[styles.cover, styles.image]}
  402. lazy
  403. previewDisabled={true}
  404. objectFit="cover"
  405. src={props.item.coverImg || PageEnum.RHYTHM_DEFAULT_COVER}
  406. />
  407. )}
  408. {/* 听音练习 */}
  409. {props.item.type === 'LISTEN' && (
  410. <NImage
  411. class={[styles.cover, styles.image]}
  412. lazy
  413. previewDisabled={true}
  414. objectFit="cover"
  415. src={props.item.coverImg}
  416. />
  417. )}
  418. {/* 乐理 */}
  419. {props.item.type === 'THEORY' && (
  420. <NImage
  421. class={[styles.cover, styles.image]}
  422. lazy
  423. previewDisabled={true}
  424. objectFit="cover"
  425. src={props.item.coverImg || PageEnum.THEORY_DEFAULT_COVER}
  426. />
  427. )}
  428. {/* 名曲 */}
  429. {props.item.type === 'MUSIC_WIKI' && (
  430. <NImage
  431. class={[styles.cover, styles.image]}
  432. lazy
  433. previewDisabled={true}
  434. objectFit="cover"
  435. src={props.item.coverImg || PageEnum.MUSIC_DEFAULT_COVER}
  436. />
  437. )}
  438. {/* 乐器 */}
  439. {props.item.type === 'INSTRUMENT' && (
  440. <NImage
  441. class={[styles.cover, styles.image]}
  442. lazy
  443. previewDisabled={true}
  444. objectFit="cover"
  445. src={
  446. props.item.coverImg || PageEnum.INSTRUMENT_DEFAULT_COVER
  447. }
  448. />
  449. )}
  450. {/* 音乐家 */}
  451. {props.item.type === 'MUSICIAN' && (
  452. <NImage
  453. class={[styles.cover, styles.image]}
  454. lazy
  455. previewDisabled={true}
  456. objectFit="cover"
  457. src={props.item.coverImg || PageEnum.MUSICIAN_DEFAULT_COVER}
  458. />
  459. )}
  460. </>
  461. ),
  462. footer: () => (
  463. <div class={styles.footer}>
  464. <div style={{ position: 'relative' }}>
  465. <div class={[styles.title, 'footerTitle']}>
  466. <NImage
  467. class={[styles.titleType]}
  468. src={formatType(props.item.type)}
  469. objectFit="cover"
  470. />
  471. <span
  472. class={[
  473. styles.titleContent,
  474. props.isDownload && styles.titleContentDownload,
  475. 'titleContent'
  476. ]}>
  477. <TheNoticeBar
  478. isAnimation={isAnimation.value}
  479. text={props.item.title}
  480. />
  481. </span>
  482. </div>
  483. {/* 收藏 */}
  484. <div class={styles.btnGroup}>
  485. {props.isDownload && (
  486. <div class={styles.btnItem} onClick={onDownload}>
  487. <NSpin show={downloadStatus.value} size={'small'}>
  488. <img
  489. src={iconDownload}
  490. key="3"
  491. class={[styles.iconCollect]}
  492. />
  493. </NSpin>
  494. </div>
  495. )}
  496. {props.isShowCollect && (
  497. <div
  498. class={[styles.iconCollect, styles.btnItem]}
  499. onClick={(e: MouseEvent) => {
  500. e.stopPropagation();
  501. e.preventDefault();
  502. // 判断是否可以收藏
  503. if (props.isCollect) {
  504. emit('collect', props.item);
  505. }
  506. }}>
  507. <Transition name="favitor" mode="out-in">
  508. {props.item.isCollect ? (
  509. <img
  510. src={iconCollectActive}
  511. key="1"
  512. class={[
  513. styles.iconCollect,
  514. props.isCollect ? styles.isCollect : ''
  515. ]}
  516. />
  517. ) : (
  518. <img
  519. src={iconCollectDefault}
  520. key="2"
  521. class={[
  522. styles.iconCollect,
  523. props.isCollect ? styles.isCollect : ''
  524. ]}
  525. />
  526. )}
  527. </Transition>
  528. </div>
  529. )}
  530. </div>
  531. </div>
  532. {/* 精选 */}
  533. {props.item.isSelected && (
  534. <span class={styles.iconSelected}></span>
  535. )}
  536. {/* 添加按钮 */}
  537. {props.isShowAdd &&
  538. (props.item.exist ? (
  539. <NButton
  540. type="primary"
  541. class={[
  542. styles.addBtn,
  543. props.item.exist ? styles.addBtnDisabled : ''
  544. ]}
  545. disabled={props.item.exist || props.isShowAddDisabled}
  546. onClick={(e: MouseEvent) => {
  547. e.stopPropagation();
  548. e.preventDefault();
  549. emit('add', props.item);
  550. }}>
  551. {props.item.exist ? '已添加' : '添加'}
  552. </NButton>
  553. ) : (
  554. !props.isShowAddDisabled && (
  555. <NButton
  556. type="primary"
  557. class={[
  558. styles.addBtn,
  559. props.item.exist ? styles.addBtnDisabled : ''
  560. ]}
  561. disabled={props.item.exist || props.isShowAddDisabled}
  562. onClick={(e: MouseEvent) => {
  563. e.stopPropagation();
  564. e.preventDefault();
  565. emit('add', props.item);
  566. }}>
  567. {props.item.exist ? '已添加' : '添加'}
  568. </NButton>
  569. )
  570. ))}
  571. </div>
  572. )
  573. }}
  574. </NCard>
  575. </div>
  576. );
  577. }
  578. });