index.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  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. },
  502. methods: {
  503. /** 查询会员营期列表 */
  504. getList() {
  505. this.loading = true;
  506. const params = { ...this.queryParams };
  507. listPeriod(params).then(response => {
  508. this.periodList = response.rows;
  509. this.total = response.total;
  510. this.loading = false;
  511. });
  512. },
  513. /** 查询左侧列表 */
  514. getLeftList() {
  515. this.leftLoading = true;
  516. // 重置页码和加载更多状态
  517. this.leftQueryParams.pageNum = 1;
  518. this.loadingMore = false;
  519. // 训练营数据
  520. listCamp(this.leftQueryParams).then(response => {
  521. if (response && response.code === 200) {
  522. this.campList = response.data.list || [];
  523. this.leftQueryParams.hasNextPage = response.data.hasNextPage;
  524. this.activeCampIndex = this.campList.length > 0 ? 0 : null;
  525. this.selectCamp(this.activeCampIndex);
  526. // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
  527. this.$nextTick(() => {
  528. const scrollEl = this.$refs.campList;
  529. if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
  530. this.loadMoreCamps();
  531. }
  532. });
  533. } else {
  534. this.$message.error(response.msg || '获取训练营列表失败');
  535. this.campList = [];
  536. this.leftQueryParams.hasNextPage = false;
  537. }
  538. this.leftLoading = false;
  539. }).catch(error => {
  540. console.error('获取训练营列表失败:', error);
  541. this.$message.error('获取训练营列表失败');
  542. this.campList = [];
  543. this.leftQueryParams.hasNextPage = false;
  544. this.leftLoading = false;
  545. });
  546. },
  547. /** 搜索按钮操作 */
  548. handleQuery() {
  549. this.queryParams.pageNum = 1;
  550. this.getList();
  551. },
  552. /** 左侧搜索按钮操作 */
  553. handleLeftQuery() {
  554. // 重置页码和列表
  555. this.leftQueryParams.pageNum = 1;
  556. this.campList = [];
  557. this.getLeftList();
  558. },
  559. /** 重置按钮操作 */
  560. resetQuery() {
  561. this.resetForm("queryForm");
  562. this.queryParams.companyIdList = [];
  563. this.handleQuery();
  564. },
  565. /** 多选框选中数据 */
  566. handleSelectionChange(selection) {
  567. this.ids = selection.map(item => item.periodId)
  568. this.single = selection.length!==1
  569. this.multiple = !selection.length
  570. // 更新批量设置红包相关数据
  571. this.selectedPeriods = selection;
  572. this.batchSetRedPacketDisabled = selection.length === 0;
  573. },
  574. handleSelectionCourseChange(selection) {
  575. this.updateCourse.ids = selection.map(item => item.id)
  576. },
  577. /** 新增按钮操作 */
  578. handleAdd() {
  579. this.reset();
  580. this.open = true;
  581. this.title = "添加会员营期";
  582. this.isDisabledDateRange = false;
  583. },
  584. /** 导出按钮操作 */
  585. handleExport() {
  586. const queryParams = this.queryParams;
  587. this.$confirm('是否确认导出所有会员营期数据项?', "警告", {
  588. confirmButtonText: "确定",
  589. cancelButtonText: "取消",
  590. type: "warning"
  591. }).then(function() {
  592. return exportPeriod(queryParams);
  593. }).then(response => {
  594. this.download(response.msg);
  595. }).catch(function() {});
  596. },
  597. /** 批量设置红包 */
  598. handleBatchSetRedPacket() {
  599. if (this.selectedPeriods.length === 0) {
  600. this.$message.warning('请至少选择一个营期');
  601. return;
  602. }
  603. this.batchRedPacketVisible = true;
  604. },
  605. // 取消按钮
  606. cancel() {
  607. this.open = false;
  608. this.reset();
  609. },
  610. // 表单重置
  611. reset() {
  612. this.form = {
  613. periodId: null,
  614. periodName: null,
  615. companyId: null,
  616. courseId: null,
  617. videoId: null,
  618. trainingCampId: null,
  619. createTime: null,
  620. updateTime: null,
  621. courseStyle: null,
  622. liveRoomStyle: null,
  623. redPacketGrantMethod: 1,
  624. periodType: 1,
  625. periodStartingTime: null,
  626. dateRange: [],
  627. date: null,
  628. days: [],
  629. maxViewNum: 0,
  630. periodEndTime: null,
  631. timeRange: [], // 看课时间范围
  632. viewStartTime: null, // 看课开始时间
  633. viewEndTime: null, // 看课结束时间
  634. lastJoinTime: null // 领取红包时间
  635. };
  636. this.resetForm("form");
  637. },
  638. /** 排序方式改变 */
  639. handleSortChange(value) {
  640. this.leftQueryParams.scs = value;
  641. // 重置页码和列表
  642. this.leftQueryParams.pageNum = 1;
  643. this.campList = [];
  644. this.getLeftList();
  645. },
  646. /** 选中训练营 */
  647. selectCamp(index) {
  648. if(index == null || index == undefined) return;
  649. this.activeCampIndex = index;
  650. // 加载对应的训练营营期数据
  651. const selectedCamp = this.campList[index];
  652. this.queryParams.trainingCampId = selectedCamp.trainingCampId;
  653. this.getList();
  654. },
  655. /** 处理滚动事件,实现滚动到底部加载更多 */
  656. handleScroll() {
  657. // 如果正在节流中或者正在加载中,则不处理
  658. if (this.scrollThrottle || this.loadingMore) return;
  659. // 设置节流,200ms内不再处理滚动事件
  660. this.scrollThrottle = true;
  661. setTimeout(() => {
  662. this.scrollThrottle = false;
  663. }, 200);
  664. const scrollEl = this.$refs.campList;
  665. if (!scrollEl) return;
  666. // 判断是否滚动到底部:滚动高度 + 可视高度 >= 总高度 - 30(添加30px的容差,提前触发加载)
  667. const isBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 30;
  668. // 如果滚动到底部,且有下一页数据,且当前不在加载中,则加载更多
  669. if (isBottom && this.leftQueryParams.hasNextPage && !this.leftLoading && !this.loadingMore) {
  670. this.loadMoreCamps();
  671. }
  672. },
  673. /** 加载更多训练营数据 */
  674. loadMoreCamps() {
  675. // 已在加载中,防止重复加载
  676. if (this.leftLoading || this.loadingMore) return;
  677. // 设置加载状态
  678. this.loadingMore = true;
  679. // 页码加1
  680. this.leftQueryParams.pageNum += 1;
  681. // 加载下一页数据
  682. listCamp(this.leftQueryParams).then(response => {
  683. if (response && response.code === 200) {
  684. // 将新数据追加到列表中
  685. const newList = response.data.list || [];
  686. if (newList.length > 0) {
  687. this.campList = [...this.campList, ...newList];
  688. }
  689. // 更新是否有下一页的标志
  690. this.leftQueryParams.hasNextPage = response.data.hasNextPage;
  691. // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
  692. this.$nextTick(() => {
  693. const scrollEl = this.$refs.campList;
  694. if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
  695. // 延迟一点再加载下一页,避免过快加载
  696. setTimeout(() => {
  697. this.loadMoreCamps();
  698. }, 300);
  699. }
  700. });
  701. } else {
  702. this.$message.error(response.msg || '加载更多训练营失败');
  703. }
  704. this.loadingMore = false;
  705. }).catch(error => {
  706. console.error('加载更多训练营失败:', error);
  707. this.$message.error('加载更多训练营失败');
  708. this.loadingMore = false;
  709. });
  710. },
  711. timeChange(type) {
  712. if (type == 1) {
  713. this.form.periodStartingTime = this.form.dateRange[0];
  714. this.form.periodEndTime = this.form.dateRange[1];
  715. if (!Array.isArray(this.form.days)) {
  716. this.form.days = [];
  717. }
  718. this.form.days = [];
  719. let days = this.getDiff(this.form.periodStartingTime, this.form.periodEndTime);
  720. for (let i = 0; i < days; i++) {
  721. this.form.days.push({ lesson: i + 1 });
  722. }
  723. }
  724. if (type == 2) {
  725. this.form.periodStartingTime = this.form.date;
  726. this.form.periodEndTime = this.form.date;
  727. this.form.days = [];
  728. }
  729. },
  730. getDiff(start, end) {
  731. if(start == null || start == undefined || start == '') return 0;
  732. if(end == null || end == undefined || end == '') return 0;
  733. if(start == end) 1;
  734. const startDate = this.getUTCDate(start);
  735. const endDate = this.getUTCDate(end);
  736. const timeDiff = endDate - startDate;
  737. return (Math.floor(timeDiff / (1000 * 3600 * 24))) + 1; // 直接取整
  738. },
  739. getUTCDate(dateStr) {
  740. const [year, month, day] = dateStr.split('-').map(Number);
  741. return new Date(Date.UTC(year, month - 1, day)); // 月份从0开始
  742. },
  743. getCourseList(){
  744. this.course.loading = true;
  745. getDays(this.course.queryParams).then(e => {
  746. this.course.list = e.rows;
  747. this.course.total = e.total;
  748. this.course.loading = false;
  749. });
  750. },
  751. courseChange(row){
  752. this.course.form.videoIds = [];
  753. videoList(row).then(response => {
  754. this.videoList=response.list
  755. });
  756. },
  757. handlePeriodSettings(row) {
  758. this.periodSettingsData = row;
  759. this.periodSettingsVisible = true;
  760. // 初始化课程列表
  761. this.course.queryParams.periodId = row.periodId;
  762. // 根据当前激活的tab加载对应数据
  763. this.handleTabClick({ name: this.activeTab });
  764. },
  765. handleBatchRedPacketSuccess() {
  766. this.batchRedPacketVisible = false;
  767. // this.getCourseList();
  768. },
  769. /** 处理tab切换 */
  770. handleTabClick(tab) {
  771. if (tab.name === 'course') {
  772. this.getCourseList();
  773. } else if (tab.name === 'company') {
  774. this.redPacketVisible = true;
  775. }
  776. },
  777. /** 营期状态格式化 */
  778. periodStatusFormatter(row) {
  779. const statusMap = {
  780. 1: '未开始',
  781. 2: '进行中',
  782. 3: '已结束'
  783. };
  784. return statusMap[row.periodStatus] || '未知状态';
  785. },
  786. /** 开课状态格式化 */
  787. courseStatusFormatter(row) {
  788. const statusMap = {
  789. 0: '未开始',
  790. 1: '进行中',
  791. 2: '已结束'
  792. };
  793. return statusMap[row.status] || '未知状态';
  794. },
  795. disabledDate(time) {
  796. return time.getTime() < new Date(new Date().setHours(0,0,0,0));
  797. },
  798. handleDetails(row) {
  799. this.open=true;
  800. setTimeout(() => {
  801. this.$refs.userCourseVideoDetails.getDetails(row.videoId);
  802. }, 500);
  803. }
  804. },
  805. };
  806. </script>
  807. <style scoped>
  808. .left-aside {
  809. background-color: #fff;
  810. border-right: 1px solid #EBEEF5;
  811. padding: 0;
  812. display: flex;
  813. flex-direction: column;
  814. height: 800px;
  815. }
  816. .left-header {
  817. padding: 10px;
  818. border-bottom: 1px solid #EBEEF5;
  819. }
  820. .left-header-top {
  821. display: flex;
  822. justify-content: space-between;
  823. align-items: center;
  824. margin-bottom: 10px;
  825. }
  826. .search-btn {
  827. width: 50%;
  828. height: 36px;
  829. background-color: #409EFF;
  830. color: white;
  831. border: none;
  832. }
  833. .search-input-wrapper {
  834. margin-bottom: 10px;
  835. }
  836. .sort-wrapper {
  837. display: flex;
  838. align-items: center;
  839. margin-bottom: 10px;
  840. }
  841. .sort-label {
  842. width: 70px;
  843. font-size: 14px;
  844. font-weight: 600;
  845. color: #909399;
  846. }
  847. .sort-select {
  848. margin-left: 10px;
  849. width: 280px;
  850. }
  851. .camp-item {
  852. margin-bottom: 5px;
  853. padding: 15px;
  854. background-color: #ffffff;
  855. position: relative;
  856. cursor: pointer;
  857. display: flex;
  858. justify-content: space-between;
  859. border: 1px solid #eaedf2;
  860. }
  861. .camp-item:last-child {
  862. margin-bottom: 0;
  863. }
  864. .camp-item:hover {
  865. background-color: #f5f9ff;
  866. }
  867. .camp-item.active {
  868. background-color: #eaf4ff;
  869. border-left: 1px solid #75b8fc;
  870. }
  871. .camp-content {
  872. flex: 1;
  873. padding-right: 10px;
  874. }
  875. .camp-title {
  876. font-weight: bold;
  877. font-size: 16px;
  878. margin-bottom: 8px;
  879. color: #333;
  880. display: flex;
  881. align-items: center;
  882. }
  883. .camp-icon {
  884. font-size: 16px;
  885. margin-right: 6px;
  886. color: #409EFF;
  887. }
  888. .camp-info {
  889. display: flex;
  890. justify-content: space-between;
  891. margin-bottom: 8px;
  892. font-size: 12px;
  893. color: #c4c1c1;
  894. line-height: 1.5;
  895. }
  896. .camp-stats {
  897. display: flex;
  898. justify-content: space-between;
  899. font-size: 12px;
  900. color: #666;
  901. background-color: #f5f9ff;
  902. padding: 6px 10px;
  903. border-radius: 4px;
  904. line-height: 1.5;
  905. }
  906. .stat-item {
  907. display: flex;
  908. align-items: center;
  909. }
  910. .stat-item i {
  911. margin-right: 4px;
  912. font-size: 14px;
  913. color: #409EFF;
  914. }
  915. .camp-actions {
  916. display: flex;
  917. flex-direction: column;
  918. justify-content: center;
  919. align-items: flex-end;
  920. gap: 8px;
  921. border-left: 1px dashed #eaedf2;
  922. padding-left: 12px;
  923. min-width: 50px;
  924. }
  925. /* 加载更多样式 */
  926. .loading-more {
  927. display: flex;
  928. align-items: center;
  929. justify-content: center;
  930. padding: 12px 0;
  931. color: #909399;
  932. font-size: 14px;
  933. }
  934. .loading-more i {
  935. margin-right: 5px;
  936. font-size: 16px;
  937. }
  938. /* 无更多数据提示 */
  939. .no-more-data {
  940. display: flex;
  941. align-items: center;
  942. justify-content: center;
  943. padding: 12px 0;
  944. color: #c0c4cc;
  945. font-size: 13px;
  946. }
  947. .no-more-data span {
  948. position: relative;
  949. display: flex;
  950. align-items: center;
  951. }
  952. </style>