Explorar el Código

feat: 优化多个功能模块的用户体验

1. 营期管理优化:
   - 新增公司下拉框全选/取消全选功能
   - 优化新增/修改营期表单的公司选择体验

2. 课程管理优化:
   - 课程关联公司选择添加全选功能
   - 优化公司选择的布局和交互

3. 图片上传组件增强:
   - 集成图片素材库选择功能
   - 新增'从素材库选择'按钮
   - 支持素材分组管理和批量上传
   - 优化按钮布局,使用flex布局

4. 课题选择优化:
   - 课程目录详情页面的课题列表新增序号列
   - 在题目标题前显示序号标识
   - 确保题目绑定准确性,避免选择错误
xw hace 3 semanas
padre
commit
98eeca8438

+ 295 - 21
src/components/ImageUpload/index.vue

@@ -1,22 +1,36 @@
 <template>
   <div class="component-upload-image">
-    <el-upload
-      :action="uploadImgUrl"
-      list-type="picture-card"
-      :on-success="handleUploadSuccess"
-      :before-upload="handleBeforeUpload"
-      :limit="limit"
-      :on-error="handleUploadError"
-      :on-exceed="handleExceed"
-      name="file"
-      :on-remove="handleRemove"
-      :show-file-list="true"
-      :file-list="fileList"
-      :on-preview="handlePictureCardPreview"
-      :class="{hide: this.fileList.length >= this.limit}"
-    >
-      <i class="el-icon-plus"></i>
-    </el-upload>
+    <div style="display: flex; align-items: flex-start; gap: 10px;">
+      <el-upload
+        :action="uploadImgUrl"
+        list-type="picture-card"
+        :on-success="handleUploadSuccess"
+        :before-upload="handleBeforeUpload"
+        :limit="limit"
+        :on-error="handleUploadError"
+        :on-exceed="handleExceed"
+        name="file"
+        :on-remove="handleRemove"
+        :show-file-list="true"
+        :file-list="fileList"
+        :on-preview="handlePictureCardPreview"
+        :class="{hide: this.fileList.length >= this.limit}"
+      >
+        <i class="el-icon-plus"></i>
+      </el-upload>
+
+      <!-- 素材库按钮 -->
+      <el-button
+        v-if="showMaterialBtn && fileList.length < limit"
+        type="primary"
+        size="small"
+        icon="el-icon-picture"
+        @click="openMaterialDialog"
+        style="height: 32px;"
+      >
+        从素材库选择
+      </el-button>
+    </div>
 
     <!-- 上传提示 -->
 <!--    <div class="el-upload__tip" slot="tip" v-if="showTip">
@@ -37,13 +51,116 @@
         style="display: block; max-width: 100%; margin: 0 auto"
       />
     </el-dialog>
