Browse Source

添加上传曲谱功能

lex 8 months ago
parent
commit
0db54e6490

BIN
src/components/col-upload/images/icon-close.png


BIN
src/components/col-upload/images/icon-document.png


+ 8 - 0
src/components/col-upload/index.module.less

@@ -1,5 +1,10 @@
 .colUpload {
   line-height: 0;
+
+  .uploadFileMusic {
+    color: #14BC9C;
+    text-decoration-line: underline;
+  }
 }
 
 .uploadSection {
@@ -23,6 +28,7 @@
   align-items: center;
   padding: 0 15px;
   color: var(--el-text-color-regular);
+
   :global {
     .el-icon {
       margin-right: 5px;
@@ -42,6 +48,7 @@
       height: 42px !important;
       justify-content: center;
       margin-top: -20px !important;
+
       svg {
         width: 20px;
         height: 20px;
@@ -60,6 +67,7 @@
       margin-top: -33px;
       height: 85px;
     }
+
     .el-loading-mask {
       height: 50px;
       border-radius: 10px;

+ 51 - 8
src/components/col-upload/index.tsx

@@ -3,6 +3,8 @@ import { defineComponent, PropType } from 'vue'
 import { Document } from '@element-plus/icons-vue'
 import styles from './index.module.less'
 import iconUpload from './images/icon_upload.png'
+import iconDocument from './images/icon-document.png'
+import iconDelete from './images/icon-close.png'
 import request from '@/helpers/request'
 import { getUploadSign, onOnlyFileUpload } from '@/helpers/oss-file-upload'
 
@@ -13,6 +15,11 @@ export default defineComponent({
       type: String,
       default: ''
     },
+    /** 当前类型 */
+    type: {
+      type: String,
+      default: ''
+    },
     uploadType: {
       type: String as PropType<'image' | 'file'>,
       default: 'image'
@@ -43,7 +50,15 @@ export default defineComponent({
     },
     onChange: {
       type: Function,
-      default: () => {}
+      default: () => ({})
+    },
+    onRemove: {
+      type: Function,
+      default: () => ({})
+    },
+    btnText: {
+      type: String,
+      default: '上传文件'
     }
   },
   data() {
@@ -206,15 +221,34 @@ export default defineComponent({
                 />
               ) : (
                 <div class={styles.uploadFile}>
-                  <ElIcon>
-                    <Document />
+                  <ElIcon size={20}>
+                    {this.type === 'music' ? (
+                      <img src={iconDocument} />
+                    ) : (
+                      <Document />
+                    )}
                   </ElIcon>
                   <span
-                    class="whitespace-nowrap overflow-hidden text-ellipsis"
+                    class="whitespace-nowrap overflow-hidden text-ellipsis flex-1"
                     style={{ lineHeight: '1.2' }}
                   >
                     {this.fileName(this.modelValue)}
                   </span>
+
+                  <ElIcon
+                    size={18}
+                    style="margin-right: 0;"
+                    class="ml-1 cursor-pointer"
+                  >
+                    <img
+                      src={iconDelete}
+                      onClick={e => {
+                        e.stopPropagation()
+                        this.$emit('update:modelValue', '')
+                        this.onRemove()
+                      }}
+                    />
+                  </ElIcon>
                 </div>
               )
             ) : this.uploadType === 'image' ? (
@@ -228,11 +262,20 @@ export default defineComponent({
                 <p>{this.tips}</p>
               </div>
             ) : (
-              <div class={styles.uploadFile}>
-                <ElIcon>
-                  <Document />
+              <div
+                class={[
+                  styles.uploadFile,
+                  this.type === 'music' ? styles.uploadFileMusic : ''
+                ]}
+              >
+                <ElIcon size={20}>
+                  {this.type === 'music' ? (
+                    <img src={iconDocument} />
+                  ) : (
+                    <Document />
+                  )}
                 </ElIcon>
-                上传文件
+                {this.btnText}
               </div>
             )}
           </div>

+ 2 - 1
src/views/user-info/music-class/list.tsx

@@ -48,10 +48,11 @@ export default defineComponent({
       this.loading = true
       try {
         const { data } = await request.post(
+          // '/api-website/musicSheetAuthRecord/list',
           '/api-website/music/sheet/teacher/list',
           {
             data: {
-              auditStatus: this.auditStatus,
+              authStatus: this.auditStatus,
               page: this.pageInfo.page,
               rows: this.pageInfo.limit
             }

BIN
src/views/user-info/music-operation/images/bell.png


BIN
src/views/user-info/music-operation/images/error-icon.png


BIN
src/views/user-info/music-operation/images/icon-question.png


BIN
src/views/user-info/music-operation/images/message-top-bg.png


BIN
src/views/user-info/music-operation/images/message-top-bg2.png


+ 730 - 0
src/views/user-info/music-operation/index copy.tsx

@@ -0,0 +1,730 @@
+import ColCropper from '@/components/col-cropper'
+import ColUpload from '@/components/col-upload'
+import request from '@/helpers/request'
+import { verifyNumberIntegerAndFloat } from '@/helpers/toolsValidate'
+import { getCodeBaseUrl } from '@/helpers/utils'
+import {
+  ElButton,
+  ElForm,
+  ElFormItem,
+  ElInput,
+  ElOption,
+  ElOptionGroup,
+  ElRadioButton,
+  ElRadioGroup,
+  ElSelect,
+  ElDialog,
+  ElMessage
+} from 'element-plus'
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+
+export type BackgroundMp3 = {
+  url?: string
+  track?: string
+}
+
+export const validator = (rule, value, callback) => {
+  console.log(value)
+  if (value == '') {
+    callback(new Error('请输入收费价格'))
+  } else if (Number(value) <= 0) {
+    callback(new Error('收费金额必须大于0'))
+  } else {
+    callback()
+  }
+}
+
+export default defineComponent({
+  name: 'music-operation',
+  data() {
+    const query = this.$route.query
+    return {
+      type: query.type || 'create',
+      subjectList: [],
+      tagList: [],
+      submitLoading: false,
+      reason: '',
+      form: {
+        titleImg: '',
+        accompanimentType: 'HOMEMODE',
+        audioType: 'MP3',
+        xmlFileUrl: '',
+
+        mp3Url: '',
+        bgmp3Url: '',
+        midiUrl: '',
+        musicSheetName: '',
+        composer: '',
+        musicSubject: null as any,
+        tags: [] as string[],
+        hasBeat: 0,
+        notation: 1,
+        canEvaluate: 1,
+        showFingering: 1,
+        chargeType: 0,
+        exquisiteFlag: 0, // 是否精品
+        paymentType: '',
+        musicPrice: '',
+        backgroundMp3s: [
+          {
+            url: '',
+            track: ''
+          }
+        ] as BackgroundMp3[]
+      },
+      radioList: [], // 选中的人数
+      tagStatus: false,
+      music_sheet_service_fee: 0,
+      music_account_period: 0
+    }
+  },
+  async mounted() {
+    document.title = this.type === 'create' ? '新建曲谱' : '编辑曲谱'
+    try {
+      await request
+        .get('/api-website/sysConfig/queryByParamNameList', {
+          params: {
+            paramNames: 'music_sheet_service_fee,music_account_period'
+          }
+        })
+        .then((res: any) => {
+          console.log(res, 'res')
+          const data = res.data || []
+          data.forEach((item: any) => {
+            if (item.paramName === 'music_sheet_service_fee') {
+              this.music_sheet_service_fee = item.paramValue
+            } else if (item.paramName === 'music_account_period') {
+              this.music_account_period = item.paramValue
+            }
+          })
+        })
+      await request.get('/api-website/open/subject/subjectSelect').then(res => {
+        this.subjectList = res.data || []
+      })
+
+      await request.get('/api-website/open/MusicTag/tree').then(res => {
+        this.tagList = res.data || []
+      })
+
+      if (this.$route.query.id) {
+        this.setDetail(this.$route.query.id as string)
+      }
+    } catch {}
+  },
+  methods: {
+    async setDetail(id: string) {
+      try {
+        const res = await request.get(
+          '/api-website/open/music/sheet/detail/' + id
+        )
+        this.form.chargeType = res.data.chargeType === 'FREE' ? 0 : 2
+        this.form.exquisiteFlag = res.data.exquisiteFlag
+        this.form.showFingering = res.data.showFingering
+        this.form.notation = res.data.notation
+        this.form.canEvaluate = res.data.canEvaluate
+        if (this.form.chargeType) {
+          this.form.musicPrice = res.data.musicPrice
+        }
+
+        this.form.composer = res.data.composer
+        this.form.musicSheetName = res.data.musicSheetName
+        this.form.audioType = res.data.audioType
+        this.form.musicSubject = Number(res.data.musicSubject)
+
+        const musicTag = res.data.musicTag.split(',')
+
+        const filterMusicTag = musicTag.filter((el: any) => {
+          return el != ''
+        })
+        this.form.tags = filterMusicTag.map((item: any) => {
+          return Number(item)
+        })
+
+        this.radioList = musicTag.map((item: any) => {
+          return Number(item)
+        })
+
+        this.form.xmlFileUrl = res.data.xmlFileUrl
+        this.form.accompanimentType = res.data.accompanimentType
+        this.form.titleImg = res.data.titleImg
+
+        // this.form.audioType = res.data.mp3Type
+
+        if (this.form.audioType === 'MP3') {
+          this.form.hasBeat = res.data.hasBeat || 0
+          this.form.mp3Url =
+            res.data.audioFileUrl || res.data.metronomeUrl || res.data.url
+        } else {
+          this.form.midiUrl = res.data.midiUrl
+        }
+
+        this.form.backgroundMp3s = (res.data.background || []).map(
+          (item: any, index: any) => {
+            if (index === 0) {
+              this.form.bgmp3Url = item.audioFileUrl || item.metronomeUrl
+            }
+            return {
+              url: this.form.hasBeat ? item.metronomeUrl : item.audioFileUrl,
+              track: item.track
+            }
+          }
+        )
+        this.reason = res.data.reason
+
+        // console.log(this.form.bgmp3Url)
+      } catch (error) {
+        console.log(error)
+      }
+    },
+    createSubmitData() {
+      const { form } = this
+      const beatType = form.hasBeat ? 'MP3_METRONOME' : 'MP3'
+      const mp3Type = form.audioType === 'MP3' ? beatType : 'MIDI'
+      return {
+        audioType: form.audioType,
+        sourceType: 'TEACHER',
+        mp3Type,
+        accompanimentType: form.accompanimentType,
+        titleImg: form.titleImg,
+        hasBeat: Number(form.hasBeat),
+        // url: form.hasBeat ? '' : form.mp3Url,
+        // metronomeUrl: form.hasBeat ? form.mp3Url : '',
+        audioFileUrl: form.mp3Url,
+        showFingering: Number(form.showFingering),
+        notation: Number(form.notation),
+        musicTag: form.tags.join(','),
+        musicSubject: form.musicSubject || undefined,
+        musicSheetName: form.musicSheetName,
+        midiUrl: form.midiUrl,
+        xmlFileUrl: form.xmlFileUrl,
+        canEvaluate: Number(form.canEvaluate),
+        chargeType: form.chargeType === 0 ? 'FREE' : 'CHARGE',
+        exquisiteFlag: form.exquisiteFlag,
+        paymentType: form.paymentType
+          ? form.paymentType
+          : form.chargeType === 0
+          ? 'FREE'
+          : 'CHARGE',
+        composer: form.composer,
+        musicPrice: form.chargeType === 0 ? 0 : form.musicPrice,
+        background: form.backgroundMp3s.map(item => ({
+          audioFileUrl: form.hasBeat ? '' : form.bgmp3Url,
+          track: item.track,
+          metronomeUrl: form.hasBeat ? form.bgmp3Url : ''
+        }))
+      }
+    },
+    onFormatter(e: any) {
+      e.target.value = verifyNumberIntegerAndFloat(e.target.value)
+    },
+    onSubmit() {
+      ;(this as any).$refs.form.validate(async (valid: any) => {
+        if (valid) {
+          this.submitLoading = true
+          console.log(this.createSubmitData(), 'createSubmitData')
+          try {
+            if (this.$route.query.id) {
+              await request.post('/api-website/music/sheet/update', {
+                data: {
+                  ...this.createSubmitData(),
+                  id: this.$route.query.id
+                }
+              })
+            } else {
+              await request.post('/api-website/music/sheet/create', {
+                data: this.createSubmitData()
+              })
+            }
+            this.submitLoading = false
+            ElMessage.success('上传成功')
+            sessionStorage.setItem('musicActiveName', 'DOING')
+            this.$router.back()
+          } catch (error) {
+            this.submitLoading = false
+          }
+        } else {
+          this.$nextTick(() => {
+            const isError = document.getElementsByClassName('is-error')
+            isError[0].scrollIntoView({
+              block: 'center',
+              behavior: 'smooth'
+            })
+          })
+          return false
+        }
+      })
+    },
+    onDetail(type: string) {
+      let url = `${getCodeBaseUrl('/teacher')}/#/registerProtocol`
+      if (type === 'question') {
+        url = `${getCodeBaseUrl('/teacher')}/muic-standard/question.html`
+      } else if (type === 'music') {
+        url = `${getCodeBaseUrl('/teacher')}/muic-standard/index.html`
+      }
+      window.open(url)
+    }
+  },
+  render() {
+    return (
+      <div class={styles.form}>
+        <div class="text-2xl font-semibold text-black leading-none px-6 py-5 ">
+          {this.type === 'create' ? '新建曲谱' : '编辑曲谱'}
+        </div>
+
+        <ElForm
+          size="large"
+          labelPosition="left"
+          labelWidth={'150px'}
+          model={this.form}
+          ref="form"
+          class="px-7 py-5"
+        >
+          <div class={styles.tips}>
+            <div class={styles.tipsTitle}>注意事项:</div>
+            <div class={styles.tipsContent}>
+              1、必须是上传人自己参与制作的作品。
+              <br />
+              2、歌曲及歌曲信息中请勿涉及政治、宗教、广告、涉毒、犯罪、色情、低俗、暴力、血腥、消极等违规内容,违反者直接删除内容。多次违反将封号。
+              <br />
+              3、点击查看{' '}
+              <span onClick={() => this.onDetail('protocol')}>
+                《用户注册协议》
+              </span>
+              ,如果您上传了文件,即认为您完全同意并遵守该协议的内容;
+            </div>
+          </div>
+          <ElFormItem
+            label="上传XML"
+            prop="xmlFileUrl"
+            rules={[{ required: true, message: '请选择MusicXML文件' }]}
+          >
+            <ColUpload
+              v-model:modelValue={this.form.xmlFileUrl}
+              bucket={'cloud-coach'}
+              accept={'application/xml'}
+              uploadType={'file'}
+              extraTips="文件最大不能超过5MB"
+            />
+          </ElFormItem>
+          <div class={styles.tips}>
+            <div class={styles.tipsTitle}>曲谱审核标准:</div>
+            <div class={styles.tipsContent}>
+              1、文件大小不要超过5MB,不符合版面规范的乐谱,审核未通过的不予上架,详情参考
+              <span onClick={() => this.onDetail('music')}>
+                《曲谱排版规范》
+              </span>
+              ; 1、必须是上传人自己参与制作的作品。
+              <br />
+              2、XML与MIDI文件内容必须一致,推荐使用Sibelius打谱软件。导出设置:导出XML-未压缩(*.xml)/导出MIDI:音色-其他回放设备General
+              MIDI、MIDI、MIDI文件类型-类型0、不要勾选“将弱拍小节导出为具有休止符的完整小节”。点击查看
+              <span onClick={() => this.onDetail('question')}>
+                《常见问题》
+              </span>
+            </div>
+          </div>
+          <ElFormItem
+            label="播放类型"
+            prop="audioType"
+            rules={[{ required: true, message: '请选择播放类型' }]}
+          >
+            <ElRadioGroup v-model={this.form.audioType}>
+              <ElRadioButton label={'MIDI'} class="mr-3 w-24">
+                MIDI
+              </ElRadioButton>
+              <ElRadioButton label={'MP3'} class="w-24">
+                MP3
+              </ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem>
+
+          {this.form.audioType === 'MP3' ? (
+            <>
+              {/* <ElFormItem
+                label="是否带节拍器"
+                prop="hasBeat"
+                rules={[{ required: true, message: '请选择是否带节拍器' }]}
+              >
+                <ElRadioGroup v-model={this.form.hasBeat}>
+                  <ElRadioButton label={0} class="mr-3 w-24">
+                    否
+                  </ElRadioButton>
+                  <ElRadioButton label={1} class="w-24">
+                    是
+                  </ElRadioButton>
+                </ElRadioGroup>
+              </ElFormItem> */}
+              <ElFormItem
+                label="伴奏类型"
+                prop="accompanimentType"
+                rules={[{ required: true, message: '请选择伴奏类型' }]}
+              >
+                <ElRadioGroup v-model={this.form.accompanimentType}>
+                  <ElRadioButton label={'HOMEMODE'} class="mr-3 w-24">
+                    自制伴奏
+                  </ElRadioButton>
+                  <ElRadioButton label={'COMMON'} class="w-24">
+                    普通伴奏
+                  </ElRadioButton>
+                </ElRadioGroup>
+              </ElFormItem>
+              <ElFormItem label="伴奏文件" prop="mp3Url">
+                <ColUpload
+                  v-model:modelValue={this.form.mp3Url}
+                  bucket={'cloud-coach'}
+                  accept={'.mp3'}
+                  uploadType={'file'}
+                  size={8}
+                  extraTips="文件最大不能超过8MB"
+                />
+              </ElFormItem>
+            </>
+          ) : (
+            <>
+              <ElFormItem
+                label="伴奏类型"
+                prop="accompanimentType"
+                rules={[{ required: true, message: '请选择伴奏类型' }]}
+              >
+                <ElRadioGroup v-model={this.form.accompanimentType}>
+                  <ElRadioButton label={'HOMEMODE'} class="mr-3 w-24">
+                    自制伴奏
+                  </ElRadioButton>
+                  <ElRadioButton label={'COMMON'} class="w-24">
+                    普通伴奏
+                  </ElRadioButton>
+                </ElRadioGroup>
+              </ElFormItem>
+              <ElFormItem
+                label="MIDI文件"
+                prop="midiUrl"
+                rules={[{ required: true, message: '请选择MIDI文件' }]}
+              >
+                <ColUpload
+                  v-model:modelValue={this.form.midiUrl}
+                  bucket={'cloud-coach'}
+                  accept={'.midi,.mid'}
+                  uploadType={'file'}
+                  size={8}
+                  extraTips="文件最大不能超过8MB"
+                />
+              </ElFormItem>
+            </>
+          )}
+          <div class={styles.tips}>
+            <div class={styles.tipsContent}>
+              1、推荐上传自制伴奏,伴奏和谱面必须对齐。自制伴奏可以设置更高的收费标准。
+              <br />
+              2、普通伴奏如果涉及到版权纠纷,根据
+              <span onClick={() => this.onDetail('protocol')}>
+                《用户注册协议》
+              </span>
+              平台有权进行下架处理。
+            </div>
+          </div>
+          {this.form.audioType === 'MP3' && (
+            <ElFormItem
+              label="原音文件"
+              prop="bgmp3Url"
+              rules={[{ required: true, message: '请选择原音文件' }]}
+            >
+              <ColUpload
+                v-model:modelValue={this.form.bgmp3Url}
+                bucket={'cloud-coach'}
+                accept={'.mp3'}
+                uploadType={'file'}
+                size={8}
+                extraTips="文件最大不能超过8MB"
+              />
+            </ElFormItem>
+          )}
+          <ElFormItem
+            label="曲目名称"
+            prop="musicSheetName"
+            rules={[{ required: true, message: '请输入曲目名称' }]}
+          >
+            <ElInput
+              v-model={this.form.musicSheetName}
+              placeholder="请选择曲目名称"
+            />
+          </ElFormItem>
+          <div class={styles.tips}>
+            <div class={styles.tipsContent}>
+              1、同一首曲目不可重复上传,如有不同版本统一用“()”补充。举例:人生的旋转木马(长笛二重奏版)。
+              <br />
+              2、曲目名后可添加曲目信息备注,包含但不限于曲目类型等。曲目名《xxxx》,举例:人生的旋转木马《哈尔的移动城堡》(长笛二重奏版)
+              <br />
+              3、其他信息不要写在曲目名里,如歌手、上传人员昵称等。
+            </div>
+          </div>
+          <ElFormItem
+            label="曲谱封面"
+            prop="titleImg"
+            rules={[
+              {
+                required: true,
+                message: '请上传曲谱封面'
+              }
+            ]}
+          >
+            <ColCropper
+              modelValue={this.form.titleImg}
+              bucket={'cloud-coach'}
+              cropUploadSuccess={(data: any) => {
+                this.form.titleImg = data
+              }}
+              domSize={{ height: '150px' }}
+              options={{
+                title: '曲谱封面',
+                enlarge: 2,
+                autoCropWidth: 300,
+                autoCropHeight: 300
+              }}
+            />
+          </ElFormItem>
+          <ElFormItem
+            label="艺术家"
+            prop="composer"
+            rules={[{ required: true, message: '请输入艺术家' }]}
+          >
+            <ElInput v-model={this.form.composer} placeholder="请输入艺术家" />
+          </ElFormItem>
+          <ElFormItem
+            label="曲目声部"
+            prop="musicSubject"
+            rules={[
+              { required: true, message: '请选择曲目声部', trigger: 'change' }
+            ]}
+          >
+            <ElSelect
+              filterable
+              v-model={this.form.musicSubject}
+              placeholder="请选择曲目声部"
+              class="w-full"
+            >
+              {this.subjectList.map((group: any) => (
+                <ElOptionGroup key={group.id} label={group.name}>
+                  {group.subjects &&
+                    group.subjects.map((item: any) => (
+                      <ElOption
+                        key={item.id}
+                        value={item.id}
+                        label={item.name}
+                      />
+                    ))}
+                </ElOptionGroup>
+              ))}
+            </ElSelect>
+          </ElFormItem>
+          <div class={styles.tips}>
+            <div class={styles.tipsContent}>
+              XML文件中,选择的曲目声部需要在总谱的置顶位置。
+            </div>
+          </div>
+          <ElFormItem
+            label="曲目标签"
+            prop="tags"
+            rules={[{ required: true, message: '请选择曲目标签' }]}
+          >
+            {/* <div class="w-full relative"> */}
+            {/* <div
+                class=" w-full block h-[42px] absolute top-0 left-0 z-10"
+                onClick={() => {
+                  console.log(111)
+                  this.tagStatus = true
+                }}
+              ></div> */}
+            <ElSelect
+              multiple
+              v-model={this.form.tags}
+              placeholder="请选择曲目标签"
+              multipleLimit={3}
+              class="w-full"
+            >
+              {this.tagList.map((item: any) => (
+                <ElOption key={item.id} value={item.id} label={item.name} />
+              ))}
+            </ElSelect>
+            {/* </div> */}
+          </ElFormItem>
+          {/* <ElFormItem
+            label="支持简谱"
+            prop="notation"
+            rules={[{ required: true, message: '请选择是否支持简谱' }]}
+          >
+            <ElRadioGroup v-model={this.form.notation}>
+              <ElRadioButton label={0} class="mr-3 w-24">
+                否
+              </ElRadioButton>
+              <ElRadioButton label={1} class="w-24">
+                是
+              </ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem> */}
+          {/* <ElFormItem
+            label="是否评测"
+            prop="canEvaluate"
+            rules={[{ required: true, message: '请选择是否评测' }]}
+          >
+            <ElRadioGroup v-model={this.form.canEvaluate}>
+              <ElRadioButton label={0} class="mr-3 w-24">
+                否
+              </ElRadioButton>
+              <ElRadioButton label={1} class="w-24">
+                是
+              </ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem> */}
+          {/* <ElFormItem
+            label="指法展示"
+            prop="showFingering"
+            rules={[{ required: true, message: '请选择指法展示' }]}
+          >
+            <ElRadioGroup v-model={this.form.showFingering}>
+              <ElRadioButton label={0} class="mr-3 w-24">
+                否
+              </ElRadioButton>
+              <ElRadioButton label={1} class="w-24">
+                是
+              </ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem> */}
+          <ElFormItem
+            label="是否收费"
+            prop="chargeType"
+            rules={[{ required: true, message: '请选择是否收费' }]}
+          >
+            <ElRadioGroup v-model={this.form.chargeType}>
+              <ElRadioButton label={0} class="mr-3 w-24">
+                否
+              </ElRadioButton>
+              <ElRadioButton label={2} class="w-24">
+                是
+              </ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem>
+          {this.form.chargeType === 2 && (
+            <>
+              <ElFormItem
+                label="收费价格"
+                prop="musicPrice"
+                rules={[{ required: true, validator }]}
+              >
+                <ElInput
+                  v-model={this.form.musicPrice}
+                  placeholder="请输入收费价格"
+                  // @ts-ignore
+                  maxlength={5}
+                  onKeyup={this.onFormatter}
+                  v-slots={{
+                    suffix: () => <span class="text-base text-[#999]">元</span>
+                  }}
+                />
+              </ElFormItem>
+
+              <ElFormItem>
+                <div class={styles.rule}>
+                  <p>扣除手续费后该曲目预计收入为:</p>
+                  <p>
+                    每人:
+                    <span>
+                      {((parseFloat(this.form.musicPrice || '0') || 0) *
+                        (100 - this.music_sheet_service_fee)) /
+                        100}
+                    </span>
+                    元/人
+                  </p>
+                  <p>
+                    您的乐谱收入在学员购买后{this.music_account_period}
+                    天结算到您的账户中
+                  </p>
+                </div>
+              </ElFormItem>
+            </>
+          )}
+          <ElFormItem
+            label="是否精品乐谱"
+            prop="exquisiteFlag"
+            rules={[{ required: true, message: '请选择是否精品乐谱' }]}
+          >
+            <ElRadioGroup v-model={this.form.exquisiteFlag}>
+              <ElRadioButton label={0} class="mr-3 w-24">
+                否
+              </ElRadioButton>
+              <ElRadioButton label={1} class="w-24">
+                是
+              </ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem>
+        </ElForm>
+        <div class="text-center pt-6 pb-7">
+          <ElButton
+            class="!w-44 !h-[48px]"
+            round
+            onClick={() => {
+              this.$router.back()
+            }}
+          >
+            取消
+          </ElButton>
+          <ElButton
+            type="primary"
+            class="!w-44 !h-[48px]"
+            round
+            onClick={this.onSubmit}
+            loading={this.submitLoading}
+          >
+            提交审核
+          </ElButton>
+        </div>
+
+        {/* <ElDialog
+          modelValue={this.tagStatus}
+          onUpdate:modelValue={val => (this.tagStatus = val)}
+          width="35%"
+          title="全部标签"
+        >
+          {this.tagList.map((item: any, index: number) => (
+            <div class={[styles.tags, 'py-2']}>
+              <div class="text-sm pb-2">{item.name}</div>
+              {item.children.map((child: any) => (
+                <ElRadioGroup v-model={this.radioList[index]} class="pb-2">
+                  <ElRadioButton label={child.id} class="mr-3">
+                    {child.name}
+                  </ElRadioButton>
+                </ElRadioGroup>
+              ))}
+            </div>
+          ))}
+          <div class="text-center pt-2">
+            <ElButton
+              class="!w-36 !h-[48px]"
+              round
+              size="large"
+              onClick={() => {
+                this.radioList = []
+              }}
+            >
+              重置
+            </ElButton>
+            <ElButton
+              class="!w-36 !h-[48px]"
+              round
+              size="large"
+              type="primary"
+              onClick={() => {
+                this.form.tags = this.radioList.filter((el: any) => {
+                  return el != ''
+                })
+                this.tagStatus = false
+                ;(this as any).$refs.form.clearValidate('tags')
+              }}
+            >
+              确认
+            </ElButton>
+          </div>
+        </ElDialog> */}
+      </div>
+    )
+  }
+})

+ 95 - 0
src/views/user-info/music-operation/index.module copy.less

@@ -0,0 +1,95 @@
+.form,
+.tags {
+  --el-border-radius-small: 10px !important;
+  --el-component-size-large: 48px;
+  :global {
+    .el-input,
+    .el-select--large,
+    .el-form-item--large .el-form-item__label {
+      height: 48px;
+      line-height: 48px;
+    }
+    .el-form-item__label {
+      font-size: 16px;
+      color: rgba(0, 0, 0, 0.85);
+    }
+    .el-radio-button__inner {
+      border: var(--el-border);
+      border-radius: var(--el-border-radius-base) !important;
+      width: 100%;
+      padding: 16px 19px !important;
+    }
+    .el-radio-button__original-radio:checked + .el-radio-button__inner {
+      background-color: #e9fff8;
+      color: var(--el-color-primary);
+      box-shadow: none;
+    }
+
+    .el-dialog__body {
+      padding-top: 0;
+    }
+
+    .el-select .el-select__tags .el-tag {
+      background-color: #dffff8;
+      border: 1px solid #4bb39e;
+      color: #4bb39e !important;
+      border-radius: 10px;
+      margin: 0 6px 0 0;
+      height: 30px;
+    }
+    .el-tag .el-icon {
+      color: #4bb39e;
+      background-color: transparent;
+    }
+  }
+}
+
+// .from {
+//   --el-component-size-large: 48px;
+//   :global {
+//     .el-input,
+//     .el-select--large,
+//     .el-form-item--large .el-form-item__label {
+//       height: 48px;
+//       line-height: 48px;
+//     }
+//     .el-form-item__label {
+//       font-size: 16px;
+//       color: rgba(0, 0, 0, 0.85);
+//     }
+//   }
+// }
+
+.tips {
+  font-size: 12px;
+  color: #e0945a;
+  line-height: 18px;
+  padding: 15px 11px;
+  background: #fff3eb;
+  border-radius: 10px;
+  margin: 0 0 22px;
+
+  .tipsTitle {
+    font-size: 14px;
+    font-weight: 600;
+    color: #e0945a;
+    line-height: 20px;
+    padding-bottom: 6px;
+  }
+
+  span {
+    color: #5aa9e0;
+    cursor: pointer;
+  }
+}
+
+.rule {
+  font-size: 14px;
+  line-height: 27px;
+  color: #999;
+  margin: 0 14px;
+  > p > span {
+    color: #ff4e19;
+    font-weight: bold;
+  }
+}

+ 87 - 2
src/views/user-info/music-operation/index.module.less

@@ -2,24 +2,37 @@
 .tags {
   --el-border-radius-small: 10px !important;
   --el-component-size-large: 48px;
+
   :global {
+
     .el-input,
     .el-select--large,
     .el-form-item--large .el-form-item__label {
       height: 48px;
       line-height: 48px;
     }
+
+    .el-input-number {
+      width: 100%;
+
+      .el-input__inner {
+        text-align: left;
+      }
+    }
+
     .el-form-item__label {
       font-size: 16px;
       color: rgba(0, 0, 0, 0.85);
     }
+
     .el-radio-button__inner {
       border: var(--el-border);
       border-radius: var(--el-border-radius-base) !important;
       width: 100%;
       padding: 16px 19px !important;
     }
-    .el-radio-button__original-radio:checked + .el-radio-button__inner {
+
+    .el-radio-button__original-radio:checked+.el-radio-button__inner {
       background-color: #e9fff8;
       color: var(--el-color-primary);
       box-shadow: none;
@@ -37,11 +50,46 @@
       margin: 0 6px 0 0;
       height: 30px;
     }
+
+    .el-textarea__inner {
+      min-height: 110px !important;
+      padding-top: 12px;
+      padding-bottom: 12px;
+    }
+
     .el-tag .el-icon {
       color: #4bb39e;
       background-color: transparent;
     }
   }
+
+  .iconQesution {
+    display: inline-block;
+    width: 13px;
+    height: 14px;
+    background: url('./images/icon-question.png') no-repeat center;
+    background-size: contain;
+    margin-left: 3px;
+  }
+}
+
+.uploadTips {
+  padding: 4px 12px;
+  background: #2DC7AA;
+  border-radius: 15px;
+  font-weight: 600;
+  font-size: 16px;
+  color: #FFFFFF;
+  line-height: 22px;
+  cursor: pointer;
+}
+
+.formItem {
+  background: #F6F8F9;
+  border-radius: 10px;
+  padding: 16px;
+  width: 100%;
+  margin-bottom: 16px !important;
 }
 
 // .from {
@@ -88,8 +136,45 @@
   line-height: 27px;
   color: #999;
   margin: 0 14px;
-  > p > span {
+
+  >p>span {
     color: #ff4e19;
     font-weight: bold;
   }
 }
+
+
+.fAlert {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 42px;
+  background: #F6F8F9;
+  font-weight: 600;
+  font-size: 18px;
+  color: #131415;
+  line-height: 25px;
+  margin-bottom: 32px;
+}
+
+
+.messageDialog,
+.messageDialog2 {
+  width: 618px;
+  border-radius: 18px;
+
+  :global {
+    .el-dialog__headerbtn {
+      font-size: 21px;
+    }
+
+    .el-dialog__header,
+    .el-dialog__body {
+      padding: 0;
+    }
+  }
+}
+
+.messageDialog2 {
+  width: 320px;
+}

+ 374 - 382
src/views/user-info/music-operation/index.tsx

@@ -14,14 +14,22 @@ import {
   ElRadioGroup,
   ElSelect,
   ElDialog,
-  ElMessage
+  ElMessage,
+  ElInputNumber,
+  ElTooltip
 } from 'element-plus'
 import { defineComponent } from 'vue'
 import styles from './index.module.less'
+import requestOrigin from 'umi-request'
+import { FormatXMLInfo, getXmlInfo } from './music-xml'
+import MessageTip from './message-tip'
 
 export type BackgroundMp3 = {
   url?: string
+  id?: string
+  trackName?: string
   track?: string
+  loading?: boolean
 }
 
 export const validator = (rule, value, callback) => {
@@ -45,50 +53,38 @@ export default defineComponent({
       tagList: [],
       submitLoading: false,
       reason: '',
+      formated: {} as FormatXMLInfo,
       form: {
-        titleImg: '',
+        musicCover: '',
         accompanimentType: 'HOMEMODE',
-        audioType: 'MP3',
+        playMode: 'MP3',
         xmlFileUrl: '',
-
+        playSpeed: '100',
         mp3Url: '',
-        bgmp3Url: '',
-        midiUrl: '',
-        musicSheetName: '',
+        // bgmp3Url: '',
+        midiFileUrl: '',
+        name: '',
         composer: '',
-        musicSubject: null as any,
-        tags: [] as string[],
-        hasBeat: 0,
-        notation: 1,
-        canEvaluate: 1,
-        showFingering: 1,
-        chargeType: 0,
-        exquisiteFlag: 0, // 是否精品
-        paymentType: '',
+        remark: '',
+        tags: [] as any[],
+        paymentType: 'CHARGE',
         musicPrice: '',
-        backgroundMp3s: [
-          {
-            url: '',
-            track: ''
-          }
-        ] as BackgroundMp3[]
+        backgroundMp3s: [] as BackgroundMp3[]
       },
       radioList: [], // 选中的人数
       tagStatus: false,
       music_sheet_service_fee: 0,
-      music_account_period: 0
+      music_account_period: 0,
+      visibleShow: false,
+      visibleShow2: false,
+      messageTipTitle: '上传须知',
+      messageTipType: 'upload' as 'upload' | 'error' | 'origin',
+      cbsInstrumentList: [] as any
     }
   },
   async mounted() {
     document.title = this.type === 'create' ? '新建曲谱' : '编辑曲谱'
     try {
-      // await request
-      //   .get('/api-website/sysConfig/queryByParamName', {
-      //     params: {
-      //       paramName: 'music_sheet_service_fee'
-      //     }
-      //   })
-      //   .then(res => (this.music_sheet_service_fee = res.data.paramValue))
       await request
         .get('/api-website/sysConfig/queryByParamNameList', {
           params: {
@@ -106,125 +102,181 @@ export default defineComponent({
             }
           })
         })
-      await request.get('/api-website/open/subject/subjectSelect').then(res => {
-        this.subjectList = res.data || []
-      })
+      // await request.get('/api-website/open/subject/subjectSelect').then(res => {
+      //   this.subjectList = res.data || []
+      // })
 
       await request.get('/api-website/open/MusicTag/tree').then(res => {
         this.tagList = res.data || []
       })
 
+      await request
+        .post('/api-teacher/musicalInstrument/list')
+        .then((response: any) => {
+          const data = response.data || []
+          data.forEach((item: any) => {
+            this.cbsInstrumentList.push({
+              id: item.id,
+              name: item.name,
+              code: item.code,
+              loading: false
+            })
+          })
+        })
+
       if (this.$route.query.id) {
         this.setDetail(this.$route.query.id as string)
       }
-    } catch {}
+    } catch {
+      //
+    }
+  },
+  watch: {
+    formated() {
+      this.mergeXmlData(this.formated)
+    }
   },
   methods: {
+    mergeXmlData(data: FormatXMLInfo) {
+      this.formated = data
+      // this.backgroundMp3s = data.partNames.map((partName: string) => ({
+      //   track: partName
+      // }))
+      if (!this.form.name) {
+        this.form.name = data.title
+      }
+      if (!this.form.composer) {
+        this.form.composer = data.composer
+      }
+      if (!this.form.playSpeed && data.speed) {
+        this.form.playSpeed = '' + data.speed
+      }
+    },
     async setDetail(id: string) {
       try {
-        const res = await request.get(
+        const { data } = await request.get(
           '/api-website/open/music/sheet/detail/' + id
         )
-        this.form.chargeType = res.data.chargeType === 'FREE' ? 0 : 2
-        this.form.exquisiteFlag = res.data.exquisiteFlag
-        this.form.showFingering = res.data.showFingering
-        this.form.notation = res.data.notation
-        this.form.canEvaluate = res.data.canEvaluate
-        if (this.form.chargeType) {
-          this.form.musicPrice = res.data.musicPrice
-        }
-
-        this.form.composer = res.data.composer
-        this.form.musicSheetName = res.data.musicSheetName
-        this.form.audioType = res.data.audioType
-        this.form.musicSubject = Number(res.data.musicSubject)
-
-        const musicTag = res.data.musicTag.split(',')
-
-        const filterMusicTag = musicTag.filter((el: any) => {
-          return el != ''
-        })
-        this.form.tags = filterMusicTag.map((item: any) => {
-          return Number(item)
-        })
-
-        this.radioList = musicTag.map((item: any) => {
-          return Number(item)
-        })
-
-        this.form.xmlFileUrl = res.data.xmlFileUrl
-        this.form.accompanimentType = res.data.accompanimentType
-        this.form.titleImg = res.data.titleImg
-
-        // this.form.audioType = res.data.mp3Type
-
-        if (this.form.audioType === 'MP3') {
-          this.form.hasBeat = res.data.hasBeat || 0
-          this.form.mp3Url =
-            res.data.audioFileUrl || res.data.metronomeUrl || res.data.url
-        } else {
-          this.form.midiUrl = res.data.midiUrl
-        }
+        this.form.playMode = data.audioType || 'MP3'
+        this.form.xmlFileUrl = data.xmlFileUrl
+        this.form.name = data.musicSheetName
+        this.form.composer = data.composer
+        this.form.playSpeed = data.playSpeed
+        // this.form.tags = data.musicTag?.split(',')
+        // const names = data.musicTagNames.split(',')
+        this.form.tags = data.musicTag.split(',')
+        this.form.tags = this.form.tags
+          .filter((el: any) => {
+            return el != ''
+          })
+          .map(e => Number(e))
+        // for (let i = 0; i < names.length; i++) {
+        //   this.form.tagsNames[this.form.tags[i]] = names[i]
+        // }
 
-        this.form.backgroundMp3s = (res.data.background || []).map(
-          (item: any, index: any) => {
-            if (index === 0) {
-              this.form.bgmp3Url = item.audioFileUrl || item.metronomeUrl
-            }
-            return {
-              url: this.form.hasBeat ? item.metronomeUrl : item.audioFileUrl,
-              track: item.track
-            }
+        this.form.musicCover = data.titleImg
+        this.form.midiFileUrl = data.midiUrl
+        this.form.mp3Url = data.metronomeUrl
+        this.form.remark = data.remark
+        this.form.paymentType = data.paymentType
+        this.form.musicPrice = data.musicPrice || 0
+        // this.form.extConfigJson = data.extConfigJson
+        this.form.backgroundMp3s = data.background.map((item: any) => {
+          return {
+            url: item.audioFileUrl,
+            trackName: item.musicalInstrumentName,
+            id: item.musicalInstrumentId,
+            track: item.track,
+            loading: false
           }
-        )
-        this.reason = res.data.reason
-
-        // console.log(this.form.bgmp3Url)
+        })
       } catch (error) {
         console.log(error)
       }
     },
     createSubmitData() {
       const { form } = this
-      const beatType = form.hasBeat ? 'MP3_METRONOME' : 'MP3'
-      const mp3Type = form.audioType === 'MP3' ? beatType : 'MIDI'
       return {
-        audioType: form.audioType,
-        sourceType: 'TEACHER',
-        mp3Type,
-        accompanimentType: form.accompanimentType,
-        titleImg: form.titleImg,
-        hasBeat: Number(form.hasBeat),
-        // url: form.hasBeat ? '' : form.mp3Url,
-        // metronomeUrl: form.hasBeat ? form.mp3Url : '',
-        audioFileUrl: form.mp3Url,
-        showFingering: Number(form.showFingering),
-        notation: Number(form.notation),
-        musicTag: form.tags.join(','),
-        musicSubject: form.musicSubject || undefined,
-        musicSheetName: form.musicSheetName,
-        midiUrl: form.midiUrl,
-        xmlFileUrl: form.xmlFileUrl,
-        canEvaluate: Number(form.canEvaluate),
-        chargeType: form.chargeType === 0 ? 'FREE' : 'CHARGE',
-        exquisiteFlag: form.exquisiteFlag,
-        paymentType: form.paymentType
-          ? form.paymentType
-          : form.chargeType === 0
-          ? 'FREE'
-          : 'CHARGE',
-        composer: form.composer,
-        musicPrice: form.chargeType === 0 ? 0 : form.musicPrice,
-        background: form.backgroundMp3s.map(item => ({
-          audioFileUrl: form.hasBeat ? '' : form.bgmp3Url,
-          track: item.track,
-          metronomeUrl: form.hasBeat ? form.bgmp3Url : ''
-        }))
+        musicSheetJson: {
+          playMode: form.playMode, // 播放模式
+          xmlFileUrl: form.xmlFileUrl, // XML
+          name: form.name, // 曲目名称
+          composer: form.composer, // 音乐人
+          playSpeed: form.playSpeed, // 曲目速度
+          musicTagIds: form.tags?.join(','),
+          remark: form.remark,
+          musicCover: form.musicCover, // 曲目封面
+          multiTracksSelection: form.backgroundMp3s
+            .map(item => item.track)
+            ?.join(','), // 声轨名
+          midiFileUrl: form.midiFileUrl, // MID文件
+          musicPrice: form.musicPrice,
+          paymentType: form.paymentType,
+          musicSheetAccompanimentList: [
+            {
+              audioFileUrl: form.mp3Url,
+              sortNumber: 1,
+              audioPlayType: 'PLAY'
+            }
+          ], // 伴奏
+          // playMode: 'HOMEMODE', // HOMEMODE  默认自制
+          musicSheetSoundList: form.backgroundMp3s.map(item => ({
+            musicalInstrumentId: item.id,
+            musicalInstrumentName: item.trackName,
+            audioFileUrl: item.url,
+            audioPlayType: 'PLAY' // SING
+          })), // 原音
+          musicalInstrumentIds: form.backgroundMp3s
+            .map(item => item.id)
+            ?.join(','), // 乐器编号
+          extConfigJson: '{"repeatedBeats":0,"gradualTimes":{},"isEvxml":0}'
+        }
       }
     },
     onFormatter(e: any) {
       e.target.value = verifyNumberIntegerAndFloat(e.target.value)
     },
+    readerFile(file: string) {
+      requestOrigin(file).then(res => {
+        // this.formated = getXmlInfo(res)
+        const formated = getXmlInfo(res)
+
+        let resultIndexStatus = false
+        const partNames = formated.partNames || []
+
+        const tempMp3s: BackgroundMp3[] = []
+        for (const i of partNames) {
+          const index = this.cbsInstrumentList.findIndex(
+            cbs => cbs.code?.indexOf(i) > -1
+          )
+          if (index === -1) {
+            resultIndexStatus = true
+            break
+          }
+
+          const currentItem = this.cbsInstrumentList[index]
+          if (currentItem) {
+            console.log(currentItem, 'currentItem')
+            tempMp3s.push({
+              url: '',
+              id: currentItem.id,
+              trackName: currentItem.name,
+              track: currentItem.code,
+              loading: currentItem.loading
+            })
+          }
+        }
+
+        if (partNames.length <= 0 || resultIndexStatus) {
+          this.visibleShow2 = true
+          this.form.xmlFileUrl = ''
+          return
+        }
+
+        this.formated = formated
+        this.form.backgroundMp3s = tempMp3s
+      })
+    },
     onSubmit() {
       ;(this as any).$refs.form.validate(async (valid: any) => {
         if (valid) {
@@ -232,14 +284,14 @@ export default defineComponent({
           console.log(this.createSubmitData(), 'createSubmitData')
           try {
             if (this.$route.query.id) {
-              await request.post('/api-website/music/sheet/update', {
+              await request.post('/api-teacher/musicSheetAuthRecord/update', {
                 data: {
                   ...this.createSubmitData(),
                   id: this.$route.query.id
                 }
               })
             } else {
-              await request.post('/api-website/music/sheet/create', {
+              await request.post('/api-teacher/musicSheetAuthRecord/save', {
                 data: this.createSubmitData()
               })
             }
@@ -275,8 +327,15 @@ export default defineComponent({
   render() {
     return (
       <div class={styles.form}>
-        <div class="text-2xl font-semibold text-black leading-none px-6 py-5 ">
+        <div class="text-2xl font-semibold text-black leading-none px-6 py-5 flex justify-between">
           {this.type === 'create' ? '新建曲谱' : '编辑曲谱'}
+
+          <div
+            class={styles.uploadTips}
+            onClick={() => (this.visibleShow = true)}
+          >
+            上传须知
+          </div>
         </div>
 
         <ElForm
@@ -287,186 +346,180 @@ export default defineComponent({
           ref="form"
           class="px-7 py-5"
         >
-          <div class={styles.tips}>
-            <div class={styles.tipsTitle}>注意事项:</div>
-            <div class={styles.tipsContent}>
-              1、必须是上传人自己参与制作的作品。
-              <br />
-              2、歌曲及歌曲信息中请勿涉及政治、宗教、广告、涉毒、犯罪、色情、低俗、暴力、血腥、消极等违规内容,违反者直接删除内容。多次违反将封号。
-              <br />
-              3、点击查看{' '}
-              <span onClick={() => this.onDetail('protocol')}>
-                《用户注册协议》
-              </span>
-              ,如果您上传了文件,即认为您完全同意并遵守该协议的内容;
-            </div>
-          </div>
-          <ElFormItem
-            label="上传XML"
-            prop="xmlFileUrl"
-            rules={[{ required: true, message: '请选择MusicXML文件' }]}
-          >
-            <ColUpload
-              v-model:modelValue={this.form.xmlFileUrl}
-              bucket={'cloud-coach'}
-              accept={'application/xml'}
-              uploadType={'file'}
-              extraTips="文件最大不能超过5MB"
-            />
-          </ElFormItem>
-          <div class={styles.tips}>
-            <div class={styles.tipsTitle}>曲谱审核标准:</div>
-            <div class={styles.tipsContent}>
-              1、文件大小不要超过5MB,不符合版面规范的乐谱,审核未通过的不予上架,详情参考
-              <span onClick={() => this.onDetail('music')}>
-                《曲谱排版规范》
-              </span>
-              ; 1、必须是上传人自己参与制作的作品。
-              <br />
-              2、XML与MIDI文件内容必须一致,推荐使用Sibelius打谱软件。导出设置:导出XML-未压缩(*.xml)/导出MIDI:音色-其他回放设备General
-              MIDI、MIDI、MIDI文件类型-类型0、不要勾选“将弱拍小节导出为具有休止符的完整小节”。点击查看
-              <span onClick={() => this.onDetail('question')}>
-                《常见问题》
-              </span>
-            </div>
-          </div>
+          <div class={styles.fAlert}>曲目上传</div>
           <ElFormItem
             label="播放类型"
-            prop="audioType"
+            prop="playMode"
             rules={[{ required: true, message: '请选择播放类型' }]}
           >
-            <ElRadioGroup v-model={this.form.audioType}>
-              <ElRadioButton label={'MIDI'} class="mr-3 w-24">
-                MIDI
-              </ElRadioButton>
-              <ElRadioButton label={'MP3'} class="w-24">
+            <ElRadioGroup v-model={this.form.playMode}>
+              <ElRadioButton label={'MP3'} class="mr-3 w-24">
                 MP3
               </ElRadioButton>
+              <ElRadioButton label={'MIDI'} class="w-24">
+                MIDI
+              </ElRadioButton>
             </ElRadioGroup>
           </ElFormItem>
 
-          {this.form.audioType === 'MP3' ? (
-            <>
-              {/* <ElFormItem
-                label="是否带节拍器"
-                prop="hasBeat"
-                rules={[{ required: true, message: '请选择是否带节拍器' }]}
-              >
-                <ElRadioGroup v-model={this.form.hasBeat}>
-                  <ElRadioButton label={0} class="mr-3 w-24">
-                    否
-                  </ElRadioButton>
-                  <ElRadioButton label={1} class="w-24">
-                    是
-                  </ElRadioButton>
-                </ElRadioGroup>
-              </ElFormItem> */}
-              <ElFormItem
-                label="伴奏类型"
-                prop="accompanimentType"
-                rules={[{ required: true, message: '请选择伴奏类型' }]}
-              >
-                <ElRadioGroup v-model={this.form.accompanimentType}>
-                  <ElRadioButton label={'HOMEMODE'} class="mr-3 w-24">
-                    自制伴奏
-                  </ElRadioButton>
-                  <ElRadioButton label={'COMMON'} class="w-24">
-                    普通伴奏
-                  </ElRadioButton>
-                </ElRadioGroup>
-              </ElFormItem>
-              <ElFormItem label="伴奏文件" prop="mp3Url">
-                <ColUpload
-                  v-model:modelValue={this.form.mp3Url}
-                  bucket={'cloud-coach'}
-                  accept={'.mp3'}
-                  uploadType={'file'}
-                  size={8}
-                  extraTips="文件最大不能超过8MB"
-                />
-              </ElFormItem>
-            </>
-          ) : (
-            <>
-              <ElFormItem
-                label="伴奏类型"
-                prop="accompanimentType"
-                rules={[{ required: true, message: '请选择伴奏类型' }]}
-              >
-                <ElRadioGroup v-model={this.form.accompanimentType}>
-                  <ElRadioButton label={'HOMEMODE'} class="mr-3 w-24">
-                    自制伴奏
-                  </ElRadioButton>
-                  <ElRadioButton label={'COMMON'} class="w-24">
-                    普通伴奏
-                  </ElRadioButton>
-                </ElRadioGroup>
-              </ElFormItem>
-              <ElFormItem
-                label="MIDI文件"
-                prop="midiUrl"
-                rules={[{ required: true, message: '请选择MIDI文件' }]}
-              >
-                <ColUpload
-                  v-model:modelValue={this.form.midiUrl}
-                  bucket={'cloud-coach'}
-                  accept={'.midi,.mid'}
-                  uploadType={'file'}
-                  size={8}
-                  extraTips="文件最大不能超过8MB"
-                />
-              </ElFormItem>
-            </>
-          )}
-          <div class={styles.tips}>
-            <div class={styles.tipsContent}>
-              1、推荐上传自制伴奏,伴奏和谱面必须对齐。自制伴奏可以设置更高的收费标准。
-              <br />
-              2、普通伴奏如果涉及到版权纠纷,根据
-              <span onClick={() => this.onDetail('protocol')}>
-                《用户注册协议》
-              </span>
-              平台有权进行下架处理。
-            </div>
-          </div>
-          {this.form.audioType === 'MP3' && (
+          {this.form.playMode === 'MP3' ? (
             <ElFormItem
-              label="原音文件"
-              prop="bgmp3Url"
-              rules={[{ required: true, message: '请选择原音文件' }]}
+              label="上传伴奏"
+              prop="mp3Url"
+              rules={[{ required: true, message: '请选择伴奏' }]}
             >
               <ColUpload
-                v-model:modelValue={this.form.bgmp3Url}
+                v-model:modelValue={this.form.mp3Url}
                 bucket={'cloud-coach'}
                 accept={'.mp3'}
                 uploadType={'file'}
+                type="music"
+                btnText="上传伴奏文件"
+                size={8}
+                extraTips="仅支持MP3格式文件,文件最大不能超过8MB"
+              />
+            </ElFormItem>
+          ) : (
+            <ElFormItem
+              label="MIDI文件"
+              prop="midiFileUrl"
+              rules={[{ required: true, message: '请选择MIDI文件' }]}
+            >
+              <ColUpload
+                v-model:modelValue={this.form.midiFileUrl}
+                bucket={'cloud-coach'}
+                accept={'.midi,.mid'}
+                uploadType={'file'}
+                type="music"
+                btnText="上传MIDI文件"
                 size={8}
-                extraTips="文件最大不能超过8MB"
+                extraTips="仅支持MID格式文件,文件最大不能超过8MB"
               />
             </ElFormItem>
           )}
+
+          <ElFormItem
+            label="上传XML"
+            prop="xmlFileUrl"
+            rules={[{ required: true, message: '请选择MusicXML文件' }]}
+          >
+            <ColUpload
+              v-model:modelValue={this.form.xmlFileUrl}
+              bucket={'cloud-coach'}
+              accept={'application/xml'}
+              uploadType={'file'}
+              type="music"
+              btnText="上传XML文件"
+              extraTips="仅支持XML/MXML格式文件,文件最大不能超过8MB"
+              onChange={this.readerFile}
+              onRemove={() => {
+                this.form.backgroundMp3s = []
+              }}
+            />
+          </ElFormItem>
+
+          {this.form.backgroundMp3s.length > 0 && (
+            <ElFormItem>
+              {{
+                label: () => (
+                  <div class="flex items-center">
+                    <i style="color: var(--el-color-danger);margin-right: 4px;">
+                      *
+                    </i>
+                    上传原音
+                    <ElTooltip effect="dark" placement="top">
+                      {{
+                        content: () => (
+                          <div class="w-[445px]">
+                            1、同一首曲目不可重复上传,如有不同版本统一用“()”补充。举例:人生的旋转木马(长笛二重奏版)
+                            <br />
+                            2、曲目名后可添加曲目信息备注,包含但不限于曲目类型等。曲目名《XXX》,举例:人声的旋转木马《哈尔的移动城堡》(长笛二重奏版)
+                            <br />
+                            3、其他信息不要写在曲目名里,如歌手、上传人员昵称等。
+                          </div>
+                        ),
+                        default: () => <i class={styles.iconQesution}></i>
+                      }}
+                    </ElTooltip>
+                  </div>
+                ),
+                default: () => (
+                  <>
+                    {this.form.backgroundMp3s.map((mp3, index) => (
+                      <ElFormItem
+                        prop={`backgroundMp3s.${index}.url`}
+                        rules={[
+                          {
+                            required: true,
+                            message: '请选择原音文件',
+                            trigger: 'blur,change'
+                          }
+                        ]}
+                        class={styles.formItem}
+                      >
+                        <ColUpload
+                          v-model:modelValue={mp3.url}
+                          bucket={'cloud-coach'}
+                          accept={'.mp3'}
+                          uploadType={'file'}
+                          size={8}
+                          type="music"
+                          btnText="上传原音文件"
+                          extraTips="仅支持MP3格式文件,文件最大不能超过8"
+                        />
+                      </ElFormItem>
+                    ))}
+                  </>
+                )
+              }}
+            </ElFormItem>
+          )}
+
+          <div class={styles.fAlert}>曲目信息</div>
           <ElFormItem
             label="曲目名称"
-            prop="musicSheetName"
+            prop="name"
             rules={[{ required: true, message: '请输入曲目名称' }]}
           >
             <ElInput
-              v-model={this.form.musicSheetName}
+              v-model={this.form.name}
               placeholder="请选择曲目名称"
+              // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+              // @ts-ignore
+              maxlength={50}
+              showWordLimit
+            />
+          </ElFormItem>
+
+          <ElFormItem
+            label="音乐人"
+            prop="composer"
+            rules={[{ required: true, message: '请输入音乐人' }]}
+          >
+            <ElInput
+              v-model={this.form.composer}
+              placeholder="请输入音乐人"
+              // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+              // @ts-ignore
+              maxlength={14}
+              showWordLimit
+            />
+          </ElFormItem>
+
+          <ElFormItem label="曲目描述" prop="remark">
+            <ElInput
+              v-model={this.form.remark}
+              placeholder="请输入曲目描述"
+              // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+              // @ts-ignore
+              maxlength={200}
+              showWordLimit
+              type="textarea"
             />
           </ElFormItem>
-          <div class={styles.tips}>
-            <div class={styles.tipsContent}>
-              1、同一首曲目不可重复上传,如有不同版本统一用“()”补充。举例:人生的旋转木马(长笛二重奏版)。
-              <br />
-              2、曲目名后可添加曲目信息备注,包含但不限于曲目类型等。曲目名《xxxx》,举例:人生的旋转木马《哈尔的移动城堡》(长笛二重奏版)
-              <br />
-              3、其他信息不要写在曲目名里,如歌手、上传人员昵称等。
-            </div>
-          </div>
           <ElFormItem
             label="曲谱封面"
-            prop="titleImg"
+            prop="musicCover"
             rules={[
               {
                 required: true,
@@ -475,10 +528,10 @@ export default defineComponent({
             ]}
           >
             <ColCropper
-              modelValue={this.form.titleImg}
+              modelValue={this.form.musicCover}
               bucket={'cloud-coach'}
               cropUploadSuccess={(data: any) => {
-                this.form.titleImg = data
+                this.form.musicCover = data
               }}
               domSize={{ height: '150px' }}
               options={{
@@ -489,58 +542,26 @@ export default defineComponent({
               }}
             />
           </ElFormItem>
+
           <ElFormItem
-            label="艺术家"
-            prop="composer"
-            rules={[{ required: true, message: '请输入艺术家' }]}
-          >
-            <ElInput v-model={this.form.composer} placeholder="请输入艺术家" />
-          </ElFormItem>
-          <ElFormItem
-            label="曲目声部"
-            prop="musicSubject"
-            rules={[
-              { required: true, message: '请选择曲目声部', trigger: 'change' }
-            ]}
+            label="曲目速度"
+            prop="playSpeed"
+            rules={[{ required: true, message: '请输入曲目速度' }]}
           >
-            <ElSelect
-              filterable
-              v-model={this.form.musicSubject}
-              placeholder="请选择曲目声部"
-              class="w-full"
-            >
-              {this.subjectList.map((group: any) => (
-                <ElOptionGroup key={group.id} label={group.name}>
-                  {group.subjects &&
-                    group.subjects.map((item: any) => (
-                      <ElOption
-                        key={item.id}
-                        value={item.id}
-                        label={item.name}
-                      />
-                    ))}
-                </ElOptionGroup>
-              ))}
-            </ElSelect>
+            <ElInputNumber
+              controls={false}
+              v-model={this.form.playSpeed}
+              placeholder="请输入曲目速度"
+              min={45}
+              max={270}
+            />
           </ElFormItem>
-          <div class={styles.tips}>
-            <div class={styles.tipsContent}>
-              XML文件中,选择的曲目声部需要在总谱的置顶位置。
-            </div>
-          </div>
+
           <ElFormItem
             label="曲目标签"
             prop="tags"
             rules={[{ required: true, message: '请选择曲目标签' }]}
           >
-            {/* <div class="w-full relative"> */}
-            {/* <div
-                class=" w-full block h-[42px] absolute top-0 left-0 z-10"
-                onClick={() => {
-                  console.log(111)
-                  this.tagStatus = true
-                }}
-              ></div> */}
             <ElSelect
               multiple
               v-model={this.form.tags}
@@ -554,63 +575,22 @@ export default defineComponent({
             </ElSelect>
             {/* </div> */}
           </ElFormItem>
-          {/* <ElFormItem
-            label="支持简谱"
-            prop="notation"
-            rules={[{ required: true, message: '请选择是否支持简谱' }]}
-          >
-            <ElRadioGroup v-model={this.form.notation}>
-              <ElRadioButton label={0} class="mr-3 w-24">
-                否
-              </ElRadioButton>
-              <ElRadioButton label={1} class="w-24">
-                是
-              </ElRadioButton>
-            </ElRadioGroup>
-          </ElFormItem> */}
-          {/* <ElFormItem
-            label="是否评测"
-            prop="canEvaluate"
-            rules={[{ required: true, message: '请选择是否评测' }]}
-          >
-            <ElRadioGroup v-model={this.form.canEvaluate}>
-              <ElRadioButton label={0} class="mr-3 w-24">
-                否
-              </ElRadioButton>
-              <ElRadioButton label={1} class="w-24">
-                是
-              </ElRadioButton>
-            </ElRadioGroup>
-          </ElFormItem> */}
-          {/* <ElFormItem
-            label="指法展示"
-            prop="showFingering"
-            rules={[{ required: true, message: '请选择指法展示' }]}
-          >
-            <ElRadioGroup v-model={this.form.showFingering}>
-              <ElRadioButton label={0} class="mr-3 w-24">
-                否
-              </ElRadioButton>
-              <ElRadioButton label={1} class="w-24">
-                是
-              </ElRadioButton>
-            </ElRadioGroup>
-          </ElFormItem> */}
+
           <ElFormItem
             label="是否收费"
-            prop="chargeType"
+            prop="paymentType"
             rules={[{ required: true, message: '请选择是否收费' }]}
           >
-            <ElRadioGroup v-model={this.form.chargeType}>
-              <ElRadioButton label={0} class="mr-3 w-24">
+            <ElRadioGroup v-model={this.form.paymentType}>
+              <ElRadioButton label={'FREE'} class="mr-3 w-24">
               </ElRadioButton>
-              <ElRadioButton label={2} class="w-24">
+              <ElRadioButton label={'CHARGE'} class="w-24">
               </ElRadioButton>
             </ElRadioGroup>
           </ElFormItem>
-          {this.form.chargeType === 2 && (
+          {this.form.paymentType === 'CHARGE' && (
             <>
               <ElFormItem
                 label="收费价格"
@@ -620,6 +600,7 @@ export default defineComponent({
                 <ElInput
                   v-model={this.form.musicPrice}
                   placeholder="请输入收费价格"
+                  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                   // @ts-ignore
                   maxlength={5}
                   onKeyup={this.onFormatter}
@@ -649,20 +630,6 @@ export default defineComponent({
               </ElFormItem>
             </>
           )}
-          <ElFormItem
-            label="是否精品乐谱"
-            prop="exquisiteFlag"
-            rules={[{ required: true, message: '请选择是否精品乐谱' }]}
-          >
-            <ElRadioGroup v-model={this.form.exquisiteFlag}>
-              <ElRadioButton label={0} class="mr-3 w-24">
-                否
-              </ElRadioButton>
-              <ElRadioButton label={1} class="w-24">
-                是
-              </ElRadioButton>
-            </ElRadioGroup>
-          </ElFormItem>
         </ElForm>
         <div class="text-center pt-6 pb-7">
           <ElButton
@@ -731,6 +698,31 @@ export default defineComponent({
             </ElButton>
           </div>
         </ElDialog> */}
+
+        <ElDialog
+          modelValue={this.visibleShow}
+          onUpdate:modelValue={val => (this.visibleShow = val)}
+          destroyOnClose={true}
+          customClass={styles.messageDialog}
+        >
+          <MessageTip
+            type={'upload'}
+            title={'上传须知'}
+            onConfirm={() => (this.visibleShow = false)}
+          />
+        </ElDialog>
+        <ElDialog
+          modelValue={this.visibleShow2}
+          onUpdate:modelValue={val => (this.visibleShow2 = val)}
+          destroyOnClose={true}
+          customClass={styles.messageDialog2}
+        >
+          <MessageTip
+            type={'error'}
+            title={'解析失败'}
+            onConfirm={() => (this.visibleShow2 = false)}
+          />
+        </ElDialog>
       </div>
     )
   }

+ 139 - 0
src/views/user-info/music-operation/message-tip/index.module.less

@@ -0,0 +1,139 @@
+// .wxPopupDialog {
+//   // position: relative;
+//   overflow: initial;
+
+//   // margin-top: -160px;
+//   &::before {
+//     position: absolute;
+//     content: ' ';
+//     top: -23px;
+//     left: 50%;
+//     margin-left: -50px;
+//     display: inline-block;
+//     background: url('../images/bell.png') no-repeat top center;
+//     background-size: contain;
+//     width: 100px;
+//     height: 60px;
+//   }
+// }
+
+.popupContainer {
+  background: url('../images/message-top-bg.png') no-repeat top center;
+  background-size: contain;
+  border-radius: 20px;
+  overflow: hidden;
+  padding-bottom: 25px;
+
+  &::before {
+    position: absolute;
+    content: ' ';
+    top: -36px;
+    left: 50%;
+    margin-left: -61px;
+    display: inline-block;
+    background: url('../images/bell.png') no-repeat top center;
+    background-size: contain;
+    width: 123px;
+    height: 71px;
+  }
+
+  &.popupContainerError {
+    &::before {
+      background: url('../images/error-icon.png') no-repeat top center;
+      background-size: contain;
+    }
+
+    .container {
+      padding: 16px 23px 0 !important;
+
+      .cContent {
+        padding-bottom: 25px;
+      }
+    }
+  }
+
+  .title1 {
+    padding-top: 52px;
+    text-align: center;
+    font-weight: 600;
+    font-size: 18px;
+    color: #131415;
+    line-height: 30px;
+
+    span {
+      position: relative;
+      z-index: 1;
+
+      &::after {
+        content: '';
+        position: absolute;
+        left: 0;
+        bottom: 0;
+        z-index: -1;
+        width: 100%;
+        height: 8px;
+        background: linear-gradient(to right, rgba(45, 199, 170, 1), rgba(91, 236, 255, 0.20));
+      }
+    }
+  }
+
+  .popupTips {
+    position: relative;
+    font-size: 15px;
+    color: #777777;
+    line-height: 21px;
+
+
+    // &::before {
+    //   content: '';
+    //   position: absolute;
+    //   left: 0;
+    //   bottom: 0;
+    //   height: 54px;
+    //   width: 100%;
+    //   background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #FFFFFF 100%);
+    // }
+
+    .container {
+      max-height: 400px;
+      overflow-x: hidden;
+      overflow-y: auto;
+      padding: 16px 42px 0;
+    }
+
+    .cTitle {
+      font-weight: 600;
+      font-size: 15px;
+      color: #333333;
+      line-height: 21px;
+    }
+
+    .cContent {
+      font-size: 15px;
+      color: #777777;
+      line-height: 25px;
+      padding-bottom: 14px;
+
+      span {
+        color: #14BC9C;
+        cursor: pointer;
+        font-weight: 500;
+      }
+    }
+  }
+
+  .btnGroup {
+    text-align: center;
+  }
+
+  .button {
+    width: 260px;
+    height: 39px;
+    background: #2DC7AF;
+    border-radius: 10px;
+    font-weight: 600;
+    font-size: 15px;
+    color: #FFFFFF;
+    line-height: 21px;
+  }
+}

+ 123 - 0
src/views/user-info/music-operation/message-tip/index.tsx

@@ -0,0 +1,123 @@
+import { Button, Popup } from 'vant'
+import { PropType, defineComponent, onMounted, ref, watch } from 'vue'
+import styles from './index.module.less'
+import { postMessage } from '@/helpers/native-message'
+import { getCodeBaseUrl } from '@/helpers/utils'
+import { ElButton, ElScrollbar } from 'element-plus'
+
+export default defineComponent({
+  name: 'message-tip',
+  props: {
+    type: {
+      type: String as PropType<'upload' | 'error' | 'origin'>,
+      default: 'upload'
+    },
+    title: {
+      type: String,
+      default: '温馨提示'
+    },
+    showButton: {
+      type: Boolean,
+      default: true
+    },
+    buttonText: {
+      type: String,
+      default: '我已知晓'
+    }
+  },
+  emits: ['confirm'],
+  setup(props, { emit }) {
+    const showPopup = ref(false)
+    // props.title, props.type
+    // watch(
+    //   () => [props.title, props.type],
+    //   () => {}
+    // )
+
+    // 详情
+    const onDetail = (type: string) => {
+      let url = `${getCodeBaseUrl('/teacher')}/#/registerProtocol`
+      if (type === 'question') {
+        url = `${getCodeBaseUrl('/teacher')}/muic-standard/question.html`
+      } else if (type === 'music') {
+        url = `${getCodeBaseUrl('/teacher')}/muic-standard/index.html`
+      }
+      window.open(url)
+    }
+    return () => (
+      <div
+        class={[
+          styles.popupContainer,
+          props.type === 'error' ? styles.popupContainerError : ''
+        ]}
+      >
+        <p class={styles.title1}>
+          <span>{props.title}</span>
+        </p>
+        <div class={styles.popupTips}>
+          {props.type === 'upload' && (
+            <ElScrollbar class={styles.container}>
+              <p class={styles.cTitle}>注意事项:</p>
+              <div class={styles.cContent}>
+                1、必须是上传人自己参与制作的作品 <br />
+                2、歌曲及歌曲信息中请勿涉及政治、宗教、广告、涉毒、犯罪、色情、低俗、暴力、血腥、消极等违规内容,违反者直接删除内容。多次违反则进行封号处理;
+                <br />
+                3、点击查看
+                <span onClick={() => onDetail('protocol')}>
+                  《用户注册协议》
+                </span>
+                ,如果您上传了文件,即认为您完全同意并遵守该协议的内容。
+              </div>
+              <p class={styles.cTitle}>曲谱审核标准:</p>
+              <div class={styles.cContent}>
+                1、文件大小不要超过5MB,不符合版面规范的乐谱,审核未通过的不予上架,详情参考
+                <span onClick={() => onDetail('music')}>《曲谱排版规范》</span>
+                ;
+                <br />
+                2、XML与MIDI文件内容必须一致,推荐使用Sibelius打谱软件;导出设置:导出XML-未压缩(*.XML)/导出MIDI:音色-其他回放设备General
+                MIDI、MIDI、MIDI文件类型-类型0、不要勾选“将弱拍小节导出为具有休止符的完整小节”。点击查看
+                <span onClick={() => onDetail('question')}>《常见问题》</span>
+              </div>
+            </ElScrollbar>
+          )}
+
+          {props.type === 'error' && (
+            <div class={styles.container}>
+              <div class={styles.cContent}>
+                声轨名称解析失败,请对照
+                <span onClick={() => onDetail('protocol')}>
+                  《曲谱排版规范》
+                </span>
+                检查后重试
+              </div>
+            </div>
+          )}
+
+          {props.type === 'origin' && (
+            <div class={styles.container}>
+              <div class={styles.cContent}>
+                1、同一首曲目不可重复上传,如有不同版本统一用“()”补充。举例:人生的旋转木马(长笛二重奏版)
+                <br />
+                2、曲目名后可添加曲目信息备注,包含但不限于曲目类型等。曲目名《XXX》,举例:人声的旋转木马《哈尔的移动城堡》(长笛二重奏版)
+                <br />
+                3、其他信息不要写在曲目名里,如歌手、上传人员昵称等。
+              </div>
+            </div>
+          )}
+        </div>
+
+        {props.showButton && (
+          <div class={styles.btnGroup}>
+            <ElButton
+              type="primary"
+              class={styles.button}
+              onClick={() => emit('confirm')}
+            >
+              {props.buttonText}
+            </ElButton>
+          </div>
+        )}
+      </div>
+    )
+  }
+})

+ 105 - 0
src/views/user-info/music-operation/music-xml.ts

@@ -0,0 +1,105 @@
+export type FormatXMLResult = {
+  xml: string
+  speed: number
+  partNames: string[]
+}
+
+/** 获取到xml前处理,添加额外的节拍 */
+export const fillBeatXML = (xml: string): string => {
+  if (!xml) return ''
+  const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
+  const measures = xmlParse.getElementsByTagName('measure')
+  // let speed = -1
+  let beats = -1
+  let beatType = -1
+  // 小节中如果没有节点默认为休止符
+  for (const measure of Array.from(measures)) {
+    if (beats === -1 && measure.getElementsByTagName('beats').length) {
+      beats = parseInt(
+        measure.getElementsByTagName('beats')[0].textContent || '4'
+      )
+    }
+    if (beatType === -1 && measure.getElementsByTagName('beat-type').length) {
+      beatType = parseInt(
+        measure.getElementsByTagName('beat-type')[0].textContent || '4'
+      )
+    }
+    const divisions = parseInt(
+      measure.getElementsByTagName('divisions')[0]?.textContent || '256'
+    )
+    if (measure.getElementsByTagName('note').length === 0) {
+      const forwardTimeElement = measure
+        .getElementsByTagName('forward')[0]
+        ?.getElementsByTagName('duration')[0]
+      if (forwardTimeElement) {
+        forwardTimeElement.textContent = '0'
+      }
+      measure.innerHTML += `
+        <note>
+          <rest measure="yes"/>
+          <duration>${divisions * beats}</duration>
+          <voice>1</voice>
+          <type>whole</type>
+        </note>`
+    }
+  }
+  return new XMLSerializer().serializeToString(xmlParse)
+}
+
+export type FormatXMLInfo = {
+  speed: number
+  title: string
+  composer: string
+  partNames: string[]
+}
+
+/** 获取xml基本信息,标题,速度等信息 */
+export const getXmlInfo = (xml: string): FormatXMLInfo => {
+  const data: FormatXMLInfo = {
+    /** 速度 */
+    speed: 0,
+    /** 标题 */
+    title: '',
+    /** 作曲人 */
+    composer: '',
+    /** 声部列表 */
+    partNames: []
+  }
+
+  const xmlParse = new DOMParser().parseFromString(xml, 'text/xml')
+  data.title = xmlParse.getElementsByTagName('work-title')[0]?.textContent || ''
+  data.composer = xmlParse.getElementsByTagName('creator')[0]?.textContent || ''
+  const measures = xmlParse.getElementsByTagName('measure')
+
+  for (const item of Array.from(xmlParse.getElementsByTagName('part-name'))) {
+    if (item.textContent) {
+      data.partNames.push(item.textContent)
+    }
+  }
+
+  for (const measure of Array.from(measures)) {
+    const perMinute = measure.getElementsByTagName('per-minute')
+    if (perMinute.length && perMinute[perMinute.length - 1]) {
+      data.speed = parseFloat(
+        perMinute[perMinute.length - 1].textContent || '0'
+      )
+      break
+    }
+  }
+  return data
+}
+
+/** 解析xml */
+export const formatXMLFile = (file: File): FormatXMLResult => {
+  console.log(file)
+  const reader = new FileReader()
+  reader.onload = e => {
+    console.log(e)
+  }
+  reader.readAsText(file)
+  return {
+    xml: '',
+    speed: 0,
+    partNames: []
+  }
+}

+ 5 - 1
vite.config.ts

@@ -15,7 +15,7 @@ function resolve(dir: string) {
 // https://vitejs.dev/config/
 // https://github.com/vitejs/vite/issues/1930 .env
 // const proxyUrl = 'https://www.colexiu.com/';
-const proxyUrl = 'https://test.colexiu.com/'
+const proxyUrl = 'https://dev.colexiu.com/'
 export default defineConfig({
   base: './',
   plugins: [
@@ -73,6 +73,10 @@ export default defineConfig({
         target: proxyUrl,
         changeOrigin: true
       },
+      '/api-teacher': {
+        target: proxyUrl,
+        changeOrigin: true
+      },
       '/api-website': {
         target: proxyUrl,
         changeOrigin: true