index.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import { defineComponent } from 'vue'
  2. import {
  3. Button,
  4. Field,
  5. Sticky,
  6. Form,
  7. Tag,
  8. Radio,
  9. RadioGroup,
  10. Popup,
  11. Icon,
  12. Empty,
  13. Picker,
  14. Toast
  15. } from 'vant'
  16. import ColFieldGroup from '@/components/col-field-group'
  17. import { MusicType } from 'src/teacher/music/list/item.d'
  18. import SubjectModel from '@/business-components/subject-list'
  19. import ColField from '@/components/col-field'
  20. import {
  21. teachercanEvaluateType,
  22. teacherChargeType,
  23. teachershowAudiType,
  24. teachershowFingeringType,
  25. teachershowHasBeatType
  26. } from '@/constant/music'
  27. import { getXmlInfo, FormatXMLInfo } from '@/helpers/music-xml'
  28. import Upload from './upload'
  29. import styles from './index.module.less'
  30. import SelectTag from '@/student/music/search/select-tag'
  31. import { browser } from '@/helpers/utils'
  32. import { postMessage } from '@/helpers/native-message'
  33. import { teacherState } from '@/teacher/teacher-cert/teacherState'
  34. import request from '@/helpers/request'
  35. import requestOrigin from 'umi-request'
  36. import UploadIcon from './upload.svg'
  37. export type BackgroundMp3 = {
  38. url?: string
  39. track?: string
  40. }
  41. export default defineComponent({
  42. name: 'MusicUpload',
  43. data() {
  44. return {
  45. audioType: 'MP3',
  46. xmlFileUrl: '',
  47. xmlFileLoading: false,
  48. midiUrl: '',
  49. midiLoading: false,
  50. mp3Url: '',
  51. bgmp3Url: '',
  52. mp3Loading: false,
  53. bgmp3Loading: false,
  54. musicSheetName: '',
  55. composer: '',
  56. speed: '',
  57. hasBeat: 0,
  58. chargeType: 0,
  59. showFingering: 1,
  60. canEvaluate: 1,
  61. musicPrice: '',
  62. selectTagVisible: false,
  63. subJectVisible: false,
  64. tags: [] as string[],
  65. tagsNames: [] as Array<{ [id in string]: string }>,
  66. formated: {} as FormatXMLInfo,
  67. tagVisibility: false,
  68. subjectListNames: {} as any,
  69. selectedSubjectList: null as any,
  70. vlewSubjectList: null as any,
  71. submitLoading: false,
  72. showPicker: false,
  73. music_sheet_service_fee: 0,
  74. backgroundMp3s: [
  75. {
  76. url: '',
  77. track: ''
  78. }
  79. ] as BackgroundMp3[]
  80. }
  81. },
  82. watch: {
  83. formated() {
  84. this.mergeXmlData(this.formated)
  85. },
  86. chargeType() {
  87. if (this.chargeType === 0) {
  88. this.musicPrice = ''
  89. }
  90. }
  91. },
  92. computed: {
  93. choiceSubjectIds() {
  94. // 选择的科目编号
  95. let ids = teacherState.teacherCert.subjectId
  96. ? teacherState.teacherCert.subjectId.split(',')
  97. : []
  98. ids = ids.map((item: any) => Number(item))
  99. return ids
  100. },
  101. subjectList() {
  102. // 学科列表
  103. return teacherState.subjectList || []
  104. },
  105. choiceSubject() {
  106. // 选择的科目
  107. let tempArr: any[] = []
  108. this.subjectList.forEach((parent: any) => {
  109. parent.subjects &&
  110. parent.subjects.forEach((sub: any) => {
  111. if (this.choiceSubjectIds.includes(sub.id)) {
  112. tempArr.push(sub as never)
  113. }
  114. })
  115. })
  116. return tempArr
  117. }
  118. },
  119. async mounted() {
  120. request
  121. .get('/api-teacher/sysConfig/queryByParamName', {
  122. params: {
  123. paramName: 'music_sheet_service_fee'
  124. }
  125. })
  126. .then(res => (this.music_sheet_service_fee = res.data.paramValue))
  127. // if (teacherState.subjectList.length <= 0) {
  128. request.get('/api-teacher/subject/subjectSelect').then(res => {
  129. teacherState.subjectList = res.data || []
  130. this.subjectListNames = this.getSubjectListNames(teacherState.subjectList)
  131. })
  132. // }
  133. },
  134. methods: {
  135. async submit(vals: any) {
  136. this.submitLoading = true
  137. try {
  138. const beatType = this.hasBeat ? 'MP3_METRONOME' : 'MP3'
  139. const mp3Type = this.audioType === 'MP3' ? beatType : 'MIDI'
  140. await request.post('/api-teacher/music/sheet/create', {
  141. data: {
  142. audioType: this.audioType,
  143. sourceType: 'TEACHER',
  144. mp3Type,
  145. url: this.hasBeat ? '' : this.mp3Url,
  146. metronomeUrl: this.hasBeat ? this.mp3Url : '',
  147. showFingering: Number(this.showFingering) || undefined,
  148. musicTag: this.tags.join(','),
  149. musicSubject: Number(this.selectedSubjectList?.label) || undefined,
  150. musicSheetName: this.musicSheetName,
  151. midiUrl: this.midiUrl,
  152. xmlFileUrl: this.xmlFileUrl,
  153. canEvaluate: Number(this.canEvaluate) || undefined,
  154. chargeType: this.chargeType === 0 ? 'FREE' : 'CHARGE',
  155. composer: this.composer,
  156. musicPrice: this.musicPrice,
  157. background: this.backgroundMp3s.map(item => ({
  158. audioFileUrl: this.hasBeat ? '' : this.bgmp3Url,
  159. track: item.track,
  160. metronomeUrl: this.hasBeat ? this.bgmp3Url : ''
  161. }))
  162. }
  163. })
  164. } catch (error) {}
  165. this.submitLoading = false
  166. Toast('上传成功')
  167. setTimeout(() => {
  168. postMessage({
  169. api: 'back'
  170. })
  171. }, 800)
  172. console.log(vals)
  173. },
  174. getSubjectListNames(list) {
  175. const data = {}
  176. for (const item of list) {
  177. data[item.id] = item.name
  178. if (item.subjects) {
  179. for (const sub of item.subjects) {
  180. data[sub.id] = sub.name
  181. }
  182. }
  183. }
  184. return data
  185. },
  186. failed() {
  187. console.log('failed', this.backgroundMp3s)
  188. },
  189. mergeXmlData(data: FormatXMLInfo) {
  190. this.formated = data
  191. this.backgroundMp3s = data.partNames.map((partName: string) => ({
  192. track: partName
  193. }))
  194. if (!this.musicSheetName) {
  195. this.musicSheetName = data.title
  196. }
  197. if (!this.composer) {
  198. this.composer = data.composer
  199. }
  200. if (!this.speed && data.speed) {
  201. this.speed = '' + data.speed
  202. }
  203. },
  204. readerFile(file: File) {
  205. const reader = new FileReader()
  206. reader.onload = () => {
  207. const xml = reader.result as string
  208. this.formated = getXmlInfo(xml)
  209. }
  210. reader.readAsText(file)
  211. },
  212. onChoice(val: any) {
  213. this.subJectVisible = false
  214. this.selectedSubjectList = [val]
  215. },
  216. onComfirm(tags: any, names: any) {
  217. this.tagsNames = names
  218. this.tagVisibility = false
  219. const data = Object.values(tags).flat().filter(Boolean) as string[]
  220. console.log(data)
  221. this.tags = data
  222. },
  223. naiveXMLFile() {
  224. this.xmlFileLoading = true
  225. postMessage({ api: 'chooseFile', content: { type: 'xml' } }, evt => {
  226. // @ts-ignore
  227. this.xmlFileUrl = evt?.fileUrl || this.xmlFileUrl || ''
  228. this.xmlFileLoading = false
  229. if (this.xmlFileUrl) {
  230. requestOrigin(this.xmlFileUrl).then(
  231. res => (this.formated = getXmlInfo(res))
  232. )
  233. }
  234. })
  235. },
  236. naiveMidFile() {
  237. this.midiLoading = true
  238. postMessage({ api: 'chooseFile', content: { type: 'midi' } }, evt => {
  239. // @ts-ignore
  240. this.midiUrl = evt?.fileUrl || this.midiUrl || ''
  241. this.midiLoading = false
  242. // this.midiUrl = path
  243. })
  244. },
  245. naiveMp3File() {
  246. this.mp3Loading = true
  247. postMessage({ api: 'chooseFile', content: { type: 'mp3' } }, evt => {
  248. // @ts-ignore
  249. this.mp3Url = evt?.fileUrl || this.mp3Url || ''
  250. this.mp3Loading = false
  251. // this.midiUrl = path
  252. })
  253. },
  254. naiveBGMp3File() {
  255. this.bgmp3Loading = true
  256. postMessage({ api: 'chooseFile', content: { type: 'mp3' } }, evt => {
  257. this.bgmp3Url
  258. // @ts-ignore
  259. this.bgmp3Url = evt?.fileUrl || this.bgmp3Url || ''
  260. this.bgmp3Loading = false
  261. // this.midiUrl = path
  262. })
  263. },
  264. fileName(name = '') {
  265. return name.split('/').pop()
  266. }
  267. },
  268. render() {
  269. console.log(this.formated)
  270. const browserInfo = browser()
  271. return (
  272. <Form onSubmit={this.submit} onFailed={this.failed}>
  273. <div class={styles.container}>
  274. <ColFieldGroup class={styles.area}>
  275. <ColField border={false} required title="MusicXML文件">
  276. <Field
  277. name="xmlFileUrl"
  278. modelValue={this.xmlFileUrl}
  279. rules={[{ required: true, message: '请选择MusicXML文件' }]}
  280. // @ts-ignore
  281. vSlots={{
  282. input: () =>
  283. browserInfo.isApp ? (
  284. <Button
  285. icon={UploadIcon}
  286. class={styles.upbtn}
  287. onClick={this.naiveXMLFile}
  288. loading={this.xmlFileLoading}
  289. >
  290. {this.xmlFileUrl
  291. ? this.fileName(this.xmlFileUrl)
  292. : '上传文件'}
  293. </Button>
  294. ) : (
  295. <Upload
  296. onUpdate:modelValue={val => (this.xmlFileUrl = val)}
  297. accept=".xml"
  298. formatFile={this.readerFile}
  299. />
  300. )
  301. }}
  302. />
  303. </ColField>
  304. {/* <ColField required title="播放类型" border={false}>
  305. <RadioGroup
  306. class={styles['radio-group']}
  307. modelValue={this.audioType}
  308. onUpdate:modelValue={val => (this.audioType = val)}
  309. >
  310. {Object.keys(teachershowAudiType).map((item: string) => {
  311. const isActive = item === this.audioType
  312. const type = isActive ? 'primary' : 'default'
  313. return (
  314. <Radio class={styles.radio} name={item}>
  315. <Tag size="large" plain={isActive} type={type}>
  316. {teachershowAudiType[item]}
  317. </Tag>
  318. </Radio>
  319. )
  320. })}
  321. </RadioGroup>
  322. </ColField> */}
  323. {this.audioType === 'MP3' ? (
  324. <>
  325. <ColField required title="是否带节拍器" border={false}>
  326. <RadioGroup
  327. class={styles['radio-group']}
  328. modelValue={this.hasBeat}
  329. onUpdate:modelValue={val => (this.hasBeat = val)}
  330. >
  331. {Object.keys(teachershowHasBeatType).map((item: string) => {
  332. const isActive = item === String(this.hasBeat)
  333. const type = isActive ? 'primary' : 'default'
  334. return (
  335. <Radio class={styles.radio} name={item}>
  336. <Tag size="large" plain={isActive} type={type}>
  337. {teachershowHasBeatType[item]}
  338. </Tag>
  339. </Radio>
  340. )
  341. })}
  342. </RadioGroup>
  343. </ColField>
  344. <ColField border={false} title="伴奏文件">
  345. <Field
  346. name="mp3Url"
  347. modelValue={this.mp3Url}
  348. // @ts-ignore
  349. vSlots={{
  350. input: () =>
  351. browserInfo.isApp ? (
  352. <Button
  353. icon={UploadIcon}
  354. class={styles.upbtn}
  355. onClick={this.naiveMp3File}
  356. loading={this.mp3Loading}
  357. >
  358. {this.mp3Url
  359. ? this.fileName(this.mp3Url)
  360. : '上传文件'}
  361. </Button>
  362. ) : (
  363. <Upload
  364. onUpdate:modelValue={val => (this.mp3Url = val)}
  365. accept=".mp3"
  366. />
  367. )
  368. }}
  369. />
  370. </ColField>
  371. </>
  372. ) : (
  373. <ColField border={false} required title="MIDI文件">
  374. <Field
  375. name="midiUrl"
  376. modelValue={this.midiUrl}
  377. rules={[{ required: true, message: '请选择MIDI文件' }]}
  378. // @ts-ignore
  379. vSlots={{
  380. input: () =>
  381. browserInfo.isApp ? (
  382. <Button
  383. icon={UploadIcon}
  384. class={styles.upbtn}
  385. onClick={this.naiveMidFile}
  386. loading={this.midiLoading}
  387. >
  388. {this.midiUrl
  389. ? this.fileName(this.midiUrl)
  390. : '上传文件'}
  391. </Button>
  392. ) : (
  393. <Upload
  394. onUpdate:modelValue={val => (this.midiUrl = val)}
  395. accept=".mid"
  396. />
  397. )
  398. }}
  399. />
  400. </ColField>
  401. )}
  402. {this.backgroundMp3s.map(item => (
  403. <ColField required border={false} title="原音文件">
  404. <Field
  405. name="url"
  406. modelValue={this.bgmp3Url}
  407. rules={[{ required: true, message: '请选择原音文件' }]}
  408. // @ts-ignore
  409. vSlots={{
  410. input: () =>
  411. browserInfo.isApp ? (
  412. <Button
  413. icon={UploadIcon}
  414. class={styles.upbtn}
  415. onClick={this.naiveBGMp3File}
  416. loading={this.bgmp3Loading}
  417. >
  418. {this.bgmp3Url
  419. ? this.fileName(this.bgmp3Url)
  420. : '上传文件'}
  421. </Button>
  422. ) : (
  423. <Upload
  424. onUpdate:modelValue={val => (this.bgmp3Url = val)}
  425. accept=".mp3"
  426. />
  427. )
  428. }}
  429. />
  430. </ColField>
  431. ))}
  432. </ColFieldGroup>
  433. <ColFieldGroup class={styles.area}>
  434. <ColField required title="曲目名称">
  435. <Field
  436. clearable
  437. name="musicSheetName"
  438. modelValue={this.musicSheetName}
  439. rules={[{ required: true, message: '请输入曲目名称' }]}
  440. class={styles['clear-px']}
  441. placeholder="请输入曲目名称"
  442. onUpdate:modelValue={val => (this.musicSheetName = val)}
  443. />
  444. </ColField>
  445. <ColField required title="作曲人">
  446. <Field
  447. clearable
  448. class={styles['clear-px']}
  449. placeholder="请输入作曲人姓名"
  450. name="composer"
  451. modelValue={this.composer}
  452. rules={[{ required: true, message: '请输入作曲人姓名' }]}
  453. onUpdate:modelValue={val => (this.composer = val)}
  454. />
  455. </ColField>
  456. <ColField required title="默认速度">
  457. <Field
  458. clearable
  459. name="playSpeed"
  460. modelValue={this.speed}
  461. rules={[{ required: true, message: '请输入默认速度' }]}
  462. onUpdate:modelValue={val => (this.speed = val)}
  463. class={styles['clear-px']}
  464. placeholder="请输入默认速度"
  465. />
  466. </ColField>
  467. <ColField required title="曲目声部">
  468. <Field
  469. is-link
  470. readonly
  471. class={styles['clear-px']}
  472. placeholder="请选择曲目声部"
  473. name="vlewSubjectList"
  474. modelValue={this.vlewSubjectList?.value}
  475. rules={[{ required: true, message: '请选择曲目声部' }]}
  476. // onUpdate:modelValue={val => (this.selectedSubjectList = )}
  477. onClick={() => (this.showPicker = true)}
  478. ></Field>
  479. </ColField>
  480. </ColFieldGroup>
  481. <ColFieldGroup class={styles.area}>
  482. <ColField
  483. border={false}
  484. required
  485. title="曲目标签"
  486. v-slots={{
  487. right: () => (
  488. <Button
  489. class={styles.select}
  490. round
  491. type="primary"
  492. size="small"
  493. onClick={() => (this.tagVisibility = true)}
  494. >
  495. 选择
  496. </Button>
  497. )
  498. }}
  499. >
  500. <Field
  501. name="tags"
  502. modelValue={this.tags.length ? 1 : undefined}
  503. rules={[{ required: true, message: '请选择曲目标签' }]}
  504. // @ts-ignore
  505. vSlots={{
  506. input: () =>
  507. this.tags.length > 0 ? (
  508. this.tags.map((item: any) => (
  509. <Tag type="primary" size="large" class={styles.tags}>
  510. {this.tagsNames[item]}
  511. </Tag>
  512. ))
  513. ) : (
  514. <Empty
  515. style={{ width: '100%' }}
  516. description="请选择曲目标签"
  517. imageSize={0}
  518. />
  519. )
  520. }}
  521. />
  522. </ColField>
  523. </ColFieldGroup>
  524. <ColFieldGroup class={styles.area}>
  525. <ColField required title="是否评测" border={false}>
  526. <RadioGroup
  527. class={styles['radio-group']}
  528. modelValue={this.canEvaluate}
  529. onUpdate:modelValue={val => (this.canEvaluate = val)}
  530. >
  531. {Object.keys(teachercanEvaluateType).map((item: string) => {
  532. const isActive = item === String(this.canEvaluate)
  533. const type = isActive ? 'primary' : 'default'
  534. return (
  535. <Radio class={styles.radio} name={item}>
  536. <Tag size="large" plain={isActive} type={type}>
  537. {teachercanEvaluateType[item]}
  538. </Tag>
  539. </Radio>
  540. )
  541. })}
  542. </RadioGroup>
  543. </ColField>
  544. <ColField required title="指法展示" border={false}>
  545. <RadioGroup
  546. class={styles['radio-group']}
  547. modelValue={this.showFingering}
  548. onUpdate:modelValue={val => (this.showFingering = val)}
  549. >
  550. {Object.keys(teachershowFingeringType).map((item: string) => {
  551. const isActive = item === String(this.showFingering)
  552. const type = isActive ? 'primary' : 'default'
  553. return (
  554. <Radio class={styles.radio} name={item}>
  555. <Tag size="large" plain={isActive} type={type}>
  556. {teachershowFingeringType[item]}
  557. </Tag>
  558. </Radio>
  559. )
  560. })}
  561. </RadioGroup>
  562. </ColField>
  563. <ColField required title="是否收费" border={false}>
  564. <RadioGroup
  565. class={styles['radio-group']}
  566. modelValue={this.chargeType}
  567. onUpdate:modelValue={val => {
  568. this.chargeType = Number(val)
  569. }}
  570. >
  571. {Object.keys(teacherChargeType).map((item: string) => {
  572. const isActive = item === String(this.chargeType)
  573. const type = isActive ? 'primary' : 'default'
  574. return (
  575. <Radio class={styles.radio} name={item}>
  576. <Tag size="large" plain={isActive} type={type}>
  577. {teacherChargeType[item]}
  578. </Tag>
  579. </Radio>
  580. )
  581. })}
  582. </RadioGroup>
  583. </ColField>
  584. {this.chargeType === 2 && (
  585. <ColField required title="收费价格">
  586. <Field
  587. clearable
  588. class={styles['clear-px']}
  589. placeholder="请输入收费价格"
  590. v-slots={{ button: () => '元' }}
  591. modelValue={this.musicPrice}
  592. rules={[{ required: true, message: '请输入收费价格' }]}
  593. onUpdate:modelValue={val => (this.musicPrice = val)}
  594. />
  595. </ColField>
  596. )}
  597. </ColFieldGroup>
  598. {this.chargeType === 2 && (
  599. <div class={styles.rule}>
  600. <p>扣除手续费后该曲目预计收入为:</p>
  601. <p>
  602. 每人:
  603. <span>
  604. {((parseFloat(this.musicPrice || '0') || 0) *
  605. (100 - this.music_sheet_service_fee)) /
  606. 100}
  607. </span>
  608. 元/人
  609. </p>
  610. <p>您的乐谱收入将在学员购买后结算到您的账户中</p>
  611. </div>
  612. )}
  613. </div>
  614. <Sticky offsetBottom={0} position="bottom">
  615. <div class={styles['button-area']}>
  616. <Button
  617. type="primary"
  618. block
  619. round
  620. native-type="submit"
  621. loading={this.submitLoading}
  622. >
  623. 确认
  624. </Button>
  625. </div>
  626. </Sticky>
  627. <Popup
  628. show={this.showPicker}
  629. round
  630. position="bottom"
  631. teleport="body"
  632. onUpdate:show={val => (this.showPicker = val)}
  633. >
  634. <Picker
  635. columnsFieldNames={{
  636. text: 'value'
  637. }}
  638. columns={Object.entries(this.subjectListNames).map(
  639. ([key, value]) => ({ label: key, value })
  640. )}
  641. onCancel={() => (this.showPicker = false)}
  642. onConfirm={val => {
  643. this.selectedSubjectList = val
  644. this.vlewSubjectList = val
  645. this.showPicker = false
  646. }}
  647. />
  648. </Popup>
  649. <Popup
  650. show={this.subJectVisible}
  651. round
  652. closeable
  653. position="bottom"
  654. style={{ height: '60%' }}
  655. teleport="body"
  656. onUpdate:show={val => (this.subJectVisible = val)}
  657. >
  658. <SubjectModel
  659. subjectList={this.subjectList}
  660. choiceSubjectIds={this.choiceSubjectIds}
  661. onChoice={this.onChoice}
  662. selectType="Radio"
  663. />
  664. </Popup>
  665. <Popup
  666. show={this.tagVisibility}
  667. round
  668. closeable
  669. position="bottom"
  670. style={{ height: '60%' }}
  671. teleport="body"
  672. onUpdate:show={val => (this.tagVisibility = val)}
  673. >
  674. <SelectTag
  675. onComfirm={this.onComfirm}
  676. onCancel={() => {}}
  677. rowSingle
  678. needAllButton={false}
  679. />
  680. </Popup>
  681. </Form>
  682. )
  683. }
  684. })