+
+    <!-- 素材库对话框 -->
+    <el-dialog
+      title="图片素材库"
+      :visible.sync="materialDialogVisible"
+      width="70%"
+      append-to-body
+      @close="closeMaterialDialog"
+    >
+      <el-container style="height: 600px;">
+        <el-aside width="unset">
+          <div style="margin-bottom: 10px">
+            <el-button
+              class="el-icon-plus"
+              size="small"
+              @click="handleAddMaterialGroup()"
+            >
+              添加分组
+            </el-button>
+          </div>
+          <div class="group-list">
+            <div class="group-item" v-for="group in materialGroupList" :key="group.groupId">
+              <el-button @click="selectMaterialGroup(group)" type="primary" plain>{{ group.name }}</el-button>
+            </div>
+          </div>
+        </el-aside>
+        <el-main>
+          <el-card>
+            <div slot="header">
+              <el-row>
+                <el-col :span="12">
+                  <span>{{ currentMaterialGroup.name }}</span>
+                  <span v-if="currentMaterialGroup.groupId > 0">
+                    <el-button size="small" type="text" class="el-icon-edit" style="margin-left: 10px;" @click="handleEditMaterialGroup(currentMaterialGroup)">重命名</el-button>
+                    <el-button size="small" type="text" class="el-icon-delete" style="margin-left: 10px;color: red" @click="handleDeleteMaterialGroup(currentMaterialGroup)">删除</el-button>
+                  </span>
+                </el-col>
+                <el-col :span="12" style="text-align: right;">
+                  <el-upload
+                    :action="uploadImgUrl"
+                    :file-list="[]"
+                    :on-progress="handleMaterialProgress"
+                    :before-upload="handleBeforeUpload"
+                    :on-success="handleMaterialUploadSuccess"
+                    :data="{type: 1}"
+                    multiple
+                  >
+                    <el-button size="small" type="primary">批量上传</el-button>
+                  </el-upload>
+                </el-col>
+              </el-row>
+            </div>
+            <div v-loading="materialTableLoading">
+              <el-alert
+                v-if="materialList.length <= 0"
+                title="暂无数据"
+                type="info"
+                :closable="false"
+                center
+                show-icon
+              />
+              <el-row :gutter="5">
+                <el-checkbox-group v-model="selectedMaterials" :max="limit - fileList.length">
+                  <el-col v-for="(item, index) in materialList" :key="index" :span="4">
+                    <el-card :body-style="{ padding: '5px' }">
+                      <el-image
+                        style="width: 100%; height: 100px"
+                        :src="item.url"
+                        fit="contain"
+                        :preview-src-list="[item.url]"
+                        :z-index="9999"
+                      />
+                      <div>
+                        <el-checkbox class="material-name" :label="item.url">
+                          选择
+                        </el-checkbox>
+                        <el-row>
+                          <el-col :span="24" class="col-do">
+                            <el-button type="text" size="medium" @click="handleDeleteMaterial(item)">删除</el-button>
+                          </el-col>
+                        </el-row>
+                      </div>
+                    </el-card>
+                  </el-col>
+                </el-checkbox-group>
+              </el-row>
+              <pagination
+                v-show="materialTotal > 0"
+                :total="materialTotal"
+                :page.sync="materialQueryParams.pageNum"
+                :limit.sync="materialQueryParams.pageSize"
+                @pagination="getMaterialList"
+              />
+            </div>
+          </el-card>
+        </el-main>
+      </el-container>
+      <span slot="footer" class="dialog-footer">
+        <el-button @click="materialDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmSelectMaterial">确 定</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import { getToken } from "@/utils/auth";
-
 import { Loading } from 'element-ui';
