Browse Source

训练营

Long 1 month ago
parent
commit
7149b39367
2 changed files with 614 additions and 48 deletions
  1. 37 0
      src/api/course/userCourseCamp.js
  2. 577 48
      src/views/course/userCoursePeriod/index.vue

+ 37 - 0
src/api/course/userCourseCamp.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// 训练营列表
+export function listCamp(query) {
+  return request({
+    url: '/course/trainingCamp/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 新增训练营
+export function addCamp(data) {
+  return request({
+    url: '/course/trainingCamp',
+    method: 'post',
+    data: data
+  })
+}
+
+// 删除训练营
+export function delCamp(trainingCampId) {
+  return request({
+    url: `/course/trainingCamp/${trainingCampId}`,
+    method: 'delete'
+  })
+}
+
+// 复制训练营
+export function copyCamp(trainingCampId) {
+  return request({
+    url: `/course/trainingCamp/copy/${trainingCampId}`,
+    method: 'post'
+  })
+}
+
+

+ 577 - 48
src/views/course/userCoursePeriod/index.vue

@@ -3,46 +3,84 @@
     <el-container>
       <!-- 左侧区域 -->
       <el-aside width="360px" class="left-aside">
-        <!-- 左侧筛选条件 -->
-        <el-form :model="leftQueryParams" ref="leftQueryForm" :inline="true" v-show="showSearch" label-width="84px">
-          <el-form-item label="训练营名称" prop="trainingCampName">
+        <!-- 顶部区域 -->
+        <div class="left-header">
+          <div class="left-header-top">
+            <el-button type="primary" class="search-btn" @click="handleLeftQuery">搜索</el-button>
+            <div class="btn-group">
+              <el-button type="text" @click="showWarning">展示预警训练营</el-button>
+              <el-button type="primary" icon="el-icon-plus" @click="handleAddTrainingCamp">新建训练营</el-button>
+            </div>
+          </div>
+          <div class="search-input-wrapper">
             <el-input
               v-model="leftQueryParams.trainingCampName"
               placeholder="请输入训练营名称"
+              prefix-icon="el-icon-search"
               clearable
               size="small"
               @keyup.enter.native="handleLeftQuery"
             />
-          </el-form-item>
-          <el-form-item>
-            <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleLeftQuery">搜索</el-button>
-            <el-button icon="el-icon-refresh" size="mini" @click="resetLeftQuery">重置</el-button>
-          </el-form-item>
-        </el-form>
+          </div>
+          <div class="sort-wrapper">
+            <span class="sort-label">排序方式</span>
+            <el-select v-model="leftQueryParams.scs"
+            placeholder="按序号倒序"
+            size="small"
+            class="sort-select"
+            @change="handleSortChange"
+            >
+              <el-option label="按序号倒序" value="order_number(desc),training_camp_id(desc)" />
+              <el-option label="按序号顺序" value="order_number(asc),training_camp_id(desc)" />
+            </el-select>
+          </div>
+        </div>
 
-        <!-- 左侧列表 -->
-        <el-table v-loading="leftLoading" :data="leftList" @selection-change="handleLeftSelectionChange">
-          <el-table-column type="selection" width="55" align="center" />
-          <el-table-column label="训练营名称" align="center" prop="trainingCampName" />
-          <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-            <template slot-scope="scope">
-              <el-button
-                size="mini"
-                type="text"
-                icon="el-icon-view"
-                @click="handleView(scope.row)"
-              >查看</el-button>
-            </template>
-          </el-table-column>
-        </el-table>
+        <!-- 训练营列表 -->
+        <div class="camp-list" ref="campList" @scroll="handleScroll">
+          <div
+            v-for="(item, index) in campList"
+            :key="index"
+            class="camp-item"
+            :class="{ 'active': activeCampIndex === index }"
+            @click="selectCamp(index)"
+          >
+            <div class="camp-content">
+              <div class="camp-title">
+                <i class="el-icon-s-flag camp-icon"></i>
+                {{ item.trainingCampName }}
+              </div>
+              <div class="camp-info">
+                <span>序号:{{ item.orderNumber }}</span>
+                <span>最新营期开课:{{ item.recentDate || '-' }}</span>
+              </div>
+              <div class="camp-stats">
+                <span class="stat-item">
+                  <i class="el-icon-s-data"></i>
+                  营期数:{{ item.periodCount || 0 }}
+                </span>
+                <span class="stat-item">
+                  <i class="el-icon-user"></i>
+                  会员总数:{{ item.vipCount || 0 }}
+                </span>
+              </div>
+            </div>
+            <div class="camp-actions">
+              <el-button type="text" class="action-btn delete-btn" @click.stop="handleDeleteCamp(item)">删除</el-button>
+              <el-button type="text" class="action-btn copy-btn" @click.stop="handleCopyCamp(item)">复制</el-button>
+            </div>
+          </div>
+          <!-- 底部加载更多提示 -->
+          <div v-if="loadingMore" class="loading-more">
+            <i class="el-icon-loading"></i>
+            <span>加载中...</span>
+          </div>
 
-        <pagination
-          v-show="leftTotal>0"
-          :total="leftTotal"
-          :page.sync="leftQueryParams.pageNum"
-          :limit.sync="leftQueryParams.pageSize"
-          @pagination="getLeftList"
-        />
+          <!-- 所有数据加载完毕提示 -->
+          <div v-if="campList.length > 0 && !leftQueryParams.hasNextPage && !loadingMore" class="no-more-data">
+            <span>—— 已加载全部训练营 ——</span>
+          </div>
+        </div>
       </el-aside>
 
       <!-- 右侧区域 -->
@@ -241,12 +279,26 @@
         <el-button @click="cancel">取 消</el-button>
       </div>
     </el-dialog>
+
+    <!-- 添加训练营对话框 -->
+    <el-dialog :title="'新建训练营'" :visible.sync="campDialogVisible" width="500px" append-to-body>
+      <el-form ref="campForm" :model="campForm" :rules="campRules" label-width="100px">
+        <el-form-item label="训练营名称" prop="trainingCampName">
+          <el-input v-model="campForm.trainingCampName" placeholder="请输入训练营名称" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitCampForm">确 定</el-button>
+        <el-button @click="cancelCampForm">取 消</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import { listPeriod, getPeriod, delPeriod, addPeriod, updatePeriod, exportPeriod, pagePeriod } from "@/api/course/userCoursePeriod";
 import { getCompanyList } from "@/api/company/company";
+import { listCamp, addCamp, delCamp, copyCamp } from "@/api/course/userCourseCamp";
 
 export default {
   name: "Period",
@@ -289,6 +341,8 @@ export default {
       leftQueryParams: {
         pageNum: 1,
         pageSize: 10,
+        hasNextPage: false,
+        scs: 'order_number(desc),training_camp_id(desc)',
         trainingCampName: null
       },
       // 表单参数
@@ -297,7 +351,28 @@ export default {
       rules: {
       },
       // 公司选项
-      companyOptions: []
+      companyOptions: [],
+      // 训练营列表
+      campList: [],
+      // 激活的训练营索引
+      activeCampIndex: null,
+      // 训练营对话框是否显示
+      campDialogVisible: false,
+      // 训练营表单
+      campForm: {
+        trainingCampName: ''
+      },
+      // 训练营表单校验
+      campRules: {
+        trainingCampName: [
+          { required: true, message: '训练营名称不能为空', trigger: 'blur' },
+          { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
+        ]
+      },
+      // 滚动节流标志
+      scrollThrottle: false,
+      // 加载更多状态
+      loadingMore: false,
     };
   },
   created() {
@@ -319,8 +394,37 @@ export default {
     /** 查询左侧列表 */
     getLeftList() {
       this.leftLoading = true;
-      // TODO: 调用左侧列表接口
-      this.leftLoading = false;
+      // 重置页码和加载更多状态
+      this.leftQueryParams.pageNum = 1;
+      this.loadingMore = false;
+
+      // 训练营数据
+      listCamp(this.leftQueryParams).then(response => {
+        if (response && response.code === 200) {
+          this.campList = response.data.list || [];
+          this.leftQueryParams.hasNextPage = response.data.hasNextPage;
+          this.activeCampIndex = this.campList.length > 0 ? 0 : null;
+
+          // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
+          this.$nextTick(() => {
+            const scrollEl = this.$refs.campList;
+            if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
+              this.loadMoreCamps();
+            }
+          });
+        } else {
+          this.$message.error(response.msg || '获取训练营列表失败');
+          this.campList = [];
+          this.leftQueryParams.hasNextPage = false;
+        }
+        this.leftLoading = false;
+      }).catch(error => {
+        console.error('获取训练营列表失败:', error);
+        this.$message.error('获取训练营列表失败');
+        this.campList = [];
+        this.leftQueryParams.hasNextPage = false;
+        this.leftLoading = false;
+      });
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -329,7 +433,9 @@ export default {
     },
     /** 左侧搜索按钮操作 */
     handleLeftQuery() {
+      // 重置页码和列表
       this.leftQueryParams.pageNum = 1;
+      this.campList = [];
       this.getLeftList();
     },
     /** 重置按钮操作 */
@@ -338,25 +444,12 @@ export default {
       this.queryParams.companyIdList = [];
       this.handleQuery();
     },
-    /** 左侧重置按钮操作 */
-    resetLeftQuery() {
-      this.resetForm("leftQueryForm");
-      this.handleLeftQuery();
-    },
     // 多选框选中数据
     handleSelectionChange(selection) {
       this.ids = selection.map(item => item.periodId)
       this.single = selection.length!==1
       this.multiple = !selection.length
     },
-    // 左侧多选框选中数据
-    handleLeftSelectionChange(selection) {
-      // TODO: 处理左侧选中数据
-    },
-    /** 查看按钮操作 */
-    handleView(row) {
-      // TODO: 处理查看操作
-    },
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
@@ -456,6 +549,196 @@ export default {
         periodEndTime: null
       };
       this.resetForm("form");
+    },
+    // 处理训练营列表的逻辑
+    handleDeleteCamp(item) {
+      this.$confirm(`确定要删除训练营"${item.trainingCampName}"吗?`, '提示', {
+        confirmButtonText: '删除',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        // 调用删除训练营API
+        const trainingCampId = item.trainingCampId;
+        this.leftLoading = true;
+
+        delCamp(trainingCampId).then(response => {
+          if (response.code === 200) {
+            this.$message.success('删除成功');
+            // 从列表中移除
+            const index = this.campList.findIndex(camp => camp.trainingCampId === trainingCampId);
+            if (index !== -1) {
+              this.campList.splice(index, 1);
+            }
+            // 如果删除的是当前选中的训练营,则重置选中状态
+            if (this.activeCampIndex === index) {
+              this.activeCampIndex = this.campList.length > 0 ? 0 : null;
+              if (this.activeCampIndex !== null) {
+                // 更新右侧列表
+                this.selectCamp(this.activeCampIndex);
+              } else {
+                // 没有训练营了,清空右侧列表
+                this.periodList = [];
+              }
+            }
+          } else {
+            this.$message.error(response.msg || '删除失败');
+          }
+          this.leftLoading = false;
+        }).catch(error => {
+          this.$message.error('删除失败: ' + error.message);
+          this.leftLoading = false;
+        });
+      }).catch(() => {
+        this.$message.info('已取消删除');
+      });
+    },
+    /** 复制训练营 */
+    handleCopyCamp(item) {
+      // 调用添加训练营API
+      copyCamp(item.trainingCampId).then(response => {
+        if (response.code === 200) {
+          this.$message.success('复制成功');
+          // 重新加载训练营列表
+          this.getLeftList();
+        } else {
+          this.$message.error(response.msg || '复制训练营失败');
+        }
+      }).catch(error => {
+        this.$message.error('复制训练营失败: ' + error.message);
+      });
+    },
+    /** 展示预警训练营 */
+    showWarning() {
+      console.log('展示预警训练营')
+    },
+    /** 新建训练营按钮操作 */
+    handleAddTrainingCamp() {
+      this.resetCampForm();
+      this.campDialogVisible = true;
+    },
+    /** 重置训练营表单 */
+    resetCampForm() {
+      this.campForm = {
+        trainingCampName: ''
+      };
+      // 如果表单已经创建,则重置校验结果
+      if (this.$refs.campForm) {
+        this.$refs.campForm.resetFields();
+      }
+    },
+    /** 取消训练营表单 */
+    cancelCampForm() {
+      this.campDialogVisible = false;
+      this.resetCampForm();
+    },
+    /** 提交训练营表单 */
+    submitCampForm() {
+      this.$refs.campForm.validate(valid => {
+        if (valid) {
+          // 显示加载中
+          this.leftLoading = true;
+          // 提交数据
+          addCamp(this.campForm).then(response => {
+            if (response.code === 200) {
+              this.$message.success('新建训练营成功');
+              this.campDialogVisible = false;
+              // 重新加载训练营列表
+              this.getLeftList();
+            } else {
+              this.$message.error(response.msg || '新建训练营失败');
+              this.leftLoading = false;
+            }
+          }).catch(error => {
+            this.$message.error('新建训练营失败: ' + error.message);
+            this.leftLoading = false;
+          });
+        }
+      });
+    },
+    /** 排序方式改变 */
+    handleSortChange(value) {
+      this.leftQueryParams.scs = value;
+      // 重置页码和列表
+      this.leftQueryParams.pageNum = 1;
+      this.campList = [];
+      this.getLeftList();
+    },
+    /** 选中训练营 */
+    selectCamp(index) {
+      this.activeCampIndex = index;
+      // TODO:加载对应的训练营营期数据
+      const selectedCamp = this.campList[index];
+      // 设置查询参数
+      // this.queryParams = {
+      //   ...this.queryParams,
+      //   trainingCampId: selectedCamp.trainingCampId
+      // };
+      // this.getList();
+    },
+    /** 处理滚动事件,实现滚动到底部加载更多 */
+    handleScroll() {
+      // 如果正在节流中或者正在加载中,则不处理
+      if (this.scrollThrottle || this.loadingMore) return;
+
+      // 设置节流,200ms内不再处理滚动事件
+      this.scrollThrottle = true;
+      setTimeout(() => {
+        this.scrollThrottle = false;
+      }, 200);
+
+      const scrollEl = this.$refs.campList;
+      if (!scrollEl) return;
+
+      // 判断是否滚动到底部:滚动高度 + 可视高度 >= 总高度 - 30(添加30px的容差,提前触发加载)
+      const isBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 30;
+
+      // 如果滚动到底部,且有下一页数据,且当前不在加载中,则加载更多
+      if (isBottom && this.leftQueryParams.hasNextPage && !this.leftLoading && !this.loadingMore) {
+        this.loadMoreCamps();
+      }
+    },
+    /** 加载更多训练营数据 */
+    loadMoreCamps() {
+      // 已在加载中,防止重复加载
+      if (this.leftLoading || this.loadingMore) return;
+
+      // 设置加载状态
+      this.loadingMore = true;
+
+      // 页码加1
+      this.leftQueryParams.pageNum += 1;
+
+      // 加载下一页数据
+      listCamp(this.leftQueryParams).then(response => {
+        if (response && response.code === 200) {
+          // 将新数据追加到列表中
+          const newList = response.data.list || [];
+          if (newList.length > 0) {
+            this.campList = [...this.campList, ...newList];
+          }
+
+          // 更新是否有下一页的标志
+          this.leftQueryParams.hasNextPage = response.data.hasNextPage;
+
+          // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
+          this.$nextTick(() => {
+            const scrollEl = this.$refs.campList;
+            if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
+              // 延迟一点再加载下一页,避免过快加载
+              setTimeout(() => {
+                this.loadMoreCamps();
+              }, 300);
+            }
+          });
+        } else {
+          this.$message.error(response.msg || '加载更多训练营失败');
+        }
+        this.loadingMore = false;
+      }).catch(error => {
+        console.error('加载更多训练营失败:', error);
+        this.$message.error('加载更多训练营失败');
+        this.loadingMore = false;
+      });
     }
   }
 };
@@ -464,10 +747,256 @@ export default {
 <style scoped>
 .left-aside {
   background-color: #fff;
-  border-right: 1px solid #e6e6e6;
+  border-right: 1px solid #EBEEF5;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  height: 800px;
+}
+
+.left-header {
   padding: 10px;
+  border-bottom: 1px solid #EBEEF5;
+}
+
+.left-header-top {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.search-btn {
+  padding: 7px 15px;
+  background-color: #409EFF;
+  color: white;
+  border: none;
 }
+
+.btn-group {
+  display: flex;
+  gap: 10px;
+}
+
+.search-input-wrapper {
+  margin-bottom: 10px;
+}
+
+.sort-wrapper {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.sort-label {
+  width: 70px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #909399;
+}
+
+.sort-select {
+  margin-left: 10px;
+  width: 280px;
+}
+
+.color-wrapper {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.color-label {
+  width: 70px;
+  color: #606266;
+}
+
+.color-hint {
+  margin-left: 10px;
+  color: #606266;
+  font-size: 12px;
+}
+
+.color-help {
+  margin-left: 5px;
+  color: #909399;
+  cursor: pointer;
+}
+
+.hint-text {
+  color: #606266;
+  font-size: 12px;
+  margin-top: 5px;
+}
+
+.camp-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 3px;
+  background-color: #f5f7fa;
+}
+
+.camp-item {
+  border-radius: 8px;
+  margin-bottom: 5px;
+  padding: 15px;
+  background-color: #ffffff;
+  position: relative;
+  cursor: pointer;
+  display: flex;
+  justify-content: space-between;
+  border: 1px solid #eaedf2;
+}
+
+.camp-item:last-child {
+  margin-bottom: 0;
+}
+
+.camp-item:hover {
+  background-color: #f5f9ff;
+}
+
+.camp-item.active {
+  background-color: #eaf4ff;
+  border-left: 2px solid #75b8fc;
+}
+
+.camp-content {
+  flex: 1;
+  padding-right: 10px;
+}
+
+.camp-title {
+  font-weight: bold;
+  font-size: 16px;
+  margin-bottom: 8px;
+  color: #333;
+  display: flex;
+  align-items: center;
+}
+
+.camp-icon {
+  font-size: 16px;
+  margin-right: 6px;
+  color: #409EFF;
+}
+
+.camp-info {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 12px;
+  color: #c4c1c1;
+  line-height: 1.5;
+}
+
+.camp-stats {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #666;
+  background-color: #f5f9ff;
+  padding: 6px 10px;
+  border-radius: 4px;
+  line-height: 1.5;
+}
+
+.stat-item {
+  display: flex;
+  align-items: center;
+}
+
+.stat-item i {
+  margin-right: 4px;
+  font-size: 14px;
+  color: #409EFF;
+}
+
+.camp-actions {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: flex-end;
+  gap: 8px;
+  border-left: 1px dashed #eaedf2;
+  padding-left: 12px;
+  min-width: 50px;
+}
+
+.action-btn {
+  padding: 2px 5px;
+  font-size: 12px;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.action-btn:hover {
+  background-color: rgba(255, 255, 255, 0.8);
+}
+
+.delete-btn {
+  color: #f56c6c;
+}
+
+.delete-btn:hover {
+  background-color: rgba(245, 108, 108, 0.1);
+}
+
+.copy-btn {
+  color: #409EFF;
+}
+
+.copy-btn:hover {
+  background-color: rgba(64, 158, 255, 0.1);
+}
+
+.warning-icon {
+  color: #E6A23C;
+  font-size: 16px;
+  margin-left: 5px;
+}
+
 .el-main {
   padding: 10px;
 }
+
+/* 添加训练营表单样式 */
+.dialog-footer {
+  text-align: right;
+}
+
+.el-input-number {
+  width: 100%;
+}
+
+/* 加载更多样式 */
+.loading-more {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12px 0;
+  color: #909399;
+  font-size: 14px;
+}
+
+.loading-more i {
+  margin-right: 5px;
+  font-size: 16px;
+}
+
+/* 无更多数据提示 */
+.no-more-data {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 12px 0;
+  color: #c0c4cc;
+  font-size: 13px;
+}
+
+.no-more-data span {
+  position: relative;
+  display: flex;
+  align-items: center;
+}
 </style>