detail.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import {
  2. NBreadcrumb,
  3. NBreadcrumbItem,
  4. NButton,
  5. NImage,
  6. NSlider,
  7. NSpace,
  8. NSpin
  9. } from 'naive-ui';
  10. import { computed, defineComponent, onMounted, reactive } from 'vue';
  11. import styles from './detail.module.less';
  12. import icon_back from '../../xiaoku-music/images/icon_back.png';
  13. import icon_arrow from '../../xiaoku-music/images/icon_arrow.png';
  14. import icon_play from '../../xiaoku-music/images/icon_play.png';
  15. import icon_pause from '../../xiaoku-music/images/icon_pause.png';
  16. import icon_default from '../../xiaoku-music/images/icon_default.png';
  17. import icon_separator from '../../xiaoku-music/images/icon_separator.png';
  18. import iconT from '../images/icon-t.png';
  19. import iconAddT from '../images/icon-add-t.png';
  20. import iconPlusT from '../images/icon-plus-t.png';
  21. import musicBg from '../../xiaoku-music/images/icon_default.png';
  22. import iconPan from '../images/icon-pan.png';
  23. import { useRoute, useRouter } from 'vue-router';
  24. import PlayLoading from '../../xiaoku-music/component/play-loading';
  25. import TheNoticeBar from '/src/components/TheNoticeBar';
  26. import TheEmpty from '/src/components/TheEmpty';
  27. import PlayItem from '../../xiaoku-music/component/play-item';
  28. import { api_knowledgeWiki_detail } from '../api';
  29. import { useSpeak } from '../useSpeak';
  30. export default defineComponent({
  31. name: 'instrument-detail',
  32. setup() {
  33. const route = useRoute();
  34. const router = useRouter();
  35. const speak = useSpeak();
  36. const forms = reactive({
  37. page: 1,
  38. rows: 20,
  39. status: true,
  40. name: '', // 关键词
  41. type: route.query.type
  42. });
  43. const data = reactive({
  44. loading: false,
  45. finshed: false,
  46. reshing: false,
  47. details: {} as any,
  48. list: [] as any,
  49. listActive: 0,
  50. playState: 'pause' as 'play' | 'pause',
  51. showPlayer: false,
  52. showPreivew: false,
  53. previewUrl: '',
  54. showCloseBtn: true,
  55. fontSize: 18 // 默认18
  56. });
  57. /** 选中的item */
  58. const activeItem = computed(() => {
  59. return data.list[data.listActive] || {};
  60. });
  61. /** 播放曲目 */
  62. const handlePlay = (item: any) => {
  63. const index = data.list.findIndex((_item: any) => _item.id === item.id);
  64. if (index > -1) {
  65. if (data.listActive === index) {
  66. data.playState = data.playState === 'play' ? 'pause' : 'play';
  67. } else {
  68. data.playState = 'play';
  69. }
  70. data.showPlayer = true;
  71. data.listActive = index;
  72. }
  73. };
  74. /** 音频控制 */
  75. const handleChangeAudio = (
  76. type: 'play' | 'pause' | 'pre' | 'next' | 'favitor'
  77. ) => {
  78. if (type === 'play') {
  79. data.playState = 'play';
  80. } else if (type === 'pause') {
  81. data.playState = 'pause';
  82. } else if (type === 'pre') {
  83. if (data.list[data.listActive - 1]) {
  84. handlePlay(data.list[data.listActive - 1]);
  85. }
  86. } else if (type === 'next') {
  87. if (data.list[data.listActive + 1]) {
  88. handlePlay(data.list[data.listActive + 1]);
  89. }
  90. }
  91. };
  92. const getDetail = async () => {
  93. data.loading = true;
  94. let res = {} as any;
  95. try {
  96. res = await api_knowledgeWiki_detail({ id: route.query.id });
  97. } catch (error) {
  98. console.log(error);
  99. }
  100. if (data.reshing) {
  101. data.list = [];
  102. data.reshing = false;
  103. }
  104. data.finshed = true;
  105. data.list = res.data.knowledgeWikiResources || [];
  106. data.list.forEach((item: any) => {
  107. item.audioFileUrl = item.url;
  108. item.musicSheetName = item.name;
  109. });
  110. const knowledgeWikiCategories = res.data.knowledgeWikiCategories || [];
  111. res.data.knowledgeName =
  112. knowledgeWikiCategories.length > 0
  113. ? knowledgeWikiCategories[0].name
  114. : '';
  115. res.data.intros = res.data.intros.replace(
  116. /<video/gi,
  117. '<video style="width: 100% !important;" controlslist="nodownload"'
  118. );
  119. // 使用 DOMParser 解析 HTML 字符串
  120. const parser = new DOMParser();
  121. const doc = parser.parseFromString(res.data.intros, 'text/html');
  122. // 提取并分割 HTML 文档中的内容
  123. const result = document.createElement('div');
  124. result.classList.add('html-to-dom');
  125. result.appendChild(speak.processNode(doc.body));
  126. // result.appendChild(doc.body);
  127. // console.log(result.outerHTML)
  128. document.querySelector('#musicContent')?.appendChild(result);
  129. data.details = res.data;
  130. data.loading = false;
  131. };
  132. onMounted(() => {
  133. getDetail();
  134. });
  135. return () => (
  136. <div class={styles.container}>
  137. <NSpace align="center" wrapItem={false} size={16}>
  138. <img
  139. style={{ cursor: 'pointer' }}
  140. src={icon_back}
  141. class={styles.iconBack}
  142. onClick={() => {
  143. router.push('/content-music');
  144. }}
  145. />
  146. <NBreadcrumb separator="">
  147. <NBreadcrumbItem
  148. onClick={() => {
  149. router.push('/content-music');
  150. }}>
  151. 名曲鉴赏
  152. </NBreadcrumbItem>
  153. <img class={styles.separator} src={icon_separator} />
  154. <NBreadcrumbItem>{route.query.name}</NBreadcrumbItem>
  155. </NBreadcrumb>
  156. </NSpace>
  157. <div class={[styles.wrap, data.showPlayer ? styles.wrapBottom : '']}>
  158. <div class={styles.content}>
  159. <div class={styles.contentWrap}>
  160. <div class={[styles.musicList, 'musicList-container']}>
  161. <div class={styles.wrapList}>
  162. <div class={styles.musicInfo}>
  163. <div class={styles.musicImg}>
  164. <img
  165. src={data.details?.avatar || musicBg}
  166. class={styles.img}
  167. />
  168. <div class={styles.panSection}>
  169. <img src={iconPan} class={styles.iconPan} />
  170. <img
  171. src={data.details?.avatar || musicBg}
  172. class={styles.img2}
  173. />
  174. </div>
  175. </div>
  176. <div class={styles.info}>
  177. <div class={styles.name}>
  178. <TheNoticeBar
  179. text={data.details.name}
  180. style={{ marginRight: '0' }}
  181. />
  182. {/* {data.details.name} */}
  183. </div>
  184. <div class={styles.c}>
  185. {data.details.composers ? (
  186. <div>
  187. <span>作曲:</span>
  188. <TheNoticeBar
  189. text={data.details.composers}
  190. style={{ marginRight: '0' }}
  191. />
  192. </div>
  193. ) : (
  194. ''
  195. )}
  196. {data.details.lyricists ? (
  197. <div>
  198. <span>作词:</span>
  199. <TheNoticeBar
  200. text={data.details.lyricists}
  201. style={{ marginRight: '0' }}
  202. />
  203. </div>
  204. ) : (
  205. ''
  206. )}
  207. </div>
  208. </div>
  209. </div>
  210. <div class={styles.titlec}>
  211. <i class={styles.icon2}></i>名曲鉴赏
  212. </div>
  213. {data.list.map((item: any, index: any) => {
  214. return (
  215. <div class={styles.itemContainer}>
  216. <div
  217. class={[styles.item]}
  218. onClick={(e: Event) => {
  219. e.stopPropagation();
  220. handlePlay(item);
  221. }}>
  222. <div class={styles.img}>
  223. <NImage
  224. lazy
  225. objectFit="cover"
  226. previewDisabled={true}
  227. src={item.titleImg || icon_default}
  228. onLoad={e => {
  229. (e.target as any).dataset.loaded = 'true';
  230. }}
  231. />
  232. <PlayLoading
  233. class={[
  234. data.listActive === index &&
  235. data.playState === 'play'
  236. ? ''
  237. : styles.showPlayLoading
  238. ]}
  239. />
  240. </div>
  241. <div class={styles.title}>
  242. <div class={styles.titleName}>
  243. <TheNoticeBar
  244. text={item.name}
  245. style={{ marginRight: '12px' }}
  246. />
  247. </div>
  248. </div>
  249. <NButton
  250. color="#259CFE"
  251. textColor="#fff"
  252. round
  253. class={styles.btn}
  254. type="primary"
  255. onClick={(e: Event) => {
  256. e.stopPropagation();
  257. handlePlay(item);
  258. speak.onCloseSpeak();
  259. }}>
  260. 播放
  261. <img
  262. src={
  263. data.listActive === index &&
  264. data.playState === 'play'
  265. ? icon_pause
  266. : icon_play
  267. }
  268. />
  269. </NButton>
  270. <img class={styles.arrow} src={icon_arrow} />
  271. </div>
  272. </div>
  273. );
  274. })}
  275. {!data.finshed && (
  276. <div class={styles.loadingWrap}>
  277. <NSpin show={true}></NSpin>
  278. </div>
  279. )}
  280. {!data.loading && data.list.length === 0 && (
  281. <div class={styles.empty}>
  282. <TheEmpty
  283. description="暂无名曲鉴赏"
  284. style={{ paddingTop: '0px' }}></TheEmpty>
  285. </div>
  286. )}
  287. </div>
  288. </div>
  289. <div class={styles.musicStaff}>
  290. <div class={styles.musicTitle}>
  291. <div class={styles.musicTitleLeft}>
  292. <i class={styles.icon1}></i>名曲故事
  293. </div>
  294. <div class={styles.musicTitleRight}>
  295. {speak.isSpeak.value ? (
  296. <span
  297. class={styles.textClose}
  298. onClick={speak.onCloseSpeak}>
  299. <i class={styles.icon}></i>关闭朗读
  300. </span>
  301. ) : (
  302. <span
  303. class={styles.textRead}
  304. onClick={() => {
  305. speak.onAllSpeak();
  306. handleChangeAudio('pause');
  307. }}>
  308. <i class={styles.icon}></i>全文朗读
  309. </span>
  310. )}
  311. </div>
  312. </div>
  313. <div
  314. class={styles.musicContent}
  315. id="musicContent"
  316. style={{ fontSize: data.fontSize + 'px' }}>
  317. {/* 选中的内容 */}
  318. <div
  319. id="selectionCouser"
  320. class={[
  321. styles.selectionCouser,
  322. !speak.showDom.value && styles.hide
  323. ]}>
  324. <span
  325. class={styles.textStart}
  326. onClick={() => {
  327. speak.onTextStart();
  328. handleChangeAudio('pause');
  329. }}>
  330. 开始朗读<i class={styles.icon}></i>
  331. </span>
  332. <span
  333. class={styles.textReadOnly}
  334. onClick={() => {
  335. speak.onTextReadOnly();
  336. handleChangeAudio('pause');
  337. }}>
  338. 只读这段<i class={styles.icon}></i>
  339. </span>
  340. </div>
  341. </div>
  342. </div>
  343. <div class={styles.changeSizeSection}>
  344. <img src={iconT} class={styles.iconT} />
  345. <img
  346. src={iconAddT}
  347. class={styles.iconAddT}
  348. onClick={() => {
  349. if (data.fontSize >= 32) return;
  350. data.fontSize += 1;
  351. }}
  352. />
  353. <NSlider
  354. v-model:value={data.fontSize}
  355. placement="left"
  356. vertical
  357. min={12}
  358. max={32}
  359. />
  360. <img
  361. src={iconPlusT}
  362. class={styles.iconPlusT}
  363. onClick={() => {
  364. if (data.fontSize <= 12) return;
  365. data.fontSize -= 1;
  366. }}
  367. />
  368. </div>
  369. </div>
  370. </div>
  371. </div>
  372. {data.list.length !== 0 && (
  373. <PlayItem
  374. show={data.showPlayer}
  375. playState={data.playState}
  376. item={activeItem.value}
  377. onChange={value => handleChangeAudio(value)}
  378. onShow={(status: boolean) => {
  379. data.showPlayer = status;
  380. }}
  381. />
  382. )}
  383. </div>
  384. );
  385. }
  386. });