|  | @@ -221,6 +221,27 @@
 | 
	
		
			
				|  |  |              bucket_name="cloud-coach"
 | 
	
		
			
				|  |  |          />
 | 
	
		
			
				|  |  |        </el-form-item>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      <div v-if="gradual && gradual.length">
 | 
	
		
			
				|  |  | +        <el-alert :closable="false" style="margin-bottom: 20px;">识别到共{{gradual.length}}处渐变速度,请输入对应小节时间信息</el-alert>
 | 
	
		
			
				|  |  | +        <div v-for="item in gradual">
 | 
	
		
			
				|  |  | +          <el-form-item
 | 
	
		
			
				|  |  | +            :label="item[0].measureIndex + ' 小节'"
 | 
	
		
			
				|  |  | +            :rules="[{required: true, message: '请输入合奏曲目时间'}]"
 | 
	
		
			
				|  |  | +            :prop="`graduals.${item[0].measureIndex}`"
 | 
	
		
			
				|  |  | +          >
 | 
	
		
			
				|  |  | +            <el-input placeholder="00:00:000" v-model="form.graduals[item[0].measureIndex]"></el-input>
 | 
	
		
			
				|  |  | +          </el-form-item>
 | 
	
		
			
				|  |  | +          <el-form-item
 | 
	
		
			
				|  |  | +            :label="item[1].measureIndex + ' 小节'"
 | 
	
		
			
				|  |  | +            :rules="[{required: true, message: '请输入合奏曲目时间'}]"
 | 
	
		
			
				|  |  | +            :prop="`graduals.${item[1].measureIndex}`"
 | 
	
		
			
				|  |  | +          >
 | 
	
		
			
				|  |  | +            <el-input placeholder="00:00:000" v-model="form.graduals[item[1].measureIndex]"></el-input>
 | 
	
		
			
				|  |  | +          </el-form-item>
 | 
	
		
			
				|  |  | +        </div>
 | 
	
		
			
				|  |  | +      </div>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |        <div
 | 
	
		
			
				|  |  |          class="files"
 | 
	
		
			
				|  |  |          v-for="(song, index) in form.sysMusicScoreAccompaniments"
 | 
	
	
		
			
				|  | @@ -398,12 +419,14 @@ export default {
 | 
	
		
			
				|  |  |    props: ["detail", "type"],
 | 
	
		
			
				|  |  |    data() {
 | 
	
		
			
				|  |  |      return {
 | 
	
		
			
				|  |  | +      gradual: null,
 | 
	
		
			
				|  |  |        xmlFirstSpeed: '',
 | 
	
		
			
				|  |  |        partListNames: [],
 | 
	
		
			
				|  |  |        tree: [],
 | 
	
		
			
				|  |  |        extConfigJson: {},
 | 
	
		
			
				|  |  |        memberRankList: [], // 会员列表
 | 
	
		
			
				|  |  |        form: {
 | 
	
		
			
				|  |  | +        graduals: {},
 | 
	
		
			
				|  |  |          rankIdType: 0, // 收费会员类型 默认免费
 | 
	
		
			
				|  |  |          repeatedBeats: 0, // 重复节拍
 | 
	
		
			
				|  |  |          sysMusicScore: {
 | 
	
	
		
			
				|  | @@ -453,6 +476,7 @@ export default {
 | 
	
		
			
				|  |  |          this.extConfigJson = {...initailExtConfigJson}
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |        this.form.repeatedBeats = this.extConfigJson.repeatedBeats;
 | 
	
		
			
				|  |  | +      this.form.gradual = this.extConfigJson.gradualTimes || {};
 | 
	
		
			
				|  |  |        this.$set(this.form, "sysMusicScore", {
 | 
	
		
			
				|  |  |          isOpenMetronome:Number(this.detail.isOpenMetronome),
 | 
	
		
			
				|  |  |          name: this.detail.name,
 | 
	
	
		
			
				|  | @@ -475,6 +499,7 @@ export default {
 | 
	
		
			
				|  |  |          this.form.rankIdType = 0;
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |        const xmlres = await axios(this.detail.xmlUrl)
 | 
	
		
			
				|  |  | +      this.gradual = getGradualLengthByXml(xmlres.data)
 | 
	
		
			
				|  |  |        this.partListNames = this.getPartListNames(xmlres.data)
 | 
	
		
			
				|  |  |        this.FeatchDetailList();
 | 
	
		
			
				|  |  |      } else {
 | 
	
	
		
			
				|  | @@ -496,6 +521,7 @@ export default {
 | 
	
		
			
				|  |  |        const xmlRead = new FileReader()
 | 
	
		
			
				|  |  |        xmlRead.onload = res => {
 | 
	
		
			
				|  |  |          this.partListNames = this.getPartListNames(res.target.result)
 | 
	
		
			
				|  |  | +        this.partListNames = this.getPartListNames(res.target.result)
 | 
	
		
			
				|  |  |          for (let j = 0; j < this.form.sysMusicScoreAccompaniments.length; j++) {
 | 
	
		
			
				|  |  |            this.form.sysMusicScoreAccompaniments[j].track =  this.partListNames[j]
 | 
	
		
			
				|  |  |            if (!this.form.sysMusicScoreAccompaniments[j].speed) {
 | 
	
	
		
			
				|  | @@ -634,6 +660,7 @@ export default {
 | 
	
		
			
				|  |  |                  ...this.form.sysMusicScore,
 | 
	
		
			
				|  |  |                  extConfigJson: JSON.stringify({
 | 
	
		
			
				|  |  |                    repeatedBeats: this.form.repeatedBeats,
 | 
	
		
			
				|  |  | +                  gradualTimes: this.form.graduals,
 | 
	
		
			
				|  |  |                  }),
 | 
	
		
			
				|  |  |                  type: "COMMON",
 | 
	
		
			
				|  |  |                  showFlag: 0,
 | 
	
	
		
			
				|  | @@ -650,6 +677,7 @@ export default {
 | 
	
		
			
				|  |  |                  ...this.form.sysMusicScore,
 | 
	
		
			
				|  |  |                  extConfigJson: JSON.stringify({
 | 
	
		
			
				|  |  |                    repeatedBeats: this.form.repeatedBeats,
 | 
	
		
			
				|  |  | +                  gradualTimes: this.form.graduals,
 | 
	
		
			
				|  |  |                  }),
 | 
	
		
			
				|  |  |                  type: "COMMON",
 | 
	
		
			
				|  |  |                  id: this.detail.id,
 | 
	
	
		
			
				|  | @@ -668,6 +696,211 @@ export default {
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  |    },
 | 
	
		
			
				|  |  |  };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 获取指定元素下一个Note元素
 | 
	
		
			
				|  |  | + * @param ele 指定元素
 | 
	
		
			
				|  |  | + * @param selectors 选择器
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | + const getNextNote = (ele, selectors) => {
 | 
	
		
			
				|  |  | +  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, partIndex) => {
 | 
	
		
			
				|  |  | +  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 => item.getElementsByTagName('part-name')?.[0].textContent || '')
 | 
	
		
			
				|  |  | +  const parts = xmlParse.getElementsByTagName('part')
 | 
	
		
			
				|  |  | +  // const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true)
 | 
	
		
			
				|  |  | +  // const firstMeasures = [...Array.from(parts[0]?.getElementsByTagName('measure') || [])]
 | 
	
		
			
				|  |  | +  // const metronomes = [...Array.from(parts[0]?.getElementsByTagName('metronome') || [])]
 | 
	
		
			
				|  |  | +  // const words = [...Array.from(parts[0]?.getElementsByTagName('words') || [])]
 | 
	
		
			
				|  |  | +  // const codas = [...Array.from(parts[0]?.getElementsByTagName('coda') || [])]
 | 
	
		
			
				|  |  | +  // const rehearsals = [...Array.from(parts[0]?.getElementsByTagName('rehearsal') || [])]
 | 
	
		
			
				|  |  | +  const visiblePartInfo = partList[partIndex]
 | 
	
		
			
				|  |  | +  // console.log(visiblePartInfo, partIndex)
 | 
	
		
			
				|  |  | +  // state.partListNames = partListNames
 | 
	
		
			
				|  |  | +  if (visiblePartInfo) {
 | 
	
		
			
				|  |  | +    const id = visiblePartInfo.getAttribute('id')
 | 
	
		
			
				|  |  | +    Array.from(parts).forEach(part => {
 | 
	
		
			
				|  |  | +      if (part && part.getAttribute('id') !== id) {
 | 
	
		
			
				|  |  | +        part.parentNode?.removeChild(part)
 | 
	
		
			
				|  |  | +        // 不等于第一行才添加避免重复添加
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        // words.forEach(word => {
 | 
	
		
			
				|  |  | +        //   const text = word.textContent || ''
 | 
	
		
			
				|  |  | +        //   if(isSpeedKeyword(text) && text) {
 | 
	
		
			
				|  |  | +        //     const wordContainer = word.parentElement?.parentElement?.parentElement
 | 
	
		
			
				|  |  | +        //     if (wordContainer && wordContainer.firstElementChild && wordContainer.firstElementChild !== word) {
 | 
	
		
			
				|  |  | +        //       const wordParent = word.parentElement?.parentElement
 | 
	
		
			
				|  |  | +        //       const fisrt = wordContainer.firstElementChild
 | 
	
		
			
				|  |  | +        //       wordContainer.insertBefore(wordParent, fisrt)
 | 
	
		
			
				|  |  | +        //     }
 | 
	
		
			
				|  |  | +        //   }
 | 
	
		
			
				|  |  | +        // })
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // 最后一个小节的结束线元素不在最后 调整
 | 
	
		
			
				|  |  | +      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 = lastParent?.children || []
 | 
	
		
			
				|  |  | +          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) => {
 | 
	
		
			
				|  |  | +      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) {
 | 
	
		
			
				|  |  | +        // if (i !== 0) {
 | 
	
		
			
				|  |  | +          note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
 | 
	
		
			
				|  |  | +        // }
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    })
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  // console.log(new XMLSerializer().serializeToString(xmlParse))
 | 
	
		
			
				|  |  | +  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,
 | 
	
		
			
				|  |  | +  lentando: 1.333333333,
 | 
	
		
			
				|  |  | +  allargando: 1.333333333,
 | 
	
		
			
				|  |  | +  morendo: 1.333333333,
 | 
	
		
			
				|  |  | +  'accel.': 0.8,
 | 
	
		
			
				|  |  | +  calando: 2,
 | 
	
		
			
				|  |  | +  'poco accel.': 0.8,
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * 按照xml进行减慢速度的计算
 | 
	
		
			
				|  |  | + * @param xml 始终按照第一分谱进行减慢速度的计算
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | + export function getGradualLengthByXml (xml) {
 | 
	
		
			
				|  |  | +  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) => item.nodeName === 'note')
 | 
	
		
			
				|  |  | +        .findIndex((item) => item === note)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      let allDuration = 0
 | 
	
		
			
				|  |  | +      let leftDuration = 0
 | 
	
		
			
				|  |  | +      for (let i = 0; i < measureNotes.length; i++) {
 | 
	
		
			
				|  |  | +        const n = 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),
 | 
	
		
			
				|  |  | +        type: ele.tagName,
 | 
	
		
			
				|  |  | +        allDuration,
 | 
	
		
			
				|  |  | +        leftDuration,
 | 
	
		
			
				|  |  | +      })
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  const gradualNotes = []
 | 
	
		
			
				|  |  | +  eles.sort((a, b) => a.index - b.index)
 | 
	
		
			
				|  |  | +  const keys = Object.keys(speedInfo).map(w => w.toLocaleLowerCase())
 | 
	
		
			
				|  |  | +  for (const ele of eles) {
 | 
	
		
			
				|  |  | +    const textContent = ele.textContent?.toLocaleLowerCase().trim()
 | 
	
		
			
				|  |  | +    if (ele.type === 'words' && keys.includes(textContent)) {
 | 
	
		
			
				|  |  | +      gradualNotes.push([
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +          start: ele.index,
 | 
	
		
			
				|  |  | +          measureIndex: ele.measureIndex,
 | 
	
		
			
				|  |  | +          noteInMeasureIndex: ele.noteInMeasureIndex,
 | 
	
		
			
				|  |  | +          allDuration: ele.allDuration,
 | 
	
		
			
				|  |  | +          leftDuration: ele.leftDuration,
 | 
	
		
			
				|  |  | +          type: textContent,
 | 
	
		
			
				|  |  | +        },
 | 
	
		
			
				|  |  | +      ])
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +    if (
 | 
	
		
			
				|  |  | +      ele.type === 'metronome' ||
 | 
	
		
			
				|  |  | +      (ele.type === 'words' && textContent === 'a tempo')
 | 
	
		
			
				|  |  | +    ) {
 | 
	
		
			
				|  |  | +      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,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  return gradualNotes
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  |  </script>
 | 
	
		
			
				|  |  |  <style lang="less" scoped>
 | 
	
		
			
				|  |  |  .btns {
 |