index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. import {
  2. TransitionGroup,
  3. computed,
  4. defineComponent,
  5. nextTick,
  6. onMounted,
  7. reactive,
  8. ref,
  9. watch
  10. } from 'vue';
  11. import styles from './index.module.less';
  12. import MSearch from '@/components/m-search';
  13. import icon_play from '@/common/images/icon_play.svg';
  14. import {
  15. Empty,
  16. Icon,
  17. List,
  18. Loading,
  19. NoticeBar,
  20. Popover,
  21. Popup,
  22. showLoadingToast,
  23. showToast
  24. } from 'vant';
  25. import icon_back from './image/icon_back.svg';
  26. import icon_down from '@/common/images/icon_down.svg';
  27. import icon_jianpu from '@/common/images/icon_jianpu.svg';
  28. import icon_jianpuActive from '@/common/images/icon_jianpuActive.svg';
  29. import icons from '@/common/images/index.json';
  30. import {
  31. listenerMessage,
  32. postMessage,
  33. promisefiyPostMessage
  34. } from '@/helpers/native-message';
  35. import html2canvas from 'html2canvas';
  36. import { api_musicSheetCategoriesPage, api_musicSheetPage } from './api';
  37. import { state } from '@/state';
  38. import MEmpty from '@/components/m-empty';
  39. import Coaiguide from '@/custom-plugins/guide-page/coai-guide';
  40. import { usePageVisibility } from '@vant/use';
  41. import TheVip from '@/components/the-vip';
  42. import request from '@/helpers/request';
  43. import { useRoute } from 'vue-router';
  44. import { addWatermark, convasToImg, imgToCanvas } from './imageFunction';
  45. import { browser } from '@/helpers/utils';
  46. export default defineComponent({
  47. name: 'co-ai',
  48. setup() {
  49. const route = useRoute();
  50. const categorForms = reactive({
  51. page: 1,
  52. rows: 999,
  53. subjectId: state.user.data?.subjectId || '',
  54. musicTagIds: route.query.musicTagId ? [route.query.musicTagId] : []
  55. });
  56. const musicForms = reactive({
  57. page: 1,
  58. rows: 20,
  59. status: 1,
  60. keyword: '', // 关键词
  61. musicSheetCategoriesId: route.query.id as any
  62. });
  63. const data = reactive({
  64. musicSheetCategoriesName: route.query.name as any,
  65. /** 教材Index */
  66. typeIndex: 0,
  67. /** 音乐Index */
  68. musicIndex: 0,
  69. /** 显示哪种曲谱 */
  70. showMusicImg: 'first' as 'staff' | 'first' | 'fixed',
  71. popoverShow: false,
  72. popoverMusicShow: false,
  73. /** 教材列表 */
  74. types: [] as any[],
  75. /** 音乐列表 */
  76. musics: [] as any[],
  77. loading: true,
  78. finshed: false,
  79. searchNoticeShow: false,
  80. searchNotice: {
  81. left: '',
  82. top: '',
  83. width: '',
  84. height: ''
  85. },
  86. showVip: false,
  87. vipMember: state.user.data?.vipMember
  88. });
  89. const downRef = ref();
  90. const showGuide = ref(false);
  91. const _actions = computed(() => {
  92. return [
  93. {
  94. value: 'staff',
  95. text: '五线谱'
  96. },
  97. {
  98. value: 'first',
  99. text: '首调'
  100. },
  101. {
  102. value: 'fixed',
  103. text: '固定调'
  104. }
  105. ].map((item, index) => {
  106. return {
  107. ...item,
  108. color:
  109. data.showMusicImg === item.value ? 'var(--van-primary-color)' : '',
  110. className: data.showMusicImg === item.value ? 'fontBlod' : ''
  111. };
  112. });
  113. });
  114. const _types = computed(() => {
  115. return data.types.map((item: any) => {
  116. return {
  117. ...item,
  118. color:
  119. musicForms.musicSheetCategoriesId == item.value
  120. ? 'var(--van-primary-color)'
  121. : '',
  122. className:
  123. musicForms.musicSheetCategoriesId == item.value ? 'fontBlod' : ''
  124. };
  125. });
  126. });
  127. // 返回
  128. const goback = () => {
  129. postMessage({ api: 'goBack' });
  130. };
  131. /** 去云教练 */
  132. const handleGoto = () => {
  133. if (!data.vipMember) {
  134. data.showVip = true;
  135. return;
  136. }
  137. // 默认进页面显示对应的曲谱
  138. let lineType = 'staff';
  139. if (data.showMusicImg === 'first') {
  140. lineType = 'firstTone';
  141. } else if (data.showMusicImg === 'fixed') {
  142. lineType = 'fixedTone';
  143. } else if (data.showMusicImg === 'staff') {
  144. lineType = 'staff';
  145. }
  146. let src = `${location.origin}/instrument?id=${
  147. data.musics[data.musicIndex]?.id
  148. }&musicRenderType=${lineType}&showGuide=true`;
  149. postMessage({
  150. api: 'openAccompanyWebView',
  151. content: {
  152. url: src,
  153. orientation: 0,
  154. isHideTitle: true,
  155. statusBarTextColor: false,
  156. isOpenLight: true,
  157. c_orientation: 0 // 0 横屏 1 竖屏
  158. }
  159. });
  160. };
  161. /** 保存图片 */
  162. const handleSave = async () => {
  163. showLoadingToast({ message: '正在保存', duration: 0 });
  164. try {
  165. html2canvas(downRef.value, {
  166. backgroundColor: '#fff',
  167. allowTaint: true,
  168. useCORS: true
  169. })
  170. .then(async canvas => {
  171. // 添加水印
  172. const waterCanvasImg = await addWatermark(canvas);
  173. // canvas转图片
  174. const dataURL = await convasToImg(waterCanvasImg);
  175. setTimeout(() => {
  176. showToast('已保存到相册');
  177. }, 500);
  178. await promisefiyPostMessage({
  179. api: 'savePicture',
  180. content: {
  181. base64: dataURL
  182. }
  183. });
  184. })
  185. .catch(() => {
  186. setTimeout(() => {
  187. showToast('保存失败');
  188. }, 500);
  189. });
  190. } catch (error) {
  191. setTimeout(() => {
  192. showToast('保存失败');
  193. }, 500);
  194. }
  195. };
  196. /** 获取音乐教材列表 */
  197. const getMusicSheetCategories = async () => {
  198. try {
  199. const res = await api_musicSheetCategoriesPage({
  200. ...categorForms
  201. });
  202. if (res.code === 200 && Array.isArray(res?.data?.rows)) {
  203. const temp: any = [];
  204. res.data.rows.forEach((item: any) => {
  205. temp.push({
  206. value: item.id,
  207. text: item.name
  208. });
  209. });
  210. data.types = temp;
  211. }
  212. } catch (error) {
  213. console.log('🚀 ~ error:', error);
  214. }
  215. };
  216. /** 获取曲谱列表 */
  217. const getMusicList = async () => {
  218. data.loading = true;
  219. try {
  220. const res = await api_musicSheetPage({
  221. ...musicForms
  222. });
  223. if (res.code === 200 && Array.isArray(res?.data?.rows)) {
  224. data.musics = [...data.musics, ...res.data.rows];
  225. data.finshed = !res.data.next;
  226. }
  227. showGuide.value = true;
  228. } catch (error) {
  229. console.log('🚀 ~ error:', error);
  230. }
  231. data.loading = false;
  232. };
  233. const handleReset = () => {
  234. musicForms.page = 1;
  235. data.musics = [];
  236. getMusicList();
  237. };
  238. const spinRef = ref();
  239. const handleResh = () => {
  240. if (data.loading || data.finshed) return;
  241. musicForms.page = musicForms.page + 1;
  242. getMusicList();
  243. };
  244. const setSearchBox = () => {
  245. const el = document.querySelector('.searchNotice .van-field__control');
  246. if (el) {
  247. const rect = el.getBoundingClientRect();
  248. data.searchNotice.left = rect.x + 'px';
  249. data.searchNotice.top = rect.y + 'px';
  250. data.searchNotice.width = rect.width + 'px';
  251. data.searchNotice.height = rect.height + 'px';
  252. }
  253. };
  254. onMounted(async () => {
  255. // 安卓的状态栏
  256. postMessage({
  257. api: 'setStatusBarVisibility',
  258. content: {
  259. isVisibility: 0
  260. }
  261. });
  262. await getMusicSheetCategories();
  263. getMusicList();
  264. const obv = new IntersectionObserver(entries => {
  265. if (entries[0].intersectionRatio > 0) {
  266. handleResh();
  267. }
  268. });
  269. nextTick(() => {
  270. obv.observe(spinRef.value);
  271. });
  272. const getUserInfo = async () => {
  273. const res = await request.get('/edu-app/user/getUserInfo', {
  274. initRequest: true, // 初始化接口
  275. requestType: 'form',
  276. hideLoading: true
  277. });
  278. if (res?.code === 200) {
  279. data.vipMember = res.data.vipMember;
  280. }
  281. };
  282. listenerMessage('webViewOnResume', () => {
  283. console.log('页面显示');
  284. getUserInfo();
  285. data.typeIndex = 0;
  286. data.musicIndex = 0;
  287. handleReset();
  288. });
  289. setSearchBox();
  290. });
  291. return () => (
  292. <div class={styles.container}>
  293. <div class={styles.back} onClick={goback}>
  294. <img src={icon_back} />
  295. </div>
  296. <div class={styles.musicCFixed}>
  297. <Popover
  298. v-model:show={data.popoverMusicShow}
  299. class={styles.popoverMusic}
  300. actions={_types.value}
  301. placement="bottom"
  302. showArrow={false}
  303. onSelect={(item: any) => {
  304. // data.showMusicImg = item.value;
  305. if (item.value == musicForms.musicSheetCategoriesId) {
  306. return;
  307. }
  308. data.musics = [];
  309. musicForms.musicSheetCategoriesId = item.value;
  310. data.musicSheetCategoriesName = item.text;
  311. data.popoverMusicShow = false;
  312. getMusicList();
  313. }}>
  314. {{
  315. reference: () => (
  316. <span class={styles.musicName}>
  317. {data.musicSheetCategoriesName}
  318. <Icon name="arrow-down"></Icon>
  319. </span>
  320. )
  321. }}
  322. </Popover>
  323. </div>
  324. <div class={styles.content}>
  325. <div class={[styles.leftContent]}>
  326. <div class={styles.leftBg2}></div>
  327. {/* <div class={styles.leftBg}></div> */}
  328. {/* <div class={styles.types}>
  329. {data.types.map((item, index) => {
  330. return (
  331. <div
  332. class={[
  333. styles.type,
  334. musicForms.musicSheetCategoriesId === item.id &&
  335. styles.typeActive
  336. ]}
  337. onClick={() => {
  338. musicForms.musicSheetCategoriesId = item.id;
  339. handleReset();
  340. }}>
  341. <div class={styles.typeImg}>
  342. <img
  343. class={styles.typeIcon}
  344. src={item.coverImg}
  345. onLoad={(e: Event) => {
  346. const el = e.target as HTMLImageElement;
  347. el.setAttribute('loaded', 'true');
  348. }}
  349. />
  350. </div>
  351. </div>
  352. );
  353. })}
  354. </div> */}
  355. <div class={styles.center}>
  356. <div class={styles.centerSearch}>
  357. <div id="coai-0">
  358. <MSearch
  359. class={[
  360. 'searchNotice',
  361. data.searchNoticeShow ? styles.searchNoticeShow : ''
  362. ]}
  363. shape="round"
  364. background="transparent"
  365. clearable={false}
  366. placeholder="请输入关键字"
  367. modelValue={musicForms.keyword}
  368. onFocus={() => (data.searchNoticeShow = false)}
  369. onBlur={val => {
  370. musicForms.keyword = val?.trim() || '';
  371. requestAnimationFrame(() => {
  372. requestAnimationFrame(() => {
  373. if (musicForms.keyword) {
  374. data.searchNoticeShow = true;
  375. }
  376. });
  377. });
  378. }}
  379. onSearch={val => {
  380. musicForms.keyword = val;
  381. handleReset();
  382. }}
  383. />
  384. </div>
  385. </div>
  386. <div class={styles.musicContent}>
  387. {data.musics.map((item: any, index: number) => {
  388. return (
  389. <div
  390. class={[
  391. styles.musicItem,
  392. data.musicIndex === index
  393. ? styles.musicActive
  394. : styles.disableNotic
  395. ]}
  396. onClick={() => (data.musicIndex = index)}>
  397. <img
  398. class={styles.musicAvtor}
  399. src={item.titleImg}
  400. onLoad={(e: Event) => {
  401. const el = e.target as HTMLImageElement;
  402. el.setAttribute('loaded', 'true');
  403. }}
  404. />
  405. <div class={styles.musicInfo}>
  406. <div class={styles.musicName}>
  407. <NoticeBar
  408. text={item.musicSheetName}
  409. class={styles.noticeBar}
  410. background="none"
  411. />
  412. </div>
  413. <div class={styles.musicDes}>
  414. <div class={styles.musicFavitor}>{item.usedNum}</div>
  415. <div class={[styles.musicAuthor, 'van-ellipsis']}>
  416. {item.composer || '佚名'}
  417. </div>
  418. </div>
  419. </div>
  420. {/* <img class={[styles.musicIcon]} src={icon_play} /> */}
  421. </div>
  422. );
  423. })}
  424. {!data.finshed && (
  425. <div ref={spinRef} class={styles.loadingWrap}>
  426. <Loading color="#259CFE" />
  427. </div>
  428. )}
  429. {!data.loading && data.musics.length === 0 && (
  430. <div class={styles.empty}>
  431. <MEmpty description="暂无曲谱" />
  432. </div>
  433. )}
  434. </div>
  435. </div>
  436. </div>
  437. <div class={[styles.opacityBg, styles.right]}>
  438. <div class={styles.rightBox}>
  439. <div ref={downRef}>
  440. <div class={styles['right-musicName']}>
  441. {data.musics[data.musicIndex]?.musicSheetName}
  442. </div>
  443. {data.showMusicImg === 'first' ? (
  444. <>
  445. {data.musics[data.musicIndex]?.musicSvg
  446. ?.split(',')
  447. .map((item: any, index: number) => {
  448. return (
  449. <img
  450. class={styles.staff}
  451. src={item + '?v=' + Date.now()}
  452. key={item}
  453. crossorigin="anonymous"
  454. />
  455. );
  456. })}
  457. </>
  458. ) : data.showMusicImg === 'fixed' ? (
  459. <>
  460. <TransitionGroup name="van-fade">
  461. {data.musics[data.musicIndex]?.musicJianSvg
  462. ?.split(',')
  463. .map((item: any, index: number) => {
  464. return (
  465. <img
  466. class={styles.staff}
  467. src={item + '?v=' + Date.now()}
  468. key={item}
  469. crossorigin="anonymous"
  470. />
  471. );
  472. })}
  473. </TransitionGroup>
  474. </>
  475. ) : (
  476. <>
  477. {data.musics[data.musicIndex]?.musicImg
  478. ?.split(',')
  479. .map((item: any, index: number) => {
  480. return (
  481. <img
  482. class={styles.staff}
  483. src={item + '?v=' + Date.now()}
  484. key={item}
  485. crossorigin="anonymous"
  486. />
  487. );
  488. })}
  489. </>
  490. )}
  491. </div>
  492. </div>
  493. <div class={styles.rightBtns}>
  494. <Popover
  495. v-model:show={data.popoverShow}
  496. class={styles.popover}
  497. actions={_actions.value}
  498. placement="top-start"
  499. onSelect={(item: any) => {
  500. data.showMusicImg = item.value;
  501. data.popoverShow = false;
  502. }}
  503. // onSelect={onSelect}
  504. >
  505. {{
  506. reference: () => <img id="coai-1" src={icon_jianpuActive} />
  507. }}
  508. </Popover>
  509. <img id="coai-2" src={icon_down} onClick={handleSave} />
  510. <div class={styles.rightBtnsRight} id="coai-3">
  511. <img src={icons.icon_start} onClick={() => handleGoto()} />
  512. </div>
  513. </div>
  514. </div>
  515. </div>
  516. {data.searchNotice.width && data.searchNoticeShow && (
  517. <div class={styles.searchNotice} style={{ ...data.searchNotice }}>
  518. <NoticeBar
  519. text={musicForms.keyword}
  520. color="#333"
  521. background="none"
  522. />
  523. </div>
  524. )}
  525. {showGuide.value && <Coaiguide></Coaiguide>}
  526. <Popup
  527. class="popup-custom van-scale"
  528. transition="van-scale"
  529. closeOnClickOverlay={false}
  530. v-model:show={data.showVip}>
  531. <TheVip
  532. onClose={val => {
  533. if (val) {
  534. postMessage({
  535. api: 'openWebView',
  536. content: {
  537. url: `${location.origin}${location.pathname}#/member-center`,
  538. orientation: 1
  539. }
  540. });
  541. }
  542. data.showVip = false;
  543. }}
  544. />
  545. </Popup>
  546. </div>
  547. );
  548. }
  549. });