Procházet zdrojové kódy

Merge branch 'master' of http://60.205.254.193:3000/GCLS/GCLS_Page_Textbook

dusenyao před 1 rokem
rodič
revize
ada1700731
27 změnil soubory, kde provedl 1032 přidání a 63 odebrání
  1. 1 1
      src/api/app.js
  2. 3 0
      src/icons/svg/components/delete-black.svg
  3. 3 0
      src/icons/svg/components/mark.svg
  4. 3 0
      src/icons/svg/components/note.svg
  5. 4 0
      src/icons/svg/components/progress.svg
  6. 18 0
      src/styles/element.scss
  7. 1 0
      src/styles/variables.scss
  8. 50 19
      src/views/book/courseware/create/components/base/audio/Audio.vue
  9. 188 11
      src/views/book/courseware/create/components/base/audio/AudioSetting.vue
  10. 278 0
      src/views/book/courseware/create/components/base/common/UploadFile.vue
  11. 2 2
      src/views/book/courseware/create/components/base/divider/Divider.vue
  12. 4 4
      src/views/book/courseware/create/components/base/divider/DividerSetting.vue
  13. 18 0
      src/views/book/courseware/create/components/base/label/Label.vue
  14. 40 0
      src/views/book/courseware/create/components/base/picture/Picture.vue
  15. 231 0
      src/views/book/courseware/create/components/base/picture/PictureSetting.vue
  16. 1 1
      src/views/book/courseware/create/components/base/spacing/Spacing.vue
  17. 3 3
      src/views/book/courseware/create/components/base/spacing/SpacingSetting.vue
  18. 67 0
      src/views/book/courseware/create/components/common/FillDescribe.vue
  19. 0 1
      src/views/book/courseware/create/components/common/ModuleBase.vue
  20. 7 6
      src/views/book/courseware/create/components/common/ModuleMixin.js
  21. 5 5
      src/views/book/courseware/create/components/common/SettingMixin.js
  22. 11 3
      src/views/book/courseware/data/audio.js
  23. 4 2
      src/views/book/courseware/data/bookType.js
  24. 67 2
      src/views/book/courseware/data/common.js
  25. 3 2
      src/views/book/courseware/data/divider.js
  26. 18 0
      src/views/book/courseware/data/picture.js
  27. 2 1
      src/views/book/courseware/data/spacing.js

+ 1 - 1
src/api/app.js

@@ -72,7 +72,7 @@ export async function fileUpload(
     formData = file;
   } else {
     formData = new FormData();
-    formData.append(file.filename, file.file, file.file.name);
+    formData.append(file.name, file.raw, file.name);
   }
 
   let onUploadProgress = handleUploadProgress || null;

+ 3 - 0
src/icons/svg/components/delete-black.svg

@@ -0,0 +1,3 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7.49899 1.25V0.25L2.49899 0.250001V1.25L0.248993 1.25V2.25H1.12399L1.12399 9C1.12399 9.41421 1.45978 9.75 1.87399 9.75L8.12399 9.75C8.53821 9.75 8.87399 9.41421 8.87399 9V2.25L9.74899 2.25V1.25L7.49899 1.25ZM2.12399 8.75L2.12399 2.25L7.87399 2.25V8.75L2.12399 8.75ZM3.49899 3.5V7.25H4.49899V3.5H3.49899ZM5.49899 3.5V7.25H6.49899V3.5H5.49899Z" fill="#4E5969"/>
+</svg>

+ 3 - 0
src/icons/svg/components/mark.svg

@@ -0,0 +1,3 @@
+<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 10H1C0.72386 10 0.5 9.77615 0.5 9.5V0.5C0.5 0.22386 0.72386 0 1 0H9C9.27615 0 9.5 0.22386 9.5 0.5V9.5C9.5 9.77615 9.27615 10 9 10ZM8.5 9V1H1.5V9H8.5ZM3 2.5H7V3.5H3V2.5ZM3 4.5H7V5.5H3V4.5ZM3 6.5H5.5V7.5H3V6.5Z" fill="black"/>
+</svg>

+ 3 - 0
src/icons/svg/components/note.svg

@@ -0,0 +1,3 @@
+<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M4 7.0234V0H9.33333V1.33333H5.33333V9.33333C5.33333 10.8061 4.1394 12 2.66667 12C1.19391 12 0 10.8061 0 9.33333C0 7.8606 1.19391 6.66667 2.66667 6.66667C3.1524 6.66667 3.6078 6.79653 4 7.0234ZM2.66667 10.6667C3.40307 10.6667 4 10.0697 4 9.33333C4 8.59693 3.40307 8 2.66667 8C1.93029 8 1.33333 8.59693 1.33333 9.33333C1.33333 10.0697 1.93029 10.6667 2.66667 10.6667Z" fill="black"/>
+</svg>

+ 4 - 0
src/icons/svg/components/progress.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="8" cy="8" r="6" stroke="#94BFFF" stroke-width="4"/>
+<path d="M8 2C8.94686 2 9.88028 2.22409 10.7239 2.65396C11.5676 3.08383 12.2975 3.70726 12.8541 4.47329C13.4107 5.23932 13.778 6.12619 13.9261 7.06139C14.0743 7.9966 13.9989 8.95358 13.7063 9.8541" stroke="#165DFF" stroke-width="4"/>
+</svg>

+ 18 - 0
src/styles/element.scss

