Переглянути джерело

feat(课程编辑): 添加Toast消息组件和年级班级选择器组件

实现课程编辑页面的Toast消息提示功能,替换原有的$message提示
新增GradeClassSelector组件优化班级选择体验
调整课程名称输入框样式和自适应宽度功能
lsc 1 день тому
батько
коміт
bf368413cf

+ 330 - 0
src/components/common/GradeClassSelector.vue

@@ -0,0 +1,330 @@
+<template>
+  <div class="grade-class-selector">
+    <div class="form-row">
+      <div class="form-item required" style="flex: 2;">
+        <label class="form-label">{{ lang.ssClass }}</label>
+        <div class="class-selector">
+          <div class="select-input" @click="toggleDropdown">
+            <span class="input">{{ getSelectedClassName() || lang.ssSelectClass }}</span>
+            <span class="select-arrow">
+              <svg v-if="!dropdownVisible" t="1776672009773" class="icon" viewBox="0 0 1024 1024" version="1.1"
+                xmlns="http://www.w3.org/2000/svg" p-id="4735" xmlns:xlink="http://www.w3.org/1999/xlink" width="12"
+                height="12">
+                <path
+                  d="M562.5 771c-14.3 14.3-33.7 27.5-52 23.5-18.4 3.1-35.7-11.2-50-23.5L18.8 327.3c-22.4-22.4-22.4-59.2 0-81.6s59.2-22.4 81.6 0L511.5 668l412.1-422.3c22.4-22.4 59.2-22.4 81.6 0s22.4 59.2 0 81.6L562.5 771z"
+                  p-id="4736" fill="#909399" transform="rotate(180 512 512)"></path>
+              </svg>
+              <svg v-else t="1776672009773" class="icon" viewBox="0 0 1024 1024" version="1.1"
+                xmlns="http://www.w3.org/2000/svg" p-id="4735" xmlns:xlink="http://www.w3.org/1999/xlink" width="12"
+                height="12">
+                <path
+                  d="M562.5 771c-14.3 14.3-33.7 27.5-52 23.5-18.4 3.1-35.7-11.2-50-23.5L18.8 327.3c-22.4-22.4-22.4-59.2 0-81.6s59.2-22.4 81.6 0L511.5 668l412.1-422.3c22.4-22.4 59.2-22.4 81.6 0s22.4 59.2 0 81.6L562.5 771z"
+                  p-id="4736" fill="#909399"></path>
+              </svg>
+            </span>
+          </div>
+          <div class="dropdown-menu class-dropdown" v-if="dropdownVisible">
+            <div class="dropdown-content">
+              <div class="content-row">
+                <div class="header-item">年级</div>
+                <div class="grade-list">
+                  <div v-for="grade in gradeOptions" :key="grade.id" class="grade-item"
+                    :class="{ active: selectedGrade === grade.id }" @click="selectGrade(grade)">
+                    {{ grade.name }}
+                  </div>
+                </div>
+              </div>
+              <div class="content-row">
+                <div class="header-item">班级</div>
+                <div class="class-list">
+                  <div v-for="classItem in filteredClassOptions" :key="classItem.id" class="class-item"
+                    :class="{ active: selectedClasses.includes(classItem.id) }" @click="toggleClass(classItem.id)">
+                    {{ classItem.name }}
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'GradeClassSelector',
+  props: {
+    gradeOptions: {
+      type: Array,
+      default: () => []
+    },
+    classOptions: {
+      type: Array,
+      default: () => []
+    },
+    value: {
+      type: Array,
+      default: () => []
+    },
+    lang: {
+      type: Object,
+      required: true
+    }
+  },
+  data() {
+    return {
+      selectedGrade: '',
+      selectedClasses: [],
+      dropdownVisible: false
+    }
+  },
+  computed: {
+    selectedGradeName() {
+      const grade = this.gradeOptions.find(item => item.id === this.selectedGrade);
+      return grade ? grade.name : '';
+    },
+    filteredClassOptions() {
+      if (this.selectedGrade === '') {
+        return this.classOptions;
+      } else {
+        return this.classOptions.filter(item => item.pid === this.selectedGrade);
+      }
+    }
+  },
+  watch: {
+    value: {
+      handler(newVal) {
+        this.selectedClasses = newVal;
+      },
+      immediate: true
+    }
+  },
+  mounted() {
+    // 添加点击外部关闭下拉菜单的事件监听器
+    document.addEventListener('click', this.handleClickOutside);
+  },
+  beforeDestroy() {
+    // 移除事件监听器,避免内存泄漏
+    document.removeEventListener('click', this.handleClickOutside);
+  },
+  methods: {
+    getSelectedClassName() {
+      if (this.selectedClasses.length === 0) {
+        return '';
+      }
+      const names = this.selectedClasses.map(classId => {
+        const classItem = this.classOptions.find(item => item.id === classId);
+        return classItem ? classItem.name : '';
+      }).filter(name => name !== '');
+      return names.join(',');
+    },
+    toggleDropdown() {
+      this.dropdownVisible = !this.dropdownVisible;
+    },
+    selectGrade(grade) {
+      this.selectedGrade = grade.id;
+      // this.selectedClasses = [];
+      // this.$emit('input', this.selectedClasses);
+      this.$emit('gradeChange', this.selectedGrade);
+    },
+    toggleClass(classId) {
+      const index = this.selectedClasses.indexOf(classId);
+      if (index === -1) {
+        this.selectedClasses.push(classId);
+      } else {
+        this.selectedClasses.splice(index, 1);
+      }
+      this.$emit('input', this.selectedClasses);
+    },
+    handleClickOutside(event) {
+      // 检查点击是否发生在下拉菜单外部
+      const classSelector = this.$el.querySelector('.class-selector');
+      if (classSelector && !classSelector.contains(event.target)) {
+        this.dropdownVisible = false;
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.grade-class-selector {
+  width: 100%;
+}
+
+.form-row {
+  display: flex;
+  gap: 15px;
+  width: 100%;
+}
+
+.form-item {
+  flex: 1;
+}
+
+.form-label {
+  display: block;
+  margin-bottom: 8px;
+  font-size: 14px;
+  color: #333;
+}
+
+.grade-selector,
+.class-selector {
+  position: relative;
+  width: 100%;
+}
+
+.select-input {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 30px 8px 12px;
+  border: 1px solid #DCDFE6;
+  border-radius: 4px;
+  cursor: pointer;
+  background-color: #FFFFFF;
+  font-size: 14px;
+  position: relative;
+}
+
+.select-input:hover {
+  border-color: #FF9500;
+}
+
+.select-input .input {
+  width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.select-arrow {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  right: 12px;
+  font-size: 12px;
+  color: #909399;
+}
+
+.select-arrow svg {
+  width: 12px;
+  height: 12px;
+}
+
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  right: 0;
+  margin-top: 4px;
+  border: 1px solid #DCDFE6;
+  border-radius: 10px;
+  background-color: #FFFFFF;
+  z-index: 1000;
+}
+
+.dropdown-item {
+  padding: 10px 15px;
+  cursor: pointer;
+  font-size: 14px;
+}
+
+.dropdown-item:hover {
+  background-color: #FFF5E6;
+}
+
+.dropdown-item.active {
+  background-color: #FFF5E6;
+  color: #FF9500;
+}
+
+.class-dropdown {
+  width: 100%;
+  max-height: 300px;
+  overflow: auto;
+}
+
+.header-item {
+  height: 42px;
+  line-height: 42px;
+  padding: 0 15px;
+  font-size: 14px;
+  font-weight: bold;
+  background-color: #fafbfc;
+  color: #6b7280;
+}
+
+.dropdown-content {
+  height: 200px;
+  display: flex;
+  padding: 0;
+}
+
+.content-row {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.content-row + .content-row {
+  border-left: 1px solid #E4E7ED;
+}
+
+.grade-list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.grade-item {
+  padding: 10px 15px;
+  cursor: pointer;
+  font-size: 14px;
+  color: #374151;
+  position: relative;
+}
+
+.grade-item:hover {
+  background-color: #FFF5E6;
+}
+
+.grade-item.active {
+  background-color: #FFF5E6;
+  color: #FF9500;
+}
+
+.grade-item.active::before {
+  content: '';
+  position: absolute;
+  top: 50%;
+  left: 0;
+  width: 2px;
+  height: 85%;
+  transform: translateY(-50%);
+  border-radius: 2px;
+  background-color: #FF9500;
+}
+
+.class-list {
+  flex: 1;
+  overflow-y: auto;
+}
+
+.class-item {
+  padding: 10px 15px;
+  cursor: pointer;
+  font-size: 14px;
+  color: #374151;
+}
+
+.class-item:hover {
+  background-color: #FFF5E6;
+}
+
+.class-item.active {
+  background-color: #FFF5E6;
+  color: #FF9500;
+}
+</style>

+ 107 - 58
src/components/pages/pptEasy/addCourse3.vue

@@ -9,6 +9,10 @@
       @confirm="handleConfirm"
       @cancel="handleCancel"
     />
+    <toast-message
+      :visible.sync="toastVisible"
+      :message="toastMessage"
+    />
 
     <div class="pb_content_body" style="position: relative; margin: 0">
       <div class="right">
@@ -797,11 +801,12 @@
         </div>
         <div class="modal-body publish-modal-body">
           <div class="course-name-display">
-            <div v-if="!editingCourseName" @click="editingCourseName = true" class="course-name-text">
+            <div v-if="!editingCourseName" @click="startEditCourseName" class="course-name-text">
               {{ courseName || lang.ssUntitledCourse }}
             </div>
             <el-input v-else v-model="courseName" @blur="editingCourseName = false; handleUpdateTitle()"
-              @keyup.enter="editingCourseName = false" class="course-name-input" autofocus></el-input>
+              @keyup.enter="editingCourseName = false" @input="updateInputWidth" ref="courseNameInput"
+              class="course-name-input" :style="{ width: inputWidth + 'px' }" autofocus></el-input>
           </div>
 
           <div class="form-box">
@@ -826,39 +831,14 @@
                   />
                 </div>
               </div>
-              <div class="form-row">
-                <div class="form-item required" style="flex: 2;">
-                  <label class="form-label">{{ lang.ssClass }}</label>
-                  <selectTag
-                    v-model="checkboxList2"
-                    :options="classOptions"
-                    :placeholder=lang.ssSelectClass
-                    style="width: 100%;"
-                  />
-                </div>
-              </div>
-
-              <!-- <div class="form-row">
-                <div class="form-item required">
-                  <label class="form-label">{{ lang.ssGradeType }}</label>
-                  <selectTag
-                    v-model="selectedGrade"
-                    :options="gradeOptions"
-                    :placeholder=lang.ssSelectGrade
-                    style="width: 100%;"
-                  />
-                </div>
+              <GradeClassSelector
+                v-model="checkboxList2"
+                :grade-options="gradeOptions"
+                :class-options="classOptions"
+                :lang="lang"
+                @gradeChange="onGradeChange"
+              />
 
-                <div class="form-item required">
-                  <label class="form-label">{{ lang.ssClass }}</label>
-                  <selectTag
-                    v-model="checkboxList2"
-                    :options="classOptions"
-                    :placeholder=lang.ssSelectClass
-                    style="width: 100%;"
-                  />
-                </div>
-              </div> -->
 
               <div class="form-row">
                 <div class="form-item required">
@@ -968,6 +948,8 @@ import InteractiveToolDialog from "./dialog/InteractiveToolDialog.vue";
 import VideoUploadDialog from "./dialog/VideoUploadDialog2.vue";
 import ConfirmDialog from "../../common/ConfirmDialog";
 import selectTag from "./dialog/selectTag3.vue";
+import ToastMessage from "./dialog/ToastMessage";
+import GradeClassSelector from "../../common/GradeClassSelector";
 
 var OpenCC = require("opencc-js");
 let converter = OpenCC.Converter({
@@ -1022,7 +1004,9 @@ export default {
     BilibiliSearchDialog,
     appDialog,
     ConfirmDialog,
-    selectTag
+    selectTag,
+    ToastMessage,
+    GradeClassSelector
   },
   data() {
     return {
@@ -1044,6 +1028,8 @@ export default {
       confirmConfirmText: '',
       confirmCancelText: '',
       confirmCallback: null,
+      toastVisible: false,
+      toastMessage: '',
       loading: false,
       courseName: "",
       isTeacherSee: false,
@@ -1122,6 +1108,7 @@ export default {
       gradeList: [],
       grade: [],
       grade2: [],
+      gradeArray: [],
       noneBtnImg: false,
       inviteCode: [],
       nbOrder: 0,
@@ -1360,8 +1347,32 @@ export default {
       // 暂时使用模拟数据
       this.classOptions = this.grade2.map(item => ({
         id: item.id,
-        name: item.classname ? item.classname + '-' + item.name : item.name
+        pid: item.pid,
+        name: item.name
       }));
+      
+      // 初始化年级选项,添加全部班级选项
+      this.gradeOptions = [
+        { id: '', name: '全部班级' }
+      ].concat(this.gradeArray.map(item => ({
+        id: item.id,
+        name: item.name
+      })));
+      
+      // 初始化筛选后的班级选项
+      this.filteredClassOptions = this.classOptions;
+    },
+    // 年级选择变化时筛选班级
+    onGradeChange() {
+      if (this.selectedGrade === '') {
+        // 选择了全部班级,显示所有班级
+        this.filteredClassOptions = this.classOptions;
+      } else {
+        // 根据选择的年级筛选班级
+        this.filteredClassOptions = this.classOptions.filter(item => item.pid === this.selectedGrade);
+      }
+      // 清空已选择的班级,因为筛选后班级列表可能已经改变
+      // this.checkboxList2 = [];
     },
     // 确认发布
     confirmPublish() {
@@ -1711,6 +1722,40 @@ export default {
         this.role
       );
     },
+    showToast(message) {
+      this.toastMessage = message;
+      this.toastVisible = true;
+    },
+    updateInputWidth() {
+      // 创建一个隐藏的span元素来计算文字宽度
+      const span = document.createElement('span');
+      span.style.fontSize = '14px'; // 与输入框字体大小一致
+      span.style.fontFamily = 'Arial, sans-serif'; // 与输入框字体一致
+      span.style.whiteSpace = 'nowrap';
+      span.style.visibility = 'hidden';
+      span.style.position = 'absolute';
+      span.textContent = this.courseName;
+      document.body.appendChild(span);
+      
+      // 计算文字宽度,加上10px的padding
+      let width = span.offsetWidth + 10;
+      
+      // 确保宽度在最小和最大范围内
+      width = Math.max(width, 120); // 最小宽度:4个汉字 + 10px
+      width = Math.min(width, 500); // 最大宽度:500px
+      
+      this.inputWidth = width;
+      
+      // 移除临时span元素
+      document.body.removeChild(span);
+    },
+    startEditCourseName() {
+      this.editingCourseName = true;
+      // 在下一个DOM更新周期后计算宽度
+      this.$nextTick(() => {
+        this.updateInputWidth();
+      });
+    },
     //获取ppt的数据
     async getPPtJson() {
       const checkLoaded = async (resolve) => {
@@ -1942,10 +1987,7 @@ export default {
           this.uploadWorkLoading = false;
           console.log(this.steps);
           // if (this.steps != 1 && this.steps != 2 && this.steps != 3) {
-          this.$message({
-            message: this.lang.ssAddSucc,
-            type: "success"
-          });
+          this.showToast(this.lang.ssSaveSuccess);
           // }
           this.number = res.data.ordernumber;
           this.courseId = res.data.courseId;
@@ -2054,17 +2096,18 @@ export default {
         .then(res => {
           // if (this.steps != 1 && this.steps != 2 && this.steps != 3) {
           this.uploadWorkLoading = false;
-          if (this.cidType == 1) {
-            this.$message({
-              message: this.lang.ssModifySuccess,
-              type: "success"
-            });
-          } else {
-            this.$message({
-              message: this.lang.ssAddSucc,
-              type: "success"
-            });
-          }
+          // if (this.cidType == 1) {
+          //   this.$message({
+          //     message: this.lang.ssSaveSuccess,
+          //     type: "success"
+          //   });
+          // } else {
+          //   this.$message({
+          //     message: this.lang.ssSaveSuccess,
+          //     type: "success"
+          //   });
+          // }
+          this.showToast(this.lang.ssSaveSuccess);
           // }
           this.number = this.nbOrder;
           this.courseId = this.cid;
@@ -2108,6 +2151,7 @@ export default {
             this.grade = res.data[0];
           }
           this.grade2 = res.data[0];
+          this.gradeArray =  res.data[1];
           this.classJuri = res.data[0];
           let _check = [];
           let _check2 = [];
@@ -2122,6 +2166,9 @@ export default {
             }
           }
           this.checkAll = _check2.length === _check.length;
+          
+          // 初始化年级和班级选项
+          this.loadClassOptions();
         })
         .catch(err => {
           this.isLoading = false;
@@ -5042,15 +5089,17 @@ export default {
 }
 
 .course-name-input {
-  width: 80%;
+  display: inline-block;
+  width: auto;
+  min-width: 120px; /* 假设每个汉字宽度约为 27.5px,4个汉字 + 10px = 120px */
+  max-width: 500px;
   margin: 0 auto;
   text-align: center;
 }
 
-.course-name-input .el-input__inner {
-  font-size: 18px;
-  font-weight: 600;
-  text-align: center;
+.course-name-input >>> .el-input__inner {
+  background-color: #fff8f0;
+  border: 1px solid #f89a3d !important;
 }
 
 .publish-course-modal .form-box {
@@ -5368,8 +5417,8 @@ export default {
 }
 
 .web-search-footer .btn-cancel:hover {
-  border-color: #c6e2ff;
-  color: #409eff;
+  border-color: #ff9500;
+  color: #ff9500;
 }
 
 .web-search-footer .btn-confirm {

+ 95 - 0
src/components/pages/pptEasy/dialog/ToastMessage.vue

@@ -0,0 +1,95 @@
+<template>
+  <div class="toast_message" v-if="visible">
+    <div class="toast_content">
+      <span>{{ message }}</span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ToastMessage',
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    message: {
+      type: String,
+      default: ''
+    },
+    duration: {
+      type: Number,
+      default: 2000
+    }
+  },
+  data() {
+    return {
+      timer: null
+    }
+  },
+  watch: {
+    visible: {
+      handler(newVal) {
+        if (newVal) {
+          this.clearTimer();
+          this.timer = setTimeout(() => {
+            this.$emit('update:visible', false);
+          }, this.duration);
+        }
+      },
+      immediate: true
+    }
+  },
+  beforeDestroy() {
+    this.clearTimer();
+  },
+  methods: {
+    clearTimer() {
+      if (this.timer) {
+        clearTimeout(this.timer);
+        this.timer = null;
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.toast_message {
+  position: fixed;
+  top: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 9999;
+}
+
+.toast_content {
+  background-color: rgba(0, 0, 0, 0.7);
+  color: #FFFFFF;
+  padding: 10px 20px;
+  border-radius: 10px;
+  font-size: 14px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+  animation: fadeInOut 2s ease-in-out;
+}
+
+@keyframes fadeInOut {
+  0% {
+    opacity: 0;
+    transform: translateY(-20px);
+  }
+  10% {
+    opacity: 1;
+    transform: translateY(0);
+  }
+  90% {
+    opacity: 1;
+    transform: translateY(0);
+  }
+  100% {
+    opacity: 0;
+    transform: translateY(-20px);
+  }
+}
+</style>