|
@@ -0,0 +1,976 @@
|
|
|
+import {
|
|
|
+ NForm,
|
|
|
+ NFormItem,
|
|
|
+ NInput,
|
|
|
+ NSelect,
|
|
|
+ NSpace,
|
|
|
+ NButton,
|
|
|
+ useMessage,
|
|
|
+ NRadioGroup,
|
|
|
+ NRadio,
|
|
|
+ NGrid,
|
|
|
+ NFormItemGi,
|
|
|
+ NInputNumber,
|
|
|
+ NGi,
|
|
|
+ useDialog,
|
|
|
+ NCascader,
|
|
|
+ NAlert,
|
|
|
+ NInputGroup,
|
|
|
+ NInputGroupLabel
|
|
|
+} from 'naive-ui'
|
|
|
+import type { SelectOption } from 'naive-ui'
|
|
|
+import { defineComponent, onMounted, PropType, reactive, ref } from 'vue'
|
|
|
+import { musicSheetDetail, musicSheetSave, musicSheetUpdate } from '../../api'
|
|
|
+import UploadFile from '@/components/upload-file'
|
|
|
+import styles from './index.module.less'
|
|
|
+import deepClone from '@/utils/deep.clone'
|
|
|
+import axios from 'axios'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取指定元素下一个Note元素
|
|
|
+ * @param ele 指定元素
|
|
|
+ * @param selectors 选择器
|
|
|
+ */
|
|
|
+const getNextNote = (ele: any, selectors: any) => {
|
|
|
+ let index = 0
|
|
|
+ const parentEle = ele.closest(selectors)
|
|
|
+ let pointer = parentEle
|
|
|
+ const measure = parentEle?.closest('measure')
|
|
|
+ let siblingNote = null
|
|
|
+ // 查找到相邻的第一个note元素
|
|
|
+ while (!siblingNote && index < (measure?.childNodes.length || 50)) {
|
|
|
+ index++
|
|
|
+ if (pointer?.nextElementSibling?.tagName === 'note') {
|
|
|
+ siblingNote = pointer?.nextElementSibling
|
|
|
+ }
|
|
|
+ pointer = pointer?.nextElementSibling
|
|
|
+ }
|
|
|
+ return siblingNote
|
|
|
+}
|
|
|
+
|
|
|
+export const onlyVisible = (xml: any, partIndex: any) => {
|
|
|
+ if (!xml) return ''
|
|
|
+ const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
|
|
|
+ const partList =
|
|
|
+ xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
|
|
|
+ const parts = xmlParse.getElementsByTagName('part')
|
|
|
+ const visiblePartInfo = partList[partIndex]
|
|
|
+ if (visiblePartInfo) {
|
|
|
+ const id = visiblePartInfo.getAttribute('id')
|
|
|
+ Array.from(parts).forEach((part) => {
|
|
|
+ if (part && part.getAttribute('id') !== id) {
|
|
|
+ part.parentNode?.removeChild(part)
|
|
|
+ // 不等于第一行才添加避免重复添加
|
|
|
+ }
|
|
|
+
|
|
|
+ // 最后一个小节的结束线元素不在最后 调整
|
|
|
+ if (part && part.getAttribute('id') === id) {
|
|
|
+ const barlines = part.getElementsByTagName('barline')
|
|
|
+ const lastParent = barlines[barlines.length - 1]?.parentElement
|
|
|
+ if (lastParent?.lastElementChild?.tagName !== 'barline') {
|
|
|
+ const children: any[] = (lastParent?.children as any) || []
|
|
|
+ for (let el of children) {
|
|
|
+ if (el.tagName === 'barline') {
|
|
|
+ // 将结束线元素放到最后
|
|
|
+ lastParent?.appendChild(el)
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ Array.from(partList).forEach((part) => {
|
|
|
+ if (part && part.getAttribute('id') !== id) {
|
|
|
+ part.parentNode?.removeChild(part)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // 处理装饰音问题
|
|
|
+ const notes = xmlParse.getElementsByTagName('note')
|
|
|
+ const getNextvNoteDuration = (i: any) => {
|
|
|
+ let nextNote = notes[i + 1]
|
|
|
+ // 可能存在多个装饰音问题,取下一个非装饰音时值
|
|
|
+ for (let index = i; index < notes.length; index++) {
|
|
|
+ const note = notes[index]
|
|
|
+ if (!note.getElementsByTagName('grace')?.length) {
|
|
|
+ nextNote = note
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const nextNoteDuration = nextNote?.getElementsByTagName('duration')[0]
|
|
|
+ return nextNoteDuration
|
|
|
+ }
|
|
|
+ Array.from(notes).forEach((note, i) => {
|
|
|
+ const graces = note.getElementsByTagName('grace')
|
|
|
+ if (graces && graces.length) {
|
|
|
+ note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return new XMLSerializer().serializeToString(xmlParse)
|
|
|
+}
|
|
|
+
|
|
|
+const speedInfo = {
|
|
|
+ 'rall.': 1.333333333,
|
|
|
+ 'poco rit.': 1.333333333,
|
|
|
+ 'rit.': 1.333333333,
|
|
|
+ 'molto rit.': 1.333333333,
|
|
|
+ 'molto rall': 1.333333333,
|
|
|
+ molto: 1.333333333,
|
|
|
+ lentando: 1.333333333,
|
|
|
+ allargando: 1.333333333,
|
|
|
+ morendo: 1.333333333,
|
|
|
+ 'accel.': 0.8,
|
|
|
+ calando: 2,
|
|
|
+ 'poco accel.': 0.8,
|
|
|
+ 'gradually slowing': 1.333333333,
|
|
|
+ slowing: 1.333333333,
|
|
|
+ slow: 1.333333333,
|
|
|
+ slowly: 1.333333333,
|
|
|
+ faster: 1.333333333
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 按照xml进行减慢速度的计算
|
|
|
+ * @param xml 始终按照第一分谱进行减慢速度的计算
|
|
|
+ */
|
|
|
+export function getGradualLengthByXml(xml: string) {
|
|
|
+ const firstPartXml = onlyVisible(xml, 0)
|
|
|
+ const xmlParse = new DOMParser().parseFromString(firstPartXml, 'text/xml')
|
|
|
+ const measures = Array.from(xmlParse.querySelectorAll('measure'))
|
|
|
+ const notes = Array.from(xmlParse.querySelectorAll('note'))
|
|
|
+ const words = Array.from(xmlParse.querySelectorAll('words'))
|
|
|
+ const metronomes = Array.from(xmlParse.querySelectorAll('metronome'))
|
|
|
+
|
|
|
+ const eles = []
|
|
|
+
|
|
|
+ for (const ele of [...words, ...metronomes]) {
|
|
|
+ const note = getNextNote(ele, 'direction')
|
|
|
+ // console.log(ele, note)
|
|
|
+ if (note) {
|
|
|
+ const measure = note?.closest('measure')
|
|
|
+ const measureNotes = Array.from(measure.querySelectorAll('note'))
|
|
|
+
|
|
|
+ const noteInMeasureIndex = Array.from(measure.childNodes)
|
|
|
+ .filter((item: any) => item.nodeName === 'note')
|
|
|
+ .findIndex((item) => item === note)
|
|
|
+
|
|
|
+ let allDuration = 0
|
|
|
+ let leftDuration = 0
|
|
|
+ for (let i = 0; i < measureNotes.length; i++) {
|
|
|
+ const n: any = measureNotes[i]
|
|
|
+ const duration = +(n.querySelector('duration')?.textContent || '0')
|
|
|
+ allDuration += duration
|
|
|
+ if (i < noteInMeasureIndex) {
|
|
|
+ leftDuration = allDuration
|
|
|
+ }
|
|
|
+ }
|
|
|
+ eles.push({
|
|
|
+ ele,
|
|
|
+ index: notes.indexOf(note),
|
|
|
+ noteInMeasureIndex,
|
|
|
+ textContent: ele.textContent,
|
|
|
+ measureIndex: measures.indexOf(measure), //,measure?.getAttribute('number')
|
|
|
+ type: ele.tagName,
|
|
|
+ allDuration,
|
|
|
+ leftDuration
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 结尾处手动插入一个音符节点
|
|
|
+ eles.push({
|
|
|
+ ele: notes[notes.length - 1],
|
|
|
+ index: notes.length,
|
|
|
+ noteInMeasureIndex: 0,
|
|
|
+ textContent: '',
|
|
|
+ type: 'metronome',
|
|
|
+ allDuration: 1,
|
|
|
+ leftDuration: 1,
|
|
|
+ measureIndex: measures.length
|
|
|
+ })
|
|
|
+
|
|
|
+ const gradualNotes: any[] = []
|
|
|
+ eles.sort((a, b) => a.index - b.index)
|
|
|
+ const keys = Object.keys(speedInfo).map((w) => w.toLocaleLowerCase())
|
|
|
+ let isLastNoteAndNotClosed = false
|
|
|
+ for (const ele of eles) {
|
|
|
+ const textContent: any = ele.textContent?.toLocaleLowerCase().trim()
|
|
|
+ if (ele === eles[eles.length - 1]) {
|
|
|
+ if (gradualNotes[gradualNotes.length - 1]?.length === 1) {
|
|
|
+ isLastNoteAndNotClosed = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const isKeyWork = keys.find((k) => {
|
|
|
+ const ks = k.split(' ')
|
|
|
+ return textContent && ks.includes(textContent)
|
|
|
+ })
|
|
|
+ if (
|
|
|
+ ele.type === 'metronome' ||
|
|
|
+ (ele.type === 'words' && (textContent.startsWith('a tempo') || isKeyWork)) ||
|
|
|
+ isLastNoteAndNotClosed
|
|
|
+ ) {
|
|
|
+ const indexOf = gradualNotes.findIndex((item) => item.length === 1)
|
|
|
+ if (indexOf > -1 && ele.index > gradualNotes[indexOf]?.[0].start) {
|
|
|
+ gradualNotes[indexOf][1] = {
|
|
|
+ start: ele.index,
|
|
|
+ measureIndex: ele.measureIndex,
|
|
|
+ noteInMeasureIndex: ele.noteInMeasureIndex,
|
|
|
+ allDuration: ele.allDuration,
|
|
|
+ leftDuration: ele.leftDuration,
|
|
|
+ type: textContent
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (ele.type === 'words' && isKeyWork) {
|
|
|
+ gradualNotes.push([
|
|
|
+ {
|
|
|
+ start: ele.index,
|
|
|
+ measureIndex: ele.measureIndex,
|
|
|
+ noteInMeasureIndex: ele.noteInMeasureIndex,
|
|
|
+ allDuration: ele.allDuration,
|
|
|
+ leftDuration: ele.leftDuration,
|
|
|
+ type: textContent
|
|
|
+ }
|
|
|
+ ])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return gradualNotes
|
|
|
+}
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: 'city-operation',
|
|
|
+ props: {
|
|
|
+ type: {
|
|
|
+ type: String,
|
|
|
+ default: 'add'
|
|
|
+ },
|
|
|
+ data: {
|
|
|
+ type: Object as PropType<any>,
|
|
|
+ default: () => {}
|
|
|
+ },
|
|
|
+ tagList: {
|
|
|
+ type: Array as PropType<Array<SelectOption>>,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ subjectList: {
|
|
|
+ type: Array as PropType<Array<SelectOption>>,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ musicSheetCategories: {
|
|
|
+ type: Array as PropType<Array<SelectOption>>,
|
|
|
+ default: () => []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ emits: ['close', 'getList'],
|
|
|
+ setup(props, { slots, attrs, emit }) {
|
|
|
+ const forms = reactive({
|
|
|
+ graduals: {} as any, // 渐变速度
|
|
|
+ audioType: 'MP3', // 播放类型
|
|
|
+ mp3Type: 'MP3', // 是否含节拍器
|
|
|
+ xmlFileUrl: null, // XML
|
|
|
+ midiUrl: null, // mid
|
|
|
+ metronomeUrl: null, // 伴奏 根据mp3Type 是否是为包含节拍器
|
|
|
+ musicSheetName: null, // 曲目名称
|
|
|
+ musicTag: [] as any, // 曲目标签
|
|
|
+ composer: null, // 音乐人
|
|
|
+ playSpeed: null, // 曲谱速度
|
|
|
+ showFingering: null as any, // 是否显示指法
|
|
|
+ canEvaluate: null as any, // 是否评测
|
|
|
+ musicSubject: null as any, // 可用声部
|
|
|
+ notation: null as any, // 能否转和简谱
|
|
|
+ auditVersion: null as any, // 审核版本
|
|
|
+ accompanimentType: null, // 伴奏类型
|
|
|
+ sortNumber: null, // 排序
|
|
|
+ titleImg: null, // 曲谱封面
|
|
|
+ remark: null, // 曲谱描述
|
|
|
+ background: [] as any, // 原音
|
|
|
+ musicSheetCategoriesId: null,
|
|
|
+ status: false,
|
|
|
+ musicSheetType: 'SINGLE' as 'SINGLE' | 'CONCERT'
|
|
|
+ })
|
|
|
+ const state = reactive({
|
|
|
+ tagList: [...props.tagList] as any, // 标签列表
|
|
|
+ xmlFirstSpeed: null as any, // 第一个音轨速度
|
|
|
+ partListNames: [] as any, // 所有音轨声部列表
|
|
|
+ musicSheetCategories: [...props.musicSheetCategories] as any
|
|
|
+ })
|
|
|
+ const gradualData = reactive({
|
|
|
+ list: [] as any[],
|
|
|
+ gradualRefs: [] as any[]
|
|
|
+ })
|
|
|
+
|
|
|
+ const btnLoading = ref(false)
|
|
|
+ const formsRef = ref()
|
|
|
+ const message = useMessage()
|
|
|
+ const dialog = useDialog()
|
|
|
+
|
|
|
+ // 提交记录
|
|
|
+ const onSubmit = async () => {
|
|
|
+ formsRef.value.validate(async (error: any) => {
|
|
|
+ console.log(error, 'error')
|
|
|
+ if (error) {
|
|
|
+ message.error(error[0]?.[0]?.message)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const obj = {
|
|
|
+ ...forms,
|
|
|
+ musicTag: '-1',
|
|
|
+ extConfigJson: JSON.stringify({ gradualTimes: forms.graduals })
|
|
|
+ }
|
|
|
+ if (forms.audioType == 'MIDI') {
|
|
|
+ obj.background = []
|
|
|
+ }
|
|
|
+ btnLoading.value = true
|
|
|
+ if (props.type === 'add') {
|
|
|
+ await musicSheetSave({ ...obj })
|
|
|
+ message.success('添加成功')
|
|
|
+ } else if (props.type === 'edit') {
|
|
|
+ await musicSheetUpdate({ ...obj, id: props.data.id })
|
|
|
+ message.success('修改成功')
|
|
|
+ }
|
|
|
+ emit('getList')
|
|
|
+ emit('close')
|
|
|
+ } catch (e) {
|
|
|
+ console.log(e)
|
|
|
+ }
|
|
|
+ setTimeout(() => {
|
|
|
+ btnLoading.value = false
|
|
|
+ }, 100)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传XML,初始化音轨 音轨速度
|
|
|
+ const readFileInputEventAsArrayBuffer = (file: any) => {
|
|
|
+ const xmlRead = new FileReader()
|
|
|
+ xmlRead.onload = (res) => {
|
|
|
+ try {
|
|
|
+ gradualData.list = getGradualLengthByXml(res?.target?.result as any).filter(
|
|
|
+ (item: any) => item.length === 2
|
|
|
+ )
|
|
|
+ } catch (error) {}
|
|
|
+ state.partListNames = getPartListNames(res?.target?.result as any) as any
|
|
|
+ // 这里是如果没有当前音轨就重新写
|
|
|
+ for (let j = 0; j < state.partListNames.length; j++) {
|
|
|
+ if (!forms.background[j]) {
|
|
|
+ forms.background.push({ audioFileUrl: null, track: null })
|
|
|
+ }
|
|
|
+ forms.background[j].track = state.partListNames[j].value
|
|
|
+ }
|
|
|
+
|
|
|
+ // 循环添加所在音轨的原音
|
|
|
+ for (let index = forms.background.length; index < state.partListNames.length; index++) {
|
|
|
+ const part = state.partListNames[index].value
|
|
|
+ const sysData = {
|
|
|
+ ...forms.background[0],
|
|
|
+ track: part
|
|
|
+ }
|
|
|
+ if (!sysData.speed) {
|
|
|
+ sysData.speed = state.xmlFirstSpeed
|
|
|
+ }
|
|
|
+ createSys(sysData)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (forms.background.length == 0) {
|
|
|
+ forms.background.push({ audioFileUrl: '', track: '' })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ xmlRead.readAsText(file)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取xml中所有轨道
|
|
|
+ const getPartListNames = (xml: any) => {
|
|
|
+ if (!xml) return []
|
|
|
+ const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
|
|
|
+ const partList =
|
|
|
+ xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
|
|
|
+ const partListNames = Array.from(partList).map((item) => {
|
|
|
+ const part = item.getElementsByTagName('part-name')?.[0].textContent || ''
|
|
|
+ return {
|
|
|
+ value: part,
|
|
|
+ label: part
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ if (partListNames.length > 0) {
|
|
|
+ forms.background = forms.background.slice(0, partListNames.length)
|
|
|
+ }
|
|
|
+
|
|
|
+ state.xmlFirstSpeed = xmlParse.getElementsByTagName('per-minute')?.[0]?.textContent || ''
|
|
|
+ if (!forms.playSpeed) {
|
|
|
+ forms.playSpeed = state.xmlFirstSpeed
|
|
|
+ }
|
|
|
+ return partListNames
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断选择的音轨是否在选中
|
|
|
+ const initPartsListStatus = (track: string): any => {
|
|
|
+ const _names = state.partListNames.filter((n: any) => n.value?.toLocaleUpperCase?.() != 'COMMON')
|
|
|
+ const partListNames = deepClone(_names) || []
|
|
|
+ partListNames.forEach((item: any) => {
|
|
|
+ const index = forms.background.findIndex((ground: any) => item.value == ground.track)
|
|
|
+ if (index > -1 && track != item.value) {
|
|
|
+ item.disabled = true
|
|
|
+ } else {
|
|
|
+ item.disabled = false
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return partListNames || []
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加原音
|
|
|
+ const createSys = (initData?: any) => {
|
|
|
+ forms.background.push({
|
|
|
+ audioFileUrl: null, // 原音
|
|
|
+ track: null, // 轨道
|
|
|
+ ...initData
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 删除原音
|
|
|
+ const removeSys = (index: number) => {
|
|
|
+ dialog.warning({
|
|
|
+ title: '警告',
|
|
|
+ content: `是否确认删除此原音?`,
|
|
|
+ positiveText: '确定',
|
|
|
+ negativeText: '取消',
|
|
|
+ onPositiveClick: async () => {
|
|
|
+ forms.background.splice(index, 1)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ onMounted(async () => {
|
|
|
+ if (props.type === 'edit') {
|
|
|
+ const detail = props.data
|
|
|
+ try {
|
|
|
+ const { data } = await musicSheetDetail({ id: detail.id })
|
|
|
+ forms.audioType = data.audioType
|
|
|
+ forms.mp3Type = data.mp3Type
|
|
|
+ forms.xmlFileUrl = data.xmlFileUrl
|
|
|
+ forms.midiUrl = data.midiUrl
|
|
|
+ forms.metronomeUrl = data.metronomeUrl
|
|
|
+ forms.musicSheetName = data.musicSheetName
|
|
|
+ forms.musicTag = data.musicTag?.split(',')
|
|
|
+ forms.composer = data.composer
|
|
|
+ forms.playSpeed = data.playSpeed
|
|
|
+ forms.showFingering = Number(data.showFingering)
|
|
|
+ forms.canEvaluate = Number(data.canEvaluate)
|
|
|
+ forms.musicSubject = data.musicSubject ? Number(data.musicSubject) : null
|
|
|
+ forms.notation = Number(data.notation)
|
|
|
+ forms.auditVersion = Number(data.auditVersion)
|
|
|
+ forms.accompanimentType = data.accompanimentType
|
|
|
+ forms.sortNumber = data.sortNumber
|
|
|
+ forms.titleImg = data.titleImg
|
|
|
+ forms.remark = data.remark
|
|
|
+ forms.status = data.status
|
|
|
+ forms.musicSheetCategoriesId = data.musicSheetCategoriesId
|
|
|
+ forms.background = data.background || []
|
|
|
+ forms.musicSheetType = data.musicSheetType || "SINGLE"
|
|
|
+ // 获取渐变 和 是否多声部
|
|
|
+ try {
|
|
|
+ const extConfigJson = data.extConfigJson ? JSON.parse(data.extConfigJson) : {}
|
|
|
+ forms.graduals = extConfigJson.gradualTimes || {}
|
|
|
+ } catch (error) {}
|
|
|
+ axios.get(data.xmlFileUrl).then((res: any) => {
|
|
|
+ if (res?.data) {
|
|
|
+ gradualData.list = getGradualLengthByXml(res?.data as any).filter(
|
|
|
+ (item: any) => item.length === 2
|
|
|
+ )
|
|
|
+ state.partListNames = getPartListNames(res?.data as any) as any
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ return () => (
|
|
|
+ <div style="background: #fff; padding-top: 12px">
|
|
|
+ <NForm
|
|
|
+ class={styles.formContainer}
|
|
|
+ model={forms}
|
|
|
+ ref={formsRef}
|
|
|
+ label-placement="left"
|
|
|
+ label-width="130"
|
|
|
+ >
|
|
|
+ <NGrid cols={2}>
|
|
|
+ <NFormItemGi
|
|
|
+ label="播放类型"
|
|
|
+ path="audioType"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择播放类型'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup
|
|
|
+ v-model:value={forms.audioType}
|
|
|
+ onUpdateValue={(value: string | number | boolean) => {
|
|
|
+ console.log(value, 'value')
|
|
|
+ if (value === 'MP3') {
|
|
|
+ forms.mp3Type = 'MP3'
|
|
|
+ } else {
|
|
|
+ forms.mp3Type = ''
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <NRadio value="MP3">MP3</NRadio>
|
|
|
+ <NRadio value="MIDI">MIDI</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi>
|
|
|
+ <NFormItemGi
|
|
|
+ label="曲谱类型"
|
|
|
+ path="musicSheetType"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择曲谱类型'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup v-model:value={forms.musicSheetType}>
|
|
|
+ <NRadio value="SINGLE">单曲</NRadio>
|
|
|
+ <NRadio value="CONCERT">合奏</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi>
|
|
|
+ {/* 只有mp3 才有节拍器 */}
|
|
|
+ {forms.audioType === 'MP3' && (
|
|
|
+ <NFormItemGi
|
|
|
+ label="是否含节拍器"
|
|
|
+ path="mp3Type"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择是否含节拍器'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup v-model:value={forms.mp3Type}>
|
|
|
+ <NRadio value="MP3">不含节拍器</NRadio>
|
|
|
+ <NRadio value="MP3_METRONOME">含节拍器</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi>
|
|
|
+ )}
|
|
|
+ </NGrid>
|
|
|
+ <NGrid cols={2}>
|
|
|
+ <NFormItemGi
|
|
|
+ label="上传XML"
|
|
|
+ path="xmlFileUrl"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择上传XML'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <UploadFile
|
|
|
+ size={10}
|
|
|
+ v-model:fileList={forms.xmlFileUrl}
|
|
|
+ tips="仅支持上传.xml格式文件"
|
|
|
+ listType="image"
|
|
|
+ accept=".xml"
|
|
|
+ bucketName="cloud-coach"
|
|
|
+ text="点击上传XML"
|
|
|
+ onReadFileInputEventAsArrayBuffer={readFileInputEventAsArrayBuffer}
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ {forms.audioType === 'MIDI' && (
|
|
|
+ <NFormItemGi
|
|
|
+ label="上传mid"
|
|
|
+ path="midiUrl"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择上传mid'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <UploadFile
|
|
|
+ size={10}
|
|
|
+ v-model:fileList={forms.midiUrl}
|
|
|
+ tips="仅支持上传.mid格式文件"
|
|
|
+ listType="image"
|
|
|
+ accept=".mid"
|
|
|
+ bucketName="cloud-coach"
|
|
|
+ text="点击上传mid"
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ )}
|
|
|
+ {/* 只有播放类型为mp3时才会有伴奏 */}
|
|
|
+ {forms.audioType === 'MP3' && (
|
|
|
+ <NFormItemGi
|
|
|
+ label={`伴奏(${forms.mp3Type === 'MP3' ? '不' : ''}含节拍器)`}
|
|
|
+ path="metronomeUrl"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择上传伴奏'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <UploadFile
|
|
|
+ size={10}
|
|
|
+ v-model:fileList={forms.metronomeUrl}
|
|
|
+ tips="仅支持上传.mp3/.aac格式文件"
|
|
|
+ listType="image"
|
|
|
+ accept=".mp3,.aac"
|
|
|
+ bucketName="cloud-coach"
|
|
|
+ text={`点击上传伴奏(${forms.mp3Type === 'MP3' ? '不' : ''}含节拍器)`}
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ )}
|
|
|
+ <NFormItemGi
|
|
|
+ label="曲目名称"
|
|
|
+ path="musicSheetName"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请输入曲目名称'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NInput v-model:value={forms.musicSheetName} placeholder="请输入曲目名称" />
|
|
|
+ </NFormItemGi>
|
|
|
+ {/* <NFormItemGi
|
|
|
+ label="曲目标签"
|
|
|
+ path="musicTag"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择曲目标签'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NSelect
|
|
|
+ placeholder="请选择曲目标签"
|
|
|
+ v-model:value={forms.musicTag}
|
|
|
+ options={state.tagList}
|
|
|
+ multiple
|
|
|
+ maxTagCount="responsive"
|
|
|
+ onUpdate:value={(value: any) => {
|
|
|
+ // 判断条数
|
|
|
+ if (value.length === 3) {
|
|
|
+ state.tagList.forEach((item: any) => {
|
|
|
+ if (
|
|
|
+ forms.musicTag &&
|
|
|
+ typeof value === 'object' &&
|
|
|
+ value.includes(item.value)
|
|
|
+ ) {
|
|
|
+ item.disabled = false
|
|
|
+ } else {
|
|
|
+ item.disabled = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ state.tagList.forEach((item: any) => {
|
|
|
+ item.disabled = false
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </NFormItemGi> */}
|
|
|
+ <NFormItemGi
|
|
|
+ label="曲目分类"
|
|
|
+ path="musicSheetCategoriesId"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择曲目分类'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NCascader
|
|
|
+ valueField="id"
|
|
|
+ labelField="name"
|
|
|
+ children-field="musicSheetCategoriesList"
|
|
|
+ placeholder="请选择分类"
|
|
|
+ v-model:value={forms.musicSheetCategoriesId}
|
|
|
+ options={state.musicSheetCategories}
|
|
|
+ clearable
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ <NFormItemGi label="音乐人" path="composer">
|
|
|
+ <NInput v-model:value={forms.composer} placeholder="请输入音乐人姓名" />
|
|
|
+ </NFormItemGi>
|
|
|
+ {forms.musicSheetType != 'CONCERT' && <NFormItemGi
|
|
|
+ label="可用声部"
|
|
|
+ path="musicSubject"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择可用声部'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NSelect
|
|
|
+ v-model:value={forms.musicSubject}
|
|
|
+ options={props.subjectList}
|
|
|
+ filterable
|
|
|
+ clearable
|
|
|
+ placeholder="请选择可用声部"
|
|
|
+ />
|
|
|
+ </NFormItemGi>}
|
|
|
+ <NFormItemGi
|
|
|
+ label="指法展示"
|
|
|
+ path="showFingering"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择指法展示'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup v-model:value={forms.showFingering}>
|
|
|
+ <NRadio value={1}>展示</NRadio>
|
|
|
+ <NRadio value={0}>不展示</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi>
|
|
|
+ <NFormItemGi
|
|
|
+ label="是否评测"
|
|
|
+ path="canEvaluate"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择是否评测'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup v-model:value={forms.canEvaluate}>
|
|
|
+ <NRadio value={1}>是</NRadio>
|
|
|
+ <NRadio value={0}>否</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi>
|
|
|
+
|
|
|
+ <NFormItemGi
|
|
|
+ label="能否转简谱"
|
|
|
+ path="notation"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择能否转简谱'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup v-model:value={forms.notation}>
|
|
|
+ <NRadio value={1}>是</NRadio>
|
|
|
+ <NRadio value={0}>否</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi>
|
|
|
+
|
|
|
+ {/* <NFormItemGi
|
|
|
+ label="伴奏类型"
|
|
|
+ path="accompanimentType"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择伴奏类型'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NRadioGroup v-model:value={forms.accompanimentType}>
|
|
|
+ <NRadio value="HOMEMODE">自制伴奏</NRadio>
|
|
|
+ <NRadio value="COMMON">普通伴奏</NRadio>
|
|
|
+ </NRadioGroup>
|
|
|
+ </NFormItemGi> */}
|
|
|
+ <NFormItemGi
|
|
|
+ label="排序值"
|
|
|
+ path="sortNumber"
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请输入排序值'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NInputNumber
|
|
|
+ v-model:value={forms.sortNumber}
|
|
|
+ placeholder="请输入排序值"
|
|
|
+ min={0}
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ </NGrid>
|
|
|
+ <NGrid cols={2}>
|
|
|
+ <NFormItemGi label="曲谱封面" path="titleImg">
|
|
|
+ <UploadFile
|
|
|
+ accept=".jpg,.jpeg,.png"
|
|
|
+ tips="请上传大小2M以内的JPG、PNG图片"
|
|
|
+ v-model:fileList={forms.titleImg}
|
|
|
+ cropper
|
|
|
+ bucketName="cloud-coach"
|
|
|
+ options={{
|
|
|
+ autoCrop: true, //是否默认生成截图框
|
|
|
+ enlarge: 2, // 图片放大倍数
|
|
|
+ autoCropWidth: 200, //默框高度
|
|
|
+ fixedBox: true, //是否固定截图框大认生成截图框宽度
|
|
|
+ autoCropHeight: 200, //默认生成截图小 不允许改变
|
|
|
+ previewsCircle: false, //预览图是否是原圆形
|
|
|
+ title: '曲谱封面'
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ <NFormItemGi label="曲谱描述" path="remark">
|
|
|
+ <NInput
|
|
|
+ placeholder="请输入曲谱描述"
|
|
|
+ type="textarea"
|
|
|
+ rows={4}
|
|
|
+ showCount
|
|
|
+ maxlength={200}
|
|
|
+ v-model:value={forms.remark}
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ </NGrid>
|
|
|
+ {!!gradualData.list.length && (
|
|
|
+ <>
|
|
|
+ <NAlert showIcon={false} type="info">
|
|
|
+ 识别到共1处渐变速度,请输入Dorico对应小节时间信息
|
|
|
+ </NAlert>
|
|
|
+ <NFormItem label="rit." required style={{ marginTop: '10px' }}>
|
|
|
+ <NSpace vertical>
|
|
|
+ {gradualData.list.map((n: any, ni: number) => (
|
|
|
+ <NInputGroup>
|
|
|
+ <NFormItem
|
|
|
+ path={`graduals.${n[0].measureIndex}`}
|
|
|
+ rule={[
|
|
|
+ { required: true, message: '请输入合奏曲目时间' },
|
|
|
+ {
|
|
|
+ pattern: /^((\d{2}):?){2,3}$/,
|
|
|
+ message: '请输入正确的曲目时间',
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NInputGroup>
|
|
|
+ <NInputGroupLabel>{n[0].measureIndex}小节开始</NInputGroupLabel>
|
|
|
+ <NInput
|
|
|
+ placeholder="00:00:00"
|
|
|
+ v-model:value={forms.graduals[n[0].measureIndex]}
|
|
|
+ ></NInput>
|
|
|
+ </NInputGroup>
|
|
|
+ </NFormItem>
|
|
|
+ <div style={{ lineHeight: '30px', padding: '0 4px' }}>~</div>
|
|
|
+ <NFormItem
|
|
|
+ path={`graduals.${n[1].measureIndex}`}
|
|
|
+ rule={[
|
|
|
+ { required: true, message: '请输入合奏曲目时间' },
|
|
|
+ {
|
|
|
+ pattern: /^((\d{2}):?){2,3}$/,
|
|
|
+ message: '请输入正确的曲目时间',
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NInputGroup>
|
|
|
+ <NInput
|
|
|
+ placeholder="00:00:00"
|
|
|
+ v-model:value={forms.graduals[n[1].measureIndex]}
|
|
|
+ ></NInput>
|
|
|
+ <NInputGroupLabel>{n[1].measureIndex}小节结束</NInputGroupLabel>
|
|
|
+ </NInputGroup>
|
|
|
+ </NFormItem>
|
|
|
+ </NInputGroup>
|
|
|
+ ))}
|
|
|
+ </NSpace>
|
|
|
+ </NFormItem>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ <NFormItem label="曲谱速度">
|
|
|
+ <NInputNumber v-model:value={forms.playSpeed} showButton={false} />
|
|
|
+ </NFormItem>
|
|
|
+ {/* 只有播放类型为mp3时才会有原音 */}
|
|
|
+ {forms.audioType === 'MP3' && (
|
|
|
+ <>
|
|
|
+ {forms.background.map((item: any, index: number) => (
|
|
|
+ <>
|
|
|
+ {item.track?.toLocaleUpperCase?.() != 'COMMON' && <NGrid class={styles.audioSection}>
|
|
|
+ <NFormItemGi
|
|
|
+ span={12}
|
|
|
+ label="原音"
|
|
|
+ path={`background[${index}].audioFileUrl`}
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: `请上传${
|
|
|
+ item.track ? item.track + '的' : '第' + (index + 1) + '个'
|
|
|
+ }原音`
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <UploadFile
|
|
|
+ size={10}
|
|
|
+ v-model:fileList={item.audioFileUrl}
|
|
|
+ tips="仅支持上传.mp3/.aac格式文件"
|
|
|
+ listType="image"
|
|
|
+ accept=".mp3,.aac"
|
|
|
+ bucketName="cloud-coach"
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ {state.partListNames.length > 1 && (
|
|
|
+ <NFormItemGi
|
|
|
+ span={12}
|
|
|
+ label="所属轨道"
|
|
|
+ path={`background[${index}].track`}
|
|
|
+ rule={[
|
|
|
+ {
|
|
|
+ required: true,
|
|
|
+ message: '请选择所属轨道'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <NSelect
|
|
|
+ placeholder="请选择所属轨道"
|
|
|
+ v-model:value={item.track}
|
|
|
+ options={initPartsListStatus(item.track)}
|
|
|
+ />
|
|
|
+ </NFormItemGi>
|
|
|
+ )}
|
|
|
+ <NGi class={styles.btnRemove}>
|
|
|
+ <NButton
|
|
|
+ type="primary"
|
|
|
+ text
|
|
|
+ disabled={forms.background.length === 1}
|
|
|
+ onClick={() => removeSys(index)}
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </NButton>
|
|
|
+ </NGi>
|
|
|
+ </NGrid>}
|
|
|
+ </>
|
|
|
+ ))}
|
|
|
+
|
|
|
+ <NButton
|
|
|
+ type="primary"
|
|
|
+ dashed
|
|
|
+ block
|
|
|
+ disabled={state.partListNames.length <= forms.background.length}
|
|
|
+ style={{
|
|
|
+ marginBottom: '24px'
|
|
|
+ }}
|
|
|
+ onClick={createSys}
|
|
|
+ >
|
|
|
+ 添加原音
|
|
|
+ </NButton>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </NForm>
|
|
|
+
|
|
|
+ <NSpace justify="end">
|
|
|
+ <NButton type="default" onClick={() => emit('close')}>
|
|
|
+ 取消
|
|
|
+ </NButton>
|
|
|
+ <NButton
|
|
|
+ type="primary"
|
|
|
+ onClick={() => onSubmit()}
|
|
|
+ loading={btnLoading.value}
|
|
|
+ disabled={btnLoading.value}
|
|
|
+ >
|
|
|
+ 保存
|
|
|
+ </NButton>
|
|
|
+ </NSpace>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+ }
|
|
|
+})
|