index.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  1. <template>
  2. <div class="app-container">
  3. <el-container>
  4. <!-- 左侧区域 -->
  5. <el-aside width="360px" class="left-aside">
  6. <!-- 顶部区域 -->
  7. <div class="left-header">
  8. <div class="left-header-top">
  9. <el-button type="primary" class="search-btn" @click="handleLeftQuery" v-hasPermi="['course:trainingCamp:list']">搜索</el-button>
  10. </div>
  11. <div class="search-input-wrapper">
  12. <el-input
  13. v-model="leftQueryParams.trainingCampName"
  14. placeholder="请输入训练营名称"
  15. prefix-icon="el-icon-search"
  16. clearable
  17. size="small"
  18. @keyup.enter.native="handleLeftQuery"
  19. />
  20. </div>
  21. <div class="sort-wrapper">
  22. <span class="sort-label">排序方式</span>
  23. <el-select v-model="leftQueryParams.scs"
  24. placeholder="按序号倒序"
  25. size="small"
  26. class="sort-select"
  27. @change="handleSortChange"
  28. >
  29. <el-option label="按序号倒序" value="order_number(desc),training_camp_id(desc)" />
  30. <el-option label="按序号顺序" value="order_number(asc),training_camp_id(desc)" />
  31. </el-select>
  32. </div>
  33. </div>
  34. <!-- 训练营列表 -->
  35. <div class="camp-list" ref="campList" @scroll="handleScroll" v-loading="leftLoading">
  36. <div
  37. v-for="(item, index) in campList"
  38. :key="index"
  39. class="camp-item"
  40. :class="{ 'active': activeCampIndex === index }"
  41. @click="selectCamp(index)"
  42. >
  43. <div class="camp-content">
  44. <div class="camp-title">
  45. <i class="el-icon-s-flag camp-icon"></i>
  46. {{ item.trainingCampName }}
  47. </div>
  48. <div class="camp-info">
  49. <span>序号:{{ item.orderNumber }}</span>
  50. <span>最新营期开课:{{ item.recentDate || '-' }}</span>
  51. </div>
  52. <div class="camp-stats">
  53. <span class="stat-item">
  54. <i class="el-icon-s-data"></i>
  55. 营期数:{{ item.periodCount || 0 }}
  56. </span>
  57. <span class="stat-item">
  58. <i class="el-icon-user"></i>
  59. 会员总数:{{ item.vipCount || 0 }}
  60. </span>
  61. </div>
  62. </div>
  63. <div class="camp-actions">
  64. </div>
  65. </div>
  66. <!-- 底部加载更多提示 -->
  67. <div v-if="loadingMore" class="loading-more">
  68. <i class="el-icon-loading"></i>
  69. <span>加载中...</span>
  70. </div>
  71. <!-- 所有数据加载完毕提示 -->
  72. <div v-if="campList.length > 0 && !leftQueryParams.hasNextPage && !loadingMore" class="no-more-data">
  73. <span>—— 已加载全部训练营 ——</span>
  74. </div>
  75. </div>
  76. </el-aside>
  77. <!-- 右侧区域 -->
  78. <el-main>
  79. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
  80. <el-form-item label="营期名称" prop="periodName">
  81. <el-input
  82. v-model="queryParams.periodName"
  83. placeholder="请输入营期名称"
  84. clearable
  85. size="small"
  86. @keyup.enter.native="handleQuery"
  87. />
  88. </el-form-item>
  89. <el-form-item label="开营日期开始" prop="periodStartingTime" label-width="120px">
  90. <el-date-picker clearable size="small" style="width: 200px"
  91. v-model="queryParams.periodStartingTime"
  92. type="date"
  93. value-format="yyyy-MM-dd"
  94. placeholder="请选择开营日期开始时间">
  95. </el-date-picker>
  96. </el-form-item>
  97. <el-form-item label="开营日期结束" prop="periodEndTime" label-width="120px">
  98. <el-date-picker clearable size="small" style="width: 300px"
  99. v-model="queryParams.periodEndTime"
  100. type="date"
  101. value-format="yyyy-MM-dd"
  102. placeholder="请选择开营日期结束时间">
  103. </el-date-picker>
  104. </el-form-item>
  105. <el-form-item>
  106. <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery" v-hasPermi="['course:period:list']">搜索</el-button>
  107. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  108. </el-form-item>
  109. </el-form>
  110. <el-row :gutter="10" class="mb8">
  111. <el-col :span="1.5">
  112. <el-button
  113. type="warning"
  114. plain
  115. icon="el-icon-download"
  116. size="mini"
  117. @click="handleExport"
  118. v-hasPermi="['course:period:export']"
  119. >导出</el-button>
  120. </el-col>
  121. <el-col :span="1.5">
  122. <el-button
  123. type="primary"
  124. plain
  125. icon="el-icon-edit"
  126. size="mini"
  127. @click="handleBatchSetRedPacket"
  128. v-hasPermi="['course:period:setRedPacket']"
  129. :disabled="batchSetRedPacketDisabled"
  130. >批量设置红包</el-button>
  131. </el-col>
  132. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  133. </el-row>
  134. <el-table v-loading="loading" :data="periodList" @selection-change="handleSelectionChange" border>
  135. <el-table-column type="selection" width="55" align="center" />
  136. <el-table-column label="营期名称" align="center" prop="periodName" />
  137. <el-table-column label="营期状态" align="center" prop="periodStatus" width="100" :formatter="periodStatusFormatter" />
  138. <el-table-column label="开营开始时间" align="center" prop="periodStartingTime" width="180" />
  139. <el-table-column label="开营结束时间" align="center" prop="periodEndTime" width="180" />
  140. <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
  141. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  142. <template slot-scope="scope">
  143. <el-button
  144. size="mini"
  145. type="text"
  146. icon="el-icon-setting"
  147. @click="handlePeriodSettings(scope.row)"
  148. >营期相关设置</el-button>
  149. </template>
  150. </el-table-column>
  151. </el-table>
  152. <pagination
  153. v-show="total>0"
  154. :total="total"
  155. :page.sync="queryParams.pageNum"
  156. :limit.sync="queryParams.pageSize"
  157. @pagination="getList"
  158. />
  159. </el-main>
  160. </el-container>
  161. <!-- 营期相关设置抽屉 -->
  162. <el-drawer
  163. title="营期相关设置"
  164. :visible.sync="periodSettingsVisible"
  165. direction="rtl"
  166. size="74%"
  167. :destroy-on-close="true"
  168. append-to-body
  169. custom-class="period-settings-drawer"
  170. >
  171. <div class="drawer-content" style="margin-left: 25px">
  172. <el-tabs v-model="activeTab" @tab-click="handleTabClick">
  173. <el-tab-pane label="课程管理" name="course">
  174. <!-- <el-row :gutter="10" class="mb8">
  175. <el-col :span="1.5">
  176. <el-button
  177. v-if="(getDiff(periodSettingsData.periodStartingTime, periodSettingsData.periodEndTime) - course.total) > 0"
  178. type="primary"
  179. icon="el-icon-plus"
  180. size="mini"
  181. @click="handleAddCourse"
  182. v-hasPermi="['course:period:addCourse']"
  183. >添加课程</el-button>
  184. </el-col>
  185. <el-col :span="1.5">
  186. <el-button
  187. type="primary"
  188. size="mini"
  189. :disabled="updateCourse.ids.length <= 0"
  190. @click="handleUpdateCourse"
  191. v-hasPermi="['course:period:updateCourseTime']"
  192. >修改看课时间</el-button>
  193. </el-col>
  194. <el-col :span="1.5">
  195. <el-button
  196. type="warning"
  197. size="mini"
  198. icon="el-icon-delete"
  199. :disabled="updateCourse.ids.length <= 0"
  200. @click="handleDeleteCourse"
  201. v-hasPermi="['course:period:dayRemove']"
  202. >删除课程</el-button>
  203. </el-col>
  204. </el-row>-->
  205. <el-table ref="courseTable" v-loading="course.loading" :data="course.list" @selection-change="handleSelectionCourseChange" border>
  206. <el-table-column type="selection" width="55" align="center" />
  207. <el-table-column label="课程" align="center" prop="courseName" width="180" />
  208. <el-table-column label="小节" align="center" prop="videoName" />
  209. <el-table-column label="开课状态" align="center" prop="status" width="100" :formatter="courseStatusFormatter" />
  210. <el-table-column label="营期时间" align="center" prop="dayDate" width="100"/>
  211. <el-table-column label="开始时间" align="center" prop="startDateTime" width="100">
  212. </el-table-column>
  213. <el-table-column label="结束时间" align="center" prop="endDateTime" width="100">
  214. </el-table-column>
  215. <el-table-column label="领取红包时间" align="center" prop="lastJoinTime" width="160">
  216. <template slot-scope="scope">
  217. <el-tag type="danger">{{scope.row.lastJoinTime}}</el-tag>
  218. </template>
  219. </el-table-column>
  220. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  221. <template slot-scope="scope">
  222. <el-button
  223. size="mini"
  224. type="text"
  225. @click="handleDetails(scope.row)"
  226. >查看</el-button>
  227. </template>
  228. </el-table-column>
  229. </el-table>
  230. <pagination
  231. v-show="course.total > 0"
  232. :total="course.total"
  233. :page.sync="course.queryParams.pageNum"
  234. :limit.sync="course.queryParams.pageSize"
  235. @pagination="getCourseList"
  236. style="height: 40px"
  237. />
  238. </el-tab-pane>
  239. <!-- <el-tab-pane label="公司列表" name="company">
  240. <red-packet
  241. :visible.sync="redPacketVisible"
  242. :activeTab="activeTab"
  243. :periodId="periodSettingsData.periodId"
  244. @success="handleRedPacketSuccess"
  245. />
  246. </el-tab-pane>-->
  247. <el-tab-pane label="课程统计" name="statistics">
  248. <course-statistics
  249. :periodId="periodSettingsData.periodId"
  250. :active="activeTab === 'statistics'"
  251. />
  252. </el-tab-pane>
  253. </el-tabs>
  254. </div>
  255. </el-drawer>
  256. <el-drawer
  257. :with-header="false"
  258. size="75%"
  259. :visible.sync="open" append-to-body>
  260. <userCourseVideoDetails ref="userCourseVideoDetails" />
  261. </el-drawer>
  262. <batch-red-packet
  263. :visible.sync="batchRedPacketVisible"
  264. :selected-data="selectedPeriods"
  265. @success="handleBatchRedPacketSuccess"
  266. />
  267. </div>
  268. </template>
  269. <script>
  270. import {exportPeriod, listPeriod, getDays, closePeriod} from "@/api/course/userCoursePeriod";
  271. import { listCamp } from "@/api/course/userCourseCamp";
  272. import { courseList,videoList } from '@/api/course/courseRedPacketLog'
  273. import RedPacket from './redPacket.vue'
  274. import BatchRedPacket from './batchRedPacket.vue'
  275. import CourseStatistics from './statistics.vue'
  276. import userCourseVideoDetails from '../../components/course/userCourseVideoDetails.vue';
  277. export default {
  278. name: "Period",
  279. components: {
  280. RedPacket,
  281. BatchRedPacket,
  282. CourseStatistics,
  283. userCourseVideoDetails
  284. },
  285. data() {
  286. return {
  287. // 遮罩层
  288. loading: true,
  289. updateDateOpen: false,
  290. // 左侧遮罩层
  291. leftLoading: true,
  292. // 选中数组
  293. ids: [],
  294. // 非单个禁用
  295. single: true,
  296. // 非多个禁用
  297. multiple: true,
  298. // 显示搜索条件
  299. showSearch: true,
  300. // 总条数
  301. total: 0,
  302. // 左侧总条数
  303. leftTotal: 0,
  304. // 会员营期表格数据
  305. periodList: [],
  306. // 左侧列表数据
  307. leftList: [],
  308. videoList: [],
  309. // 弹出层标题
  310. title: "",
  311. isDisabledDateRange: false, //是否禁用开营日期
  312. // 是否显示弹出层
  313. open: false,
  314. // 查询参数
  315. queryParams: {
  316. pageNum: 1,
  317. pageSize: 10,
  318. periodName: null,
  319. periodStartingTime: null,
  320. periodEndTime: null,
  321. companyIdList: []
  322. },
  323. // 左侧查询参数
  324. leftQueryParams: {
  325. pageNum: 1,
  326. pageSize: 10,
  327. hasNextPage: false,
  328. scs: 'order_number(desc),training_camp_id(desc)',
  329. trainingCampName: null
  330. },
  331. // 表单参数
  332. form: {},
  333. // 课程相关数据
  334. course: {
  335. open: false,
  336. row:{},
  337. list:[],
  338. queryParams: {
  339. pageNum: 1,
  340. pageSize: 10,
  341. },
  342. loading: true,
  343. total: 0,
  344. addOpen: false,
  345. form: {},
  346. },
  347. //修改营期时间参数
  348. updatePeriodDate: {
  349. open: false,
  350. loading: true,
  351. ids: [],
  352. form: {},
  353. },
  354. updateCourse: {
  355. open: false,
  356. loading: true,
  357. ids: [],
  358. form: {
  359. timeRange: null,
  360. joinTime: null
  361. },
  362. },
  363. // 表单校验
  364. rules: {
  365. periodName: [
  366. { required: true, message: '营期名称不能为空', trigger: 'blur' }
  367. ],
  368. companyId: [
  369. { required: true, message: '公司不能为空', trigger: 'change' }
  370. ],
  371. courseStyle: [
  372. { required: true, message: '课程风格不能为空', trigger: 'change' }
  373. ],
  374. redPacketGrantMethod: [
  375. { required: true, message: '红包发放方式不能为空', trigger: 'change' }
  376. ],
  377. periodType: [
  378. { required: true, message: '营期类型不能为空', trigger: 'change' }
  379. ],
  380. maxViewNum: [
  381. { required: true, message: '销售可查看天数不能为空', trigger: 'blur' }
  382. ],
  383. periodStartingTime: [
  384. { required: true, message: '开营日期不能为空', trigger: 'change' }
  385. ]
  386. },
  387. // 训练营列表
  388. campList: [],
  389. // 激活的训练营索引
  390. activeCampIndex: null,
  391. // 训练营对话框是否显示
  392. campDialogVisible: false,
  393. courseList: false,
  394. // 训练营表单
  395. campForm: {
  396. trainingCampId: null,
  397. trainingCampName: ''
  398. },
  399. // 训练营表单校验
  400. campRules: {
  401. trainingCampName: [
  402. { required: true, message: '训练营名称不能为空', trigger: 'blur' },
  403. { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
  404. ]
  405. },
  406. // 添加课程表单校验
  407. courseAddRules: {
  408. courseId: [
  409. { required: true, message: '请选择课程', trigger: 'change' }
  410. ],
  411. videoIds: [
  412. { required: true, message: '请选择小节', trigger: 'change' },
  413. { type: 'array', min: 1, message: '请至少选择一个小节', trigger: 'change' }
  414. ]
  415. },
  416. // 修改看课时间表单校验
  417. courseUpdateRules: {
  418. timeRange: [
  419. {
  420. required: true,
  421. validator: (rule, value, callback) => {
  422. if (!value || !Array.isArray(value) || value.length !== 2) {
  423. callback(new Error('请选择完整的看课时间范围'));
  424. } else {
  425. // 检查时间格式是否正确(yyyy-MM-dd HH:mm:ss)
  426. const timeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
  427. if (!timeRegex.test(value[0]) || !timeRegex.test(value[1])) {
  428. callback(new Error('时间格式不正确'));
  429. } else if (value[0] >= value[1]) {
  430. callback(new Error('结束时间必须大于开始时间'));
  431. } else {
  432. callback();
  433. }
  434. }
  435. },
  436. trigger: 'change'
  437. }
  438. ],
  439. joinTime: [
  440. {
  441. required: true,
  442. validator: (rule, value, callback) => {
  443. if (!value) {
  444. callback(new Error('请选择领取红包时间'));
  445. } else {
  446. // 检查时间格式是否正确(yyyy-MM-dd HH:mm:ss)
  447. const timeRegex = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
  448. if (!timeRegex.test(value)) {
  449. callback(new Error('时间格式不正确'));
  450. } else {
  451. // 检查领取红包时间是否在看课时间范围内
  452. const timeRange = this.updateCourse.form.timeRange;
  453. if (timeRange && Array.isArray(timeRange) && timeRange.length === 2) {
  454. if (value < timeRange[0] || value > timeRange[1]) {
  455. callback(new Error('领取红包时间必须在看课时间范围内'));
  456. } else {
  457. callback();
  458. }
  459. } else {
  460. callback();
  461. }
  462. }
  463. }
  464. },
  465. trigger: 'change'
  466. }
  467. ]
  468. },
  469. // 滚动节流标志
  470. scrollThrottle: false,
  471. // 加载更多状态
  472. loadingMore: false,
  473. // 设置红包对话框
  474. redPacketVisible: false,
  475. periodCompanyList: [],
  476. currentRedPacketData: {
  477. periodId: '',
  478. videoId: ''
  479. },
  480. // 营期相关设置抽屉
  481. periodSettingsVisible: false,
  482. activeTab: 'course',
  483. periodSettingsData: {},
  484. companyList: [],
  485. courseDialogVisible: false,
  486. redPacketList: [],
  487. currentCompany: null,
  488. // 选中的营期数据
  489. selectedPeriods: [],
  490. // 批量设置红包按钮是否禁用
  491. batchSetRedPacketDisabled: true,
  492. // 批量设置红包弹出框
  493. batchRedPacketVisible: false,
  494. };
  495. },
  496. created() {
  497. courseList().then(response => {
  498. this.courseList = response.list;
  499. });
  500. this.getLeftList();
  501. this.getList();
  502. },
  503. methods: {
  504. /** 查询会员营期列表 */
  505. getList() {
  506. this.loading = true;
  507. const params = { ...this.queryParams };
  508. listPeriod(params).then(response => {
  509. this.periodList = response.rows;
  510. this.total = response.total;
  511. this.loading = false;
  512. });
  513. },
  514. /** 查询左侧列表 */
  515. getLeftList() {
  516. this.leftLoading = true;
  517. // 重置页码和加载更多状态
  518. this.leftQueryParams.pageNum = 1;
  519. this.loadingMore = false;
  520. // 训练营数据
  521. listCamp(this.leftQueryParams).then(response => {
  522. if (response && response.code === 200) {
  523. this.campList = response.data.list || [];
  524. this.leftQueryParams.hasNextPage = response.data.hasNextPage;
  525. this.activeCampIndex = this.campList.length > 0 ? 0 : null;
  526. this.selectCamp(this.activeCampIndex);
  527. // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
  528. this.$nextTick(() => {
  529. const scrollEl = this.$refs.campList;
  530. if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
  531. this.loadMoreCamps();
  532. }
  533. });
  534. } else {
  535. this.$message.error(response.msg || '获取训练营列表失败');
  536. this.campList = [];
  537. this.leftQueryParams.hasNextPage = false;
  538. }
  539. this.leftLoading = false;
  540. }).catch(error => {
  541. console.error('获取训练营列表失败:', error);
  542. this.$message.error('获取训练营列表失败');
  543. this.campList = [];
  544. this.leftQueryParams.hasNextPage = false;
  545. this.leftLoading = false;
  546. });
  547. },
  548. /** 搜索按钮操作 */
  549. handleQuery() {
  550. this.queryParams.pageNum = 1;
  551. this.getList();
  552. },
  553. /** 左侧搜索按钮操作 */
  554. handleLeftQuery() {
  555. // 重置页码和列表
  556. this.leftQueryParams.pageNum = 1;
  557. this.campList = [];
  558. this.getLeftList();
  559. },
  560. /** 重置按钮操作 */
  561. resetQuery() {
  562. this.resetForm("queryForm");
  563. this.queryParams.companyIdList = [];
  564. this.handleQuery();
  565. },
  566. /** 多选框选中数据 */
  567. handleSelectionChange(selection) {
  568. this.ids = selection.map(item => item.periodId)
  569. this.single = selection.length!==1
  570. this.multiple = !selection.length
  571. // 更新批量设置红包相关数据
  572. this.selectedPeriods = selection;
  573. this.batchSetRedPacketDisabled = selection.length === 0;
  574. },
  575. handleSelectionCourseChange(selection) {
  576. this.updateCourse.ids = selection.map(item => item.id)
  577. },
  578. /** 新增按钮操作 */
  579. handleAdd() {
  580. this.reset();
  581. this.open = true;
  582. this.title = "添加会员营期";
  583. this.isDisabledDateRange = false;
  584. },
  585. /** 导出按钮操作 */
  586. handleExport() {
  587. const queryParams = this.queryParams;
  588. this.$confirm('是否确认导出所有会员营期数据项?', "警告", {
  589. confirmButtonText: "确定",
  590. cancelButtonText: "取消",
  591. type: "warning"
  592. }).then(function() {
  593. return exportPeriod(queryParams);
  594. }).then(response => {
  595. this.download(response.msg);
  596. }).catch(function() {});
  597. },
  598. /** 批量设置红包 */
  599. handleBatchSetRedPacket() {
  600. if (this.selectedPeriods.length === 0) {
  601. this.$message.warning('请至少选择一个营期');
  602. return;
  603. }
  604. this.batchRedPacketVisible = true;
  605. },
  606. // 取消按钮
  607. cancel() {
  608. this.open = false;
  609. this.reset();
  610. },
  611. // 表单重置
  612. reset() {
  613. this.form = {
  614. periodId: null,
  615. periodName: null,
  616. companyId: null,
  617. courseId: null,
  618. videoId: null,
  619. trainingCampId: null,
  620. createTime: null,
  621. updateTime: null,
  622. courseStyle: null,
  623. liveRoomStyle: null,
  624. redPacketGrantMethod: 1,
  625. periodType: 1,
  626. periodStartingTime: null,
  627. dateRange: [],
  628. date: null,
  629. days: [],
  630. maxViewNum: 0,
  631. periodEndTime: null,
  632. timeRange: [], // 看课时间范围
  633. viewStartTime: null, // 看课开始时间
  634. viewEndTime: null, // 看课结束时间
  635. lastJoinTime: null // 领取红包时间
  636. };
  637. this.resetForm("form");
  638. },
  639. /** 排序方式改变 */
  640. handleSortChange(value) {
  641. this.leftQueryParams.scs = value;
  642. // 重置页码和列表
  643. this.leftQueryParams.pageNum = 1;
  644. this.campList = [];
  645. this.getLeftList();
  646. },
  647. /** 选中训练营 */
  648. selectCamp(index) {
  649. if(index == null || index == undefined) return;
  650. this.activeCampIndex = index;
  651. // 加载对应的训练营营期数据
  652. const selectedCamp = this.campList[index];
  653. this.queryParams.trainingCampId = selectedCamp.trainingCampId;
  654. this.getList();
  655. },
  656. /** 处理滚动事件,实现滚动到底部加载更多 */
  657. handleScroll() {
  658. // 如果正在节流中或者正在加载中,则不处理
  659. if (this.scrollThrottle || this.loadingMore) return;
  660. // 设置节流,200ms内不再处理滚动事件
  661. this.scrollThrottle = true;
  662. setTimeout(() => {
  663. this.scrollThrottle = false;
  664. }, 200);
  665. const scrollEl = this.$refs.campList;
  666. if (!scrollEl) return;
  667. // 判断是否滚动到底部:滚动高度 + 可视高度 >= 总高度 - 30(添加30px的容差,提前触发加载)
  668. const isBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 30;
  669. // 如果滚动到底部,且有下一页数据,且当前不在加载中,则加载更多
  670. if (isBottom && this.leftQueryParams.hasNextPage && !this.leftLoading && !this.loadingMore) {
  671. this.loadMoreCamps();
  672. }
  673. },
  674. /** 加载更多训练营数据 */
  675. loadMoreCamps() {
  676. // 已在加载中,防止重复加载
  677. if (this.leftLoading || this.loadingMore) return;
  678. // 设置加载状态
  679. this.loadingMore = true;
  680. // 页码加1
  681. this.leftQueryParams.pageNum += 1;
  682. // 加载下一页数据
  683. listCamp(this.leftQueryParams).then(response => {
  684. if (response && response.code === 200) {
  685. // 将新数据追加到列表中
  686. const newList = response.data.list || [];
  687. if (newList.length > 0) {
  688. this.campList = [...this.campList, ...newList];
  689. }
  690. // 更新是否有下一页的标志
  691. this.leftQueryParams.hasNextPage = response.data.hasNextPage;
  692. // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
  693. this.$nextTick(() => {
  694. const scrollEl = this.$refs.campList;
  695. if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
  696. // 延迟一点再加载下一页,避免过快加载
  697. setTimeout(() => {
  698. this.loadMoreCamps();
  699. }, 300);
  700. }
  701. });
  702. } else {
  703. this.$message.error(response.msg || '加载更多训练营失败');
  704. }
  705. this.loadingMore = false;
  706. }).catch(error => {
  707. console.error('加载更多训练营失败:', error);
  708. this.$message.error('加载更多训练营失败');
  709. this.loadingMore = false;
  710. });
  711. },
  712. timeChange(type) {
  713. if (type == 1) {
  714. this.form.periodStartingTime = this.form.dateRange[0];
  715. this.form.periodEndTime = this.form.dateRange[1];
  716. if (!Array.isArray(this.form.days)) {
  717. this.form.days = [];
  718. }
  719. this.form.days = [];
  720. let days = this.getDiff(this.form.periodStartingTime, this.form.periodEndTime);
  721. for (let i = 0; i < days; i++) {
  722. this.form.days.push({ lesson: i + 1 });
  723. }
  724. }
  725. if (type == 2) {
  726. this.form.periodStartingTime = this.form.date;
  727. this.form.periodEndTime = this.form.date;
  728. this.form.days = [];
  729. }
  730. },
  731. getDiff(start, end) {
  732. if(start == null || start == undefined || start == '') return 0;
  733. if(end == null || end == undefined || end == '') return 0;
  734. if(start == end) 1;
  735. const startDate = this.getUTCDate(start);
  736. const endDate = this.getUTCDate(end);
  737. const timeDiff = endDate - startDate;
  738. return (Math.floor(timeDiff / (1000 * 3600 * 24))) + 1; // 直接取整
  739. },
  740. getUTCDate(dateStr) {
  741. const [year, month, day] = dateStr.split('-').map(Number);
  742. return new Date(Date.UTC(year, month - 1, day)); // 月份从0开始
  743. },
  744. getCourseList(){
  745. this.course.loading = true;
  746. getDays(this.course.queryParams).then(e => {
  747. this.course.list = e.rows;
  748. this.course.total = e.total;
  749. this.course.loading = false;
  750. });
  751. },
  752. courseChange(row){
  753. this.course.form.videoIds = [];
  754. videoList(row).then(response => {
  755. this.videoList=response.list
  756. });
  757. },
  758. handlePeriodSettings(row) {
  759. this.periodSettingsData = row;
  760. this.periodSettingsVisible = true;
  761. // 初始化课程列表
  762. this.course.queryParams.periodId = row.periodId;
  763. // 根据当前激活的tab加载对应数据
  764. this.handleTabClick({ name: this.activeTab });
  765. },
  766. handleBatchRedPacketSuccess() {
  767. this.batchRedPacketVisible = false;
  768. // this.getCourseList();
  769. },
  770. /** 处理tab切换 */
  771. handleTabClick(tab) {
  772. if (tab.name === 'course') {
  773. this.getCourseList();
  774. } else if (tab.name === 'company') {
  775. this.redPacketVisible = true;
  776. }
  777. },
  778. /** 营期状态格式化 */
  779. periodStatusFormatter(row) {
  780. const statusMap = {
  781. 1: '未开始',
  782. 2: '进行中',
  783. 3: '已结束'
  784. };
  785. return statusMap[row.periodStatus] || '未知状态';
  786. },
  787. /** 开课状态格式化 */
  788. courseStatusFormatter(row) {
  789. const statusMap = {
  790. 0: '未开始',
  791. 1: '进行中',
  792. 2: '已结束'
  793. };
  794. return statusMap[row.status] || '未知状态';
  795. },
  796. disabledDate(time) {
  797. return time.getTime() < new Date(new Date().setHours(0,0,0,0));
  798. },
  799. handleDetails(row) {
  800. this.open=true;
  801. setTimeout(() => {
  802. this.$refs.userCourseVideoDetails.getDetails(row.videoId);
  803. }, 500);
  804. }
  805. },
  806. };
  807. </script>
  808. <style scoped>
  809. .left-aside {
  810. background-color: #fff;
  811. border-right: 1px solid #EBEEF5;
  812. padding: 0;
  813. display: flex;
  814. flex-direction: column;
  815. height: 800px;
  816. }
  817. .left-header {
  818. padding: 10px;
  819. border-bottom: 1px solid #EBEEF5;
  820. }
  821. .left-header-top {
  822. display: flex;
  823. justify-content: space-between;
  824. align-items: center;
  825. margin-bottom: 10px;
  826. }
  827. .search-btn {
  828. width: 50%;
  829. height: 36px;
  830. background-color: #409EFF;
  831. color: white;
  832. border: none;
  833. }
  834. .search-input-wrapper {
  835. margin-bottom: 10px;
  836. }
  837. .sort-wrapper {
  838. display: flex;
  839. align-items: center;
  840. margin-bottom: 10px;
  841. }
  842. .sort-label {
  843. width: 70px;
  844. font-size: 14px;
  845. font-weight: 600;
  846. color: #909399;
  847. }
  848. .sort-select {
  849. margin-left: 10px;
  850. width: 280px;
  851. }
  852. .camp-item {
  853. margin-bottom: 5px;
  854. padding: 15px;
  855. background-color: #ffffff;
  856. position: relative;
  857. cursor: pointer;
  858. display: flex;
  859. justify-content: space-between;
  860. border: 1px solid #eaedf2;
  861. }
  862. .camp-item:last-child {
  863. margin-bottom: 0;
  864. }
  865. .camp-item:hover {
  866. background-color: #f5f9ff;
  867. }
  868. .camp-item.active {
  869. background-color: #eaf4ff;
  870. border-left: 1px solid #75b8fc;
  871. }
  872. .camp-content {
  873. flex: 1;
  874. padding-right: 10px;
  875. }
  876. .camp-title {
  877. font-weight: bold;
  878. font-size: 16px;
  879. margin-bottom: 8px;
  880. color: #333;
  881. display: flex;
  882. align-items: center;
  883. }
  884. .camp-icon {
  885. font-size: 16px;
  886. margin-right: 6px;
  887. color: #409EFF;
  888. }
  889. .camp-info {
  890. display: flex;
  891. justify-content: space-between;
  892. margin-bottom: 8px;
  893. font-size: 12px;
  894. color: #c4c1c1;
  895. line-height: 1.5;
  896. }
  897. .camp-stats {
  898. display: flex;
  899. justify-content: space-between;
  900. font-size: 12px;
  901. color: #666;
  902. background-color: #f5f9ff;
  903. padding: 6px 10px;
  904. border-radius: 4px;
  905. line-height: 1.5;
  906. }
  907. .stat-item {
  908. display: flex;
  909. align-items: center;
  910. }
  911. .stat-item i {
  912. margin-right: 4px;
  913. font-size: 14px;
  914. color: #409EFF;
  915. }
  916. .camp-actions {
  917. display: flex;
  918. flex-direction: column;
  919. justify-content: center;
  920. align-items: flex-end;
  921. gap: 8px;
  922. border-left: 1px dashed #eaedf2;
  923. padding-left: 12px;
  924. min-width: 50px;
  925. }
  926. /* 加载更多样式 */
  927. .loading-more {
  928. display: flex;
  929. align-items: center;
  930. justify-content: center;
  931. padding: 12px 0;
  932. color: #909399;
  933. font-size: 14px;
  934. }
  935. .loading-more i {
  936. margin-right: 5px;
  937. font-size: 16px;
  938. }
  939. /* 无更多数据提示 */
  940. .no-more-data {
  941. display: flex;
  942. align-items: center;
  943. justify-content: center;
  944. padding: 12px 0;
  945. color: #c0c4cc;
  946. font-size: 13px;
  947. }
  948. .no-more-data span {
  949. position: relative;
  950. display: flex;
  951. align-items: center;
  952. }
  953. </style>