@@ -14,3 +14,21 @@ div.el-table {
 .el-pagination {
   text-align: right;
 }
+
+label.el-radio {
+  margin-right: 24px;
+  color: $font-color;
+
+  .el-radio__label {
+    padding-left: 8px;
+  }
+
+  .el-radio__input.is-checked + .el-radio__label {
+    color: $font-color;
+  }
+
+  .el-radio__input.is-checked .el-radio__inner {
+    background-color: $main-color;
+    border-color: $main-color;
+  }
+}

+ 1 - 0
src/styles/variables.scss

@@ -18,6 +18,7 @@ $error-color: #f2555a;
 $right-color: #30a47d;
 $right-bc-color: #e8f7f2;
 $label-color: #076aff;
+$setting-active-color: #4176FF;
 
 // px
 $header-h: 64px;

+ 50 - 19
src/views/book/courseware/create/components/base/audio/Audio.vue

@@ -1,43 +1,74 @@
 <template>
   <ModuleBase :type="data.type">
     <template #content>
-      <span>音频</span>
-      <el-upload action="none">
-        <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
-        <el-button size="small" type="primary">上传</el-button>
-        <el-divider />
-        <div slot="tip" class="el-upload__tip">
-          支持上传mp3、acc、wma,等格式音频文件,单个文件最大100MB,总文件体积不超1G。
-        </div>
-      </el-upload>
+      <UploadFile
+        id="AudioUploadPage"
+        :moduleData="data"
+        :labelText="labelText"
+        :acceptFileType="acceptFileType"
+        :uploadTip="uploadTip"
+        :iconClass="iconClass"
+      />
     </template>
   </ModuleBase>
 </template>
 
 <script>
 import { getAudioData } from '@/views/book/courseware/data/audio';
-
 import ModuleMixin from '../../common/ModuleMixin';
+import UploadFile from '../common/UploadFile.vue';
 
 export default {
   name: 'AudioPage',
-  components: {},
+  components: { UploadFile },
   mixins: [ModuleMixin],
   data() {
     return {
       data: getAudioData(),
+      file_info_list: [],
+      labelText: '音频',
+      acceptFileType: '.mp3,.acc,.wma',
+      uploadTip: '支持上传mp3、acc、wma,等格式音频文件,单个文件最大100MB,总文件体积不超1G。',
+      iconClass: 'note',
     };
   },
-  computed: {
-    settingStyle() {
-      return {
-        margin: `${this.data.setting.height / 2}px 0`,
-        border: 'none',
-        borderTop: `1px ${this.data.setting.type} #ebebeb`,
-      };
+  computed: {},
+  methods: {
+    onFileChange(file, fileList) {
+      console.log(fileList);
+      this.file_info_list = fileList;
+    },
+    removeFile(file, i) {
+      this.$refs.upload.handleRemove(file);
+      this.file_info_list.splice(i, 1);
+    },
+    beforeAudioUpload(file) {
+      const fileName = file.name;
+      const suffix = fileName.slice(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
+      const isNeedType = ['mp3', 'acc', 'wma'].includes(suffix);
+      if (!isNeedType) {
+        this.$message.error('音频文件只能是 mp3、acc、wma 格式!');
+        return false;
+      }
+
+      const isLt100M = file.size / 1024 / 1024 < 100;
+      if (!isLt100M) {
+        this.$message.error('单个音频文件大小不能超过 100MB!');
+        return false;
+      }
+
+      const isExceed1GB =
+        file.size + (this.$refs.upload.uploadFiles || []).reduce((acc, cur) => acc + cur.size, 0) > 1024 * 1024 * 1024;
+      // 限制总文件大小为 1GB
+      if (isExceed1GB) {
+        this.$message.error('总文件大小不能超过 1GB');
+        return false;
+      }
+    },
+    uploadAudio() {
+      this.$refs.upload.submit();
     },
   },
-  methods: {},
 };
 </script>
 

+ 188 - 11
src/views/book/courseware/create/components/base/audio/AudioSetting.vue

@@ -1,17 +1,89 @@
 <template>
   <div>
-    <el-form :model="setting" :label-position="labelPosition" label-width="72px">
+    <el-form :model="property" :label-position="labelPosition" label-width="72px">
       <el-form-item label="序号" class="serial-number">
-        <el-input v-model="setting.question_number" />
-        <SvgIcon icon-class="switch" size="14" />
+        <el-input v-model="property.serial_number" />
+        <SvgIcon icon-class="switch" size="14" @click="switchSerialNumber(property.serial_number)" />
       </el-form-item>
       <el-form-item>
-        <el-radio v-for="{ value, label } in questionNumberTypeList" :key="value" :label="value">
+        <el-radio
+          v-for="{ value, label } in snGenerationMethodList"
+          :key="value"
+          v-model="property.sn_generation_method"
+          :label="value"
+        >
           {{ label }}
         </el-radio>
       </el-form-item>
       <el-form-item label="序号位置">
-        <el-radio v-for="{ value, label } in questionNumberTypeList" :key="value" :label="value">
+        <div class="grid-container">
+          <div class="top">
+            <el-button
+              :class="['top-start', { active: 'top-start' === property.sn_position }]"
+              @click="changeNumberPosition('top-start')"
+            />
+            <el-button
+              :class="['top', { active: 'top' === property.sn_position }]"
+              @click="changeNumberPosition('top')"
+            />
+            <el-button
+              :class="['top-end', { active: 'top-end' === property.sn_position }]"
+              @click="changeNumberPosition('top-end')"
+            />
+          </div>
+          <div class="left">
+            <el-button
+              :class="['left-start', { active: 'left-start' === property.sn_position }]"
+              @click="changeNumberPosition('left-start')"
+            />
+            <el-button
+              :class="['left', { active: 'left' === property.sn_position }]"
+              @click="changeNumberPosition('left')"
+            />
+            <el-button
+              :class="['left-end', { active: 'left-end' === property.sn_position }]"
+              @click="changeNumberPosition('left-end')"
+            />
+          </div>
+          <div class="main"></div>
+          <div class="right">
+            <el-button
+              :class="['right-start', { active: 'right-start' === property.sn_position }]"
+              @click="changeNumberPosition('right-start')"
+            />
+            <el-button
+              :class="['right', { active: 'right' === property.sn_position }]"
+              @click="changeNumberPosition('right')"
+            />
+            <el-button
+              :class="['right-end', { active: 'right-end' === property.sn_position }]"
+              @click="changeNumberPosition('right-end')"
+            />
+          </div>
+          <div class="bottom">
+            <el-button
+              :class="['bottom-start', { active: 'bottom-start' === property.sn_position }]"
+              @click="changeNumberPosition('bottom-start')"
+            />
+            <el-button
+              :class="['bottom', { active: 'bottom' === property.sn_position }]"
+              @click="changeNumberPosition('bottom')"
+            />
+            <el-button
+              :class="['bottom-end', { active: 'bottom-end' === property.sn_position }]"
+              @click="changeNumberPosition('bottom-end')"
+            />
+          </div>
+        </div>
+      </el-form-item>
+      <el-divider />
+      <el-form-item label="查看方式">
+        <el-radio
+          v-for="{ value, label } in audioViewMethodList"
+          :key="value"
+          v-model="property.view_method"
+          :label="value"
+        >
           {{ label }}
         </el-radio>
       </el-form-item>
@@ -21,20 +93,53 @@
 
 <script>
 import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+import { snGenerationMethodList, audioViewMethodList, switchSerialNumber } from '@/views/book/courseware/data/common';
 
 export default {
   name: 'AudioSetting',
   mixins: [SettingMixin],
   data() {
     return {
+      switchSerialNumber,
+      snGenerationMethodList,
+      audioViewMethodList,
       labelPosition: 'left',
-      setting: {
-        height: 100,
-        type: 'solid',
+      property: {
+        serial_number: 1, // 序号
+        sn_position: 'top-start', // 序号位置:top-start top top-end 等
+        sn_generation_method: snGenerationMethodList[0].value, // 序号生成方式:recalculate 重新计算follow 跟随
+        view_method: audioViewMethodList[0].value, // 查看方式:independent 独立 list 列表icon 图标
       },
     };
   },
-  methods: {},
+  watch: {
+    setting: {
+      handler(val) {
+        if (this.isSet) {
+          this.$emit('updateSetting', val);
+        }
+      },
+      deep: true,
+    },
+  },
+  methods: {
+    /**
+     * @description 设置属性
+     * @param {Object} property 属性
+     */
+    setSetting(property) {
+      this.isSet = true;
+      this.property = property;
+    },
+
+    /**
+     * @description 改变序号位置
+     * @param {String} sn_position
+     */
+    changeNumberPosition(sn_position) {
+      this.property.sn_position = sn_position;
+    },
+  },
 };
 </script>
 
@@ -43,14 +148,86 @@ export default {
   .serial-number {
     :deep .el-form-item__content {
       display: flex;
-      column-gap: 16px;
       align-items: center;
+      justify-content: space-between;
     }
   }
 
   .el-input {
-    width: 100px;
     margin-right: 16px;
   }
+
+  .top {
+    grid-area: top;
+    column-gap: 8px;
+    justify-content: center;
+  }
+
+  .left {
+    flex-direction: column;
+    grid-area: left;
+    row-gap: 4px;
+  }
+
+  .main {
+    grid-area: main;
+  }
+
+  .right {
+    flex-direction: column;
+    grid-area: right;
+    row-gap: 4px;
+  }
+
+  .bottom {
+    grid-area: bottom;
+    column-gap: 8px;
+    justify-content: center;
+  }
+
+  .grid-container {
+    display: grid;
+    grid:
+      'top top top top top top'
+      'left main main main main right'
+      'bottom bottom bottom bottom bottom bottom';
+    width: 134px;
+    height: 80px;
+    padding: 8px 8px 0;
+    line-height: 10px;
+    border: 1px solid #ebebeb;
+
+    div {
+      display: flex;
+      margin: 0 auto;
+      text-align: center;
+      background-color: rgba(255, 255, 255, 8%);
+
+      .el-button {
+        width: 16px;
+        height: 8px;
+        padding: 0;
+        margin: 0;
+        border: 1px solid #e4e4e4;
+        border-radius: 2px;
+
+        &.active {
+          background-color: $setting-active-color;
+        }
+      }
+    }
+
+    .main {
+      width: 64px;
+      height: 32px;
+      background-color: #f8f8f8;
+      border: 1px solid #e4e4e4;
+      border-radius: 2px;
+    }
+  }
+
+  .el-divider {
+    margin: 16px 0;
+  }
 }
 </style>

+ 278 - 0
src/views/book/courseware/create/components/base/common/UploadFile.vue

@@ -0,0 +1,278 @@
+<template>
+  <div>
+    <div class="file-area">
+      <span class="label-text">{{ labelText }}</span>
+      <div class="upload-box">
+        <el-upload
+          ref="upload"
+          class="file-uploader"
+          action="no"
+          :accept="acceptFileType"
+          :multiple="true"
+          :show-file-list="false"
+          :auto-upload="false"
+          :on-change="onFileChange"
+        >
+          <el-button>选取{{ labelText }}文件</el-button>
+        </el-upload>
+        <el-button size="small" type="primary" @click="uploadFiles">上传</el-button>
+      </div>
+    </div>
+    <el-divider />
+    <div class="upload-tip">{{ uploadTip }}</div>
+    <ul slot="file-list" class="file-list">
+      <li v-for="(file, i) in file_info_list" :key="i">
+        <div class="file-name">
+          <span>
+            <SvgIcon :icon-class="iconClass" size="12" />
+            <span>{{ file.name }}</span>
+            <!-- <span>({{ file.size }})</span> -->
+          </span>
+          <span v-show="file.progress > 0">
+            <!-- <SvgIcon icon-class="progress" size="12" /> -->
+            {{ file.progress }}%
+          </span>
+        </div>
+        <SvgIcon icon-class="delete-black" size="12" @click="removeFile(file, i)" />
+        <SvgIcon v-show="moduleData.type == 'picture'" icon-class="mark" size="12" @click="viewDialog" />
+      </li>
+    </ul>
+
+    <FillDescribe :visible.sync="visible" @fillDescribeToFile="fillDescribeToFile" />
+  </div>
+</template>
+
+<script>
+import ModuleMixin from '../../common/ModuleMixin';
+import { fileUpload, GetFileStoreInfo } from '@/api/app';
+import { conversionSize } from '@/utils/common';
+import FillDescribe from '../../common/FillDescribe';
+
+export default {
+  name: 'UploadFile',
+  components: {
+    FillDescribe,
+  },
+  mixins: [ModuleMixin],
+  props: {
+    // 组件标签
+    labelText: {
+      type: String,
+      default: '',
+    },
+    // 上传支持的文件格式
+    acceptFileType: {
+      type: String,
+      default: '',
+    },
+    // 提示语
+    uploadTip: {
+      type: String,
+      default: '',
+    },
+    // 图标
+    iconClass: {
+      type: String,
+      default: '',
+    },
+    moduleData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      conversionSize,
+      file_info_list: [],
+      visible: false,
+      form: {
+        title: '',
+        describe: '',
+      },
+    };
+  },
+  computed: {},
+  watch: {},
+  methods: {
+    // 显示自定义样式文件列表
+    onFileChange(file, fileList) {
+      this.afterSelectFile(file);
+
+      fileList.forEach((file) => {
+        if (!file.progress || file.progress <= 0) file.progress = 0;
+      });
+      this.file_info_list = fileList;
+    },
+
+    // 删除文件
+    removeFile(file, i) {
+      this.$confirm('是否删除当前文件?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          this.$refs.upload.handleRemove(file);
+          // this.file_info_list.splice(i, 1);
+        })
+        .catch(() => {});
+    },
+
+    // 文件校验
+    afterSelectFile(file) {
+      const fileName = file.name;
+      let singleSizeTip = '文件[' + fileName + ']大小超过 ' + conversionSize(this.moduleData.single_size) + ',被移除!';
+
+      if (file.size > this.moduleData.single_size * 1024 * 1024) {
+        this.$message.error(singleSizeTip);
+        this.$refs.upload.handleRemove(file);
+        return false;
+      }
+
+      const suffix = fileName.slice(fileName.lastIndexOf('.') + 1, fileName.length).toLowerCase();
+      let fileType = [];
+      let typeTip = '';
+
+      if (this.moduleData.type === 'audio') {
+        fileType = ['mp3', 'acc', 'wma'];
+        typeTip = '音频文件只能是 mp3、acc、wma 格式!';
+      } else if (this.moduleData.type === 'picture') {
+        fileType = ['jpg', 'png', 'jpeg'];
+        typeTip = '图片文件只能是 jpg、png、jpeg 格式!';
+      }
+      const isNeedType = fileType.includes(suffix);
+      if (!isNeedType) {
+        typeTip += ',[' + fileName + ']被移除!';
+        this.$message.error(typeTip);
+        this.$refs.upload.handleRemove(file);
+        return false;
+      }
+    },
+
+    // 上传文件
+    uploadFiles() {
+      const files = this.$refs.upload.uploadFiles || [];
+      const totalSize = files.reduce((sum, cur) => sum + Number(cur.size || 0), 0);
+      if (totalSize > this.moduleData.total_size * 1024 * 1024) {
+        this.$message.error('文件总大小不能超过' + conversionSize(this.moduleData.total_size));
+        return false;
+      }
+      var that = this;
+      files.forEach((file) => {
+        fileUpload('Mid', file, {
+          handleUploadProgress: function (progressEvent) {
+            var en = that.file_info_list.find((p) => p.uid == file.uid);
+            var per = Number((progressEvent.progress * 100).toFixed(2) || 0);
+            if (en) {
+              // var _i = that.file_info_list.findIndex((p) => p.uid == file.uid);
+              en.progress = per;
+              // that.$set(that.file_info_list, _i, en);
+              that.$forceUpdate();
+            }
+          },
+        }).then(({ file_info_list }) => {
+          if (file_info_list.length > 0) {
+            // console.log(file_info_list);
+            this.file_info_list = file_info_list;
+          }
+        });
+      });
+    },
+
+    // 显示弹窗
+    viewDialog() {
+      this.visible = true;
+    },
+
+    //给文件加介绍
+    fillDescribeToFile() {},
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.module-content {
+  .file-area {
+    display: flex;
+    column-gap: 16px;
+    align-items: center;
+
+    .label-text {
+      font-size: 14px;
+      color: #4e5969;
+    }
+
+    div {
+      flex: 1;
+    }
+  }
+
+  .el-divider {
+    margin: 16px 0;
+  }
+
+  .upload-tip {
+    margin-bottom: 16px;
+    font-size: 12px;
+    color: #86909c;
+  }
+
+  .upload-box {
+    display: flex;
+    justify-content: space-between;
+
+    .file-uploader {
+      flex: 1;
+
+      :deep .el-upload {
+        &--text {
+          width: 100%;
+          background-color: #f2f3f5;
+          border-radius: 2px 0 0 2px;
+
+          .el-button {
+            width: 100%;
+            color: #86909c;
+            text-align: left;
+          }
+        }
+      }
+    }
+
+    .el-button {
+      border-radius: 0 2px 2px 0;
+    }
+  }
+
+  .file-list {
+    display: flex;
+    flex-direction: column;
+    row-gap: 16px;
+
+    li {
+      display: flex;
+      column-gap: 12px;
+      align-items: center;
+
+      .file-name {
+        display: flex;
+        column-gap: 14px;
+        align-items: center;
+        justify-content: space-between;
+        width: 360px;
+        max-width: 360px;
+        padding: 8px 12px;
+        font-size: 14px;
+        color: #1d2129;
+        background-color: #f7f8fa;
+
+        span {
+          display: flex;
+          column-gap: 14px;
+          align-items: center;
+        }
+      }
+    }
+  }
+}
+</style>

+ 2 - 2
src/views/book/courseware/create/components/base/divider/Divider.vue

@@ -22,9 +22,9 @@ export default {
   computed: {
     settingStyle() {
       return {
-        margin: `${this.data.setting.height / 2}px 0`,
+        margin: `${this.data.property.height / 2}px 0`,
         border: 'none',
-        borderTop: `1px ${this.data.setting.type} #ebebeb`,
+        borderTop: `1px ${this.data.property.line_type} #ebebeb`,
       };
     },
   },

+ 4 - 4
src/views/book/courseware/create/components/base/divider/DividerSetting.vue

@@ -1,11 +1,11 @@
 <template>
   <div>
-    <el-form :model="setting" :label-position="labelPosition" label-width="56px">
+    <el-form :model="property" :label-position="labelPosition" label-width="56px">
       <el-form-item label="高度">
-        <el-input v-model="setting.height" />
+        <el-input v-model="property.height" />
       </el-form-item>
       <el-form-item label="类型">
-        <el-select v-model="setting.type">
+        <el-select v-model="property.line_type">
           <el-option label="实线" value="solid" />
         </el-select>
       </el-form-item>
@@ -22,7 +22,7 @@ export default {
   data() {
     return {
       labelPosition: 'left',
-      setting: {
+      property: {
         height: 100,
         type: 'solid',
       },

+ 18 - 0
src/views/book/courseware/create/components/base/label/Label.vue

@@ -0,0 +1,18 @@
+<template>
+  <ModuleBase :type="标签">
+    <template #content>
+      <div>农行</div>
+    </template>
+  </ModuleBase>
+</template>
+<script>
+export default {
+  name: 'LabelPage',
+  data() {
+    return {
+    };
+  },
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 40 - 0
src/views/book/courseware/create/components/base/picture/Picture.vue

@@ -0,0 +1,40 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <UploadFile
+        id="PictureUploadPage"
+        :moduleData="data"
+        :labelText="labelText"
+        :acceptFileType="acceptFileType"
+        :uploadTip="uploadTip"
+        :iconClass="iconClass"
+      />
+    </template>
+  </ModuleBase>
+</template>
+
+<script>
+import { getPictureData } from '@/views/book/courseware/data/picture';
+import ModuleMixin from '../../common/ModuleMixin';
+import UploadFile from '../common/UploadFile.vue';
+
+export default {
+  name: 'PicturePage',
+  mixins: [ModuleMixin],
+  components: { UploadFile },
+  data() {
+    return {
+      data: getPictureData(),
+      file_info_list: [],
+      labelText: '图片',
+      acceptFileType: '.jpg,.png,.jpeg',
+      uploadTip: '支持上传jpg、png、jpeg,等格式图片文件,单个文件最大1MB,总文件体积不超100MB。',
+      iconClass: 'picture',
+    };
+  },
+  computed: {},
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped></style>

+ 231 - 0
src/views/book/courseware/create/components/base/picture/PictureSetting.vue

@@ -0,0 +1,231 @@
+<template>
+  <div>
+    <el-form :model="property" :label-position="labelPosition" label-width="72px">
+      <el-form-item label="序号" class="serial-number">
+        <el-input v-model="property.serial_number" />
+        <SvgIcon icon-class="switch" size="14" />
+      </el-form-item>
+      <el-form-item>
+        <el-radio
+          v-for="{ value, label } in snGenerationMethodList"
+          :key="value"
+          v-model="property.sn_generation_method"
+          :label="value"
+        >
+          {{ label }}
+        </el-radio>
+      </el-form-item>
+      <el-form-item label="序号位置">
+        <div class="grid-container">
+          <div class="top">
+            <el-button
+              :class="['top-start', { active: 'top-start' === property.sn_position }]"
+              @click="changeNumberPosition('top-start')"
+            />
+            <el-button
+              :class="['top', { active: 'top' === property.sn_position }]"
+              @click="changeNumberPosition('top')"
+            />
+            <el-button
+              :class="['top-end', { active: 'top-end' === property.sn_position }]"
+              @click="changeNumberPosition('top-end')"
+            />
+          </div>
+          <div class="left">
+            <el-button
+              :class="['left-start', { active: 'left-start' === property.sn_position }]"
+              @click="changeNumberPosition('left-start')"
+            />
+            <el-button
+              :class="['left', { active: 'left' === property.sn_position }]"
+              @click="changeNumberPosition('left')"
+            />
+            <el-button
+              :class="['left-end', { active: 'left-end' === property.sn_position }]"
+              @click="changeNumberPosition('left-end')"
+            />
+          </div>
+          <div class="main"></div>
+          <div class="right">
+            <el-button
+              :class="['right-start', { active: 'right-start' === property.sn_position }]"
+              @click="changeNumberPosition('right-start')"
+            />
+            <el-button
+              :class="['right', { active: 'right' === property.sn_position }]"
+              @click="changeNumberPosition('right')"
+            />
+            <el-button
+              :class="['right-end', { active: 'right-end' === property.sn_position }]"
+              @click="changeNumberPosition('right-end')"
+            />
+          </div>
+          <div class="bottom">
+            <el-button
+              :class="['bottom-start', { active: 'bottom-start' === property.sn_position }]"
+              @click="changeNumberPosition('bottom-start')"
+            />
+            <el-button
+              :class="['bottom', { active: 'bottom' === property.sn_position }]"
+              @click="changeNumberPosition('bottom')"
+            />
+            <el-button
+              :class="['bottom-end', { active: 'bottom-end' === property.sn_position }]"
+              @click="changeNumberPosition('bottom-end')"
+            />
+          </div>
+        </div>
+      </el-form-item>
+      <el-divider />
+      <el-form-item label="查看方式">
+        <el-radio
+          v-for="{ value, label } in viewMethodList"
+          :key="value"
+          v-model="property.view_method"
+          :label="value"
+        >
+          {{ label }}
+        </el-radio>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { snGenerationMethodList, viewMethodList } from '@/views/book/courseware/data/common';
+
+export default {
+  name: 'AudioSetting',
+  data() {
+    return {
+      snGenerationMethodList,
+      viewMethodList,
+      labelPosition: 'left',
+      isSet: false, // 父组件是否已设置
+      property: {
+        serial_number: 1, // 序号
+        sn_position: 'top-start', // 序号位置:top-start top top-end 等
+        sn_generation_method: snGenerationMethodList[0].value, // 序号生成方式:recalculate 重新计算follow 跟随
+        view_method: viewMethodList[0].value, // 查看方式:independent 独立 list 列表icon 图标
+      },
+    };
+  },
+  watch: {
+    setting: {
+      handler(val) {
+        if (this.isSet) {
+          this.$emit('updateSetting', val);
+        }
+      },
+      deep: true,
+    },
+  },
+  methods: {
+    /**
+     * @description 设置属性
+     * @param {Object} setting 属性
+     */
+    setSetting(setting) {
+      this.isSet = true;
+      this.setting = setting;
+    },
+
+    /**
+     * @description 改变序号位置
+     * @param {String} sn_position
+     */
+    changeNumberPosition(sn_position) {
+      this.property.sn_position = sn_position;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.el-form {
+  .serial-number {
+    :deep .el-form-item__content {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+    }
+  }
+
+  .el-input {
+    margin-right: 16px;
+  }
+
+  .top {
+    grid-area: top;
+    column-gap: 8px;
+    justify-content: center;
+  }
+
+  .left {
+    flex-direction: column;
+    grid-area: left;
+    row-gap: 4px;
+  }
+
+  .main {
+    grid-area: main;
+  }
+
+  .right {
+    flex-direction: column;
+    grid-area: right;
+    row-gap: 4px;
+  }
+
+  .bottom {
+    grid-area: bottom;
+    column-gap: 8px;
+    justify-content: center;
+  }
+
+  .grid-container {
+    display: grid;
+    grid:
+      'top top top top top top'
+      'left main main main main right'
+      'bottom bottom bottom bottom bottom bottom';
+    width: 134px;
+    height: 80px;
+    padding: 8px 8px 0;
+    line-height: 10px;
+    border: 1px solid #ebebeb;
+
+    div {
+      display: flex;
+      margin: 0 auto;
+      text-align: center;
+      background-color: rgba(255, 255, 255, 8%);
+
+      .el-button {
+        width: 16px;
+        height: 8px;
+        padding: 0;
+        margin: 0;
+        border: 1px solid #e4e4e4;
+        border-radius: 2px;
+
+        &.active {
+          background-color: $setting-active-color;
+        }
+      }
+    }
+
+    .main {
+      width: 64px;
+      height: 32px;
+      background-color: #f8f8f8;
+      border: 1px solid #e4e4e4;
+      border-radius: 2px;
+    }
+  }
+
+  .el-divider {
+    margin: 16px 0;
+  }
+}
+</style>

+ 1 - 1
src/views/book/courseware/create/components/base/spacing/Spacing.vue

@@ -23,7 +23,7 @@ export default {
   computed: {
     settingStyle() {
       return {
-        height: `${this.data.setting.height}px`,
+        height: `${this.data.property.height}px`,
         width: '100%',
         backgroundColor: '#f0f0f0',
       };

+ 3 - 3
src/views/book/courseware/create/components/base/spacing/SpacingSetting.vue

@@ -1,8 +1,8 @@
 <template>
   <div>
-    <el-form :model="setting" :label-position="labelPosition" label-width="56px">
+    <el-form :model="property" :label-position="labelPosition" label-width="56px">
       <el-form-item label="高度">
-        <el-input v-model="setting.height" />
+        <el-input v-model="property.height" />
       </el-form-item>
     </el-form>
   </div>
@@ -17,7 +17,7 @@ export default {
   data() {
     return {
       labelPosition: 'left',
-      setting: {
+      property: {
         height: 40,
       },
     };

+ 67 - 0
src/views/book/courseware/create/components/common/FillDescribe.vue

@@ -0,0 +1,67 @@
+<template>
+  <el-dialog title="" :visible="visible" width="260px" top="38vh" :show-close="false" @close="dialogClose">
+    <el-input v-model="form.title" autocomplete="off" placeholder="标题"></el-input>
+    <el-input type="textarea" v-model="form.describe" placeholder="介绍"></el-input>
+    <template slot="footer">
+      <el-button size="medium" @click="dialogClose">取消</el-button>
+      <el-button type="primary" size="medium" @click="confirm">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'FillDescribe',
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      form: {
+        title: '',
+        describe: '',
+      },
+    };
+  },
+  methods: {
+    dialogClose() {
+      this.$emit('update:visible', false);
+    },
+    confirm() {
+      this.$emit('update:visible', false);
+      // this.$emit('fillDescribeToFile');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.el-dialog {
+  :deep &__header {
+    padding: 0;
+  }
+
+  :deep &__body {
+    display: flex;
+    flex-direction: column;
+    row-gap: 8px;
+    padding: 8px 8px 0;
+
+    .el-textarea__inner {
+      height: 108px;
+    }
+  }
+
+  :deep &__footer {
+    display: flex;
+    padding: 8px;
+
+    .el-button {
+      flex: 1;
+    }
+  }
+}
+</style>

+ 0 - 1
src/views/book/courseware/create/components/common/ModuleBase.vue

@@ -30,7 +30,6 @@ export default {
   },
   data() {
     return {
-      isShow: false,
       componentNameList,
     };
   },

+ 7 - 6
src/views/book/courseware/create/components/common/ModuleMixin.js

@@ -1,12 +1,13 @@
 // 组件混入
 import ModuleBase from './ModuleBase.vue';
-
-import { questionNumberTypeList } from '@/views/book/courseware/data/common';
+import { snGenerationMethodList, viewMethodList, audioViewMethodList } from '@/views/book/courseware/data/common';
 
 const mixin = {
   data() {
     return {
-      questionNumberTypeList,
+      snGenerationMethodList,
+      viewMethodList,
+      audioViewMethodList,
     };
   },
   props: {
@@ -30,15 +31,15 @@ const mixin = {
      * @description 显示设置
      */
     showSetting() {
-      this.$emit('showSetting', this.data.setting, this.data.type, this.id);
+      this.$emit('showSetting', this.data.property, this.data.type, this.id);
     },
     /**
      * @description 更新属性
      * @param {object} setting 属性
      * @param {string} type 属性类型
      */
-    updateSetting(setting) {
-      this.data.setting = setting;
+    updateSetting(property) {
+      this.data.property = property;
     },
   },
 };

+ 5 - 5
src/views/book/courseware/create/components/common/SettingMixin.js

@@ -1,18 +1,18 @@
-import { questionNumberTypeList } from '../../../data/common';
+import { snGenerationMethodList } from '../../../data/common';
 
 const mixin = {
   data() {
     return {
-      questionNumberTypeList,
+      snGenerationMethodList,
     };
   },
   methods: {
     /**
      * @description 设置属性
-     * @param {object} setting 属性
+     * @param {object} property 属性
      */
-    setSetting(setting) {
-      this.setting = setting;
+    setSetting(property) {
+      this.property = property;
     },
   },
 };

+ 11 - 3
src/views/book/courseware/data/audio.js

@@ -1,10 +1,18 @@
+import { snGenerationMethodList, audioViewMethodList } from '@/views/book/courseware/data/common';
+
 export function getAudioData() {
   return {
+    id: '1',
     type: 'audio',
     title: '音频',
-    setting: {
-      serialNumber: 1,
-      height: 40,
+    single_size: 10, // 单位MB
+    total_size: 1024, // 单位MB
+    property: {
+      serial_number: 1, // 序号
+      sn_type: 'letter', //序号类型:letter字母 number数字  capital大写字母 bracket_number括号数字
+      sn_position: 'top-start', // 序号位置:top-start top top-end,left-start left left-end等
+      sn_generation_method: snGenerationMethodList[0].value, // 序号生成方式:recalculate重新计算 follow 跟随
+      view_method: audioViewMethodList[0].value, // 查看方式:independent独立 list列表 icon图标
     },
   };
 }

+ 4 - 2
src/views/book/courseware/data/bookType.js

@@ -4,6 +4,8 @@ import SpacingPage from '../create/components/base/spacing/Spacing.vue';
 import SpacingSetting from '../create/components/base/spacing/SpacingSetting.vue';
 import AudioPage from '../create/components/base/audio/Audio.vue';
 import AudioSetting from '../create/components/base/audio/AudioSetting.vue';
+import PicturePage from '../create/components/base/picture/Picture.vue';
+import PictureSetting from '../create/components/base/picture/PictureSetting.vue';
 
 export const bookTypeOption = [
   {
@@ -50,8 +52,8 @@ export const bookTypeOption = [
         value: 'picture',
         label: '图片',
         icon: 'picture',
-        component: '',
-        set: '',
+        component: PicturePage,
+        set: PictureSetting,
       },
       {
         value: 'divider',

+ 67 - 2
src/views/book/courseware/data/common.js

@@ -1,5 +1,70 @@
-// 题号类型
-export const questionNumberTypeList = [
+// 序号生成方式
+export const snGenerationMethodList = [
   { value: 'recalculate', label: '重新计算' },
   { value: 'follow', label: '跟随上题' },
 ];
+
+// 查看方式
+export const viewMethodList = [
+  { value: 'independent', label: '独立排放' },
+  { value: 'list', label: '播放列表' },
+];
+
+// 音频查看方式
+export const audioViewMethodList = [
+  { value: 'independent', label: '独立' },
+  { value: 'list', label: '列表' },
+  { value: 'icon', label: '图标' },
+];
+
+// 序号类型
+export const serialNumberTypeList = [
+  { value: 'letter', label: '字母' },
+  { value: 'number', label: '数字' },
+  { value: 'capital', label: '大写字母' },
+  { value: 'bracket_number', label: '括号数字' },
+];
+
+/**
+ * 判断序号类型
+ * @param {string} str
+ */
+export function checkString(str) {
+  const number = /\d/.test(str); // 判断是否包含数字
+  const letter = /[a-zA-Z]/.test(str); // 判断是否包含字母
+  const capital = /[A-Z]/.test(str); // 判断是否包含大写字母
+  const bracket_number = /\(\d+\)/.test(str); // 判断是否包含括号数字,例如 (123)
+  return { number, letter, capital, bracket_number };
+}
+
+/**
+ * 改变选项类型
+ * @param {string} serial_number 序号
+ */
+export function switchSerialNumber(serial_number) {
+  let sn_type = checkString(serial_number);
+  let index = serialNumberTypeList.findIndex(({ value }) => value === sn_type);
+  sn_type = serialNumberTypeList[index + 1]?.value || serialNumberTypeList[0].value;
+}
+
+// 计算选项方法
+export const computeOptionMethods = {
+  [serialNumberTypeList[0].value]: (i) => `${String.fromCharCode(97 + i)}.`,
+  [serialNumberTypeList[1].value]: (i) => `${i + 1}.`,
+  [serialNumberTypeList[2].value]: (i) => `${String.fromCharCode(65 + i)}.`,
+  [serialNumberTypeList[3].value]: (i) => `(${i + 1})`,
+};
+
+/**
+ * 计算选项序号
+ * @param {Number} i 序号
+ * @param {String} sn_type 选项类型
+ * @returns String 题号
+ */
+export function computedSerialNumber(i, sn_type) {
+  const computationMethod = computeOptionMethods[sn_type];
+  if (computationMethod) {
+    return computationMethod(i);
+  }
+  return '';
+}

+ 3 - 2
src/views/book/courseware/data/divider.js

@@ -1,10 +1,11 @@
 export function getDividerData() {
   return {
+    id: '1',
     type: 'divider',
     title: '分割线',
-    setting: {
+    property: {
       height: 40,
-      type: 'solid', // dotted 虚线
+      line_type: 'solid', // dotted 虚线
     },
   };
 }

+ 18 - 0
src/views/book/courseware/data/picture.js

@@ -0,0 +1,18 @@
+import { snGenerationMethodList, viewMethodList } from '@/views/book/courseware/data/common';
+
+export function getPictureData() {
+  return {
+    id: '1',
+    type: 'picture',
+    title: '图片',
+    single_size: 1, // 单位MB
+    total_size: 100, // 单位MB
+    property: {
+      serial_number: 1, // 序号
+      sn_type: 'letter', //序号类型:letter字母 number数字  capital大写字母 bracket_number括号数字
+      sn_position: 'top-start', // 序号位置:top-start top top-end,left-start left left-end等
+      sn_generation_method: snGenerationMethodList[0].value, // 序号生成方式:recalculate重新计算 follow跟随
+      view_method: viewMethodList[0].value, // 查看方式:independent独立 list列表 icon图标
+    },
+  };
+}

+ 2 - 1
src/views/book/courseware/data/spacing.js

@@ -1,8 +1,9 @@
 export function getSpacingData() {
   return {
+    id: '1',
     type: 'spacing',
     title: '间距',
-    setting: {
+    property: {
       height: 40,
     },
   };