import { NForm, NFormItem, NInput, NSelect, NSpace, NButton, useMessage, NRadioGroup, NRadio, NGrid, NFormItemGi, NInputNumber, NGi, useDialog, NCascader, NAlert, NInputGroup, NInputGroupLabel, NCheckbox } 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' import {Checkbox, CheckboxGroup} from "vant"; import {releaseCandidate} from "@/utils/constant"; import {getSelectDataFromObj} from "@/utils/objectUtil"; /** * 获取指定元素下一个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: 'music-operation', props: { type: { type: String, default: 'add' }, data: { type: Object as PropType, default: () => { } }, tagList: { type: Array as PropType>, default: () => [] }, subjectList: { type: Array as PropType>, default: () => [] }, musicSheetCategories: { type: Array as PropType>, 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: 1, // 伴奏类型 sortNumber: null, // 排序 titleImg: null, // 曲谱封面 remark: null, // 曲谱描述 background: [] as any, // 原音 musicSheetCategoriesId: null, status: false, musicSheetType: 'SINGLE' as 'SINGLE' | 'CONCERT', author: null, //音乐人 authorFrom: undefined, belongTo: undefined, speed: undefined, playMetronome: 1, playStyle: undefined, // 播放方式 releaseCandidate: 'NO' // 是否审核版本 }) 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' || props.type === 'preview') { 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 () => (
曲目信息 独奏 合奏 曲目上传 { if (value === 'MP3') { forms.mp3Type = 'MP3' } else { forms.mp3Type = 'MIDI' } }} > MP3 MIDI {forms.mp3Type === 'MP3' && ( 自制伴奏 普通伴奏 )} {forms.mp3Type === 'MP3' && ( )} {forms.mp3Type === 'MIDI' && ( )} { (forms.mp3Type === 'MP3' || forms.musicSheetType === 'SINGLE') && ( 长笛 竖笛 葫芦丝 萨克斯 )} {forms.mp3Type === 'MIDI' && forms.musicSheetType === 'CONCERT' && ( 长笛 竖笛 葫芦丝 萨克斯 )} {forms.playMetronome && ( 系统节拍器 MP3节拍器 )} {/* 只有播放类型为mp3时才会有原音 */} {forms.audioType === 'MP3' && ( <> {forms.background.map((item: any, index: number) => ( <> {item.track?.toLocaleUpperCase?.() != 'COMMON' && {state.partListNames.length > 1 && ( )} removeSys(index)} > 删除 } ))} 添加原音 )} {props.type !== 'preview' && ( emit('close')}> 取消 onSubmit()} loading={btnLoading.value} disabled={btnLoading.value} > 确认 )}
) } })