form.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. <template>
  2. <div>
  3. <el-form ref="form" :model="form" label-width="150px">
  4. <el-form-item
  5. prop="sysMusicScore.name"
  6. label="曲名"
  7. :rules="[{ required: true, message: '请输入曲名' }]"
  8. >
  9. <el-input placeholder="请输入曲名" v-model="form.sysMusicScore.name" />
  10. </el-form-item>
  11. <el-form-item
  12. prop="sysMusicScore.musicScoreCategoriesId"
  13. label="分类"
  14. :rules="[{ required: true, message: '请选择分类' }]"
  15. >
  16. <el-cascader
  17. v-model="form.sysMusicScore.musicScoreCategoriesId"
  18. style="width: 100%"
  19. :options="tree"
  20. placeholder="请选择分类"
  21. :props="treeProps"
  22. ></el-cascader>
  23. <!-- <el-select style="width: 100%!important;" v-model="form.sysMusicScore.musicScoreCategoriesId" placeholder="请选择声部">
  24. <el-option
  25. v-for="item in selects.subjects"
  26. :value="item.id"
  27. :label="item.name"
  28. :key="item.id"
  29. ></el-option>
  30. </el-select> -->
  31. </el-form-item>
  32. <!-- 是否收费 免费 收费 -->
  33. <el-form-item
  34. prop="rankIdType"
  35. label="是否收费"
  36. :rules="[{ required: true, message: '请选择是否收费' }]"
  37. >
  38. <el-select
  39. style="width: 100% !important"
  40. v-model="form.rankIdType"
  41. placeholder="请选择是否收费"
  42. @change="rankChange"
  43. >
  44. <!-- <el-option
  45. :value="item.id"
  46. :label="item.name"
  47. v-for="item in memberRankList"
  48. :key="item.id"
  49. ></el-option> -->
  50. <el-option :value="0" label="免费"></el-option>
  51. <el-option :value="1" label="收费"></el-option>
  52. </el-select>
  53. </el-form-item>
  54. <el-form-item
  55. prop="sysMusicScore.isOpenMetronome"
  56. label="节拍器"
  57. :rules="[{required: true, message: '请选择节拍器'}]"
  58. >
  59. <template slot="label">
  60. <span>
  61. 节拍器
  62. <el-tooltip placement="top" popper-class="mTooltip">
  63. <div slot="content">
  64. 是否播放系统自带节拍器
  65. </div>
  66. <i
  67. class="el-icon-question"
  68. style="font-size: 18px; color: #f56c6c"
  69. ></i>
  70. </el-tooltip>
  71. </span>
  72. </template>
  73. <el-select
  74. style="width: 100% !important"
  75. v-model="form.sysMusicScore.isOpenMetronome"
  76. placeholder="请选择节拍器"
  77. >
  78. <!-- <el-option
  79. :value="item.id"
  80. :label="item.name"
  81. v-for="item in memberRankList"
  82. :key="item.id"
  83. ></el-option> -->
  84. <el-option
  85. :value="0"
  86. label="不播放"
  87. ></el-option>
  88. <el-option
  89. :value="1"
  90. label="播放"
  91. ></el-option>
  92. </el-select>
  93. </el-form-item>
  94. <el-form-item
  95. label="重复节拍时长"
  96. >
  97. <template slot="label">
  98. <span>
  99. 重复节拍时长
  100. <el-tooltip placement="top" popper-class="mTooltip">
  101. <div slot="content">
  102. 2/4拍类似的节拍器是否重复时长
  103. </div>
  104. <i
  105. class="el-icon-question"
  106. style="font-size: 18px; color: #f56c6c"
  107. ></i>
  108. </el-tooltip>
  109. </span>
  110. </template>
  111. <el-select
  112. style="width: 100% !important"
  113. v-model="form.repeatedBeats"
  114. placeholder="请选择是否重复节拍器时长"
  115. >
  116. <el-option
  117. :value="0"
  118. label="不重复"
  119. ></el-option>
  120. <el-option
  121. :value="1"
  122. label="重复"
  123. ></el-option>
  124. </el-select>
  125. </el-form-item>
  126. <!-- <el-form-item
  127. prop="sysMusicScore.clientType"
  128. label="客户端类型"
  129. :rules="[{ required: true, message: '请选择客户端类型' }]"
  130. >
  131. <el-select
  132. style="width: 100% !important"
  133. v-model="form.sysMusicScore.clientType"
  134. placeholder="请选择客户端类型"
  135. >
  136. <el-option value="NETWORK_ROOM" label="网络教室"></el-option>
  137. <el-option value="SMART_PRACTICE" label="云教练"></el-option>
  138. </el-select>
  139. </el-form-item> -->
  140. <!-- <el-form-item
  141. prop="sysMusicScore.renderFrom"
  142. label="渲染模式"
  143. :rules="[{ required: true, message: '请选择渲染模式' }]"
  144. >
  145. <el-select
  146. style="width: 100% !important"
  147. v-model="form.sysMusicScore.renderFrom"
  148. placeholder="请选择渲染模式"
  149. >
  150. <el-option value="H5" label="H5"></el-option>
  151. <el-option value="APP" label="原生"></el-option>
  152. </el-select>
  153. </el-form-item> -->
  154. <el-form-item
  155. prop="sysMusicScore.playMode"
  156. label="播放模式"
  157. :rules="[{ required: true, message: '请选择播放模式' }]"
  158. >
  159. <el-select
  160. style="width: 100% !important"
  161. v-model="form.sysMusicScore.playMode"
  162. placeholder="请选择播放模式"
  163. >
  164. <el-option value="MP3" label="MP3播放"></el-option>
  165. <el-option value="XML" label="XML播放"></el-option>
  166. </el-select>
  167. </el-form-item>
  168. <el-form-item
  169. prop="sysMusicScore.enableEvaluation"
  170. label="支持评测"
  171. :rules="[{ required: true, message: '请选择支持评测' }]"
  172. >
  173. <el-radio-group v-model="form.sysMusicScore.enableEvaluation">
  174. <el-radio :label="1">是</el-radio>
  175. <el-radio :label="0">否</el-radio>
  176. </el-radio-group>
  177. </el-form-item>
  178. <el-form-item
  179. prop="sysMusicScore.order"
  180. label="排序"
  181. :rules="[{ required: true, message: '请输入排序' }, {
  182. pattern: /^([1-9]\d*|[0]{1,1})$/,
  183. message: '请输入正确的排序',
  184. trigger: 'blur',
  185. },]"
  186. >
  187. <el-input placeholder="请输入排序" v-model="form.sysMusicScore.order" />
  188. </el-form-item>
  189. <el-form-item
  190. v-if="form.sysMusicScore.isOpenMetronome ===1"
  191. label="伴奏(不含节拍器)"
  192. prop="sysMusicScore.url"
  193. >
  194. <singe-file-upload
  195. tips="仅支持上传 mp3/aac 格式音频文件"
  196. accept=".mp3, .aac"
  197. v-model="form.sysMusicScore.url"
  198. bucket_name="cloud-coach"
  199. />
  200. </el-form-item>
  201. <el-form-item
  202. v-else
  203. label="伴奏(含节拍器)"
  204. prop="sysMusicScore.metronomeUrl"
  205. >
  206. <singe-file-upload
  207. tips="仅支持上传 mp3/aac 格式音频文件"
  208. accept=".mp3, .aac"
  209. v-model="form.sysMusicScore.metronomeUrl"
  210. bucket_name="cloud-coach"
  211. />
  212. </el-form-item>
  213. <el-form-item
  214. label="MIDI"
  215. prop="sysMusicScore.midiUrl"
  216. >
  217. <singe-file-upload
  218. tips="仅支持上传 mid 格式音频文件"
  219. accept=".mid"
  220. v-model="form.sysMusicScore.midiUrl"
  221. bucket_name="cloud-coach"
  222. />
  223. </el-form-item>
  224. <div v-if="gradual && gradual.length">
  225. <el-alert :closable="false" style="margin-bottom: 20px;">识别到共{{gradual.length}}处渐变速度,请输入Dorico对应小节时间信息</el-alert>
  226. <div v-for="item in gradual">
  227. <el-form-item
  228. :label="item[0].measureIndex + 2 + ' 小节'"
  229. :rules="[{required: true, message: '请输入合奏曲目时间'}, {
  230. pattern: /^((\d{2}):?){2,3}$/,
  231. message: '请输入正确的曲目时间',
  232. trigger: 'blur',
  233. }]"
  234. :prop="`graduals.${item[0].measureIndex}`"
  235. >
  236. <el-input placeholder="00:00:00" v-model="form.graduals[item[0].measureIndex]"></el-input>
  237. </el-form-item>
  238. <el-form-item
  239. :label="item[1].measureIndex + 2 + ' 小节'"
  240. :rules="[{required: true, message: '请输入合奏曲目时间'}, {
  241. pattern: /^((\d{2}):?){2,3}$/,
  242. message: '请输入正确的曲目时间',
  243. trigger: 'blur',
  244. }]"
  245. :prop="`graduals.${item[1].measureIndex}`"
  246. >
  247. <el-input placeholder="00:00:00" v-model="form.graduals[item[1].measureIndex]"></el-input>
  248. </el-form-item>
  249. </div>
  250. </div>
  251. <div
  252. class="files"
  253. v-for="(song, index) in form.sysMusicScoreAccompaniments"
  254. :key="index"
  255. >
  256. <el-row>
  257. <el-col :span="12">
  258. <el-form-item
  259. :prop="`sysMusicScoreAccompaniments.${index}.subjectId`"
  260. label="声部"
  261. >
  262. <!-- :rules="[{required: true, message: '请选择声部'}]" -->
  263. <el-select
  264. style="width: 100% !important"
  265. v-model="song.subjectId"
  266. clearable
  267. placeholder="请选择声部"
  268. >
  269. <el-option
  270. v-for="item in selects.subjects"
  271. :value="item.id"
  272. :label="item.name"
  273. :key="item.id"
  274. :disabled="hasSubjectId(item.id)"
  275. ></el-option>
  276. </el-select>
  277. </el-form-item>
  278. </el-col>
  279. <el-col :span="12">
  280. <el-form-item
  281. :prop="`sysMusicScoreAccompaniments.${index}.speed`"
  282. label="速度"
  283. :rules="[{ required: true, message: '请输入速度' }]"
  284. >
  285. <el-input
  286. type="number"
  287. placeholder="请输入速度"
  288. v-model="song.speed"
  289. />
  290. </el-form-item>
  291. </el-col>
  292. <el-col :span="12">
  293. <el-form-item
  294. :prop="`sysMusicScoreAccompaniments.${index}.isShowFingering`"
  295. label="指法展示"
  296. :rules="[{ required: true, message: '请选择是否展示指法' }]"
  297. >
  298. <el-select
  299. style="width: 100% !important"
  300. v-model="song.isShowFingering"
  301. placeholder="请选择是否展示指法"
  302. >
  303. <el-option :value="true" label="是"></el-option>
  304. <el-option :value="false" label="否"></el-option>
  305. </el-select>
  306. </el-form-item>
  307. </el-col>
  308. <el-col :span="12">
  309. <el-form-item
  310. :prop="`sysMusicScoreAccompaniments.${index}.memo`"
  311. label="描述"
  312. >
  313. <el-input placeholder="请输入描述" v-model="song.memo" />
  314. </el-form-item>
  315. </el-col>
  316. <el-col :span="12" v-if="form.sysMusicScore.isOpenMetronome ===1">
  317. <el-form-item
  318. label="原音(不含节拍器)"
  319. :prop="`sysMusicScoreAccompaniments.${index}.mp3Url`"
  320. :rules="[
  321. {
  322. required: form.sysMusicScore.isOpenMetronome ? form.sysMusicScore.playMode === 'MP3' : false,
  323. message: '请上传原音(不含节拍器)',
  324. },
  325. ]"
  326. >
  327. <singe-file-upload
  328. tips="仅支持上传 mp3/aac 格式音频文件"
  329. accept=".mp3, .aac"
  330. v-model="song.mp3Url"
  331. bucket_name="cloud-coach"
  332. />
  333. </el-form-item>
  334. </el-col>
  335. <el-col :span="12" v-else>
  336. <el-form-item
  337. label="原音(含节拍器)"
  338. :prop="`sysMusicScoreAccompaniments.${index}.metronomeMp3Url`"
  339. :rules="[
  340. {
  341. required: form.sysMusicScore.isOpenMetronome ? false : form.sysMusicScore.playMode === 'MP3',
  342. message: '原音(含节拍器)',
  343. },
  344. ]"
  345. >
  346. <singe-file-upload
  347. tips="仅支持上传 mp3/aac 格式音频文件"
  348. accept=".mp3, .aac"
  349. v-model="song.metronomeMp3Url"
  350. bucket_name="cloud-coach"
  351. />
  352. </el-form-item>
  353. </el-col>
  354. </el-row>
  355. <el-row>
  356. <el-col :span="12">
  357. <el-form-item
  358. label="MusicXML"
  359. :prop="`sysMusicScoreAccompaniments.${index}.xmlUrl`"
  360. :rules="[{ required: true, message: '请选择MusicXML文件' }]"
  361. >
  362. <singe-file-upload
  363. tips="仅支持上传 xml 格式文件"
  364. accept=".xml"
  365. v-model="song.xmlUrl"
  366. @inputFile="inputFile"
  367. bucket_name="cloud-coach"
  368. />
  369. </el-form-item>
  370. </el-col>
  371. <el-col :span="12" v-if="partListNames.length > 1">
  372. <el-form-item
  373. label="所属轨道"
  374. :prop="`sysMusicScoreAccompaniments.${index}.track`"
  375. :rules="[{ required: true, message: '请选择所属轨道' }]"
  376. >
  377. <el-select
  378. style="width: 100% !important"
  379. v-model="song.track"
  380. clearable
  381. placeholder="请选择轨道"
  382. >
  383. <el-option
  384. v-for="item in partListNames"
  385. :value="item"
  386. :label="item"
  387. :key="item"
  388. :disabled="hasPartName(item)"
  389. ></el-option>
  390. </el-select>
  391. </el-form-item>
  392. </el-col>
  393. </el-row>
  394. <el-button
  395. class="file-remove"
  396. type="text"
  397. @click="removeSys(index)"
  398. :disabled="form.sysMusicScoreAccompaniments.length == 1"
  399. >删除</el-button
  400. >
  401. </div>
  402. <el-button
  403. @click="createSys"
  404. type="info"
  405. style="width: 100%; margin-bottom: 20px"
  406. plain
  407. >添加原音</el-button
  408. >
  409. <div class="btns">
  410. <el-button type="primary" @click="submit">提交</el-button>
  411. <el-button @click="$listeners.close">取消</el-button>
  412. </div>
  413. </el-form>
  414. </div>
  415. </template>
  416. <script>
  417. import axios from 'axios'
  418. import { Add, Update, queryPageSysExam, queryTree } from "../api";
  419. import { getAllmemberRank } from "@/views/resetTeaming/api";
  420. const initailExtConfigJson = {
  421. repeatedBeats: 0
  422. }
  423. export default {
  424. props: ["detail", "type"],
  425. data() {
  426. return {
  427. gradual: null,
  428. xmlFirstSpeed: '',
  429. partListNames: [],
  430. tree: [],
  431. extConfigJson: {},
  432. memberRankList: [], // 会员列表
  433. form: {
  434. graduals: {},
  435. rankIdType: 0, // 收费会员类型 默认免费
  436. repeatedBeats: 0, // 重复节拍
  437. sysMusicScore: {
  438. isOpenMetronome: 0, // 是否开启节拍器 默认关闭
  439. name: "",
  440. rankIds: "", // 收费会员编号
  441. url: "",
  442. metronomeUrl: "",
  443. midiUrl: "",
  444. order: "",
  445. musicScoreCategoriesId: [],
  446. // 兼容之前数据,默认选择云教练
  447. clientType: "SMART_PRACTICE",
  448. renderFrom: "",
  449. playMode: "MP3",
  450. enableEvaluation: 1,
  451. extConfigJson: "{}"
  452. },
  453. sysMusicScoreAccompaniments: [
  454. {
  455. subjectId: "",
  456. speed: "",
  457. mp3Url: "",
  458. xmlUrl: "",
  459. isShowFingering: true,
  460. mome: "",
  461. track: ""
  462. },
  463. ],
  464. delExamSongAccompanimentIds: [],
  465. },
  466. treeProps: {
  467. value: "id",
  468. label: "name",
  469. children: "sysMusicScoreCategoriesList",
  470. },
  471. };
  472. },
  473. async mounted() {
  474. this.$store.dispatch("setSubjects");
  475. await this.FetchTree();
  476. await this.memberRank();
  477. if (this.detail) {
  478. try {
  479. this.extConfigJson = JSON.parse(this.detail.extConfigJson);
  480. } catch (error) {
  481. this.extConfigJson = {...initailExtConfigJson}
  482. }
  483. this.form.repeatedBeats = this.extConfigJson.repeatedBeats;
  484. this.form.graduals = this.extConfigJson.gradualTimes || {};
  485. this.$set(this.form, "sysMusicScore", {
  486. isOpenMetronome:Number(this.detail.isOpenMetronome),
  487. name: this.detail.name,
  488. url: this.detail.url,
  489. midiUrl: this.detail.midiUrl,
  490. rankIds: this.detail.rankIds,
  491. order: this.detail.order,
  492. clientType: this.detail.clientType,
  493. enableEvaluation: +this.detail.enableEvaluation,
  494. metronomeUrl: this.detail.metronomeUrl,
  495. renderFrom: this.detail.renderFrom,
  496. playMode: this.detail.playMode,
  497. musicScoreCategoriesId: this.detail.categoriesId
  498. ? this.formatParentId(this.detail.categoriesId, this.tree)
  499. : [],
  500. });
  501. if (this.detail.rankIds) {
  502. this.form.rankIdType = 1;
  503. } else {
  504. this.form.rankIdType = 0;
  505. }
  506. const xmlres = await axios(this.detail.xmlUrl)
  507. this.gradual = getGradualLengthByXml(xmlres.data).filter(item => item.length === 2)
  508. this.partListNames = this.getPartListNames(xmlres.data)
  509. this.FeatchDetailList();
  510. } else {
  511. // 新增条件下默认设置为收费
  512. this.rankChange(1);
  513. this.form.rankIdType = 1;
  514. }
  515. },
  516. methods: {
  517. getPartListNames(xml) {
  518. if (!xml) return []
  519. const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
  520. const partList = xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
  521. const partListNames = Array.from(partList).map(item => item.getElementsByTagName('part-name')?.[0].textContent || '')
  522. this.xmlFirstSpeed = xmlParse.getElementsByTagName('per-minute')?.[0]?.textContent || ''
  523. return partListNames.filter(text => text.toLocaleUpperCase() !== 'COMMON')
  524. },
  525. inputFile(file) {
  526. const xmlRead = new FileReader()
  527. xmlRead.onload = res => {
  528. this.partListNames = this.getPartListNames(res.target.result)
  529. this.gradual = getGradualLengthByXml(res.target.result).filter(item => item.length === 2)
  530. for (let j = 0; j < this.form.sysMusicScoreAccompaniments.length; j++) {
  531. this.form.sysMusicScoreAccompaniments[j].track = this.partListNames[j]
  532. if (!this.form.sysMusicScoreAccompaniments[j].speed) {
  533. this.form.sysMusicScoreAccompaniments[j].speed = this.xmlFirstSpeed
  534. }
  535. this.$set(this.form, 'sysMusicScoreAccompaniments', this.form.sysMusicScoreAccompaniments)
  536. }
  537. for (let index = this.form.sysMusicScoreAccompaniments.length; index < this.partListNames.length; index++) {
  538. const part = this.partListNames[index]
  539. const sysData = {
  540. ...this.form.sysMusicScoreAccompaniments[0],
  541. metronomeMp3Url: "",
  542. mp3Url: "",
  543. track: part,
  544. }
  545. if (!sysData.speed) {
  546. sysData.speed = this.xmlFirstSpeed
  547. }
  548. this.createSys(sysData)
  549. }
  550. }
  551. xmlRead.readAsText(file.raw)
  552. },
  553. rankChange(value) {
  554. if (value) {
  555. let tempIds = [];
  556. this.memberRankList.forEach((item) => {
  557. tempIds.push(item.id);
  558. });
  559. this.form.sysMusicScore.rankIds = tempIds.join(",");
  560. } else {
  561. // 会员购买重置
  562. this.form.sysMusicScore.rankIds = "";
  563. }
  564. },
  565. async memberRank() {
  566. try {
  567. const res = await getAllmemberRank({ isDefault: 0 });
  568. this.memberRankList = res.data || [];
  569. } catch (e) {
  570. console.log(e);
  571. }
  572. },
  573. formatParentId(id, list, ids = []) {
  574. for (const item of list) {
  575. if (item.sysMusicScoreCategoriesList) {
  576. const cIds = this.formatParentId(
  577. id,
  578. item.sysMusicScoreCategoriesList,
  579. [...ids, item.id]
  580. );
  581. if (cIds.includes(id)) {
  582. return cIds;
  583. }
  584. }
  585. if (item.id === id) {
  586. return [...ids, id];
  587. }
  588. }
  589. return ids;
  590. },
  591. async FetchTree() {
  592. try {
  593. const res = await queryTree();
  594. this.tree = res.data;
  595. this.formatTree(this.tree)
  596. } catch (error) {}
  597. },
  598. formatTree(data){
  599. for(let i of data) {
  600. if(i.sysMusicScoreCategoriesList && i.sysMusicScoreCategoriesList.length > 0) {
  601. this.formatTree(i.sysMusicScoreCategoriesList, i)
  602. } else {
  603. i.sysMusicScoreCategoriesList = null
  604. }
  605. }
  606. },
  607. async FeatchDetailList() {
  608. try {
  609. const res = await queryPageSysExam({
  610. sysMusicScoreId: this.detail.id,
  611. });
  612. const result = res.data || [];
  613. result.forEach((item) => {
  614. if (!item.subjectId) {
  615. item.subjectId = null;
  616. }
  617. });
  618. this.$set(this.form, "sysMusicScoreAccompaniments", result);
  619. } catch (error) {}
  620. },
  621. createSys(initData) {
  622. this.form.sysMusicScoreAccompaniments.push(Object.assign({
  623. subjectId: "",
  624. speed: "",
  625. mp3Url: "",
  626. xmlUrl: "",
  627. track: "",
  628. }, (initData || {})));
  629. },
  630. async removeSys(index) {
  631. try {
  632. await this.$confirm("是否确认删除此原音?", "提示", {
  633. type: "warning",
  634. });
  635. if (this.form.sysMusicScoreAccompaniments[index]) {
  636. this.form.delExamSongAccompanimentIds.push(
  637. this.form.sysMusicScoreAccompaniments[index].id
  638. );
  639. }
  640. this.form.sysMusicScoreAccompaniments.splice(index, 1);
  641. } catch (error) {}
  642. },
  643. hasPartName(name) {
  644. const names = [];
  645. for (const item of this.form.sysMusicScoreAccompaniments) {
  646. names.push(item.track);
  647. }
  648. return names.includes(name);
  649. },
  650. hasSubjectId(id) {
  651. const ids = [];
  652. for (const item of this.form.sysMusicScoreAccompaniments) {
  653. ids.push(item.subjectId);
  654. }
  655. return ids.includes(id);
  656. },
  657. async submit() {
  658. this.$refs.form.validate(async (valid) => {
  659. if (valid) {
  660. if (!this.detail) {
  661. await Add({
  662. ...this.form,
  663. sysMusicScore: {
  664. ...this.form.sysMusicScore,
  665. extConfigJson: JSON.stringify({
  666. repeatedBeats: this.form.repeatedBeats,
  667. gradualTimes: this.form.graduals,
  668. }),
  669. type: "COMMON",
  670. showFlag: 0,
  671. musicScoreCategoriesId: (
  672. this.form.sysMusicScore.musicScoreCategoriesId || []
  673. ).pop(),
  674. },
  675. });
  676. this.$message.success("提交成功");
  677. } else {
  678. await Update({
  679. ...this.form,
  680. sysMusicScore: {
  681. ...this.form.sysMusicScore,
  682. extConfigJson: JSON.stringify({
  683. repeatedBeats: this.form.repeatedBeats,
  684. gradualTimes: this.form.graduals,
  685. }),
  686. type: "COMMON",
  687. id: this.detail.id,
  688. showFlag: this.detail.showFlag,
  689. musicScoreCategoriesId: (
  690. this.form.sysMusicScore.musicScoreCategoriesId || []
  691. ).pop(),
  692. },
  693. });
  694. this.$message.success("修改成功");
  695. }
  696. this.$listeners.close();
  697. this.$listeners.submited();
  698. }
  699. });
  700. },
  701. },
  702. };
  703. /**
  704. * 获取指定元素下一个Note元素
  705. * @param ele 指定元素
  706. * @param selectors 选择器
  707. */
  708. const getNextNote = (ele, selectors) => {
  709. let index = 0
  710. const parentEle = ele.closest(selectors)
  711. let pointer = parentEle
  712. const measure = parentEle?.closest('measure')
  713. let siblingNote = null
  714. // 查找到相邻的第一个note元素
  715. while (!siblingNote && index < (measure?.childNodes.length || 50)) {
  716. index++
  717. if (pointer?.nextElementSibling?.tagName === 'note') {
  718. siblingNote = pointer?.nextElementSibling
  719. }
  720. pointer = pointer?.nextElementSibling
  721. }
  722. return siblingNote
  723. }
  724. export const onlyVisible = (xml, partIndex) => {
  725. if (!xml) return ''
  726. const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
  727. const partList = xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || []
  728. // const partListNames = Array.from(partList).map(item => item.getElementsByTagName('part-name')?.[0].textContent || '')
  729. const parts = xmlParse.getElementsByTagName('part')
  730. // const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true)
  731. // const firstMeasures = [...Array.from(parts[0]?.getElementsByTagName('measure') || [])]
  732. // const metronomes = [...Array.from(parts[0]?.getElementsByTagName('metronome') || [])]
  733. // const words = [...Array.from(parts[0]?.getElementsByTagName('words') || [])]
  734. // const codas = [...Array.from(parts[0]?.getElementsByTagName('coda') || [])]
  735. // const rehearsals = [...Array.from(parts[0]?.getElementsByTagName('rehearsal') || [])]
  736. const visiblePartInfo = partList[partIndex]
  737. // console.log(visiblePartInfo, partIndex)
  738. // state.partListNames = partListNames
  739. if (visiblePartInfo) {
  740. const id = visiblePartInfo.getAttribute('id')
  741. Array.from(parts).forEach(part => {
  742. if (part && part.getAttribute('id') !== id) {
  743. part.parentNode?.removeChild(part)
  744. // 不等于第一行才添加避免重复添加
  745. } else {
  746. // words.forEach(word => {
  747. // const text = word.textContent || ''
  748. // if(isSpeedKeyword(text) && text) {
  749. // const wordContainer = word.parentElement?.parentElement?.parentElement
  750. // if (wordContainer && wordContainer.firstElementChild && wordContainer.firstElementChild !== word) {
  751. // const wordParent = word.parentElement?.parentElement
  752. // const fisrt = wordContainer.firstElementChild
  753. // wordContainer.insertBefore(wordParent, fisrt)
  754. // }
  755. // }
  756. // })
  757. }
  758. // 最后一个小节的结束线元素不在最后 调整
  759. if (part && part.getAttribute('id') === id) {
  760. const barlines = part.getElementsByTagName('barline')
  761. const lastParent = barlines[barlines.length - 1]?.parentElement
  762. if (lastParent?.lastElementChild?.tagName !== 'barline') {
  763. const children = lastParent?.children || []
  764. for (let el of children) {
  765. if (el.tagName === 'barline') {
  766. // 将结束线元素放到最后
  767. lastParent?.appendChild(el)
  768. break
  769. }
  770. }
  771. }
  772. }
  773. })
  774. Array.from(partList).forEach(part => {
  775. if (part && part.getAttribute('id') !== id) {
  776. part.parentNode?.removeChild(part)
  777. }
  778. })
  779. // 处理装饰音问题
  780. const notes = xmlParse.getElementsByTagName('note')
  781. const getNextvNoteDuration = (i) => {
  782. let nextNote = notes[i + 1]
  783. // 可能存在多个装饰音问题,取下一个非装饰音时值
  784. for (let index = i; index < notes.length; index++) {
  785. const note = notes[index];
  786. if (!note.getElementsByTagName('grace')?.length) {
  787. nextNote = note
  788. break
  789. }
  790. }
  791. const nextNoteDuration = nextNote?.getElementsByTagName('duration')[0]
  792. return nextNoteDuration
  793. }
  794. Array.from(notes).forEach((note, i) => {
  795. const graces = note.getElementsByTagName('grace')
  796. if (graces && graces.length) {
  797. // if (i !== 0) {
  798. note.appendChild(getNextvNoteDuration(i)?.cloneNode(true))
  799. // }
  800. }
  801. })
  802. }
  803. // console.log(new XMLSerializer().serializeToString(xmlParse))
  804. return new XMLSerializer().serializeToString(xmlParse)
  805. }
  806. const speedInfo = {
  807. 'rall.': 1.333333333,
  808. 'poco rit.': 1.333333333,
  809. 'rit.': 1.333333333,
  810. 'molto rit.': 1.333333333,
  811. 'molto rall': 1.333333333,
  812. lentando: 1.333333333,
  813. allargando: 1.333333333,
  814. morendo: 1.333333333,
  815. 'accel.': 0.8,
  816. calando: 2,
  817. 'poco accel.': 0.8,
  818. }
  819. /**
  820. * 按照xml进行减慢速度的计算
  821. * @param xml 始终按照第一分谱进行减慢速度的计算
  822. */
  823. export function getGradualLengthByXml (xml) {
  824. const firstPartXml = onlyVisible(xml, 0)
  825. const xmlParse = new DOMParser().parseFromString(firstPartXml, 'text/xml')
  826. const measures = Array.from(xmlParse.querySelectorAll('measure'))
  827. const notes = Array.from(xmlParse.querySelectorAll('note'))
  828. const words = Array.from(xmlParse.querySelectorAll('words'))
  829. const metronomes = Array.from(xmlParse.querySelectorAll('metronome'))
  830. const eles = []
  831. for (const ele of [...words, ...metronomes]) {
  832. const note = getNextNote(ele, 'direction')
  833. // console.log(ele, note)
  834. if (note) {
  835. const measure = note?.closest('measure')
  836. const measureNotes = Array.from(measure.querySelectorAll('note'))
  837. const noteInMeasureIndex = Array.from(measure.childNodes)
  838. .filter((item) => item.nodeName === 'note')
  839. .findIndex((item) => item === note)
  840. let allDuration = 0
  841. let leftDuration = 0
  842. for (let i = 0; i < measureNotes.length; i++) {
  843. const n = measureNotes[i]
  844. const duration = +(n.querySelector('duration')?.textContent || '0')
  845. allDuration += duration
  846. if (i < noteInMeasureIndex) {
  847. leftDuration = allDuration
  848. }
  849. }
  850. eles.push({
  851. ele,
  852. index: notes.indexOf(note),
  853. noteInMeasureIndex,
  854. textContent: ele.textContent,
  855. measureIndex: measures.indexOf(measure),
  856. type: ele.tagName,
  857. allDuration,
  858. leftDuration,
  859. })
  860. }
  861. }
  862. const gradualNotes = []
  863. eles.sort((a, b) => a.index - b.index)
  864. const keys = Object.keys(speedInfo).map(w => w.toLocaleLowerCase())
  865. for (const ele of eles) {
  866. const textContent = ele.textContent?.toLocaleLowerCase().trim()
  867. if (ele.type === 'words' && keys.includes(textContent)) {
  868. gradualNotes.push([
  869. {
  870. start: ele.index,
  871. measureIndex: ele.measureIndex,
  872. noteInMeasureIndex: ele.noteInMeasureIndex,
  873. allDuration: ele.allDuration,
  874. leftDuration: ele.leftDuration,
  875. type: textContent,
  876. },
  877. ])
  878. }
  879. if (
  880. ele.type === 'metronome' ||
  881. (ele.type === 'words' && textContent === 'a tempo')
  882. ) {
  883. const indexOf = gradualNotes.findIndex((item) => item.length === 1)
  884. if (indexOf > -1 && ele.index > gradualNotes[indexOf]?.[0].start) {
  885. gradualNotes[indexOf][1] = {
  886. start: ele.index,
  887. measureIndex: ele.measureIndex,
  888. noteInMeasureIndex: ele.noteInMeasureIndex,
  889. allDuration: ele.allDuration,
  890. leftDuration: ele.leftDuration,
  891. type: textContent,
  892. }
  893. }
  894. }
  895. }
  896. return gradualNotes
  897. }
  898. </script>
  899. <style lang="less" scoped>
  900. .btns {
  901. text-align: right;
  902. }
  903. .files {
  904. background-color: #f8f8f8;
  905. padding: 20px 0;
  906. padding-right: 20px;
  907. margin-bottom: 20px;
  908. border-radius: 5px;
  909. position: relative;
  910. .file-remove {
  911. position: absolute;
  912. right: 20px;
  913. bottom: 10px;
  914. }
  915. }
  916. </style>