form.vue 33 KB

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