+import { listMaterial, delMaterial, addMaterial } from "@/api/store/material";
+import { getAllMaterialGroup, delMaterialGroup, addMaterialGroup, updateMaterialGroup } from "@/api/store/materialGroup";
 
 export default {
   props: {
@@ -67,20 +184,40 @@ export default {
     isShowTip: {
       type: Boolean,
       default: true
+    },
+    // 是否显示素材库按钮
+    showMaterialBtn: {
+      type: Boolean,
+      default: true
     }
   },
   data() {
     return {
-      finalQuality:1,
+      finalQuality: 1,
       dialogImageUrl: "",
       dialogVisible: false,
       hideUpload: false,
       baseUrl: process.env.VUE_APP_BASE_API,
-      uploadImgUrl: process.env.VUE_APP_BASE_API+"/common/uploadOSS", // 上传的图片服务器地址
+      uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
       },
-      fileList: []
+      fileList: [],
+      // 素材库相关数据
+      materialDialogVisible: false,
+      materialGroupList: [],
+      currentMaterialGroup: {},
+      materialList: [],
+      materialTotal: 0,
+      materialQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        type: '1',
+        groupId: null
+      },
+      materialTableLoading: false,
+      selectedMaterials: [],
+      materialUploadResultNumber: 0
     };
   },
   watch: {
@@ -275,6 +412,127 @@ export default {
         strs += list[i].url.replace(this.baseUrl, "") + separator;
       }
       return strs != '' ? strs.substr(0, strs.length - 1) : '';
+    },
+    /** 打开素材库对话框 */
+    openMaterialDialog() {
+      this.materialDialogVisible = true;
+      this.getAllMaterialGroups();
+      this.getMaterialList();
+    },
+    /** 关闭素材库对话框 */
+    closeMaterialDialog() {
+      this.selectedMaterials = [];
+    },
+    /** 获取所有素材分组 */
+    getAllMaterialGroups() {
+      getAllMaterialGroup({}).then(response => {
+        this.materialGroupList = response.data;
+      });
+    },
+    /** 选择素材分组 */
+    selectMaterialGroup(group) {
+      this.currentMaterialGroup = group;
+      this.materialQueryParams.groupId = group.groupId;
+      this.getMaterialList();
+    },
+    /** 获取素材列表 */
+    getMaterialList() {
+      this.materialTableLoading = true;
+      listMaterial(this.materialQueryParams).then(response => {
+        this.materialList = response.rows;
+        this.materialTotal = response.total;
+        this.materialTableLoading = false;
+      });
+    },
+    /** 添加素材分组 */
+    handleAddMaterialGroup() {
+      this.$prompt('请输入分组名', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消'
+      }).then(({ value }) => {
+        addMaterialGroup({ name: value }).then(() => {
+          this.$message.success('添加成功');
+          this.currentMaterialGroup = {};
+          this.getAllMaterialGroups();
+        });
+      }).catch(() => {});
+    },
+    /** 编辑素材分组 */
+    handleEditMaterialGroup(group) {
+      this.$prompt('请输入分组名', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        inputValue: group.name
+      }).then(({ value }) => {
+        updateMaterialGroup({
+          groupId: group.groupId,
+          name: value
+        }).then(() => {
+          this.$message.success('修改成功');
+          this.currentMaterialGroup = {};
+          this.getAllMaterialGroups();
+        });
+      }).catch(() => {});
+    },
+    /** 删除素材分组 */
+    handleDeleteMaterialGroup(group) {
+      this.$confirm('是否确认删除该分组?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        delMaterialGroup(group.groupId).then(() => {
+          this.$message.success('删除成功');
+          this.currentMaterialGroup = {};
+          this.getAllMaterialGroups();
+        });
+      }).catch(() => {});
+    },
+    /** 删除素材 */
+    handleDeleteMaterial(item) {
+      this.$confirm('是否确认删除该素材?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        delMaterial(item.materialId).then(() => {
+          this.$message.success('删除成功');
+          this.getMaterialList();
+        });
+      }).catch(() => {});
+    },
+    /** 素材上传进度 */
+    handleMaterialProgress(event, file, fileList) {
+    },
+    /** 素材上传成功 */
+    handleMaterialUploadSuccess(response, file, fileList) {
+      addMaterial({
+        type: '1',
+        groupId: this.materialQueryParams.groupId,
+        name: file.name,
+        url: response.url
+      }).then(() => {
+        this.materialUploadResultNumber++;
+        if (fileList.length === this.materialUploadResultNumber) {
+          this.$message.success('上传成功');
+          this.getMaterialList();
+          this.materialUploadResultNumber = 0;
+        }
+      });
+    },
+    /** 确认选择素材 */
+    confirmSelectMaterial() {
+      if (this.selectedMaterials.length === 0) {
+        this.$message.warning('请选择至少一张图片');
+        return;
+      }
+      // 将选中的素材添加到文件列表
+      this.selectedMaterials.forEach(url => {
+        this.fileList.push({ name: url, url: url });
+      });
+      this.$emit("input", this.listToString(this.fileList));
+      this.materialDialogVisible = false;
+      this.selectedMaterials = [];
     }
   }
 };
@@ -294,5 +552,21 @@ export default {
     opacity: 0;
     transform: translateY(0);
 }
+
+// 素材库样式
+.material-name {
+  padding: 8px 0px;
+}
+.col-do {
+  text-align: center;
+}
+.group-list {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+}
+.group-item {
+  margin: 5px;
+}
 </style>
 

