Browse Source

上传曲谱

wolyshaw 2 years ago
parent
commit
6d399fd6cd

+ 0 - 0
src/components/col-radio/radio-group.tsx


+ 0 - 0
src/components/col-radio/radio.module.less


+ 15 - 0
src/components/col-radio/radio.tsx

@@ -0,0 +1,15 @@
+import { defineComponent } from 'vue'
+import { Radio, RadioProps, Tag } from 'vant'
+
+export default defineComponent({
+  name: 'ColRadio',
+  setup() {
+    return (props: RadioProps) => {
+      return (
+        <Tag>
+          <Radio {...props} />
+        </Tag>
+      )
+    }
+  }
+})

+ 13 - 0
src/constant/music.ts

@@ -4,3 +4,16 @@ export const auditStatus = {
   1: '通过',
   2: '未通过'
 }
+
+/** 曲目收费类型 */
+export const chargeType = {
+  0: '免费',
+  1: '会员',
+  2: '单曲收费'
+}
+
+/** 老师端曲目收费类型 */
+export const teacherChargeType = {
+  2: '是',
+  0: '否'
+}

+ 96 - 0
src/helpers/music-xml.ts

@@ -0,0 +1,96 @@
+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
+}
+
+/** 获取xml基本信息,标题,速度等信息 */
+export const getXmlInfo = (xml: string): FormatXMLInfo => {
+  const data: FormatXMLInfo = {
+    /** 速度 */
+    speed: 0,
+    /** 标题 */
+    title: '',
+    /** 作曲人 */
+    composer: ''
+  }
+
+  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 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: []
+  }
+}

+ 12 - 5
src/styles/index.less

@@ -12,8 +12,8 @@
   // --van-gray-8: #323233;
   // --van-red: #ee0a24;
   // --van-blue: #1989fa;
-  --van-primary: #2DC7AA !important;
-  --van-picker-confirm-action-color: #2DC7AA !important;
+  --van-primary: #2dc7aa !important;
+  --van-picker-confirm-action-color: #2dc7aa !important;
   // --van-orange: #ff976a;
   // --van-orange-dark: #ed6a0c;
   // --van-orange-light: #fffbe8;
@@ -76,13 +76,20 @@
   // --van-border-radius-lg: 8px;
   // --van-border-radius-max: 999px;
 
-  --col-background-color: #F6F8F9;
+  --van-tag-default-color: #f8f8f8 !important;
+  --van-tag-text-default-color: #c0c0c0;
+
+  --col-background-color: #f6f8f9;
 
   --white: #fff;
 
   --tips-color: #999;
-  --strong--color: #FF4E19;
+  --strong--color: #ff4e19;
   --box-shadow-color: rgba(0, 0, 0, 0.05);
