index.vue 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728
  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. <el-button type="primary" style="width: 50%" icon="el-icon-plus" @click="handleAddTrainingCamp" v-hasPermi="['course:trainingCamp:add']">新建训练营</el-button>
  11. </div>
  12. <div class="search-input-wrapper">
  13. <el-input
  14. v-model="leftQueryParams.trainingCampName"
  15. placeholder="请输入训练营名称"
  16. prefix-icon="el-icon-search"
  17. clearable
  18. size="small"
  19. @keyup.enter.native="handleLeftQuery"
  20. />
  21. </div>
  22. <div class="sort-wrapper">
  23. <span class="sort-label">排序方式</span>
  24. <el-select v-model="leftQueryParams.scs"
  25. placeholder="按序号倒序"
  26. size="small"
  27. class="sort-select"
  28. @change="handleSortChange"
  29. >
  30. <el-option label="按序号倒序" value="order_number(desc),training_camp_id(desc)" />
  31. <el-option label="按序号顺序" value="order_number(asc),training_camp_id(desc)" />
  32. </el-select>
  33. </div>
  34. </div>
  35. <!-- 训练营列表 -->
  36. <div class="camp-list" ref="campList" @scroll="handleScroll" v-loading="leftLoading">
  37. <div
  38. v-for="(item, index) in campList"
  39. :key="index"
  40. class="camp-item"
  41. :class="{ 'active': activeCampIndex === index }"
  42. @click="selectCamp(index)"
  43. >
  44. <div class="camp-content">
  45. <div class="camp-title">
  46. <i class="el-icon-s-flag camp-icon"></i>
  47. {{ item.trainingCampName }}
  48. </div>
  49. <div class="camp-info">
  50. <span>序号:{{ item.orderNumber }}</span>
  51. <span>最新营期开课:{{ item.recentDate || '-' }}</span>
  52. </div>
  53. <div class="camp-stats">
  54. <span class="stat-item">
  55. <i class="el-icon-s-data"></i>
  56. 营期数:{{ item.periodCount || 0 }}
  57. </span>
  58. <span class="stat-item">
  59. <i class="el-icon-user"></i>
  60. 会员总数:{{ item.vipCount || 0 }}
  61. </span>
  62. </div>
  63. </div>
  64. <div class="camp-actions">
  65. <el-button type="text" class="action-btn delete-btn" @click.stop="handleDeleteCamp(item)" v-hasPermi="['course:trainingCamp:remove']">删除</el-button>
  66. <!-- <el-button type="text" class="action-btn copy-btn" @click.stop="handleCopyCamp(item)" v-hasPermi="['course:trainingCamp:copy']">复制</el-button>-->
  67. <el-button type="text" class="action-btn copy-btn" @click.stop="handleEditCamp(item)" v-hasPermi="['course:trainingCamp:edit']">编辑</el-button>
  68. </div>
  69. </div>
  70. <!-- 底部加载更多提示 -->
  71. <div v-if="loadingMore" class="loading-more">
  72. <i class="el-icon-loading"></i>
  73. <span>加载中...</span>
  74. </div>
  75. <!-- 所有数据加载完毕提示 -->
  76. <div v-if="campList.length > 0 && !leftQueryParams.hasNextPage && !loadingMore" class="no-more-data">
  77. <span>—— 已加载全部训练营 ——</span>
  78. </div>
  79. </div>
  80. </el-aside>
  81. <!-- 右侧区域 -->
  82. <el-main>
  83. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
  84. <el-form-item label="营期名称" prop="periodName">
  85. <el-input
  86. v-model="queryParams.periodName"
  87. placeholder="请输入营期名称"
  88. clearable
  89. size="small"
  90. @keyup.enter.native="handleQuery"
  91. />
  92. </el-form-item>
  93. <el-form-item label="公司" prop="companyIdList">
  94. <el-select v-model="queryParams.companyIdList" placeholder="请选择公司" clearable size="small" multiple>
  95. <el-option
  96. v-for="item in companyOptions"
  97. :key="item.companyId"
  98. :label="item.companyName"
  99. :value="item.companyId"
  100. />
  101. </el-select>
  102. </el-form-item>
  103. <el-form-item label="开营日期开始" prop="periodStartingTime" label-width="120px">
  104. <el-date-picker clearable size="small" style="width: 200px"
  105. v-model="queryParams.periodStartingTime"
  106. type="date"
  107. value-format="yyyy-MM-dd"
  108. placeholder="请选择开营日期开始时间">
  109. </el-date-picker>
  110. </el-form-item>
  111. <el-form-item label="开营日期结束" prop="periodEndTime" label-width="120px">
  112. <el-date-picker clearable size="small" style="width: 300px"
  113. v-model="queryParams.periodEndTime"
  114. type="date"
  115. value-format="yyyy-MM-dd"
  116. placeholder="请选择开营日期结束时间">
  117. </el-date-picker>
  118. </el-form-item>
  119. <el-form-item>
  120. <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery" v-hasPermi="['course:period:list']">搜索</el-button>
  121. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  122. </el-form-item>
  123. </el-form>
  124. <el-row :gutter="10" class="mb8">
  125. <el-col :span="1.5">
  126. <el-button
  127. type="primary"
  128. plain
  129. icon="el-icon-plus"
  130. size="mini"
  131. @click="handleAdd"
  132. v-hasPermi="['course:period:add']"
  133. >新增</el-button>
  134. </el-col>
  135. <el-col :span="1.5">
  136. <el-button
  137. type="warning"
  138. plain
  139. icon="el-icon-download"
  140. size="mini"
  141. @click="handleExport"
  142. v-hasPermi="['course:period:export']"
  143. >导出</el-button>
  144. </el-col>
  145. <el-col :span="1.5">
  146. <el-button
  147. type="primary"
  148. plain
  149. icon="el-icon-edit"
  150. size="mini"
  151. @click="handleBatchSetRedPacket"
  152. v-hasPermi="['course:period:setRedPacket']"
  153. :disabled="batchSetRedPacketDisabled"
  154. >批量设置红包</el-button>
  155. </el-col>
  156. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  157. </el-row>
  158. <el-table v-loading="loading" :data="periodList" @selection-change="handleSelectionChange" border>
  159. <el-table-column type="selection" width="55" align="center" />
  160. <el-table-column label="营期名称" align="center" prop="periodName" />
  161. <el-table-column label="公司名称" align="center" prop="companyName" />
  162. <el-table-column label="营期状态" align="center" prop="periodStatus" width="100" :formatter="periodStatusFormatter" />
  163. <el-table-column label="开营开始时间" align="center" prop="periodStartingTime" width="180" />
  164. <el-table-column label="开营结束时间" align="center" prop="periodEndTime" width="180" />
  165. <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
  166. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  167. <template slot-scope="scope">
  168. <el-button
  169. size="mini"
  170. type="text"
  171. icon="el-icon-edit"
  172. @click="handleUpdate(scope.row)"
  173. v-hasPermi="['course:period:query']"
  174. >修改</el-button>
  175. <!-- <el-button-->
  176. <!-- size="mini"-->
  177. <!-- type="text"-->
  178. <!-- icon="el-icon-edit"-->
  179. <!-- @click="handleCourse(scope.row)"-->
  180. <!-- v-hasPermi="['course:period:edit']"-->
  181. <!-- >课程管理</el-button>-->
  182. <!-- <el-button-->
  183. <!-- size="mini"-->
  184. <!-- type="text"-->
  185. <!-- icon="el-icon-money"-->
  186. <!-- @click="setRedPacket(scope.row)"-->
  187. <!-- >设置红包</el-button>-->
  188. <el-button
  189. size="mini"
  190. type="text"
  191. icon="el-icon-setting"
  192. @click="handlePeriodSettings(scope.row)"
  193. >营期相关设置</el-button>
  194. <el-button
  195. size="mini"
  196. type="text"
  197. icon="el-icon-circle-close"
  198. @click="handleClosePeriod(scope.row)"
  199. v-hasPermi="['course:period:close']"
  200. >结束营期</el-button>
  201. <el-button
  202. size="mini"
  203. type="text"
  204. icon="el-icon-delete"
  205. @click="handleDelete(scope.row)"
  206. v-hasPermi="['course:period:remove']"
  207. >删除</el-button>
  208. </template>
  209. </el-table-column>
  210. </el-table>
  211. <pagination
  212. v-show="total>0"
  213. :total="total"
  214. :page.sync="queryParams.pageNum"
  215. :limit.sync="queryParams.pageSize"
  216. @pagination="getList"
  217. />
  218. </el-main>
  219. </el-container>
  220. <!-- 添加或修改会员营期对话框-->
  221. <el-drawer :title="title" :visible.sync="open" size="700px" append-to-body>
  222. <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  223. <el-form-item label="营期名称" prop="periodName">
  224. <el-input v-model="form.periodName" placeholder="请输入营期名称" />
  225. </el-form-item>
  226. <el-form-item label="公司" prop="companyId">
  227. <el-select v-model="form.companyId" placeholder="请选择公司" multiple>
  228. <el-option
  229. v-for="item in companyOptions"
  230. :key="item.companyId"
  231. :label="item.companyName"
  232. :value="item.companyId"
  233. />
  234. </el-select>
  235. </el-form-item>
  236. <el-form-item label="课程风格" prop="courseStyle">
  237. <image-upload v-model="form.courseStyle" :limit="1" />
  238. </el-form-item>
  239. <el-form-item label="直播间风格" prop="liveRoomStyle">
  240. <image-upload v-model="form.liveRoomStyle" :limit="1" />
  241. </el-form-item>
  242. <el-form-item label="课程LOGO" prop="courseLogo">
  243. <image-upload v-model="form.courseLogo" :limit="1" />
  244. </el-form-item>
  245. <el-form-item label="红包发放方式" prop="redPacketGrantMethod">
  246. <el-radio-group v-model="form.redPacketGrantMethod">
  247. <el-radio :label="1" >按课程</el-radio>
  248. <el-radio :label="2" >按营期</el-radio>
  249. </el-radio-group>
  250. </el-form-item>
  251. <el-form-item label="营期类型" prop="periodType">
  252. <el-radio-group v-model="form.periodType">
  253. <el-radio :label="1" >多课程</el-radio>
  254. <el-radio :label="2" >单课程</el-radio>
  255. </el-radio-group>
  256. </el-form-item>
  257. <el-form-item label="销售可查看天数" prop="maxViewNum">
  258. <el-input-number :min="0" v-model="form.maxViewNum" style="width: 200px" />
  259. </el-form-item>
  260. <el-form-item label="开营日期" prop="periodStartingTime">
  261. <el-date-picker
  262. :disabled = "isDisabledDateRange"
  263. :style="{display: form.periodType == 1 ? '' : 'none !important'}"
  264. v-model="form.dateRange"
  265. @change="timeChange(1)"
  266. type="daterange"
  267. range-separator="至"
  268. start-placeholder="开始日期"
  269. end-placeholder="结束日期"
  270. value-format="yyyy-MM-dd"
  271. :picker-options="{disabledDate}"
  272. >
  273. </el-date-picker>
  274. <el-date-picker
  275. :style="{display: form.periodType == 2 ? '' : 'none !important'}"
  276. @change="timeChange(2)"
  277. v-model="form.date"
  278. type="date"
  279. value-format="yyyy-MM-dd"
  280. placeholder="选择日期"
  281. :picker-options="{disabledDate}"
  282. >
  283. </el-date-picker>
  284. </el-form-item>
  285. <!-- <el-form-item label="看课时间" prop="timeRange">-->
  286. <!-- <el-time-picker-->
  287. <!-- is-range-->
  288. <!-- v-model="form.timeRange"-->
  289. <!-- @input="$forceUpdate()"-->
  290. <!-- range-separator="至"-->
  291. <!-- start-placeholder="开始时间"-->
  292. <!-- value-format="HH:mm:ss"-->
  293. <!-- end-placeholder="结束时间"-->
  294. <!-- placeholder="选择时间范围">-->
  295. <!-- </el-time-picker>-->
  296. <!-- </el-form-item>-->
  297. <!-- <el-form-item label="领取红包时间" prop="lastJoinTime">-->
  298. <!-- <el-time-picker-->
  299. <!-- v-model="form.lastJoinTime"-->
  300. <!-- :selectableRange="form.lastJoinTime"-->
  301. <!-- value-format="HH:mm:ss"-->
  302. <!-- placeholder="选择时间范围">-->
  303. <!-- </el-time-picker>-->
  304. <!-- <p style="color: red;margin: 0;font-size: 12px">超过领取红包时间,只允许看课,不允许领取红包</p>-->
  305. <!-- </el-form-item>-->
  306. </el-form>
  307. <div class="drawer-footer">
  308. <el-button type="primary" @click="submitForm">确 定</el-button>
  309. <el-button @click="cancel">取 消</el-button>
  310. </div>
  311. </el-drawer>
  312. <!-- 添加训练营对话框 -->
  313. <el-dialog :title="campForm.trainingCampId ? '修改训练营' : '新建训练营'" :visible.sync="campDialogVisible" width="500px" append-to-body>
  314. <el-form ref="campForm" :model="campForm" :rules="campRules" label-width="100px">
  315. <el-form-item label="训练营名称" prop="trainingCampName">
  316. <el-input v-model="campForm.trainingCampName" placeholder="请输入训练营名称" />
  317. </el-form-item>
  318. </el-form>
  319. <div slot="footer" class="dialog-footer">
  320. <el-button type="primary" @click="submitCampForm">确 定</el-button>
  321. <el-button @click="cancelCampForm">取 消</el-button>
  322. </div>
  323. </el-dialog>
  324. <!-- &lt;!&ndash; 添加或修改会员营期对话框&ndash;&gt;-->
  325. <!-- <el-dialog title="课程管理" :visible.sync="course.open" width="75%" top="10px" append-to-body style="padding-bottom: 10px">-->
  326. <!-- <el-row :gutter="10" class="mb8">-->
  327. <!-- <el-col :span="1.5">-->
  328. <!-- <el-button-->
  329. <!-- v-if="(getDiff(course.row.periodStartingTime, course.row.periodEndTime) - course.total) > 0"-->
  330. <!-- type="primary"-->
  331. <!-- icon="el-icon-plus"-->
  332. <!-- size="mini"-->
  333. <!-- @click="handleAddCourse"-->
  334. <!-- v-hasPermi="['course:period:add']"-->
  335. <!-- >添加课程</el-button>-->
  336. <!-- </el-col>-->
  337. <!-- </el-row>-->
  338. <!-- <el-table v-loading="course.loading" :data="course.list">-->
  339. <!-- <el-table-column label="课程" align="center" prop="courseName" width="180" />-->
  340. <!-- <el-table-column label="小节" align="center" prop="videoName" />-->
  341. <!-- <el-table-column label="营期时间" align="center" prop="dayDate" width="150" />-->
  342. <!-- <el-table-column label="创建时间" align="center" prop="createTime" width="150" />-->
  343. <!-- </el-table>-->
  344. <!-- <div slot="footer" class="dialog-footer">-->
  345. <!--&lt;!&ndash; <el-button type="primary" @click="saveCourseData">保存</el-button>&ndash;&gt;-->
  346. <!-- </div>-->
  347. <!-- </el-dialog>-->
  348. <!-- 添加课程对话框-->
  349. <el-dialog title="添加课程" :visible.sync="course.addOpen" width="500px" append-to-body>
  350. <el-form ref="courseAddForm" :model="course.form" label-width="100px">
  351. <el-form-item label="课程" prop="courseId">
  352. <el-select filterable v-model="course.form.courseId" placeholder="请选择课程" clearable size="small" @change="courseChange(course.form.courseId)" style="width: 100%" :value-key="'dictValue'">
  353. <el-option
  354. v-for="dict in courseList"
  355. :key="dict.dictValue"
  356. :label="dict.dictLabel"
  357. :value="parseInt(dict.dictValue)"
  358. />
  359. </el-select>
  360. </el-form-item>
  361. <el-form-item label="小节" prop="videoId">
  362. <el-select filterable v-model="course.form.videoIds" placeholder="请选择小节" :multiple-limit="getDiff(course.row.periodStartingTime, course.row.periodEndTime) - course.total" multiple clearable size="small" style="width: 100%" :value-key="'dictValue'">
  363. <el-option
  364. v-for="dict in videoList"
  365. :key="dict.dictValue"
  366. :label="dict.dictLabel"
  367. :value="parseInt(dict.dictValue)"
  368. />
  369. </el-select>
  370. </el-form-item>
  371. </el-form>
  372. <div slot="footer" class="dialog-footer">
  373. <el-button type="primary" @click="submitCourseForm">确 定</el-button>
  374. <el-button @click="closeAddCourse">取 消</el-button>
  375. </div>
  376. </el-dialog>
  377. <el-dialog title="修改看课时间" :visible.sync="updateCourse.open" width="500px" append-to-body>
  378. <el-form ref="courseUpdateForm" :model="updateCourse.form" label-width="100px">
  379. <el-form-item label="看课时间" prop="timeRange">
  380. <el-time-picker
  381. is-range
  382. v-model="updateCourse.form.timeRange"
  383. range-separator="至"
  384. start-placeholder="开始时间"
  385. value-format="HH:mm:ss"
  386. end-placeholder="结束时间"
  387. placeholder="选择时间范围">
  388. </el-time-picker>
  389. </el-form-item>
  390. <el-form-item label="领取红包时间" prop="lastJoinTime">
  391. <el-time-picker
  392. v-model="updateCourse.form.joinTime"
  393. :selectableRange="updateCourse.form.timeRange"
  394. value-format="HH:mm:ss"
  395. format="HH:mm:ss"
  396. :picker-options="{
  397. selectableRange: updateCourse.form.timeRange || '00:00:00 - 23:59:59'
  398. }"
  399. placeholder="选择时间范围">
  400. </el-time-picker>
  401. <p style="color: red;margin: 0;font-size: 12px">超过领取红包时间,只允许看课,不允许领取红包</p>
  402. </el-form-item>
  403. </el-form>
  404. <div slot="footer" class="dialog-footer">
  405. <el-button type="primary" @click="submitUpdateCourseForm">确 定</el-button>
  406. <el-button @click="closeUpdateCourse">取 消</el-button>
  407. </div>
  408. </el-dialog>
  409. <!-- <el-dialog title="修改营期时间" :visible.sync="updateDateOpen" width="500px" append-to-body>-->
  410. <!-- <el-form ref="courseUpdateForm" :model="form" label-width="100px">-->
  411. <!-- <el-form-item label="营期时间" prop="dayDate">-->
  412. <!-- <el-date-picker-->
  413. <!-- v-model="form.dayDate"-->
  414. <!-- :selectableRange="form.dayDate"-->
  415. <!-- value-format="yyyy-MM-dd"-->
  416. <!-- type="date"-->
  417. <!-- placeholder="选择时间">-->
  418. <!-- </el-date-picker>-->
  419. <!-- </el-form-item>-->
  420. <!-- </el-form>-->
  421. <!-- <div slot="footer" class="dialog-footer">-->
  422. <!-- <el-button type="primary" @click="updateDate">确 定</el-button>-->
  423. <!-- <el-button @click="updateDateOpen = false">取 消</el-button>-->
  424. <!-- </div>-->
  425. <!-- </el-dialog>-->
  426. <!-- <red-packet-->
  427. <!-- :visible.sync="redPacketVisible"-->
  428. <!-- :periodId="currentRedPacketData.periodId"-->
  429. <!-- :videoId="currentRedPacketData.videoId"-->
  430. <!-- @success="handleRedPacketSuccess"-->
  431. <!-- />-->
  432. <!-- 营期相关设置抽屉 -->
  433. <el-drawer
  434. title="营期相关设置"
  435. :visible.sync="periodSettingsVisible"
  436. direction="rtl"
  437. size="74%"
  438. :destroy-on-close="true"
  439. append-to-body
  440. custom-class="period-settings-drawer"
  441. >
  442. <div class="drawer-content" style="margin-left: 25px">
  443. <el-tabs v-model="activeTab" @tab-click="handleTabClick">
  444. <el-tab-pane label="课程管理" name="course">
  445. <el-row :gutter="10" class="mb8">
  446. <el-col :span="1.5">
  447. <el-button
  448. v-if="(getDiff(periodSettingsData.periodStartingTime, periodSettingsData.periodEndTime) - course.total) > 0"
  449. type="primary"
  450. icon="el-icon-plus"
  451. size="mini"
  452. @click="handleAddCourse"
  453. v-hasPermi="['course:period:addCourse']"
  454. >添加课程</el-button>
  455. </el-col>
  456. <el-col :span="1.5">
  457. <el-button
  458. type="primary"
  459. size="mini"
  460. :disabled="updateCourse.ids.length <= 0"
  461. @click="handleUpdateCourse"
  462. v-hasPermi="['course:period:updateCourseTime']"
  463. >修改看课时间</el-button>
  464. </el-col>
  465. </el-row>
  466. <el-table v-loading="course.loading" :data="course.list" @selection-change="handleSelectionCourseChange" border>
  467. <el-table-column type="selection" width="55" align="center" />
  468. <el-table-column label="课程" align="center" prop="courseName" width="180" />
  469. <el-table-column label="小节" align="center" prop="videoName" />
  470. <el-table-column label="开课状态" align="center" prop="status" width="100" :formatter="courseStatusFormatter" />
  471. <el-table-column label="营期时间" align="center" prop="dayDate" />
  472. <el-table-column label="开始时间" align="center" prop="startDateTime" width="100">
  473. <template slot-scope="scope">
  474. <el-tag>{{parseTime(scope.row.startDateTime, '{h}:{i}:{s}')}}</el-tag>
  475. </template>
  476. </el-table-column>
  477. <el-table-column label="结束时间" align="center" prop="endDateTime" width="100">
  478. <template slot-scope="scope">
  479. <el-tag type="success">{{parseTime(scope.row.endDateTime, '{h}:{i}:{s}')}}</el-tag>
  480. </template>
  481. </el-table-column>
  482. <el-table-column label="领取红包时间" align="center" prop="lastJoinTime" width="100">
  483. <template slot-scope="scope">
  484. <el-tag type="danger">{{parseTime(scope.row.lastJoinTime, '{h}:{i}:{s}')}}</el-tag>
  485. </template>
  486. </el-table-column>
  487. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  488. <template slot-scope="scope">
  489. <el-button
  490. size="mini"
  491. type="text"
  492. icon="el-icon-top"
  493. @click="handleTop(scope.row)"
  494. v-hasPermi="['course:period:courseMove']"
  495. >上移</el-button>
  496. <el-button
  497. size="mini"
  498. type="text"
  499. icon="el-icon-bottom"
  500. @click="handleBottom(scope.row)"
  501. v-hasPermi="['course:period:courseMove']"
  502. >下移</el-button>
  503. <!-- <el-button-->
  504. <!-- size="mini"-->
  505. <!-- type="text"-->
  506. <!-- icon="el-icon-edit"-->
  507. <!-- @click="handleUpdateDate(scope.row)"-->
  508. <!-- >修改营期时间</el-button>-->
  509. </template>
  510. </el-table-column>
  511. </el-table>
  512. </el-tab-pane>
  513. <el-tab-pane label="公司列表" name="company">
  514. <red-packet
  515. :visible.sync="redPacketVisible"
  516. :activeTab="activeTab"
  517. :periodId="periodSettingsData.periodId"
  518. @success="handleRedPacketSuccess"
  519. />
  520. </el-tab-pane>
  521. <el-tab-pane label="课程统计" name="statistics">
  522. <course-statistics
  523. :periodId="periodSettingsData.periodId"
  524. :active="activeTab === 'statistics'"
  525. />
  526. </el-tab-pane>
  527. </el-tabs>
  528. </div>
  529. </el-drawer>
  530. <batch-red-packet
  531. :visible.sync="batchRedPacketVisible"
  532. :selected-data="selectedPeriods"
  533. @success="handleBatchRedPacketSuccess"
  534. />
  535. </div>
  536. </template>
  537. <script>
  538. import {addPeriod, delPeriod, exportPeriod, getPeriod, pagePeriod, updatePeriod, getDays, addCourse, updateCourseTime, updateCourseDate, updateListCourseData, periodCourseMove, closePeriod} from "@/api/course/userCoursePeriod";
  539. import {getCompanyList} from "@/api/company/company";
  540. import { listCamp, addCamp, editCamp, delCamp, copyCamp } from "@/api/course/userCourseCamp";
  541. import { courseList,videoList } from '@/api/course/courseRedPacketLog'
  542. import RedPacket from './redPacket.vue'
  543. import BatchRedPacket from './batchRedPacket.vue'
  544. import CourseStatistics from './statistics.vue'
  545. export default {
  546. name: "Period",
  547. components: {
  548. RedPacket,
  549. BatchRedPacket,
  550. CourseStatistics
  551. },
  552. data() {
  553. return {
  554. // 遮罩层
  555. loading: true,
  556. updateDateOpen: false,
  557. // 左侧遮罩层
  558. leftLoading: true,
  559. // 选中数组
  560. ids: [],
  561. // 非单个禁用
  562. single: true,
  563. // 非多个禁用
  564. multiple: true,
  565. // 显示搜索条件
  566. showSearch: true,
  567. // 总条数
  568. total: 0,
  569. // 左侧总条数
  570. leftTotal: 0,
  571. // 会员营期表格数据
  572. periodList: [],
  573. // 左侧列表数据
  574. leftList: [],
  575. videoList: [],
  576. // 弹出层标题
  577. title: "",
  578. isDisabledDateRange: false, //是否禁用开营日期
  579. // 是否显示弹出层
  580. open: false,
  581. // 查询参数
  582. queryParams: {
  583. pageNum: 1,
  584. pageSize: 10,
  585. periodName: null,
  586. periodStartingTime: null,
  587. periodEndTime: null,
  588. companyIdList: []
  589. },
  590. // 左侧查询参数
  591. leftQueryParams: {
  592. pageNum: 1,
  593. pageSize: 10,
  594. hasNextPage: false,
  595. scs: 'order_number(desc),training_camp_id(desc)',
  596. trainingCampName: null
  597. },
  598. // 表单参数
  599. form: {},
  600. course: {
  601. open: false,
  602. row:{},
  603. list:[],
  604. queryParams: {
  605. pageNum: 1,
  606. pageSize: 10,
  607. },
  608. loading: true,
  609. total: 0,
  610. addOpen: false,
  611. form: {},
  612. },
  613. updateCourse: {
  614. open: false,
  615. loading: true,
  616. ids: [],
  617. form: {},
  618. },
  619. // 表单校验
  620. rules: {
  621. periodName: [
  622. { required: true, message: '营期名称不能为空', trigger: 'blur' }
  623. ],
  624. companyId: [
  625. { required: true, message: '公司不能为空', trigger: 'change' }
  626. ],
  627. redPacketGrantMethod: [
  628. { required: true, message: '红包发放方式不能为空', trigger: 'change' }
  629. ],
  630. periodType: [
  631. { required: true, message: '营期类型不能为空', trigger: 'change' }
  632. ],
  633. maxViewNum: [
  634. { required: true, message: '销售可查看天数不能为空', trigger: 'blur' }
  635. ],
  636. periodStartingTime: [
  637. { required: true, message: '开营日期不能为空', trigger: 'change' }
  638. ]
  639. },
  640. // 公司选项
  641. companyOptions: [],
  642. // 训练营列表
  643. campList: [],
  644. // 激活的训练营索引
  645. activeCampIndex: null,
  646. // 训练营对话框是否显示
  647. campDialogVisible: false,
  648. courseList: false,
  649. // 训练营表单
  650. campForm: {
  651. trainingCampId: null,
  652. trainingCampName: ''
  653. },
  654. // 训练营表单校验
  655. campRules: {
  656. trainingCampName: [
  657. { required: true, message: '训练营名称不能为空', trigger: 'blur' },
  658. { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
  659. ]
  660. },
  661. // 滚动节流标志
  662. scrollThrottle: false,
  663. // 加载更多状态
  664. loadingMore: false,
  665. // 设置红包对话框
  666. redPacketVisible: false,
  667. periodCompanyList: [],
  668. currentRedPacketData: {
  669. periodId: '',
  670. videoId: ''
  671. },
  672. // 营期相关设置抽屉
  673. periodSettingsVisible: false,
  674. activeTab: 'course',
  675. periodSettingsData: {},
  676. companyList: [],
  677. courseDialogVisible: false,
  678. redPacketList: [],
  679. currentCompany: null,
  680. // 选中的营期数据
  681. selectedPeriods: [],
  682. // 批量设置红包按钮是否禁用
  683. batchSetRedPacketDisabled: true,
  684. // 批量设置红包弹出框
  685. batchRedPacketVisible: false,
  686. };
  687. },
  688. created() {
  689. courseList().then(response => {
  690. this.courseList = response.list;
  691. });
  692. // this.getList();
  693. this.getLeftList();
  694. this.getCompanyList();
  695. },
  696. methods: {
  697. /** 查询会员营期列表 */
  698. getList() {
  699. this.loading = true;
  700. const params = { ...this.queryParams };
  701. pagePeriod(params).then(response => {
  702. this.periodList = response.rows;
  703. this.total = response.total;
  704. this.loading = false;
  705. });
  706. },
  707. /** 查询左侧列表 */
  708. getLeftList() {
  709. this.leftLoading = true;
  710. // 重置页码和加载更多状态
  711. this.leftQueryParams.pageNum = 1;
  712. this.loadingMore = false;
  713. // 训练营数据
  714. listCamp(this.leftQueryParams).then(response => {
  715. if (response && response.code === 200) {
  716. this.campList = response.data.list || [];
  717. this.leftQueryParams.hasNextPage = response.data.hasNextPage;
  718. this.activeCampIndex = this.campList.length > 0 ? 0 : null;
  719. this.selectCamp(this.activeCampIndex);
  720. // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
  721. this.$nextTick(() => {
  722. const scrollEl = this.$refs.campList;
  723. if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
  724. this.loadMoreCamps();
  725. }
  726. });
  727. } else {
  728. this.$message.error(response.msg || '获取训练营列表失败');
  729. this.campList = [];
  730. this.leftQueryParams.hasNextPage = false;
  731. }
  732. this.leftLoading = false;
  733. }).catch(error => {
  734. console.error('获取训练营列表失败:', error);
  735. this.$message.error('获取训练营列表失败');
  736. this.campList = [];
  737. this.leftQueryParams.hasNextPage = false;
  738. this.leftLoading = false;
  739. });
  740. },
  741. /** 搜索按钮操作 */
  742. handleQuery() {
  743. this.queryParams.pageNum = 1;
  744. this.getList();
  745. },
  746. /** 左侧搜索按钮操作 */
  747. handleLeftQuery() {
  748. // 重置页码和列表
  749. this.leftQueryParams.pageNum = 1;
  750. this.campList = [];
  751. this.getLeftList();
  752. },
  753. /** 重置按钮操作 */
  754. resetQuery() {
  755. this.resetForm("queryForm");
  756. this.queryParams.companyIdList = [];
  757. this.handleQuery();
  758. },
  759. /** 多选框选中数据 */
  760. handleSelectionChange(selection) {
  761. this.ids = selection.map(item => item.periodId)
  762. this.single = selection.length!==1
  763. this.multiple = !selection.length
  764. // 更新批量设置红包相关数据
  765. this.selectedPeriods = selection;
  766. this.batchSetRedPacketDisabled = selection.length === 0;
  767. },
  768. handleSelectionCourseChange(selection) {
  769. this.updateCourse.ids = selection.map(item => item.id)
  770. },
  771. /** 新增按钮操作 */
  772. handleAdd() {
  773. this.reset();
  774. this.open = true;
  775. this.title = "添加会员营期";
  776. this.isDisabledDateRange = false;
  777. },
  778. /** 修改按钮操作 */
  779. handleUpdate(row) {
  780. this.reset();
  781. const periodId = row.periodId || this.ids
  782. getPeriod(periodId).then(response => {
  783. this.form = response.data;
  784. if (this.form.companyId) {
  785. this.form.companyId = this.form.companyId.split(',').map(id => Number(id));
  786. }
  787. // 设置看课时间范围(回显)
  788. if (this.form.viewStartTime && this.form.viewEndTime) {
  789. this.form.timeRange = [this.form.viewStartTime, this.form.viewEndTime];
  790. }
  791. if(this.form.periodType == 1){
  792. this.form.dateRange = [this.form.periodStartingTime, this.form.periodEndTime];
  793. }
  794. if(this.form.periodType == 1){
  795. this.form.date = this.form.periodStartingTime;
  796. }
  797. this.open = true;
  798. this.title = "修改会员营期";
  799. this.isDisabledDateRange = true;
  800. });
  801. },
  802. /** 提交按钮 */
  803. submitForm() {
  804. this.$refs["form"].validate(valid => {
  805. if (valid) {
  806. let data = JSON.parse(JSON.stringify(this.form));
  807. // 处理看课时间范围
  808. if (data.timeRange && data.timeRange.length === 2) {
  809. data.viewStartTime = data.timeRange[0];
  810. data.viewEndTime = data.timeRange[1];
  811. }
  812. data.companyId = data.companyId.join()
  813. data.trainingCampId = this.queryParams.trainingCampId
  814. if (data.periodId != null) {
  815. updatePeriod(data).then(response => {
  816. if (response.code === 200) {
  817. this.msgSuccess("修改成功");
  818. this.open = false;
  819. this.getList();
  820. }
  821. });
  822. } else {
  823. addPeriod(data).then(response => {
  824. if (response.code === 200) {
  825. this.msgSuccess("新增成功");
  826. this.open = false;
  827. this.getList();
  828. }
  829. });
  830. }
  831. }
  832. });
  833. },
  834. /** 删除按钮操作 */
  835. handleDelete(row) {
  836. //添加删除判断,只能删除未开始的营期
  837. console.log(row.periodStatus)
  838. if(row.periodStatus !== 1){
  839. this.$message.error('营期处于进行中或者结束,不能删除');
  840. return;
  841. }
  842. const periodIds = row.periodId || this.ids;
  843. this.$confirm('是否确认删除该营期?', "警告", {
  844. confirmButtonText: "确定",
  845. cancelButtonText: "取消",
  846. type: "warning"
  847. }).then(function() {
  848. return delPeriod(periodIds);
  849. }).then(() => {
  850. this.getList();
  851. this.msgSuccess("删除成功");
  852. }).catch(function() {});
  853. },
  854. /** 导出按钮操作 */
  855. handleExport() {
  856. const queryParams = this.queryParams;
  857. this.$confirm('是否确认导出所有会员营期数据项?', "警告", {
  858. confirmButtonText: "确定",
  859. cancelButtonText: "取消",
  860. type: "warning"
  861. }).then(function() {
  862. return exportPeriod(queryParams);
  863. }).then(response => {
  864. this.download(response.msg);
  865. }).catch(function() {});
  866. },
  867. /** 批量设置红包 */
  868. handleBatchSetRedPacket() {
  869. if (this.selectedPeriods.length === 0) {
  870. this.$message.warning('请至少选择一个营期');
  871. return;
  872. }
  873. this.batchRedPacketVisible = true;
  874. },
  875. /** 处理批量设置红包保存 */
  876. // handleBatchRedPacketSave(data) {
  877. // // 这里等待接口提供后补充具体实现
  878. // // 示例代码:
  879. // // batchSetRedPacket(data).then(response => {
  880. // // if (response.code === 200) {
  881. // // this.$message.success('批量设置成功');
  882. // // this.getList();
  883. // // }
  884. // // });
  885. // this.batchRedPacketVisible = false;
  886. // },
  887. /** 获取公司下拉列表*/
  888. getCompanyList() {
  889. this.loading = true;
  890. getCompanyList().then(response => {
  891. this.companyOptions = response.data;
  892. this.loading = false;
  893. });
  894. },
  895. // 取消按钮
  896. cancel() {
  897. this.open = false;
  898. this.reset();
  899. },
  900. // 表单重置
  901. reset() {
  902. this.form = {
  903. periodId: null,
  904. periodName: null,
  905. companyId: null,
  906. courseId: null,
  907. videoId: null,
  908. trainingCampId: null,
  909. createTime: null,
  910. updateTime: null,
  911. courseStyle: null,
  912. liveRoomStyle: null,
  913. redPacketGrantMethod: 1,
  914. periodType: 1,
  915. periodStartingTime: null,
  916. dateRange: [],
  917. date: null,
  918. days: [],
  919. maxViewNum: 0,
  920. periodEndTime: null,
  921. timeRange: [], // 看课时间范围
  922. viewStartTime: null, // 看课开始时间
  923. viewEndTime: null, // 看课结束时间
  924. lastJoinTime: null // 领取红包时间
  925. };
  926. this.resetForm("form");
  927. },
  928. // 处理训练营列表的逻辑
  929. handleDeleteCamp(item) {
  930. this.$confirm(`确定要删除训练营"${item.trainingCampName}"吗?`, '提示', {
  931. confirmButtonText: '删除',
  932. cancelButtonText: '取消',
  933. type: 'warning'
  934. }).then(() => {
  935. // 调用删除训练营API
  936. const trainingCampId = item.trainingCampId;
  937. this.leftLoading = true;
  938. delCamp(trainingCampId).then(response => {
  939. if (response.code === 200) {
  940. this.$message.success('删除成功');
  941. // 从列表中移除
  942. const index = this.campList.findIndex(camp => camp.trainingCampId === trainingCampId);
  943. if (index !== -1) {
  944. this.campList.splice(index, 1);
  945. }
  946. // 如果删除的是当前选中的训练营,则重置选中状态
  947. if (this.activeCampIndex === index) {
  948. this.activeCampIndex = this.campList.length > 0 ? 0 : null;
  949. if (this.activeCampIndex !== null) {
  950. // 更新右侧列表
  951. this.selectCamp(this.activeCampIndex);
  952. } else {
  953. // 没有训练营了,清空右侧列表
  954. this.periodList = [];
  955. }
  956. }
  957. } else {
  958. this.$message.error(response.msg || '删除失败');
  959. }
  960. this.leftLoading = false;
  961. }).catch(error => {
  962. this.$message.error('删除失败: ' + error.message);
  963. this.leftLoading = false;
  964. });
  965. }).catch(() => {
  966. this.$message.info('已取消删除');
  967. });
  968. },
  969. /** 复制训练营 */
  970. handleCopyCamp(item) {
  971. // 调用添加训练营API
  972. copyCamp(item.trainingCampId).then(response => {
  973. if (response.code === 200) {
  974. this.$message.success('复制成功');
  975. // 重新加载训练营列表
  976. this.getLeftList();
  977. } else {
  978. this.$message.error(response.msg || '复制训练营失败');
  979. }
  980. }).catch(error => {
  981. this.$message.error('复制训练营失败: ' + error.message);
  982. });
  983. },
  984. /** 修改训练营按钮操作 */
  985. handleEditCamp(item) {
  986. this.resetCampForm();
  987. this.leftLoading = true;
  988. // 获取最新的训练营数据
  989. const trainingCampId = item.trainingCampId;
  990. // 应该调用获取训练营详情的API
  991. setTimeout(() => {
  992. // 填充表单数据
  993. this.campForm = {
  994. trainingCampId: item.trainingCampId,
  995. trainingCampName: item.trainingCampName,
  996. orderNumber: item.orderNumber || 1,
  997. status: item.status !== undefined ? item.status : 1
  998. };
  999. this.campDialogVisible = true;
  1000. this.leftLoading = false;
  1001. }, 300);
  1002. },
  1003. /** 新建训练营按钮操作 */
  1004. handleAddTrainingCamp() {
  1005. this.resetCampForm();
  1006. this.campDialogVisible = true;
  1007. },
  1008. /** 重置训练营表单 */
  1009. resetCampForm() {
  1010. this.campForm = {
  1011. trainingCampId: null,
  1012. trainingCampName: ''
  1013. };
  1014. // 如果表单已经创建,则重置校验结果
  1015. if (this.$refs.campForm) {
  1016. this.$refs.campForm.resetFields();
  1017. }
  1018. },
  1019. /** 取消训练营表单 */
  1020. cancelCampForm() {
  1021. this.campDialogVisible = false;
  1022. this.resetCampForm();
  1023. },
  1024. /** 提交训练营表单 */
  1025. submitCampForm() {
  1026. this.$refs.campForm.validate(valid => {
  1027. if (valid) {
  1028. // 显示加载中
  1029. this.leftLoading = true;
  1030. // 准备提交的数据
  1031. const submitData = JSON.parse(JSON.stringify(this.campForm));
  1032. // 判断是新增还是修改
  1033. if (submitData.trainingCampId) {
  1034. // 修改训练营
  1035. editCamp(submitData).then(response => {
  1036. if (response.code === 200) {
  1037. this.$message.success('修改训练营成功');
  1038. this.campDialogVisible = false;
  1039. // 更新列表中的数据
  1040. const index = this.campList.findIndex(camp => camp.trainingCampId === submitData.trainingCampId);
  1041. if (index !== -1) {
  1042. this.campList[index] = { ...this.campList[index], ...submitData };
  1043. // 如果修改的是当前选中的训练营,更新右侧列表
  1044. if (this.activeCampIndex === index) {
  1045. this.selectCamp(index);
  1046. }
  1047. }
  1048. // 重新加载训练营列表
  1049. this.getLeftList();
  1050. } else {
  1051. this.$message.error(response.msg || '修改训练营失败');
  1052. this.leftLoading = false;
  1053. }
  1054. }).catch(error => {
  1055. this.$message.error('修改训练营失败: ' + (error.message || '未知错误'));
  1056. this.leftLoading = false;
  1057. });
  1058. } else {
  1059. // 新增训练营
  1060. addCamp(submitData).then(response => {
  1061. if (response.code === 200) {
  1062. this.$message.success('新建训练营成功');
  1063. this.campDialogVisible = false;
  1064. // 重新加载训练营列表
  1065. this.getLeftList();
  1066. } else {
  1067. this.$message.error(response.msg || '新建训练营失败');
  1068. this.leftLoading = false;
  1069. }
  1070. }).catch(error => {
  1071. this.$message.error('新建训练营失败: ' + (error.message || '未知错误'));
  1072. this.leftLoading = false;
  1073. });
  1074. }
  1075. }
  1076. });
  1077. },
  1078. /** 排序方式改变 */
  1079. handleSortChange(value) {
  1080. this.leftQueryParams.scs = value;
  1081. // 重置页码和列表
  1082. this.leftQueryParams.pageNum = 1;
  1083. this.campList = [];
  1084. this.getLeftList();
  1085. },
  1086. /** 选中训练营 */
  1087. selectCamp(index) {
  1088. if(index == null || index == undefined) return;
  1089. this.activeCampIndex = index;
  1090. // 加载对应的训练营营期数据
  1091. const selectedCamp = this.campList[index];
  1092. this.queryParams.trainingCampId = selectedCamp.trainingCampId;
  1093. this.getList();
  1094. },
  1095. /** 处理滚动事件,实现滚动到底部加载更多 */
  1096. handleScroll() {
  1097. // 如果正在节流中或者正在加载中,则不处理
  1098. if (this.scrollThrottle || this.loadingMore) return;
  1099. // 设置节流,200ms内不再处理滚动事件
  1100. this.scrollThrottle = true;
  1101. setTimeout(() => {
  1102. this.scrollThrottle = false;
  1103. }, 200);
  1104. const scrollEl = this.$refs.campList;
  1105. if (!scrollEl) return;
  1106. // 判断是否滚动到底部:滚动高度 + 可视高度 >= 总高度 - 30(添加30px的容差,提前触发加载)
  1107. const isBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 30;
  1108. // 如果滚动到底部,且有下一页数据,且当前不在加载中,则加载更多
  1109. if (isBottom && this.leftQueryParams.hasNextPage && !this.leftLoading && !this.loadingMore) {
  1110. this.loadMoreCamps();
  1111. }
  1112. },
  1113. /** 加载更多训练营数据 */
  1114. loadMoreCamps() {
  1115. // 已在加载中,防止重复加载
  1116. if (this.leftLoading || this.loadingMore) return;
  1117. // 设置加载状态
  1118. this.loadingMore = true;
  1119. // 页码加1
  1120. this.leftQueryParams.pageNum += 1;
  1121. // 加载下一页数据
  1122. listCamp(this.leftQueryParams).then(response => {
  1123. if (response && response.code === 200) {
  1124. // 将新数据追加到列表中
  1125. const newList = response.data.list || [];
  1126. if (newList.length > 0) {
  1127. this.campList = [...this.campList, ...newList];
  1128. }
  1129. // 更新是否有下一页的标志
  1130. this.leftQueryParams.hasNextPage = response.data.hasNextPage;
  1131. // 如果当前显示的列表高度不足以触发滚动,但还有更多数据,自动加载下一页
  1132. this.$nextTick(() => {
  1133. const scrollEl = this.$refs.campList;
  1134. if (scrollEl && this.leftQueryParams.hasNextPage && scrollEl.scrollHeight <= scrollEl.clientHeight) {
  1135. // 延迟一点再加载下一页,避免过快加载
  1136. setTimeout(() => {
  1137. this.loadMoreCamps();
  1138. }, 300);
  1139. }
  1140. });
  1141. } else {
  1142. this.$message.error(response.msg || '加载更多训练营失败');
  1143. }
  1144. this.loadingMore = false;
  1145. }).catch(error => {
  1146. console.error('加载更多训练营失败:', error);
  1147. this.$message.error('加载更多训练营失败');
  1148. this.loadingMore = false;
  1149. });
  1150. },
  1151. timeChange(type){
  1152. if(type == 1){
  1153. this.form.periodStartingTime = this.form.dateRange[0];
  1154. this.form.periodEndTime = this.form.dateRange[1];
  1155. // 转换为天数
  1156. let days = this.getDiff(this.form.periodStartingTime, this.form.periodEndTime);
  1157. for (let i = 0; i < days; i++) {
  1158. this.form.days.push({lesson: i + 1});
  1159. }
  1160. }
  1161. if(type == 2){
  1162. this.form.periodStartingTime = this.form.date;
  1163. this.form.periodEndTime = this.form.date;
  1164. }
  1165. },
  1166. getDiff(start, end) {
  1167. if(start == null || start == undefined || start == '') return 0;
  1168. if(end == null || end == undefined || end == '') return 0;
  1169. if(start == end) 1;
  1170. const startDate = this.getUTCDate(start);
  1171. const endDate = this.getUTCDate(end);
  1172. const timeDiff = endDate - startDate;
  1173. return (Math.floor(timeDiff / (1000 * 3600 * 24))) + 1; // 直接取整
  1174. },
  1175. getUTCDate(dateStr) {
  1176. const [year, month, day] = dateStr.split('-').map(Number);
  1177. return new Date(Date.UTC(year, month - 1, day)); // 月份从0开始
  1178. },
  1179. handleCourse(row){
  1180. this.course = {
  1181. open: false,
  1182. row:{},
  1183. list:[],
  1184. queryParams: {
  1185. pageNum: 1,
  1186. pageSize: 9999,
  1187. },
  1188. loading: true,
  1189. total: 0,
  1190. addOpen: false,
  1191. form: {},
  1192. };
  1193. this.course.open = true;
  1194. this.course.row = row;
  1195. this.course.queryParams.periodId = row.periodId;
  1196. this.getCourseList();
  1197. },
  1198. getCourseList(){
  1199. this.course.loading = true;
  1200. getDays(this.course.queryParams).then(e => {
  1201. this.course.list = e.rows;
  1202. this.course.total = e.total;
  1203. this.course.loading = false;
  1204. });
  1205. },
  1206. handleAddCourse() {
  1207. this.course.addOpen = true;
  1208. this.course.form = {
  1209. periodId: this.course.queryParams.periodId,
  1210. courseId: null,
  1211. videoIds: []
  1212. };
  1213. // 重置表单
  1214. this.$nextTick(() => {
  1215. if (this.$refs.courseAddForm) {
  1216. this.$refs.courseAddForm.resetFields();
  1217. }
  1218. });
  1219. },
  1220. handleUpdateCourse() {
  1221. this.updateCourse.open = true;
  1222. this.updateCourse.form = {
  1223. ids: this.updateCourse.ids,
  1224. timeRange: [], // 初始化timeRange
  1225. joinTime: null // 初始化joinTime
  1226. };
  1227. },
  1228. closeAddCourse() {
  1229. this.course.addOpen = false;
  1230. this.course.form = {
  1231. periodId: null,
  1232. courseId: null,
  1233. videoIds: []
  1234. };
  1235. // 重置表单
  1236. if (this.$refs.courseAddForm) {
  1237. this.$refs.courseAddForm.resetFields();
  1238. }
  1239. },
  1240. closeUpdateCourse() {
  1241. this.updateCourse.open = false;
  1242. },
  1243. courseChange(row){
  1244. this.course.form.videoIds = [];
  1245. videoList(row).then(response => {
  1246. this.videoList=response.list
  1247. });
  1248. },
  1249. submitCourseForm(){
  1250. this.$refs.courseAddForm.validate(valid => {
  1251. if (valid) {
  1252. if(this.course.form.timeRange != null && this.course.form.timeRange.length === 2){
  1253. this.course.form.startTime = this.course.form.timeRange[0];
  1254. this.course.form.endTime1 = this.course.form.timeRange[1];
  1255. }
  1256. // 提交数据
  1257. addCourse(this.course.form).then(response => {
  1258. this.$message.success('添加成功');
  1259. this.course.addOpen = false;
  1260. // 重新加载训练营列表
  1261. this.getCourseList();
  1262. });
  1263. }
  1264. });
  1265. },
  1266. submitUpdateCourseForm(){
  1267. this.$refs.courseUpdateForm.validate(valid => {
  1268. if (valid) {
  1269. if(this.updateCourse.form.timeRange != null && this.updateCourse.form.timeRange.length === 2){
  1270. this.updateCourse.form.startTime = this.updateCourse.form.timeRange[0];
  1271. this.updateCourse.form.endTime1 = this.updateCourse.form.timeRange[1];
  1272. }
  1273. // 提交数据
  1274. updateCourseTime(this.updateCourse.form).then(response => {
  1275. this.$message.success('添加成功');
  1276. this.updateCourse.open = false;
  1277. // 重新加载训练营列表
  1278. this.getCourseList();
  1279. });
  1280. }
  1281. });
  1282. },
  1283. updateDate(){
  1284. updateCourseDate(this.form).then(response => {
  1285. this.$message.success('修改成功');
  1286. this.updateDateOpen = false;
  1287. // 重新加载训练营列表
  1288. this.getCourseList();
  1289. });
  1290. },
  1291. saveCourseData(){
  1292. updateListCourseData(this.course.list).then(response => {
  1293. this.$message.success('保存成功');
  1294. this.getCourseList();
  1295. });
  1296. },
  1297. setRedPacket(row) {
  1298. this.currentRedPacketData = {
  1299. periodId: row.periodId
  1300. // videoId: row.videoId
  1301. };
  1302. this.redPacketVisible = true;
  1303. },
  1304. handleRedPacketSuccess() {
  1305. this.getCourseList();
  1306. },
  1307. handlePeriodSettings(row) {
  1308. this.periodSettingsData = row;
  1309. this.periodSettingsVisible = true;
  1310. // 初始化课程列表
  1311. this.course.queryParams.periodId = row.periodId;
  1312. // 根据当前激活的tab加载对应数据
  1313. this.handleTabClick({ name: this.activeTab });
  1314. },
  1315. // 结束营期
  1316. handleClosePeriod(row) {
  1317. const msg = `注: 1.确认结束营期,该营期的开营结束时间改为当天24点。2.当天正在播放中的课程不变。3.第二天如有未开始的课程,统一改为已结束。是否确认结束 ${row.periodName} 营期吗?`
  1318. this.$confirm(msg, "警告", {
  1319. confirmButtonText: "确定",
  1320. cancelButtonText: "取消",
  1321. type: "warning"
  1322. }).then(() => {
  1323. closePeriod({id: row.periodId}).then(response => {
  1324. if (response.code === 200) {
  1325. this.getList()
  1326. } else {
  1327. this.$message.error(response.msg)
  1328. }
  1329. })
  1330. }).catch(() => {})
  1331. },
  1332. handleBatchRedPacketSuccess() {
  1333. this.batchRedPacketVisible = false;
  1334. this.getCourseList();
  1335. },
  1336. /** 处理tab切换 */
  1337. handleTabClick(tab) {
  1338. if (tab.name === 'course') {
  1339. this.getCourseList();
  1340. } else if (tab.name === 'company') {
  1341. this.redPacketVisible = true;
  1342. }
  1343. },
  1344. /** 上移课程 */
  1345. handleTop(row) {
  1346. const currentIndex = this.course.list.findIndex(item => item.id === row.id);
  1347. if (currentIndex <= 0) {
  1348. this.$message.warning('已经是第一条数据');
  1349. return;
  1350. }
  1351. // 获取上一条数据
  1352. const prevRow = this.course.list[currentIndex - 1];
  1353. console.log({
  1354. id: row.id,
  1355. targetId: prevRow.id,
  1356. type: 1 //上移
  1357. })
  1358. periodCourseMove({
  1359. id: row.id,
  1360. targetId: prevRow.id,
  1361. type: 1 //上移
  1362. }).then(response => {
  1363. if (response.code === 200) {
  1364. this.$message.success('上移成功');
  1365. this.getCourseList();
  1366. } else {
  1367. this.$message.error(response.msg || '上移失败');
  1368. }
  1369. }).catch(() => {
  1370. this.$message.error('上移失败');
  1371. });
  1372. },
  1373. /** 下移课程 */
  1374. handleBottom(row) {
  1375. const currentIndex = this.course.list.findIndex(item => item.id === row.id);
  1376. if (currentIndex === -1 || currentIndex >= this.course.list.length - 1) {
  1377. this.$message.warning('已经是最后一条数据');
  1378. return;
  1379. }
  1380. // 获取下一条数据
  1381. const nextRow = this.course.list[currentIndex + 1];
  1382. periodCourseMove({
  1383. id: row.id,
  1384. targetId: nextRow.id,
  1385. type: 2 //下移
  1386. }).then(response => {
  1387. if (response.code === 200) {
  1388. this.$message.success('下移成功');
  1389. this.getCourseList(); // 重新加载列表
  1390. } else {
  1391. this.$message.error(response.msg || '下移失败');
  1392. }
  1393. }).catch(() => {
  1394. this.$message.error('下移失败');
  1395. });
  1396. },
  1397. /** 营期状态格式化 */
  1398. periodStatusFormatter(row) {
  1399. const statusMap = {
  1400. 1: '未开始',
  1401. 2: '进行中',
  1402. 3: '已结束'
  1403. };
  1404. return statusMap[row.periodStatus] || '未知状态';
  1405. },
  1406. /** 开课状态格式化 */
  1407. courseStatusFormatter(row) {
  1408. const statusMap = {
  1409. 0: '未开始',
  1410. 1: '进行中',
  1411. 2: '已结束'
  1412. };
  1413. return statusMap[row.status] || '未知状态';
  1414. },
  1415. /** 营期状态格式化 */
  1416. handleUpdateDate(row) {
  1417. this.form = {id: row.id, dayDate: row.dayDate};
  1418. this.updateDateOpen = true;
  1419. },
  1420. disabledDate(time) {
  1421. return time.getTime() < new Date(new Date().setHours(0,0,0,0));
  1422. },
  1423. },
  1424. };
  1425. </script>
  1426. <style scoped>
  1427. .left-aside {
  1428. background-color: #fff;
  1429. border-right: 1px solid #EBEEF5;
  1430. padding: 0;
  1431. display: flex;
  1432. flex-direction: column;
  1433. height: 800px;
  1434. }
  1435. .left-header {
  1436. padding: 10px;
  1437. border-bottom: 1px solid #EBEEF5;
  1438. }
  1439. .left-header-top {
  1440. display: flex;
  1441. justify-content: space-between;
  1442. align-items: center;
  1443. margin-bottom: 10px;
  1444. }
  1445. .search-btn {
  1446. width: 50%;
  1447. height: 36px;
  1448. background-color: #409EFF;
  1449. color: white;
  1450. border: none;
  1451. }
  1452. .search-input-wrapper {
  1453. margin-bottom: 10px;
  1454. }
  1455. .sort-wrapper {
  1456. display: flex;
  1457. align-items: center;
  1458. margin-bottom: 10px;
  1459. }
  1460. .sort-label {
  1461. width: 70px;
  1462. font-size: 14px;
  1463. font-weight: 600;
  1464. color: #909399;
  1465. }
  1466. .sort-select {
  1467. margin-left: 10px;
  1468. width: 280px;
  1469. }
  1470. .color-wrapper {
  1471. display: flex;
  1472. align-items: center;
  1473. margin-bottom: 10px;
  1474. }
  1475. .color-label {
  1476. width: 70px;
  1477. color: #606266;
  1478. }
  1479. .color-hint {
  1480. margin-left: 10px;
  1481. color: #606266;
  1482. font-size: 12px;
  1483. }
  1484. .color-help {
  1485. margin-left: 5px;
  1486. color: #909399;
  1487. cursor: pointer;
  1488. }
  1489. .hint-text {
  1490. color: #606266;
  1491. font-size: 12px;
  1492. margin-top: 5px;
  1493. }
  1494. .camp-list {
  1495. flex: 1;
  1496. overflow-y: auto;
  1497. padding: 3px;
  1498. }
  1499. .camp-item {
  1500. margin-bottom: 5px;
  1501. padding: 15px;
  1502. background-color: #ffffff;
  1503. position: relative;
  1504. cursor: pointer;
  1505. display: flex;
  1506. justify-content: space-between;
  1507. border: 1px solid #eaedf2;
  1508. }
  1509. .camp-item:last-child {
  1510. margin-bottom: 0;
  1511. }
  1512. .camp-item:hover {
  1513. background-color: #f5f9ff;
  1514. }
  1515. .camp-item.active {
  1516. background-color: #eaf4ff;
  1517. border-left: 1px solid #75b8fc;
  1518. }
  1519. .camp-content {
  1520. flex: 1;
  1521. padding-right: 10px;
  1522. }
  1523. .camp-title {
  1524. font-weight: bold;
  1525. font-size: 16px;
  1526. margin-bottom: 8px;
  1527. color: #333;
  1528. display: flex;
  1529. align-items: center;
  1530. }
  1531. .camp-icon {
  1532. font-size: 16px;
  1533. margin-right: 6px;
  1534. color: #409EFF;
  1535. }
  1536. .camp-info {
  1537. display: flex;
  1538. justify-content: space-between;
  1539. margin-bottom: 8px;
  1540. font-size: 12px;
  1541. color: #c4c1c1;
  1542. line-height: 1.5;
  1543. }
  1544. .camp-stats {
  1545. display: flex;
  1546. justify-content: space-between;
  1547. font-size: 12px;
  1548. color: #666;
  1549. background-color: #f5f9ff;
  1550. padding: 6px 10px;
  1551. border-radius: 4px;
  1552. line-height: 1.5;
  1553. }
  1554. .stat-item {
  1555. display: flex;
  1556. align-items: center;
  1557. }
  1558. .stat-item i {
  1559. margin-right: 4px;
  1560. font-size: 14px;
  1561. color: #409EFF;
  1562. }
  1563. .camp-actions {
  1564. display: flex;
  1565. flex-direction: column;
  1566. justify-content: center;
  1567. align-items: flex-end;
  1568. gap: 8px;
  1569. border-left: 1px dashed #eaedf2;
  1570. padding-left: 12px;
  1571. min-width: 50px;
  1572. }
  1573. .action-btn {
  1574. padding: 2px 5px;
  1575. font-size: 12px;
  1576. border-radius: 4px;
  1577. transition: all 0.2s;
  1578. }
  1579. .action-btn:hover {
  1580. background-color: rgba(255, 255, 255, 0.8);
  1581. }
  1582. .delete-btn {
  1583. color: #f56c6c;
  1584. }
  1585. .delete-btn:hover {
  1586. background-color: rgba(245, 108, 108, 0.1);
  1587. }
  1588. .copy-btn {
  1589. color: #409EFF;
  1590. }
  1591. .copy-btn:hover {
  1592. background-color: rgba(64, 158, 255, 0.1);
  1593. }
  1594. .warning-icon {
  1595. color: #E6A23C;
  1596. font-size: 16px;
  1597. margin-left: 5px;
  1598. }
  1599. .el-main {
  1600. padding: 10px;
  1601. }
  1602. /* 添加训练营表单样式 */
  1603. .drawer-footer {
  1604. //position: absolute;
  1605. bottom: 0;
  1606. left: 0;
  1607. right: 0;
  1608. padding: 20px;
  1609. background: #fff;
  1610. text-align: right;
  1611. border-top: 1px solid #e8e8e8;
  1612. }
  1613. .el-input-number {
  1614. width: 100%;
  1615. }
  1616. /* 加载更多样式 */
  1617. .loading-more {
  1618. display: flex;
  1619. align-items: center;
  1620. justify-content: center;
  1621. padding: 12px 0;
  1622. color: #909399;
  1623. font-size: 14px;
  1624. }
  1625. .loading-more i {
  1626. margin-right: 5px;
  1627. font-size: 16px;
  1628. }
  1629. /* 无更多数据提示 */
  1630. .no-more-data {
  1631. display: flex;
  1632. align-items: center;
  1633. justify-content: center;
  1634. padding: 12px 0;
  1635. color: #c0c4cc;
  1636. font-size: 13px;
  1637. }
  1638. .no-more-data span {
  1639. position: relative;
  1640. display: flex;
  1641. align-items: center;
  1642. }
  1643. </style>