+ 7 - 2
src/views/components/course/userCourseCatalogDetails.vue

@@ -149,12 +149,17 @@
         <el-form-item label="课题选择" prop="questionBankId">
           <el-button size="small" type="primary" @click="chooseQuestionBank">选取课题</el-button>
           <el-table border width="100%" style="margin-top:5px;" :data="form.questionBankList">
-
+            <el-table-column label="序号" align="center" width="80">
+              <template slot-scope="scope">
+                <el-tag size="small" type="primary">{{ scope.row.sort }}</el-tag>
+              </template>
+            </el-table-column>
             <el-table-column label="问题" align="center" prop="title">
               <template slot-scope="scope">
-                <el-tooltip class="item" effect="dark" :content="scope.row.title" placement="top">
+                <el-tooltip class="item" effect="dark" :content="'[序号:' + scope.row.sort + '] ' + scope.row.title" placement="top">
                   <div
                     style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+                    <span style="color: #409EFF; font-weight: 500; margin-right: 8px;">[序号:{{ scope.row.sort }}]</span>
                     <span>{{ scope.row.title }}</span>
                   </div>
                 </el-tooltip>

+ 44 - 9
src/views/course/userCourse/index.vue

@@ -244,14 +244,24 @@
           <ImageUpload v-model="form.imgUrl" type="image" :num="10" :width="150" :height="150"/>
         </el-form-item>
         <el-form-item label="关联公司" prop="tags">
-          <el-select v-model="companyIds" multiple placeholder="请选择公司" filterable clearable style="width: 90%;">
-            <el-option
-              v-for="dict in companyOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-            />
-          </el-select>
+          <div style="display: flex; gap: 10px; align-items: center;">
+            <el-select v-model="companyIds" multiple placeholder="请选择公司" filterable clearable style="flex: 1;">
+              <el-option
+                v-for="dict in companyOptions"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="dict.dictValue"
+              />
+            </el-select>
+            <el-button
+              type="primary"
+              size="small"
+              @click="handleSelectAllCompanies"
+              :disabled="!companyOptions || companyOptions.length === 0"
+            >
+              {{ isAllCompaniesSelected ? '取消全选' : '全选' }}
+            </el-button>
+          </div>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -417,7 +427,17 @@ export default {
           {required: true, message: "小节兑换积分不能为空", trigger: "blur"}
         ],
       }
-    };
+    };  
+  },
+  computed: {
+    /** 判断是否已全选公司 */
+    isAllCompaniesSelected() {
+      if (!this.companyOptions || this.companyOptions.length === 0) {
+        return false;
+      }
+      const allCompanyIds = this.companyOptions.map(item => item.dictValue);
+      return this.companyIds.length === allCompanyIds.length;
+    }
   },
   created() {
     this.getList();
@@ -740,6 +760,21 @@ export default {
         this.msgSuccess("下架成功");
       }).catch(function () {
       });
+    },
+    /** 全选/取消全选公司 */
+    handleSelectAllCompanies() {
+      if (!this.companyOptions || this.companyOptions.length === 0) {
+        this.$message.warning('暂无可选公司');
+        return;
+      }
+
+      if (this.isAllCompaniesSelected) {
+        // 当前已全选,则取消全选
+        this.companyIds = [];
+      } else {
+        // 当前未全选,则全选
+        this.companyIds = this.companyOptions.map(item => item.dictValue);
+      }
     }
   }
 };

+ 79 - 17
src/views/course/userCoursePeriod/index.vue

@@ -94,14 +94,24 @@
             />
           </el-form-item>
           <el-form-item label="公司" prop="companyIdList">