+
+  --tag-border-color: #2dc7aa;
+  --tag-bg-color: #e9fff8;
+  --tag-color: #2dc7aa;
 }
 
 * {
@@ -100,7 +107,7 @@
 }
 
 body {
-  background-color: #F6F8F9;
+  background-color: #f6f8f9;
 }
 
 .mb12 {

+ 46 - 7
src/teacher/music/upload/index.module.less

@@ -1,31 +1,70 @@
-.container{
+.container {
   background-color: var(--col-background-color);
   min-height: 100vh;
-  margin: 14px;
-  .area{
+  margin: 14px 0;
+  .area {
     padding: 20px;
     margin-bottom: 12px;
   }
 }
 
-.clear-px{
+.clear-px {
   padding-left: 0;
   padding-right: 0;
 }
 
-.rule{
+.rule {
   font-size: 14px;
   line-height: 27px;
   color: var(--tips-color);
+  margin: 0 14px;
   margin-bottom: 20px;
-  >p>span{
+  > p > span {
     color: var(--strong--color);
     font-weight: bold;
   }
 }
 
-.button-area{
+.button-area {
   padding: 10px 14px;
   background-color: var(--white);
   box-shadow: 0 0 10px var(--box-shadow-color);
 }
+
+.radio-group {
+  display: flex;
+  margin-top: 14px;
+  .radio:first-child {
+    :global {
+      .van-radio__label {
+        margin-left: 0;
+      }
+    }
+  }
+}
+
+.radio {
+  :global {
+    .van-radio__icon {
+      display: none;
+    }
+    .van-tag--large {
+      width: 94px;
+      height: 30px;
+      font-size: 16px;
+      text-align: center;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+    .van-tag {
+      box-sizing: border-box;
+    }
+    .van-tag--default {
+      color: var(--van-tag-text-default-color);
+    }
+    .van-tag--primary {
+      background-color: var(--tag-bg-color);
+    }
+  }
+}

+ 124 - 33
src/teacher/music/upload/index.tsx

@@ -1,8 +1,11 @@
 import { defineComponent } from 'vue'
-import { Button, Field, Sticky, Form } from 'vant'
-import ColArea from '@/components/col-area'
+import { Button, Field, Sticky, Form, Tag, Radio, RadioGroup } from 'vant'
+import ColFieldGroup from '@/components/col-field-group'
 import { MusicType } from 'src/teacher/music/list/item.d'
 import ColField from '@/components/col-field'
+import { teacherChargeType } from '@/constant/music'
+import { getXmlInfo, FormatXMLInfo } from '@/helpers/music-xml'
+import Upload from './upload'
 import styles from './index.module.less'
 
 export default defineComponent({
@@ -10,7 +13,23 @@ export default defineComponent({
   data() {
     return {
       musicSheetName: '',
-      selectTagVisible: false
+      composer: '',
+      speed: '',
+      chargeType: '0',
+      musicPrice: '',
+      selectTagVisible: false,
+      tags: ['12312'] as string[],
+      formated: {} as FormatXMLInfo
+    }
+  },
+  watch: {
+    formated() {
+      this.mergeXmlData(this.formated)
+    },
+    chargeType() {
+      if (this.chargeType === '0') {
+        this.musicPrice = ''
+      }
     }
   },
   methods: {
@@ -19,15 +38,45 @@ export default defineComponent({
     },
     failed() {
       console.log('failed', this.musicSheetName)
+    },
+    mergeXmlData(data: FormatXMLInfo) {
+      this.formated = data
+      if (!this.musicSheetName) {
+        this.musicSheetName = data.title
+      }
+      if (!this.composer) {
+        this.composer = data.composer
+      }
+      if (!this.speed && data.speed) {
+        this.speed = '' + data.speed
+      }
+    },
+    readerFile(file: File) {
+      const reader = new FileReader()
+      reader.onload = () => {
+        const xml = reader.result as string
+        this.formated = getXmlInfo(xml)
+      }
+      reader.readAsText(file)
     }
   },
   render() {
     return (
       <Form onSubmit={this.submit} onFailed={this.failed}>
         <div class={styles.container}>
-          <ColArea class={styles.area}>
+          <ColFieldGroup class={styles.area}>
+            <ColField required title="MusicXML文件">
+              <Upload
+                onUpdate:modelValue={val => console.log(val)}
+                accept=".xml"
+                formatFile={this.readerFile}
+              />
+            </ColField>
+          </ColFieldGroup>
+          <ColFieldGroup class={styles.area}>
             <ColField required title="曲目名称">
               <Field
+                clearable
                 name="musicSheetName"
                 modelValue={this.musicSheetName}
                 rules={[{ required: true, message: '请输入曲目名称' }]}
@@ -38,16 +87,21 @@ export default defineComponent({
             </ColField>
             <ColField required title="作曲人">
               <Field
+                clearable
                 class={styles['clear-px']}
                 placeholder="请输入作曲人姓名"
+                name="composer"
+                modelValue={this.composer}
+                rules={[{ required: true, message: '请输入作曲人姓名' }]}
+                onUpdate:modelValue={val => (this.composer = val)}
               />
             </ColField>
-          </ColArea>
-          <ColArea class={styles.area}>
+          </ColFieldGroup>
+          <ColFieldGroup class={styles.area}>
             <ColField
               required
               title="曲目标签"
-              v-slos={{
+              v-slots={{
                 right: () => (
                   <Button
                     class={styles.select}
@@ -61,36 +115,73 @@ export default defineComponent({
                 )
               }}
             >
-              <Field class={styles['clear-px']} placeholder="请输入曲目名称" />
-            </ColField>
-          </ColArea>
-          <ColArea class={styles.area}>
-            <ColField required title="默认速度">
-              <Field class={styles['clear-px']} placeholder="请输入默认速度" />
-            </ColField>
-          </ColArea>
-          <ColArea class={styles.area}>
-            <ColField required title="默认速度">
-              <Field class={styles['clear-px']} placeholder="请输入默认速度" />
+              {this.tags.length > 0 &&
+                this.tags.map((item: any) => (
+                  <Tag
+                    type="primary"
+                    size="large"
+                    closeable
+                    onClose={() => console.log(item)}
+                  >
+                    {item}
+                  </Tag>
+                ))}
             </ColField>
-          </ColArea>
-          <ColArea class={styles.area}>
+          </ColFieldGroup>
+          <ColFieldGroup class={styles.area}>
             <ColField required title="默认速度">
-              <Field class={styles['clear-px']} placeholder="请输入默认速度" />
+              <Field
+                clearable
+                name="playSpeed"
+                modelValue={this.speed}
+                rules={[{ required: true, message: '请输入默认速度' }]}
+                onUpdate:modelValue={val => (this.speed = val)}
+                class={styles['clear-px']}
+                placeholder="请输入默认速度"
+              />
             </ColField>
-          </ColArea>
-          <ColArea class={styles.area}>
-            <ColField required title="收费价格">
-              <Field class={styles['clear-px']} placeholder="请输入收费价格" />
+            <ColField required title="是否收费">
+              <RadioGroup
+                class={styles['radio-group']}
+                modelValue={this.chargeType}
+                onUpdate:modelValue={val => (this.chargeType = val)}
+              >
+                {Object.keys(teacherChargeType).map((item: string) => {
+                  const isActive = item === this.chargeType
+                  const type = isActive ? 'primary' : 'default'
+                  return (
+                    <Radio class={styles.radio} name={item}>
+                      <Tag size="large" plain={isActive} type={type}>
+                        {teacherChargeType[item]}
+                      </Tag>
+                    </Radio>
+                  )
+                })}
+              </RadioGroup>
             </ColField>
-          </ColArea>
-          <div class={styles.rule}>
-            <p>扣除手续费后该曲目预计收入为:</p>
-            <p>
-              单课时:<span>5.8</span>元/人
-            </p>
-            <p>您的乐谱收入将在学员购买后结算到您的账户中</p>
-          </div>
+            {this.chargeType === '2' && (
+              <ColField required title="收费价格">
+                <Field
+                  clearable
+                  class={styles['clear-px']}
+                  placeholder="请输入收费价格"
+                  v-slots={{ button: () => '元' }}
+                  modelValue={this.musicPrice}
+                  rules={[{ required: true, message: '请输入收费价格' }]}
+                  onUpdate:modelValue={val => (this.musicPrice = val)}
+                />
+              </ColField>
+            )}
+          </ColFieldGroup>
+          {this.chargeType === '2' && (
+            <div class={styles.rule}>
+              <p>扣除手续费后该曲目预计收入为:</p>
+              <p>
+                单课时:<span>5.8</span>元/人
+              </p>
+              <p>您的乐谱收入将在学员购买后结算到您的账户中</p>
+            </div>
+          )}
         </div>
         <Sticky offsetBottom={0} position="bottom">
           <div class={styles['button-area']}>

+ 61 - 0
src/teacher/music/upload/upload.tsx

@@ -0,0 +1,61 @@
+import { defineComponent } from 'vue'
+import { Uploader, Button } from 'vant'
+import request from '@/helpers/request'
+
+export default defineComponent({
+  name: 'Upload',
+  props: {
+    accept: {
+      type: String
+    },
+    formatFile: {
+      type: Function,
+      default: (file: any) => file
+    },
+    'onUpdate:modelValue': {
+      type: Function,
+      default: (val: any) => {}
+    }
+  },
+  data() {
+    return {
+      list: [],
+      uploading: false
+    }
+  },
+  methods: {
+    async beforeRead(file: File): Promise<boolean> {
+      console.log('beforeRead', file)
+      return true
+    },
+    async upload(file: File) {
+      this.uploading = true
+      const form = new FormData()
+      form.append('file', file)
+      try {
+        const res = await request.post('/api-teacher/uploadFile', {
+          data: form
+        })
+        this.$emit('update:modelValue', res.data.url)
+      } catch (error) {}
+      this.uploading = false
+    }
+  },
+  render() {
+    return (
+      <Uploader
+        accept={this.accept}
+        maxCount={1}
+        modelValue={this.list}
+        beforeDelete={this.beforeRead}
+        onUpdate:modelValue={async val => {
+          await this.upload(val[0].file)
+          this.list = val
+          this.formatFile(val[0].file)
+        }}
+      >
+        <Button loading={this.uploading}>上传文件</Button>
+      </Uploader>
+    )
+  }
+})