music-operation.tsx 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  1. import {
  2. NForm,
  3. NFormItem,
  4. NInput,
  5. NSelect,
  6. NSpace,
  7. NButton,
  8. useMessage,
  9. NRadioGroup,
  10. NRadio,
  11. NGrid,
  12. NFormItemGi,
  13. NInputNumber,
  14. NGi,
  15. useDialog,
  16. NCascader,
  17. NAlert,
  18. NInputGroup,
  19. NInputGroupLabel, NCheckbox
  20. } from 'naive-ui'
  21. import type {SelectOption} from 'naive-ui'
  22. import {defineComponent, onMounted, PropType, reactive, ref} from 'vue'
  23. import {musicSheetDetail, musicSheetSave, musicSheetUpdate} from '../../api'
  24. import UploadFile from '@/components/upload-file'
  25. import styles from './index.module.less'
  26. import deepClone from '@/utils/deep.clone'
  27. import axios from 'axios'
  28. import {Checkbox, CheckboxGroup} from "vant";
  29. import {releaseCandidate} from "@/utils/constant";
  30. import {getSelectDataFromObj} from "@/utils/objectUtil";
  31. /**
  32. * 获取指定元素下一个Note元素
  33. * @param ele 指定元素
  34. * @param selectors 选择器
  35. */
  36. const getNextNote = (ele: any, selectors: any) => {
  37. let index = 0
  38. const parentEle = ele.closest(selectors)
  39. let pointer = parentEle
  40. const measure = parentEle?.closest('measure')
  41. let siblingNote = null
  42. // 查找到相邻的第一个note元素
  43. while (!siblingNote && index < (measure?.childNodes.length || 50)) {
  44. index++
  45. if (pointer?.nextElementSibling?.tagName === 'note') {
  46. siblingNote = pointer?.nextElementSibling
  47. }
  48. pointer = pointer?.nextElementSibling
  49. }
  50. return siblingNote
  51. }
  52. export const onlyVisible = (xml: any, partIndex: any) => {
  53. if (!xml) return ''
  54. const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
  55. const partList =
  56. xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
  57. const parts = xmlParse.getElementsByTagName('part')
  58. const visiblePartInfo = partList[partIndex]
  59. if (visiblePartInfo) {
  60. const id = visiblePartInfo.getAttribute('id')
  61. Array.from(parts).forEach((part) => {
  62. if (part && part.getAttribute('id') !== id) {
  63. part.parentNode?.removeChild(part)
  64. // 不等于第一行才添加避免重复添加
  65. }
  66. // 最后一个小节的结束线元素不在最后 调整
  67. if (part && part.getAttribute('id') === id) {
  68. const barlines = part.getElementsByTagName('barline')
  69. const lastParent = barlines[barlines.length - 1]?.parentElement
  70. if (lastParent?.lastElementChild?.tagName !== 'barline') {
  71. const children: any[] = (lastParent?.children as any) || []
  72. for (let el of children) {
  73. if (el.tagName === 'barline') {
  74. // 将结束线元素放到最后
  75. lastParent?.appendChild(el)
  76. break
  77. }
  78. }
  79. }
  80. }
  81. })
  82. Array.from(partList).forEach((part) => {
  83. if (part && part.getAttribute('id') !== id) {
  84. part.parentNode?.removeChild(part)
  85. }
  86. })
  87. // 处理装饰音问题
  88. const notes = xmlParse.getElementsByTagName('note')
  89. const getNextvNoteDuration = (i: any) => {
  90. let nextNote = notes[i + 1]
  91. // 可能存在多个装饰音问题,取下一个非装饰音时值
  92. for (let index = i; index < notes.length; index++) {
  93. const note = notes[index]
  94. if (!note.getElementsByTagName('grace')?.length) {
  95. nextNote = note
  96. break
  97. }
  98. }
  99. const nextNoteDuration = nextNote?.getElementsByTagName('duration')[0]
  100. return nextNoteDuration
  101. }
  102. Array.from(notes).forEach((note, i) => {
  103. const graces = note.getElementsByTagName('grace')
  104. if (graces && graces.length) {
  105. note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
  106. }
  107. })
  108. }
  109. return new XMLSerializer().serializeToString(xmlParse)
  110. }
  111. const speedInfo = {
  112. 'rall.': 1.333333333,
  113. 'poco rit.': 1.333333333,
  114. 'rit.': 1.333333333,
  115. 'molto rit.': 1.333333333,
  116. 'molto rall': 1.333333333,
  117. molto: 1.333333333,
  118. lentando: 1.333333333,
  119. allargando: 1.333333333,
  120. morendo: 1.333333333,
  121. 'accel.': 0.8,
  122. calando: 2,
  123. 'poco accel.': 0.8,
  124. 'gradually slowing': 1.333333333,
  125. slowing: 1.333333333,
  126. slow: 1.333333333,
  127. slowly: 1.333333333,
  128. faster: 1.333333333
  129. }
  130. /**
  131. * 按照xml进行减慢速度的计算
  132. * @param xml 始终按照第一分谱进行减慢速度的计算
  133. */
  134. export function getGradualLengthByXml(xml: string) {
  135. const firstPartXml = onlyVisible(xml, 0)
  136. const xmlParse = new DOMParser().parseFromString(firstPartXml, 'text/xml')
  137. const measures = Array.from(xmlParse.querySelectorAll('measure'))
  138. const notes = Array.from(xmlParse.querySelectorAll('note'))
  139. const words = Array.from(xmlParse.querySelectorAll('words'))
  140. const metronomes = Array.from(xmlParse.querySelectorAll('metronome'))
  141. const eles = []
  142. for (const ele of [...words, ...metronomes]) {
  143. const note = getNextNote(ele, 'direction')
  144. // console.log(ele, note)
  145. if (note) {
  146. const measure = note?.closest('measure')
  147. const measureNotes = Array.from(measure.querySelectorAll('note'))
  148. const noteInMeasureIndex = Array.from(measure.childNodes)
  149. .filter((item: any) => item.nodeName === 'note')
  150. .findIndex((item) => item === note)
  151. let allDuration = 0
  152. let leftDuration = 0
  153. for (let i = 0; i < measureNotes.length; i++) {
  154. const n: any = measureNotes[i]
  155. const duration = +(n.querySelector('duration')?.textContent || '0')
  156. allDuration += duration
  157. if (i < noteInMeasureIndex) {
  158. leftDuration = allDuration
  159. }
  160. }
  161. eles.push({
  162. ele,
  163. index: notes.indexOf(note),
  164. noteInMeasureIndex,
  165. textContent: ele.textContent,
  166. measureIndex: measures.indexOf(measure), //,measure?.getAttribute('number')
  167. type: ele.tagName,
  168. allDuration,
  169. leftDuration
  170. })
  171. }
  172. }
  173. // 结尾处手动插入一个音符节点
  174. eles.push({
  175. ele: notes[notes.length - 1],
  176. index: notes.length,
  177. noteInMeasureIndex: 0,
  178. textContent: '',
  179. type: 'metronome',
  180. allDuration: 1,
  181. leftDuration: 1,
  182. measureIndex: measures.length
  183. })
  184. const gradualNotes: any[] = []
  185. eles.sort((a, b) => a.index - b.index)
  186. const keys = Object.keys(speedInfo).map((w) => w.toLocaleLowerCase())
  187. let isLastNoteAndNotClosed = false
  188. for (const ele of eles) {
  189. const textContent: any = ele.textContent?.toLocaleLowerCase().trim()
  190. if (ele === eles[eles.length - 1]) {
  191. if (gradualNotes[gradualNotes.length - 1]?.length === 1) {
  192. isLastNoteAndNotClosed = true
  193. }
  194. }
  195. const isKeyWork = keys.find((k) => {
  196. const ks = k.split(' ')
  197. return textContent && ks.includes(textContent)
  198. })
  199. if (
  200. ele.type === 'metronome' ||
  201. (ele.type === 'words' && (textContent.startsWith('a tempo') || isKeyWork)) ||
  202. isLastNoteAndNotClosed
  203. ) {
  204. const indexOf = gradualNotes.findIndex((item) => item.length === 1)
  205. if (indexOf > -1 && ele.index > gradualNotes[indexOf]?.[0].start) {
  206. gradualNotes[indexOf][1] = {
  207. start: ele.index,
  208. measureIndex: ele.measureIndex,
  209. noteInMeasureIndex: ele.noteInMeasureIndex,
  210. allDuration: ele.allDuration,
  211. leftDuration: ele.leftDuration,
  212. type: textContent
  213. }
  214. }
  215. }
  216. if (ele.type === 'words' && isKeyWork) {
  217. gradualNotes.push([
  218. {
  219. start: ele.index,
  220. measureIndex: ele.measureIndex,
  221. noteInMeasureIndex: ele.noteInMeasureIndex,
  222. allDuration: ele.allDuration,
  223. leftDuration: ele.leftDuration,
  224. type: textContent
  225. }
  226. ])
  227. }
  228. }
  229. return gradualNotes
  230. }
  231. export default defineComponent({
  232. name: 'music-operation',
  233. props: {
  234. type: {
  235. type: String,
  236. default: 'add'
  237. },
  238. data: {
  239. type: Object as PropType<any>,
  240. default: () => {
  241. }
  242. },
  243. tagList: {
  244. type: Array as PropType<Array<SelectOption>>,
  245. default: () => []
  246. },
  247. subjectList: {
  248. type: Array as PropType<Array<SelectOption>>,
  249. default: () => []
  250. },
  251. musicSheetCategories: {
  252. type: Array as PropType<Array<SelectOption>>,
  253. default: () => []
  254. }
  255. },
  256. emits: ['close', 'getList'],
  257. setup(props, {slots, attrs, emit}) {
  258. const forms = reactive({
  259. graduals: {} as any, // 渐变速度
  260. audioType: 'MP3', // 播放类型
  261. mp3Type: 'MP3', // 是否含节拍器
  262. xmlFileUrl: null, // XML
  263. midiUrl: null, // mid
  264. metronomeUrl: null, // 伴奏 根据mp3Type 是否是为包含节拍器
  265. musicSheetName: null, // 曲目名称
  266. musicTag: [] as any, // 曲目标签
  267. composer: null, // 音乐人
  268. playSpeed: null, // 曲谱速度
  269. showFingering: null as any, // 是否显示指法
  270. canEvaluate: null as any, // 是否评测
  271. musicSubject: null as any, // 可用声部
  272. notation: null as any, // 能否转和简谱
  273. auditVersion: null as any, // 审核版本
  274. accompanimentType: 1, // 伴奏类型
  275. sortNumber: null, // 排序
  276. titleImg: null, // 曲谱封面
  277. remark: null, // 曲谱描述
  278. background: [] as any, // 原音
  279. musicSheetCategoriesId: null,
  280. status: false,
  281. musicSheetType: 'SINGLE' as 'SINGLE' | 'CONCERT',
  282. author: null, //音乐人
  283. authorFrom: undefined,
  284. belongTo: undefined,
  285. speed: undefined,
  286. playMetronome: 1,
  287. playStyle: undefined, // 播放方式
  288. releaseCandidate: 'NO' // 是否审核版本
  289. })
  290. const state = reactive({
  291. tagList: [...props.tagList] as any, // 标签列表
  292. xmlFirstSpeed: null as any, // 第一个音轨速度
  293. partListNames: [] as any, // 所有音轨声部列表
  294. musicSheetCategories: [...props.musicSheetCategories] as any
  295. })
  296. const gradualData = reactive({
  297. list: [] as any[],
  298. gradualRefs: [] as any[]
  299. })
  300. const btnLoading = ref(false)
  301. const formsRef = ref()
  302. const message = useMessage()
  303. const dialog = useDialog()
  304. // 提交记录
  305. const onSubmit = async () => {
  306. formsRef.value.validate(async (error: any) => {
  307. console.log(error, 'error')
  308. if (error) {
  309. message.error(error[0]?.[0]?.message)
  310. return
  311. }
  312. try {
  313. const obj = {
  314. ...forms,
  315. musicTag: '-1',
  316. extConfigJson: JSON.stringify({gradualTimes: forms.graduals})
  317. }
  318. if (forms.audioType == 'MIDI') {
  319. obj.background = []
  320. }
  321. btnLoading.value = true
  322. if (props.type === 'add') {
  323. await musicSheetSave({...obj})
  324. message.success('添加成功')
  325. } else if (props.type === 'edit') {
  326. await musicSheetUpdate({...obj, id: props.data.id})
  327. message.success('修改成功')
  328. }
  329. emit('getList')
  330. emit('close')
  331. } catch (e) {
  332. console.log(e)
  333. }
  334. setTimeout(() => {
  335. btnLoading.value = false
  336. }, 100)
  337. })
  338. }
  339. // 上传XML,初始化音轨 音轨速度
  340. const readFileInputEventAsArrayBuffer = (file: any) => {
  341. const xmlRead = new FileReader()
  342. xmlRead.onload = (res) => {
  343. try {
  344. gradualData.list = getGradualLengthByXml(res?.target?.result as any).filter(
  345. (item: any) => item.length === 2
  346. )
  347. } catch (error) {
  348. }
  349. state.partListNames = getPartListNames(res?.target?.result as any) as any
  350. // 这里是如果没有当前音轨就重新写
  351. for (let j = 0; j < state.partListNames.length; j++) {
  352. if (!forms.background[j]) {
  353. forms.background.push({audioFileUrl: null, track: null})
  354. }
  355. forms.background[j].track = state.partListNames[j].value
  356. }
  357. // 循环添加所在音轨的原音
  358. for (let index = forms.background.length; index < state.partListNames.length; index++) {
  359. const part = state.partListNames[index].value
  360. const sysData = {
  361. ...forms.background[0],
  362. track: part
  363. }
  364. if (!sysData.speed) {
  365. sysData.speed = state.xmlFirstSpeed
  366. }
  367. createSys(sysData)
  368. }
  369. if (forms.background.length == 0) {
  370. forms.background.push({audioFileUrl: '', track: ''})
  371. }
  372. }
  373. xmlRead.readAsText(file)
  374. }
  375. // 获取xml中所有轨道
  376. const getPartListNames = (xml: any) => {
  377. if (!xml) return []
  378. const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
  379. const partList =
  380. xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
  381. const partListNames = Array.from(partList).map((item) => {
  382. const part = item.getElementsByTagName('part-name')?.[0].textContent || ''
  383. return {
  384. value: part,
  385. label: part
  386. }
  387. })
  388. if (partListNames.length > 0) {
  389. forms.background = forms.background.slice(0, partListNames.length)
  390. }
  391. state.xmlFirstSpeed = xmlParse.getElementsByTagName('per-minute')?.[0]?.textContent || ''
  392. if (!forms.playSpeed) {
  393. forms.playSpeed = state.xmlFirstSpeed
  394. }
  395. return partListNames
  396. }
  397. // 判断选择的音轨是否在选中
  398. const initPartsListStatus = (track: string): any => {
  399. const _names = state.partListNames.filter((n: any) => n.value?.toLocaleUpperCase?.() != 'COMMON')
  400. const partListNames = deepClone(_names) || []
  401. partListNames.forEach((item: any) => {
  402. const index = forms.background.findIndex((ground: any) => item.value == ground.track)
  403. if (index > -1 && track != item.value) {
  404. item.disabled = true
  405. } else {
  406. item.disabled = false
  407. }
  408. })
  409. return partListNames || []
  410. }
  411. // 添加原音
  412. const createSys = (initData?: any) => {
  413. forms.background.push({
  414. audioFileUrl: null, // 原音
  415. track: null, // 轨道
  416. ...initData
  417. })
  418. }
  419. // 删除原音
  420. const removeSys = (index: number) => {
  421. dialog.warning({
  422. title: '警告',
  423. content: `是否确认删除此原音?`,
  424. positiveText: '确定',
  425. negativeText: '取消',
  426. onPositiveClick: async () => {
  427. forms.background.splice(index, 1)
  428. }
  429. })
  430. }
  431. onMounted(async () => {
  432. if (props.type === 'edit' || props.type === 'preview') {
  433. const detail = props.data
  434. try {
  435. const {data} = await musicSheetDetail({id: detail.id})
  436. forms.audioType = data.audioType
  437. forms.mp3Type = data.mp3Type
  438. forms.xmlFileUrl = data.xmlFileUrl
  439. forms.midiUrl = data.midiUrl
  440. forms.metronomeUrl = data.metronomeUrl
  441. forms.musicSheetName = data.musicSheetName
  442. forms.musicTag = data.musicTag?.split(',')
  443. forms.composer = data.composer
  444. forms.playSpeed = data.playSpeed
  445. forms.showFingering = Number(data.showFingering)
  446. forms.canEvaluate = Number(data.canEvaluate)
  447. forms.musicSubject = data.musicSubject ? Number(data.musicSubject) : null
  448. forms.notation = Number(data.notation)
  449. forms.auditVersion = Number(data.auditVersion)
  450. forms.accompanimentType = data.accompanimentType
  451. forms.sortNumber = data.sortNumber
  452. forms.titleImg = data.titleImg
  453. forms.remark = data.remark
  454. forms.status = data.status
  455. forms.musicSheetCategoriesId = data.musicSheetCategoriesId
  456. forms.background = data.background || []
  457. forms.musicSheetType = data.musicSheetType || "SINGLE"
  458. // 获取渐变 和 是否多声部
  459. try {
  460. const extConfigJson = data.extConfigJson ? JSON.parse(data.extConfigJson) : {}
  461. forms.graduals = extConfigJson.gradualTimes || {}
  462. } catch (error) {
  463. }
  464. axios.get(data.xmlFileUrl).then((res: any) => {
  465. if (res?.data) {
  466. gradualData.list = getGradualLengthByXml(res?.data as any).filter(
  467. (item: any) => item.length === 2
  468. )
  469. state.partListNames = getPartListNames(res?.data as any) as any
  470. }
  471. })
  472. } catch (error) {
  473. console.log(error)
  474. }
  475. }
  476. })
  477. return () => (
  478. <div style="background: #fff; padding-top: 12px">
  479. <NForm
  480. class={styles.formContainer}
  481. model={forms}
  482. ref={formsRef}
  483. label-placement="left"
  484. label-width="130"
  485. >
  486. <NAlert showIcon={false} style={{marginBottom:"12px" }}>曲目信息</NAlert>
  487. <NGrid cols={2}>
  488. <NFormItemGi
  489. label="曲目名称"
  490. path="musicSheetName"
  491. rule={[
  492. {
  493. required: true,
  494. message: '请输入曲目名称'
  495. }
  496. ]}
  497. >
  498. <NInput v-model:value={forms.musicSheetName} placeholder="请输入曲目名称"/>
  499. </NFormItemGi>
  500. <NFormItemGi
  501. label="音乐人"
  502. path="author"
  503. rule={[
  504. {
  505. required: true,
  506. message: '请输入音乐人'
  507. }
  508. ]}
  509. >
  510. <NInput v-model:value={forms.author} placeholder="请输入音乐人"
  511. showCount
  512. maxlength={14}
  513. />
  514. </NFormItemGi>
  515. </NGrid>
  516. <NGrid cols={2}>
  517. <NFormItemGi label="曲目描述" path="remark">
  518. <NInput
  519. placeholder="请输入曲目描述"
  520. type="textarea"
  521. rows={4}
  522. showCount
  523. maxlength={200}
  524. v-model:value={forms.remark}
  525. />
  526. </NFormItemGi>
  527. <NFormItemGi label="曲谱封面" path="titleImg"
  528. rule={[
  529. {
  530. required: true
  531. }
  532. ]}>
  533. <UploadFile
  534. accept=".jpg,.jpeg,.png"
  535. tips="请上传大小2M以内的JPG、PNG图片"
  536. v-model:fileList={forms.titleImg}
  537. cropper
  538. bucketName="cloud-coach"
  539. options={{
  540. autoCrop: true, //是否默认生成截图框
  541. enlarge: 2, // 图片放大倍数
  542. autoCropWidth: 200, //默框高度
  543. fixedBox: true, //是否固定截图框大认生成截图框宽度
  544. autoCropHeight: 200, //默认生成截图小 不允许改变
  545. previewsCircle: false, //预览图是否是原圆形
  546. title: '曲谱封面'
  547. }}
  548. />
  549. </NFormItemGi>
  550. </NGrid>
  551. <NGrid cols={2}>
  552. <NFormItemGi
  553. label="曲目类型"
  554. path="musicSheetType"
  555. rule={[
  556. {
  557. required: true,
  558. message: '请选择曲目类型'
  559. }
  560. ]}
  561. >
  562. <NRadioGroup v-model:value={forms.musicSheetType}>
  563. <NRadio value="SINGLE">独奏</NRadio>
  564. <NRadio value="CONCERT">合奏</NRadio>
  565. </NRadioGroup>
  566. </NFormItemGi>
  567. <NFormItemGi
  568. label="作者属性"
  569. path="authorFrom"
  570. rule={[
  571. {
  572. required: true,
  573. message: '请选择作者属性'
  574. }
  575. ]}
  576. >
  577. <NSelect
  578. v-model:value={forms.authorFrom}
  579. options={
  580. [
  581. {
  582. label: '平台',
  583. value: 'PALTFORM'
  584. },
  585. {
  586. label: '机构',
  587. value: 'TENANT'
  588. },
  589. {
  590. label: '个人',
  591. value: 'SELF'
  592. }
  593. ] as any
  594. }
  595. filterable
  596. clearable
  597. placeholder="请选择作者属性"
  598. />
  599. </NFormItemGi>
  600. </NGrid>
  601. <NGrid cols={2}>
  602. <NFormItemGi
  603. label="所属人"
  604. path="belongTo"
  605. rule={[
  606. {
  607. required: true,
  608. message: '请选择曲目所属人'
  609. }
  610. ]}
  611. >
  612. <NSelect
  613. v-model:value={forms.belongTo}
  614. options={
  615. [
  616. {
  617. label: '小A',
  618. value: '小A'
  619. }
  620. ] as any
  621. }
  622. filterable
  623. clearable
  624. placeholder="请选择曲目所属人"
  625. />
  626. </NFormItemGi>
  627. <NFormItemGi label="速度" path="speed">
  628. <NInputNumber
  629. placeholder="请输入速度"
  630. v-model:value={forms.speed}
  631. />
  632. </NFormItemGi>
  633. </NGrid>
  634. <NGrid cols={2}>
  635. <NFormItemGi label="审核版本" path="speed">
  636. <NSelect
  637. options={getSelectDataFromObj(releaseCandidate)}
  638. v-model:value={forms.releaseCandidate}
  639. defaultValue={'NO'}
  640. />
  641. </NFormItemGi>
  642. </NGrid>
  643. <NAlert showIcon={false} style={{marginBottom:"12px" }}>曲目上传</NAlert>
  644. <NGrid cols={2}>
  645. <NFormItemGi label="播放模式" path="audioType"
  646. rule={[
  647. {
  648. required: true,
  649. message: '请选择播放模式'
  650. }
  651. ]}
  652. >
  653. <NRadioGroup
  654. v-model:value={forms.audioType}
  655. onUpdateValue={(value: string | number | boolean) => {
  656. if (value === 'MP3') {
  657. forms.mp3Type = 'MP3'
  658. } else {
  659. forms.mp3Type = 'MIDI'
  660. }
  661. }}
  662. >
  663. <NRadio value="MP3">MP3</NRadio>
  664. <NRadio value="MIDI">MIDI</NRadio>
  665. </NRadioGroup>
  666. </NFormItemGi>
  667. {forms.mp3Type === 'MP3' && (
  668. <NFormItemGi
  669. label="伴奏类型"
  670. path="audioType"
  671. rule={[
  672. {
  673. required: true
  674. }
  675. ]}
  676. >
  677. <NRadioGroup
  678. v-model:value={forms.accompanimentType}
  679. >
  680. <NRadio value={1}>自制伴奏</NRadio>
  681. <NRadio value={2}>普通伴奏</NRadio>
  682. </NRadioGroup>
  683. </NFormItemGi>
  684. )}
  685. </NGrid>
  686. <NGrid cols={2}>
  687. {forms.mp3Type === 'MP3' && (
  688. <NFormItemGi
  689. label="上传伴奏"
  690. path="xmlFileUrl"
  691. rule={[
  692. {
  693. required: true,
  694. message: '请选择上传.mp3/.aac'
  695. }
  696. ]}
  697. >
  698. <UploadFile
  699. size={10}
  700. v-model:fileList={forms.xmlFileUrl}
  701. tips="仅支持上传.mp3/.aac格式文件"
  702. listType="image"
  703. accept=".mp3,.aac"
  704. bucketName="cloud-coach"
  705. text="点击上传伴奏文件"
  706. onReadFileInputEventAsArrayBuffer={readFileInputEventAsArrayBuffer}
  707. />
  708. </NFormItemGi>
  709. )}
  710. {forms.mp3Type === 'MIDI' && (
  711. <NFormItemGi
  712. label="上传MIDI"
  713. path="xmlFileUrl"
  714. rule={[
  715. {
  716. required: true,
  717. message: '请选择上传.MIDI格式文件'
  718. }
  719. ]}
  720. >
  721. <UploadFile
  722. size={10}
  723. v-model:fileList={forms.xmlFileUrl}
  724. tips="仅支持上传.MIDI格式文件"
  725. listType="image"
  726. accept=".mp3,.aac"
  727. bucketName="cloud-coach"
  728. text="点击上传MIDI文件"
  729. onReadFileInputEventAsArrayBuffer={readFileInputEventAsArrayBuffer}
  730. />
  731. </NFormItemGi>
  732. )}
  733. <NFormItemGi
  734. label="上传XML"
  735. path="xmlFileUrl"
  736. rule={[
  737. {
  738. required: true,
  739. message: '请选择上传XML'
  740. }
  741. ]}
  742. >
  743. <UploadFile
  744. size={10}
  745. v-model:fileList={forms.xmlFileUrl}
  746. tips="仅支持上传.xml格式文件"
  747. listType="image"
  748. accept=".xml"
  749. bucketName="cloud-coach"
  750. text="点击上传XML文件"
  751. onReadFileInputEventAsArrayBuffer={readFileInputEventAsArrayBuffer}
  752. />
  753. </NFormItemGi>
  754. </NGrid>
  755. <NGrid cols={2}>
  756. <NFormItemGi label="可用声部" path="musicSubject"
  757. rule={[
  758. {
  759. required: true
  760. }
  761. ]}
  762. >
  763. <NSelect
  764. v-model:value={forms.musicSubject}
  765. options={props.subjectList}
  766. multiple
  767. filterable
  768. clearable
  769. placeholder="请选择可用声部"
  770. />
  771. </NFormItemGi>
  772. { (forms.mp3Type === 'MP3' || forms.musicSheetType === 'SINGLE') && (
  773. <NFormItemGi
  774. label="页面渲染声轨"
  775. path="audioType"
  776. rule={[
  777. {
  778. required: true,
  779. message: '请选择页面渲染声轨'
  780. }
  781. ]}
  782. >
  783. <CheckboxGroup>
  784. <NCheckbox value="长笛">长笛</NCheckbox>
  785. <NCheckbox value="竖笛">竖笛</NCheckbox>
  786. <NCheckbox value="葫芦丝">葫芦丝</NCheckbox>
  787. <NCheckbox value="萨克斯">萨克斯</NCheckbox>
  788. </CheckboxGroup>
  789. </NFormItemGi>
  790. )}
  791. {forms.mp3Type === 'MIDI' && forms.musicSheetType === 'CONCERT' && (
  792. <NFormItemGi
  793. label="用户可切换声轨"
  794. path="audioType"
  795. rule={[
  796. {
  797. required: true,
  798. message: '请选择用户可切换声轨'
  799. }
  800. ]}
  801. >
  802. <CheckboxGroup>
  803. <NCheckbox value="长笛">长笛</NCheckbox>
  804. <NCheckbox value="竖笛">竖笛</NCheckbox>
  805. <NCheckbox value="葫芦丝">葫芦丝</NCheckbox>
  806. <NCheckbox value="萨克斯">萨克斯</NCheckbox>
  807. </CheckboxGroup>
  808. </NFormItemGi>
  809. )}
  810. </NGrid>
  811. <NGrid cols={2}>
  812. <NFormItemGi label="是否播放节拍器" path="audioType"
  813. rule={[
  814. {
  815. required: true,
  816. message: '请选择是否播放节拍器'
  817. }
  818. ]}
  819. >
  820. <NRadioGroup
  821. v-model:value={forms.playMetronome}
  822. >
  823. <NRadio value={1}>是</NRadio>
  824. <NRadio value={0}>否</NRadio>
  825. </NRadioGroup>
  826. </NFormItemGi>
  827. {forms.playMetronome && (
  828. <NFormItemGi label="播放方式" path="audioType"
  829. rule={[
  830. {
  831. required: true,
  832. message: '请选择播放方式'
  833. }
  834. ]}
  835. >
  836. <NRadioGroup
  837. v-model:value={forms.playStyle}
  838. >
  839. <NRadio value="MP3">系统节拍器</NRadio>
  840. <NRadio value="MIDI">MP3节拍器</NRadio>
  841. </NRadioGroup>
  842. </NFormItemGi>
  843. )}
  844. </NGrid>
  845. {/* 只有播放类型为mp3时才会有原音 */}
  846. {forms.audioType === 'MP3' && (
  847. <>
  848. {forms.background.map((item: any, index: number) => (
  849. <>
  850. {item.track?.toLocaleUpperCase?.() != 'COMMON' && <NGrid class={styles.audioSection}>
  851. <NFormItemGi
  852. span={12}
  853. label="原音"
  854. path={`background[${index}].audioFileUrl`}
  855. rule={[
  856. {
  857. required: true,
  858. message: `请上传${
  859. item.track ? item.track + '的' : '第' + (index + 1) + '个'
  860. }原音`
  861. }
  862. ]}
  863. >
  864. <UploadFile
  865. size={10}
  866. v-model:fileList={item.audioFileUrl}
  867. tips="仅支持上传.mp3/.aac格式文件"
  868. listType="image"
  869. accept=".mp3,.aac"
  870. bucketName="cloud-coach"
  871. />
  872. </NFormItemGi>
  873. {state.partListNames.length > 1 && (
  874. <NFormItemGi
  875. span={12}
  876. label="所属轨道"
  877. path={`background[${index}].track`}
  878. rule={[
  879. {
  880. required: true,
  881. message: '请选择所属轨道'
  882. }
  883. ]}
  884. >
  885. <NSelect
  886. placeholder="请选择所属轨道"
  887. v-model:value={item.track}
  888. options={initPartsListStatus(item.track)}
  889. />
  890. </NFormItemGi>
  891. )}
  892. <NGi class={styles.btnRemove}>
  893. <NButton
  894. type="primary"
  895. text
  896. disabled={forms.background.length === 1}
  897. onClick={() => removeSys(index)}
  898. >
  899. 删除
  900. </NButton>
  901. </NGi>
  902. </NGrid>}
  903. </>
  904. ))}
  905. <NButton
  906. type="primary"
  907. dashed
  908. block
  909. disabled={state.partListNames.length <= forms.background.length}
  910. style={{
  911. marginBottom: '24px'
  912. }}
  913. onClick={createSys}
  914. >
  915. 添加原音
  916. </NButton>
  917. </>
  918. )}
  919. </NForm>
  920. {props.type !== 'preview' &&
  921. (
  922. <NSpace justify="end">
  923. <NButton type="default" onClick={() => emit('close')}>
  924. 取消
  925. </NButton>
  926. <NButton
  927. type="primary"
  928. onClick={() => onSubmit()}
  929. loading={btnLoading.value}
  930. disabled={btnLoading.value}
  931. >
  932. 确认
  933. </NButton>
  934. </NSpace>
  935. )}
  936. </div>
  937. )
  938. }
  939. })