-            <el-select v-model="queryParams.companyIdList" placeholder="请选择公司" clearable size="small" multiple>
-              <el-option
-                v-for="item in companyOptions"
-                :key="item.companyId"
-                :label="item.companyName"
-                :value="item.companyId"
-              />
-            </el-select>
+            <div style="display: flex; gap: 10px; align-items: center;">
+              <el-select v-model="queryParams.companyIdList" placeholder="请选择公司" clearable size="small" multiple style="flex: 1;">
+                <el-option
+                  v-for="item in companyOptions"
+                  :key="item.companyId"
+                  :label="item.companyName"
+                  :value="item.companyId"
+                />
+              </el-select>
+              <el-button
+                type="primary"
+                size="small"
+                @click="handleSelectAllCompanies('query')"
+                :disabled="!companyOptions || companyOptions.length === 0"
+              >
+                {{ isAllCompaniesSelected('query') ? '取消全选' : '全选' }}
+              </el-button>
+            </div>
           </el-form-item>
           <el-form-item label="开营日期开始" prop="periodStartingTime" label-width="120px">
             <el-date-picker clearable size="small" style="width: 200px"
@@ -250,14 +260,24 @@
           <el-input v-model="form.periodName" placeholder="请输入营期名称" />
         </el-form-item>
          <el-form-item label="公司" prop="companyId">
-          <el-select v-model="form.companyId" placeholder="请选择公司" multiple>
-            <el-option
-              v-for="item in companyOptions"
-              :key="item.companyId"
-              :label="item.companyName"
-              :value="item.companyId"
-            />
-          </el-select>
+          <div style="display: flex; gap: 10px; align-items: flex-start;">
+            <el-select v-model="form.companyId" placeholder="请选择公司" multiple style="flex: 1;">
+              <el-option
+                v-for="item in companyOptions"
+                :key="item.companyId"
+                :label="item.companyName"
+                :value="item.companyId"
+              />
+            </el-select>
+            <el-button
+              type="primary"
+              size="small"
+              @click="handleSelectAllCompanies('form')"
+              :disabled="!companyOptions || companyOptions.length === 0"
+            >
+              {{ isAllCompaniesSelected('form') ? '取消全选' : '全选' }}
+            </el-button>
+          </div>
         </el-form-item>
         <el-form-item label="课程风格" prop="courseStyle">
           <image-upload v-model="form.courseStyle" :limit="1" />
@@ -2216,6 +2236,48 @@ export default {
         this.$refs.createCampPeriodForm.clearValidate();
       }
     },
+    /** 全选/取消全选公司 */
+    handleSelectAllCompanies(type) {
+      if (!this.companyOptions || this.companyOptions.length === 0) {
+        this.$message.warning('暂无可选公司');
+        return;
+      }
+
+      if (type === 'form') {
+        // 营期表单中的公司选择
+        if (this.isAllCompaniesSelected('form')) {
+          // 当前已全选,则取消全选
+          this.form.companyId = [];
+        } else {
+          // 当前未全选,则全选
+          this.form.companyId = this.companyOptions.map(item => item.companyId);
+        }
+      } else if (type === 'query') {
+        // 查询条件中的公司选择
+        if (this.isAllCompaniesSelected('query')) {
+          // 当前已全选,则取消全选
+          this.queryParams.companyIdList = [];
+        } else {
+          // 当前未全选,则全选
+          this.queryParams.companyIdList = this.companyOptions.map(item => item.companyId);
+        }
+      }
+    },
+    /** 判断是否已全选公司 */
+    isAllCompaniesSelected(type) {
+      if (!this.companyOptions || this.companyOptions.length === 0) {
+        return false;
+      }
+
+      let selectedCompanies = [];
+      if (type === 'form') {
+        selectedCompanies = this.form.companyId || [];
+      } else if (type === 'query') {
+        selectedCompanies = this.queryParams.companyIdList || [];
+      }
+
+      return selectedCompanies.length === this.companyOptions.length;
+    },
     handleTrainingCampChange(trainingCampId){
       this.otherCampPeriods = [];
       this.createCampPeriodForm.periodId = '';
@@ -2232,7 +2294,7 @@ export default {
       }).catch(error => {
         this.$message.error('查询营期失败: ' + (error.message || '查询营期失败'));
       });
-    }
+    },
   },
 };
 </script>