userCourseCatalogDetailsZM.vue 79 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117
  1. <template>
  2. <div class="app-container">
  3. <div style="padding-bottom: 20px">
  4. <span v-if="courseName != null">{{ courseName }}</span>
  5. </div>
  6. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
  7. <el-form-item label="小节名称" prop="title">
  8. <el-input v-model="queryParams.title" placeholder="请输入小节名称" clearable size="small"
  9. @keyup.enter.native="handleQuery"/>
  10. </el-form-item>
  11. <el-form-item label="小节id" prop="videoId">
  12. <el-input v-model="queryParams.videoId" placeholder="请输入小节id" clearable size="small"
  13. @keyup.enter.native="handleQuery"/>
  14. </el-form-item>
  15. <el-form-item>
  16. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  17. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  18. </el-form-item>
  19. </el-form>
  20. <el-row :gutter="10" class="mb8">
  21. <el-col :span="1.5">
  22. <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
  23. v-hasPermi="['course:userCourseVideo:add']">新增目录
  24. </el-button>
  25. </el-col>
  26. <el-col :span="1.5">
  27. <el-button type="primary" plain :disabled="!ids || ids.length <= 0" size="mini" @click="openUpdates"
  28. v-hasPermi="['course:userCourseVideo:updateTime']">修改时间
  29. </el-button>
  30. </el-col>
  31. <el-col :span="1.5">
  32. <el-button type="primary" plain size="mini" @click="openAdds"
  33. v-hasPermi="['course:userCourseVideo:batchAdd']">批量添加
  34. </el-button>
  35. </el-col>
  36. <el-col :span="1.5">
  37. <el-button type="primary" plain size="mini" @click="updateRedPageckeOpen"
  38. v-hasPermi="['course:userCourseVideo:updateRed']">修改红包
  39. </el-button>
  40. </el-col>
  41. <el-col :span="1.5">
  42. <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
  43. v-hasPermi="['course:userCourseVideo:remove']">删除
  44. </el-button>
  45. </el-col>
  46. <el-col :span="1.5">
  47. <el-button type="warning" plain icon="el-icon-edit" size="mini" @click="handleCourseSort"
  48. v-hasPermi="['course:userCourseVideo:sort']">修改课节排序
  49. </el-button>
  50. </el-col>
  51. <el-col :span="1.5">
  52. <el-button type="primary" plain icon="el-icon-delete" size="mini" @click="handleSync"
  53. v-hasPermi="['course:userCourseVideo:sync']">同步模板数据
  54. </el-button>
  55. </el-col>
  56. <el-col :span="1.5">
  57. <el-button type="primary" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="handleDown"
  58. v-hasPermi="['course:userCourseVideo:batchDown']">批量下架
  59. </el-button>
  60. </el-col>
  61. <el-col :span="1.5">
  62. <el-button type="primary" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="handleUp"
  63. v-hasPermi="['course:userCourseVideo:batchUp']">批量上架</el-button>
  64. </el-col>
  65. <el-col :span="1.5">
  66. <el-button type="primary" plain icon="el-icon-edit" size="mini" :disabled="multiple" @click="handleEditCover"
  67. v-hasPermi="['course:userCourseVideo:batchEditCover']">批量修改封面图
  68. </el-button>
  69. </el-col>
  70. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  71. </el-row>
  72. <el-table border v-loading="loading" :data="userCourseVideoList" @selection-change="handleSelectionChange">
  73. <el-table-column type="selection" width="55" align="center"/>
  74. <el-table-column label="视频ID" align="center" prop="videoId"/>
  75. <el-table-column label="小节名称" align="center" show-overflow-tooltip prop="title"/>
  76. <el-table-column label="视频文件名称" align="center" show-overflow-tooltip prop="fileName">
  77. </el-table-column>
  78. <el-table-column label="视频时长" align="center" prop="duration">
  79. <template slot-scope="{ row }">
  80. {{ formatDuration(row.duration) }}
  81. </template>
  82. </el-table-column>
  83. <el-table-column label="看课开始时间" align="center" prop="duration">
  84. <template slot-scope="{ row }">
  85. <el-tag v-if="row.viewStartTime">{{ row.viewStartTime }}</el-tag>
  86. <el-tag type="danger" v-if="!row.viewStartTime">无</el-tag>
  87. </template>
  88. </el-table-column>
  89. <el-table-column label="看课结束时间" align="center" prop="duration">
  90. <template slot-scope="{ row }">
  91. <el-tag v-if="row.viewEndTime">{{ row.viewEndTime }}</el-tag>
  92. <el-tag type="danger" v-if="!row.viewEndTime">无</el-tag>
  93. </template>
  94. </el-table-column>
  95. <el-table-column label="领取红包时间" align="center" prop="duration">
  96. <template slot-scope="{ row }">
  97. <el-tag v-if="row.lastJoinTime">{{ row.lastJoinTime }}</el-tag>
  98. <el-tag type="danger" v-if="!row.lastJoinTime">无</el-tag>
  99. </template>
  100. </el-table-column>
  101. <el-table-column label="红包金额" align="center" prop="redPacketMoney"/>
  102. <el-table-column label="排序" align="center" prop="courseSort"/>
  103. <el-table-column label="上传时间" align="center" prop="createTime"/>
  104. <el-table-column label="是否上架" align="center" prop="isOnPut">
  105. <template slot-scope="{ row }">
  106. <el-tag v-if="row.isOnPut == 0">是</el-tag>
  107. <el-tag type="danger" v-if="row.isOnPut == 1">否</el-tag>
  108. </template>
  109. </el-table-column>
  110. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  111. <template slot-scope="scope">
  112. <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
  113. v-hasPermi="['course:userCourseVideo:edit']">修改
  114. </el-button>
  115. <el-button size="mini" type="text" icon="el-icon-edit" @click="handleComment(scope.row)"
  116. v-hasPermi="['course:courseWatchComment:list']">查看评论
  117. </el-button>
  118. <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
  119. v-hasPermi="['course:userCourseVideo:remove']">删除
  120. </el-button>
  121. </template>
  122. </el-table-column>
  123. </el-table>
  124. <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
  125. @pagination="getList"/>
  126. <el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
  127. <el-form ref="form" :model="form" :rules="rules" label-width="110px" v-loading="uploadLoading">
  128. <el-form-item label="视频标题" prop="title">
  129. <el-input v-model="form.title" placeholder="请输入内容"/>
  130. </el-form-item>
  131. <el-form-item label="视频描述" prop="description">
  132. <el-input v-model="form.description" type="textarea" :rows="2" placeholder="请输入内容"/>
  133. </el-form-item>
  134. <el-form-item label="课程排序" prop="courseSort">
  135. <el-input-number v-model="form.courseSort" :min="1"></el-input-number>
  136. </el-form-item>
  137. <el-form-item label="视频缩略图" prop="thumbnail">
  138. <el-upload v-model="form.thumbnail" class="avatar-uploader" :action="uploadUrl" :show-file-list="false"
  139. :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
  140. <img v-if="form.thumbnail" :src="form.thumbnail" class="avatar" width="300px">
  141. <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  142. </el-upload>
  143. </el-form-item>
  144. <video-upload :type="1" :isPrivate="isPrivate" :fileKey.sync="form.fileKey" :fileSize.sync="form.fileSize"
  145. :videoUrl.sync="videoUrl" :fileName.sync="form.fileName" :line_1.sync="form.lineOne"
  146. :line_2.sync="form.lineTwo" :line_3.sync="form.lineThree" :thumbnail.sync="form.thumbnail"
  147. :uploadType.sync="form.uploadType" :isTranscode.sync="form.isTranscode"
  148. :transcodeFileKey.sync="form.transcodeFileKey" @video-duration="handleVideoDuration"
  149. @change="handleVideoChange" @selectProjects="handleSelectProjects" ref="videoUpload"
  150. append-to-body/>
  151. <el-form-item label="关联疗法" >
  152. <el-button size="small" type="primary" @click="choosePackage">选取疗法</el-button>
  153. <el-table border width="100%" style="margin-top:5px;" :data="packageList">
  154. <el-table-column label="疗法名称" align="center" prop="packageName"/>
  155. <el-table-column label="疗法图片" align="center" prop="imgUrl">
  156. <template slot-scope="scope">
  157. <img :src="scope.row.imgUrl" style="height: 80px;">
  158. </template>
  159. </el-table-column>
  160. <el-table-column label="疗法别名" align="center" prop="secondName"/>
  161. <el-table-column label="总金额" align="center" prop="totalPrice"/>
  162. <!-- 根据课程类型控制是否显示弹出时间列:0是公域(显示),1是私域(不显示) -->
  163. <el-table-column label="弹出时间" align="center" width="250px" v-if="isPrivate == 0">
  164. <template slot-scope="scope">
  165. <div>
  166. <el-time-select
  167. v-model="scope.row.duration"
  168. size="mini"
  169. placeholder="选择时间"
  170. :picker-options="getPickerOptions()"
  171. @change="handleTimeChange(scope.$index, scope.row)"
  172. ></el-time-select>
  173. </div>
  174. </template>
  175. </el-table-column>
  176. <el-table-column label="操作" align="center" width="100px" fixed="right">
  177. <template slot-scope="scope">
  178. <el-button
  179. size="mini"
  180. type="text"
  181. icon="el-icon-delete"
  182. @click="handlePackageDelete(scope.row)"
  183. >删除</el-button>
  184. </template>
  185. </el-table-column>
  186. </el-table>
  187. </el-form-item >
  188. <el-form-item label="课题选择" prop="questionBankId">
  189. <el-button size="small" type="primary" @click="chooseQuestionBank">选取课题</el-button>
  190. <el-table border width="100%" style="margin-top:5px;" :data="form.questionBankList">
  191. <el-table-column label="问题" align="center" prop="title">
  192. <template slot-scope="scope">
  193. <el-tooltip class="item" effect="dark" :content="scope.row.title" placement="top">
  194. <div
  195. style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
  196. <span>{{ scope.row.title }}</span>
  197. </div>
  198. </el-tooltip>
  199. </template>
  200. </el-table-column>
  201. <el-table-column label="类别" align="center" prop="type">
  202. <template slot-scope="scope">
  203. <dict-tag :options="typeOptions" :value="scope.row.type"/>
  204. </template>
  205. </el-table-column>
  206. <el-table-column label="答案" align="center" prop="answer"/>
  207. <el-table-column label="操作" align="center" width="100px" fixed="right">
  208. <template slot-scope="scope">
  209. <el-button size="mini" type="text" icon="el-icon-delete"
  210. @click="handleQuestionBankDelete(scope.row)">删除
  211. </el-button>
  212. </template>
  213. </el-table-column>
  214. </el-table>
  215. </el-form-item>
  216. <el-form-item label="红包金额" prop="redPacketMoney">
  217. <el-input-number v-model="form.redPacketMoney" :min="0.1" :max="200" :step="0.1"></el-input-number>
  218. </el-form-item>
  219. <!-- v-if="!!form.randomRedPacketRulesArr" -->
  220. <el-form-item v-if="!!enableRandomRedPacket" label="随机红包金额" >
  221. <template >
  222. <div v-for="(rule, index) in form.randomRedPacketRulesArr" :key="index" class="form-row">
  223. <el-form-item
  224. label="随机红包金额区间"
  225. :prop="`randomRedPacketRulesArr.${index}.minAmount`"
  226. :rules="[
  227. { required: true, message: '请输入最小金额', trigger: 'blur' },
  228. { validator: validateMinAmount, trigger: 'blur', index: index }
  229. ]"
  230. class="form-item-amount"
  231. >
  232. <el-input
  233. v-model.number="rule.minAmount"
  234. type="number"
  235. :min="0.01"
  236. :precision="2"
  237. :step="0.01"
  238. placeholder="最小金额"
  239. size="small"
  240. class="amount-input"
  241. @input="handleAmountInput(rule, 'minAmount')"
  242. ></el-input>
  243. <span class="separator">-</span>
  244. <el-input
  245. v-model.number="rule.maxAmount"
  246. type="number"
  247. :min="rule.minAmount || 0.01"
  248. :precision="2"
  249. :step="0.01"
  250. placeholder="最大金额"
  251. size="small"
  252. class="amount-input"
  253. @input="handleAmountInput(rule, 'maxAmount')"
  254. ></el-input>
  255. <span class="suffix">元</span>
  256. </el-form-item>
  257. <el-form-item
  258. label="随机权重"
  259. :prop="`randomRedPacketRulesArr.${index}.weight`"
  260. :rules="[
  261. { required: true, message: '请输入权重', trigger: 'blur' },
  262. { type: 'integer', message: '权重必须为整数', trigger: 'blur' },
  263. ]"
  264. class="form-item-weight"
  265. >
  266. <el-input
  267. v-model.number="rule.weight"
  268. type="number"
  269. :min="1"
  270. placeholder="权重"
  271. size="small"
  272. ></el-input>
  273. </el-form-item>
  274. <el-tooltip class="item" effect="dark" content="权重越高,被随机到的概率越大" placement="top">
  275. <i class="el-icon-question"></i>
  276. </el-tooltip>
  277. <div class="action-buttons">
  278. <el-button
  279. icon="el-icon-plus"
  280. size="mini"
  281. type="text"
  282. @click="addRule(index)"
  283. class="add-btn"
  284. >
  285. 新增
  286. </el-button>
  287. <el-button
  288. icon="el-icon-delete"
  289. size="mini"
  290. type="text"
  291. @click="deleteRule(index)"
  292. :disabled="form.randomRedPacketRulesArr.length <= 1"
  293. class="delete-btn"
  294. >
  295. 删除
  296. </el-button>
  297. </div>
  298. </div>
  299. </template>
  300. </el-form-item>
  301. <el-form-item label="是否关联商品" prop="isProduct">
  302. <el-radio-group v-model="form.isProduct">
  303. <el-radio :label="1">是</el-radio>
  304. <el-radio :label="0">否</el-radio>
  305. </el-radio-group>
  306. </el-form-item>
  307. <el-form-item label="是否先导课" prop="isFirst">
  308. <el-radio-group v-model="form.isFirst">
  309. <el-radio :label="1">是</el-radio>
  310. <el-radio :label="0">否</el-radio>
  311. </el-radio-group>
  312. </el-form-item>
  313. <el-form-item label="是否启用倍速" prop="isSpeed">
  314. <el-radio-group v-model="form.isSpeed">
  315. <el-radio :label="1">是</el-radio>
  316. <el-radio :label="0">否</el-radio>
  317. </el-radio-group>
  318. </el-form-item>
  319. <el-form-item label="是否上架" prop="isOnPut">
  320. <el-radio-group v-model="form.isOnPut">
  321. <el-radio :label="0">上架</el-radio>
  322. <el-radio :label="1">下架</el-radio>
  323. </el-radio-group>
  324. </el-form-item>
  325. <el-form-item label="商品选择" v-if="form.isProduct === 1">
  326. <el-button size="small" type="primary" @click="chooseCourseProduct">选取商品</el-button>
  327. <el-table
  328. border
  329. width="100%"
  330. class="course-product-table"
  331. style="margin-top:5px;"
  332. :data="form.courseProducts"
  333. max-height="400"
  334. >
  335. <el-table-column label="商品名称" align="center" prop="productName"/>
  336. <el-table-column label="商品价格" align="center" prop="price"/>
  337. <el-table-column label="库存" align="center" min-width="180">
  338. <template slot-scope="scope">
  339. <span style="white-space:pre-wrap">{{ scope.row.stockDesc || scope.row.stock || 0 }}</span>
  340. </template>
  341. </el-table-column>
  342. <el-table-column label="上架时间" align="center" prop="onShelfTime" width="180">
  343. <template slot-scope="scope">
  344. <el-time-picker
  345. v-model="scope.row.onShelfTime"
  346. value-format="HH:mm:ss"
  347. format="HH:mm:ss"
  348. placeholder="选择时间"
  349. size="mini"
  350. :disabled="!form.duration || form.duration <= 0"
  351. @change="(val) => handleProductTimeChange(scope.$index, 'onShelfTime', val)"
  352. style="width: 100%"
  353. ></el-time-picker>
  354. </template>
  355. </el-table-column>
  356. <el-table-column label="卡片弹出时间" align="center" prop="cardPopupTime" width="180">
  357. <template slot-scope="scope">
  358. <el-time-picker
  359. v-model="scope.row.cardPopupTime"
  360. value-format="HH:mm:ss"
  361. format="HH:mm:ss"
  362. placeholder="选择时间"
  363. size="mini"
  364. :disabled="!form.duration || form.duration <= 0"
  365. @change="(val) => handleProductTimeChange(scope.$index, 'cardPopupTime', val)"
  366. style="width: 100%"
  367. ></el-time-picker>
  368. </template>
  369. </el-table-column>
  370. <el-table-column label="卡片关闭时间" align="center" prop="cardCloseTime" width="180">
  371. <template slot-scope="scope">
  372. <el-time-picker
  373. v-model="scope.row.cardCloseTime"
  374. value-format="HH:mm:ss"
  375. format="HH:mm:ss"
  376. placeholder="选择时间"
  377. size="mini"
  378. :disabled="!form.duration || form.duration <= 0"
  379. @change="(val) => handleProductTimeChange(scope.$index, 'cardCloseTime', val)"
  380. style="width: 100%"
  381. ></el-time-picker>
  382. </template>
  383. </el-table-column>
  384. <el-table-column label="下架时间" align="center" prop="offShelfTime" width="180">
  385. <template slot-scope="scope">
  386. <el-time-picker
  387. v-model="scope.row.offShelfTime"
  388. value-format="HH:mm:ss"
  389. format="HH:mm:ss"
  390. placeholder="选择时间"
  391. size="mini"
  392. :disabled="!form.duration || form.duration <= 0"
  393. @change="(val) => handleProductTimeChange(scope.$index, 'offShelfTime', val)"
  394. style="width: 100%"
  395. ></el-time-picker>
  396. </template>
  397. </el-table-column>
  398. <el-table-column label="热卖标签" align="center" prop="hotSaleTags" min-width="140">
  399. <template slot-scope="scope">
  400. <el-tag
  401. v-for="(tag, tagIdx) in getHotSaleTagList(scope.row)"
  402. :key="tagIdx"
  403. size="mini"
  404. type="danger"
  405. style="margin: 2px;"
  406. >{{ tag }}</el-tag>
  407. </template>
  408. </el-table-column>
  409. <el-table-column label="操作" align="center" width="96" class-name="course-product-op-col">
  410. <template slot-scope="scope">
  411. <div class="course-product-op-btns">
  412. <el-button
  413. size="mini"
  414. type="text"
  415. icon="el-icon-delete"
  416. @click="handleCourseProductDelete(scope.row)"
  417. >删除</el-button>
  418. <el-button
  419. v-if="getHotSaleTagList(scope.row).length"
  420. size="mini"
  421. type="text"
  422. icon="el-icon-circle-close"
  423. @click="handleClearHotSaleTags(scope.row, scope.$index)"
  424. >清空标签</el-button>
  425. <el-button
  426. v-else
  427. size="mini"
  428. type="text"
  429. icon="el-icon-price-tag"
  430. @click="handleOpenHotSaleTagDialog(scope.row, scope.$index)"
  431. >热卖标签</el-button>
  432. </div>
  433. </template>
  434. </el-table-column>
  435. </el-table>
  436. </el-form-item>
  437. <!-- <el-row>-->
  438. <!-- <el-col :span="12">-->
  439. <!-- <el-form-item v-if="form.isProduct === 1" label="商品售卖时间" prop="listingStartTime">-->
  440. <!-- <el-input-number v-model="form.listingStartTime" :min="0" label="商品售卖时间"></el-input-number>-->
  441. <!-- </el-form-item>-->
  442. <!-- </el-col>-->
  443. <!-- <el-col :span="12">-->
  444. <!-- <el-form-item v-if="form.isProduct === 1" label="结束售卖时间" prop="listingStartTime">-->
  445. <!-- <el-input-number v-model="form.listingEndTime" :min="0" label="结束售卖时间"></el-input-number>-->
  446. <!-- </el-form-item>-->
  447. <!-- </el-col>-->
  448. <!-- </el-row>-->
  449. <el-form-item label="课程介绍图" prop="courseIntroImg">
  450. <el-upload
  451. class="avatar-uploader"
  452. :action="uploadUrl"
  453. :show-file-list="false"
  454. :on-success="handleCourseIntroSuccess"
  455. :before-upload="beforeCourseIntroUpload">
  456. <img v-if="form.courseIntroImg" :src="form.courseIntroImg" class="avatar" style="max-width: 300px; max-height: 200px;">
  457. <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  458. </el-upload>
  459. <div style="margin-top:5px;">
  460. <el-button v-if="form.courseIntroImg" size="mini" type="danger" @click="form.courseIntroImg = null">删除图片</el-button>
  461. </div>
  462. </el-form-item>
  463. <el-form-item label="精选留言">
  464. <el-button size="small" type="primary" @click="openCommentImportDialog">导入留言</el-button>
  465. <el-button size="small" style="margin-left: 8px;" @click="openUserCommentDialog">用户留言</el-button>
  466. </el-form-item>
  467. </el-form>
  468. <div slot="footer" class="dialog-footer">
  469. <el-button type="primary" @click="submitForm">确 定</el-button>
  470. <el-button @click="cancel">取 消</el-button>
  471. </div>
  472. </el-dialog>
  473. <el-dialog title="导入精选留言" :visible.sync="commentImportDialog.visible" width="600px" append-to-body>
  474. <el-upload
  475. drag
  476. :auto-upload="false"
  477. :limit="1"
  478. accept=".xls,.xlsx"
  479. :on-change="handleCommentFileChange"
  480. :file-list="commentImportDialog.fileList">
  481. <i class="el-icon-upload"></i>
  482. <div class="el-upload__text">将Excel文件拖到此处,或<em>点击上传</em></div>
  483. <div class="el-upload__tip" slot="tip">仅支持.xls/.xlsx格式</div>
  484. </el-upload>
  485. <div style="margin-top: 10px;">
  486. <el-button size="small" type="text" @click="handleDownloadTemplate">下载Excel导入模板</el-button>
  487. </div>
  488. <div v-if="commentImportDialog.historyList.length > 0" style="margin-top: 15px;">
  489. <el-divider content-position="left">历史上传留言</el-divider>
  490. <el-table :data="commentImportDialog.historyList" border size="small" max-height="300">
  491. <el-table-column label="昵称" prop="nickName" width="100"/>
  492. <el-table-column label="评论内容" prop="content" show-overflow-tooltip/>
  493. <el-table-column label="上传时间" prop="createTime" width="160"/>
  494. <el-table-column label="操作" width="70" align="center">
  495. <template slot-scope="scope">
  496. <el-button type="text" size="mini" style="color:#F56C6C" @click="handleDeleteComment(scope.row)">删除</el-button>
  497. </template>
  498. </el-table-column>
  499. </el-table>
  500. </div>
  501. <div slot="footer" class="dialog-footer">
  502. <el-button type="primary" :loading="commentImportDialog.loading" @click="handleImportComments">确认导入</el-button>
  503. <el-button @click="commentImportDialog.visible = false">取 消</el-button>
  504. </div>
  505. </el-dialog>
  506. <el-dialog :title="title" :visible.sync="updateBatchData.open" width="1000px" append-to-body>
  507. <el-form ref="form" :model="updateBatchData.form" label-width="110px">
  508. <el-form-item label="看课时间" prop="timeRange">
  509. <el-time-picker is-range v-model="updateBatchData.form.timeRange" range-separator="至"
  510. start-placeholder="开始时间"
  511. value-format="HH:mm:ss" end-placeholder="结束时间" placeholder="选择时间范围">
  512. </el-time-picker>
  513. </el-form-item>
  514. <el-form-item label="领取红包时间" prop="lastJoinTime">
  515. <el-time-picker v-model="updateBatchData.form.lastJoinTime" :selectableRange="updateBatchData.form.timeRange"
  516. value-format="HH:mm:ss" placeholder="选择时间范围">
  517. </el-time-picker>
  518. <p style="color: red;margin: 0;font-size: 12px">超过领取红包时间,只允许看课,不允许领取红包</p>
  519. </el-form-item>
  520. </el-form>
  521. <div slot="footer" class="dialog-footer">
  522. <el-button type="primary" @click="updateBatch">确 定</el-button>
  523. <el-button @click="updateBatchData.open = false">取 消</el-button>
  524. </div>
  525. </el-dialog>
  526. <el-dialog :title="questionBank.title" :visible.sync="questionBank.open" width="1000px" append-to-body>
  527. <question-bank ref="questionBank" @questionBankResult="questionBankResult"></question-bank>
  528. </el-dialog>
  529. <el-dialog :title="courseProduct.title" :visible.sync="courseProduct.open" width="1000px" append-to-body>
  530. <course-product ref="courseProduct" @courseProductResult="courseProductResult"></course-product>
  531. </el-dialog>
  532. <el-dialog title="热卖标签" :visible.sync="hotSaleTagDialog.visible" width="480px" append-to-body>
  533. <p v-if="hotSaleTagDialog.productName" style="margin-bottom: 10px; color: #606266;">
  534. 商品:{{ hotSaleTagDialog.productName }}
  535. </p>
  536. <el-input
  537. v-model="hotSaleTagDialog.tagInput"
  538. type="textarea"
  539. :rows="3"
  540. placeholder="多个标签用逗号分隔;留空表示清除标签"
  541. />
  542. <template slot="footer">
  543. <el-button @click="hotSaleTagDialog.visible = false">取 消</el-button>
  544. <el-button type="primary" @click="confirmHotSaleTag">确 定</el-button>
  545. </template>
  546. </el-dialog>
  547. <el-dialog title="视频库选择" :visible.sync="addBatchData.open" width="900px" append-to-body>
  548. <!-- 搜索条件 -->
  549. <el-form :inline="true" :model="addBatchData.queryParams" class="library-search">
  550. <el-form-item label="素材名称">
  551. <el-input v-model="addBatchData.queryParams.resourceName" placeholder="请输入素材名称" clearable size="small"
  552. @keyup.enter.native="resourceList"/>
  553. </el-form-item>
  554. <el-form-item label="类型">
  555. <el-select v-model="addBatchData.queryParams.typeId" @change="changeCateType" placeholder="请选择素材类型"
  556. clearable
  557. size="small">
  558. <el-option v-for="item in addBatchData.typeOptions" :key="item.dictValue" :label="item.dictLabel"
  559. :value="item.dictValue"/>
  560. </el-select>
  561. </el-form-item>
  562. <el-form-item label="子类型">
  563. <el-select v-model="addBatchData.queryParams.typeSubId" placeholder="请选择素材子类型" clearable size="small">
  564. <el-option v-for="item in addBatchData.typeSubOptions" :key="item.dictValue" :label="item.dictLabel"
  565. :value="item.dictValue"/>
  566. </el-select>
  567. </el-form-item>
  568. <el-form-item>
  569. <el-button type="primary" icon="el-icon-search" size="mini" @click="resourceList">搜索</el-button>
  570. </el-form-item>
  571. </el-form>
  572. <!-- 视频列表 -->
  573. <el-table v-loading="addBatchData.loading" :data="addBatchData.list"
  574. @selection-change="handVideoleSelectionChange" height="400px">
  575. <el-table-column type="selection" width="55" align="center"/>
  576. <el-table-column label="素材名称" align="center" prop="resourceName"/>
  577. <el-table-column label="文件名称" align="center" prop="fileName"/>
  578. <el-table-column label="排序" align="center" prop="sort"/>
  579. <el-table-column label="缩略图" align="center">
  580. <template slot-scope="scope">
  581. <el-popover placement="right" title="" trigger="hover">
  582. <img alt="" slot="reference" :src="scope.row.thumbnail" style="width: 80px; height: 50px"/>
  583. <img alt="" :src="scope.row.thumbnail" style="max-width: 150px;"/>
  584. </el-popover>
  585. </template>
  586. </el-table-column>
  587. <el-table-column label="视频时长" align="center">
  588. <template slot-scope="scope">
  589. <span>{{ formatDuration(scope.row.duration) }}</span>
  590. </template>
  591. </el-table-column>
  592. </el-table>
  593. <!-- 分页 -->
  594. <pagination v-show="addBatchData.total > 0" :total="addBatchData.total"
  595. :page.sync="addBatchData.queryParams.pageNum" :limit.sync="addBatchData.queryParams.pageSize"
  596. @pagination="resourceList"/>
  597. <div slot="footer" class="dialog-footer">
  598. <el-button type="primary" @click="batchVideoSave">确 定</el-button>
  599. </div>
  600. </el-dialog>
  601. <el-dialog title="章节红包" :visible.sync="redData.open" :close-on-click-modal="false" width="900px" append-to-body>
  602. <el-table border v-loading="redData.loading" :data="redData.list" height="600px">
  603. <el-table-column label="小节名称" align="center" show-overflow-tooltip prop="title">
  604. <template slot-scope="scope">
  605. <el-input class="el-input" v-model="scope.row.title"/>
  606. </template>
  607. </el-table-column>
  608. <el-table-column label="视频文件名称" align="center" show-overflow-tooltip prop="fileName">
  609. </el-table-column>
  610. <el-table-column label="视频时长" align="center" prop="duration">
  611. <template slot-scope="{ row }">
  612. {{ formatDuration(row.duration) }}
  613. </template>
  614. </el-table-column>
  615. <el-table-column label="红包金额" align="center" prop="redPacketMoney">
  616. <template slot-scope="scope">
  617. <el-input class="el-input" v-model="scope.row.redPacketMoney"/>
  618. </template>
  619. </el-table-column>
  620. <el-table-column label="排序" align="center" prop="courseSort"/>
  621. <el-table-column label="上传时间" align="center" prop="createTime"/>
  622. </el-table>
  623. <div slot="footer" class="dialog-footer">
  624. <el-button type="primary" @click="batchRedSave">确 定</el-button>
  625. </div>
  626. </el-dialog>
  627. <el-dialog :title="commentDialog.title" :visible.sync="commentDialog.open" width="1000px" append-to-body
  628. :close-on-click-modal="false">
  629. <course-watch-comment ref="courseWatchComment" :courseId="commentDialog.courseId" :videoId="commentDialog.videoId"
  630. v-if="commentDialog.open">
  631. </course-watch-comment>
  632. </el-dialog>
  633. <el-dialog :title="userCommentDialog.title" :visible.sync="userCommentDialog.open" width="1000px" append-to-body
  634. :close-on-click-modal="false" @opened="onUserCommentDialogOpened">
  635. <user-course-section-comment
  636. v-if="userCommentDialog.open"
  637. ref="userCourseSectionComment"
  638. :course-id="userCommentDialog.courseId"
  639. :video-id="userCommentDialog.videoId"
  640. />
  641. </el-dialog>
  642. <el-dialog title="修改课节排序" :visible.sync="openVideoSort" style="width: 1600px;" append-to-body>
  643. <draggable v-model="userCourseVideoSortList" @end="onDragEndDay" style="padding: 10px">
  644. <el-button style="margin: 8px 4px;" v-for="(item, index) in userCourseVideoSortList"
  645. :class="item.newCourseSort != item.courseSort ? 'red':''">第{{
  646. item.newCourseSort
  647. }}序(原排序第{{ item.courseSort }})
  648. </el-button>
  649. </draggable>
  650. <div style="float: right;margin-top: -20px">
  651. <el-button type="primary" @click="saveSorts">保存</el-button>
  652. </div>
  653. </el-dialog>
  654. <!-- 批量修改封面 -->
  655. <el-dialog :title="batchEditCoverDialog.title" :visible.sync="batchEditCoverDialog.visible" width="500px" append-to-body>
  656. <el-form ref="batchEditCoverDialogForm"
  657. :model="batchEditCoverDialog.form"
  658. :rules="batchEditCoverDialog.rules"
  659. v-loading="batchEditCoverDialog.uploadLoading">
  660. <el-form-item label="视频封面" prop="thumbnail">
  661. <el-upload v-model="batchEditCoverDialog.form.thumbnail"
  662. class="avatar-uploader"
  663. :action="uploadUrl"
  664. :show-file-list="false"
  665. :on-success="handleCoverSuccess"
  666. :before-upload="beforeAvatarUpload">
  667. <img v-if="batchEditCoverDialog.form.thumbnail" :src="batchEditCoverDialog.form.thumbnail" class="avatar" width="300px">
  668. <i v-else class="el-icon-plus avatar-uploader-icon"></i>
  669. </el-upload>
  670. </el-form-item>
  671. </el-form>
  672. <div slot="footer" class="dialog-footer">
  673. <el-button type="primary" @click="submitEditCoverForm">确 定</el-button>
  674. <el-button @click="cancelEditCoverForm">取 消</el-button>
  675. </div>
  676. </el-dialog>
  677. </div>
  678. </template>
  679. <script>
  680. import {
  681. addUserCourseVideo,
  682. batchSaveVideo,
  683. batchUpdateRed,
  684. delUserCourseVideo,
  685. getSort,
  686. getUserCourseVideo,
  687. getVideoListByCourseId,
  688. getPublicVideoListByCourseId,
  689. getVideoListByCourseIdAll,
  690. sortCourseVideo,
  691. updates,
  692. updateUserCourseVideo,
  693. syncTemplate, batchDownUserCourseVideo, batchEditCover, batchUpUserCourseVideo, getPublicUserCourseVideo
  694. } from '@/api/course/userCourseVideo'
  695. // import {syncTemplate} from '@/api/course/userCourse'
  696. import QuestionBank from "@/views/course/courseQuestionBank/QuestionBank.vue";
  697. import CourseProduct from "@/views/course/fsCourseProduct/CourseProductZM.vue";
  698. import VideoUpload from "@/components/VideoUpload/index.vue";
  699. import {listVideoResource} from '@/api/course/videoResource';
  700. import {getByIds} from '@/api/course/courseQuestionBank'
  701. import CourseWatchComment from "./courseWatchComment.vue";
  702. import UserCourseSectionComment from "./userCourseSectionComment.vue";
  703. import {getCateListByPid, getCatePidList} from '@/api/course/userCourseCategory'
  704. import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
  705. import draggable from 'vuedraggable'
  706. import { getConfigByKey } from '@/api/system/config'
  707. import { listStoreProductAttrValue } from '@/api/hisStore/storeProductAttrValue'
  708. export default {
  709. name: "userCourseCatalog",
  710. components: {VideoUpload, QuestionBank, CourseWatchComment, UserCourseSectionComment, CourseProduct, draggable},
  711. props: {
  712. usePublicVideoLibrary: {
  713. type: Boolean,
  714. default: false
  715. }
  716. },
  717. watch:{
  718. // 深度监听 rules 数组的变化,以更新总权重
  719. "form.randomRedPacketRulesArr": {
  720. handler(val) {
  721. // this.calculateTotalWeight();
  722. this.validateRules();
  723. },
  724. deep: true,
  725. },
  726. },
  727. data() {
  728. return {
  729. duration: null,
  730. packageList: [],
  731. //课题
  732. package: {
  733. title: '',
  734. open: false,
  735. },
  736. //课题
  737. questionBank: {
  738. title: '',
  739. open: false,
  740. },
  741. //拍商品
  742. courseProduct: {
  743. title: '',
  744. open: false,
  745. },
  746. hotSaleTagDialog: {
  747. visible: false,
  748. index: -1,
  749. productName: '',
  750. tagInput: ''
  751. },
  752. isPrivate: null,
  753. videoUrl: "",
  754. uploadTypeOptions: [
  755. {dictLabel: "线路一", dictValue: 2},
  756. {dictLabel: "线路二", dictValue: 3},
  757. ],
  758. uploadLoading: false,
  759. courseId: null,
  760. videoName: '',
  761. title: "",
  762. // 是否显示弹出层
  763. open: false,
  764. uploadUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS",
  765. baseUrl: process.env.VUE_APP_BASE_API,
  766. typeOptions: [],
  767. files: [],
  768. fileList: [],
  769. // 上传成功后的地址
  770. videoURL: '',
  771. // 进度条百分比
  772. progress: 0,
  773. // 上传视频获取成功后拿到的fileID【备用】
  774. fileId: '',
  775. courseName: null,
  776. userCourseVideoList: [],
  777. userCourseVideoSortList: [],
  778. total: 0,
  779. redData: {
  780. queryParams: {
  781. pageNum: 1,
  782. pageSize: 99999,
  783. courseId: null,
  784. },
  785. list: [],
  786. open: false,
  787. loading: true,
  788. form: {}
  789. },
  790. queryParams: {
  791. pageNum: 1,
  792. pageSize: 10,
  793. courseId: null,
  794. videoId: null,
  795. title: null
  796. },
  797. addBatchData: {
  798. open: false,
  799. loading: true,
  800. form: {},
  801. select: [], // 按用户选择顺序存储视频ID
  802. total: 0,
  803. queryParams: {
  804. pageNum: 1,
  805. pageSize: 10,
  806. resourceName: null,
  807. typeId: null,
  808. typeSubId: null
  809. },
  810. typeOptions: [],
  811. typeSubOptions: []
  812. },
  813. // 显示搜索条件
  814. showSearch: true,
  815. // 遮罩层
  816. loading: true,
  817. // 导出遮罩层
  818. exportLoading: false,
  819. // 选中数组
  820. ids: [],
  821. // 非单个禁用
  822. single: true,
  823. // 非多个禁用
  824. multiple: true,
  825. loading3: false,
  826. openVideoSort: false,
  827. // 表单参数
  828. form: {
  829. isOnPut: 0,
  830. courseProducts: [],
  831. courseIntroImg: null,
  832. randomRedPacketRules:null,
  833. randomRedPacketRulesArr:[
  834. {
  835. minAmount: 0.01,
  836. maxAmount: 0.01,
  837. weight: 100,
  838. }
  839. ]
  840. },
  841. updateBatchData: {
  842. open: false,
  843. form: {}
  844. },
  845. // 表单校验
  846. rules: {
  847. title: [
  848. {required: true, message: "小节名称不能为空", trigger: "change"}
  849. ],
  850. courseSort: [
  851. {required: true, message: "排序不能为空", trigger: "change"}
  852. ],
  853. },
  854. // 评论弹窗数据
  855. commentDialog: {
  856. open: false,
  857. courseId: null,
  858. videoId: null,
  859. title: ""
  860. },
  861. userCommentDialog: {
  862. open: false,
  863. courseId: null,
  864. videoId: null,
  865. title: ""
  866. },
  867. enableRandomRedPacket:false,
  868. // 精选留言导入弹窗
  869. commentImportDialog: {
  870. visible: false,
  871. loading: false,
  872. fileList: [],
  873. file: null,
  874. historyList: [],
  875. courseId: null,
  876. videoId: null
  877. },
  878. // 批量修改封面
  879. batchEditCoverDialog: {
  880. title: '修改视频封面',
  881. visible: false,
  882. uploadLoading: false,
  883. form: {
  884. thumbnail: null,
  885. },
  886. rules: {
  887. thumbnail: [
  888. {required: true, message: "视频封面不能为空", trigger: "change"}
  889. ],
  890. }
  891. }
  892. }
  893. },
  894. created() {
  895. this.getDicts("sys_course_temp_type").then(response => {
  896. this.typeOptions = response.data;
  897. });
  898. getConfigByKey('randomRedpacket:config').then(res=>{
  899. let configData = res.data;
  900. if(!!configData && !!configData.configValue){
  901. let configValue = JSON.parse(configData.configValue);
  902. if(!!configValue.enableRandomRedpacket){
  903. this.enableRandomRedPacket = configValue.enableRandomRedpacket;
  904. console.log("this.enableRandomRedPacket ::" + this.enableRandomRedPacket)
  905. }
  906. }
  907. }).catch(res=>{
  908. })
  909. },
  910. methods: {
  911. getPickerOptions() {
  912. const durationInMinutes = Math.floor(this.form.duration / 60); // 将秒转换为分钟
  913. const endHour = Math.floor(durationInMinutes / 60); // 起始小时
  914. const endMinute = durationInMinutes % 60; // 起始分钟
  915. return {
  916. start: "00:00", // 固定开始时间
  917. step: "00:01", // 时间间隔
  918. end: `${endHour.toString().padStart(2, "0")}:${endMinute
  919. .toString()
  920. .padStart(2, "0")}`, // 动态结束时间
  921. };
  922. },
  923. // 处理时间选择框值变化
  924. handleTimeChange(index, row) {
  925. // 确保 packageList 中的数据被正确更新
  926. this.$set(this.packageList, index, row);
  927. // 同步更新 form.packageJson 字段
  928. this.$nextTick(() => {
  929. // 确保每个疗法包都有 duration 字段
  930. this.packageList.forEach(item => {
  931. if (item.duration === undefined || item.duration === null) {
  932. item.duration = ''; // 空值应初始化为空字符串而不是null,避免显示"null"
  933. }
  934. });
  935. this.form.packageJson = JSON.stringify(this.packageList);
  936. });
  937. },
  938. handlePackageDelete(row) {
  939. this.packageList.splice(this.packageList.findIndex(item => item.packageId === row.packageId), 1)
  940. },
  941. choosePackage() {
  942. this.package.open = true;
  943. this.package.title = '疗法选择';
  944. },
  945. /**
  946. * 选择课题
  947. */
  948. chooseQuestionBank() {
  949. this.questionBank.open = true;
  950. this.questionBank.title = '课题选择';
  951. },
  952. /**
  953. * 选择拍商品
  954. */
  955. chooseCourseProduct() {
  956. this.courseProduct.open = true;
  957. this.courseProduct.title = '拍商品选择';
  958. },
  959. //选择疗法
  960. selectPackage(row) {
  961. const drug = {};
  962. for (var i = 0; i < this.packageList.length; i++) {
  963. if (this.packageList[i].packageId == row.packageId) {
  964. this.$message.warning("疗法已存在!")
  965. return;
  966. }
  967. }
  968. drug.packageId = row.packageId;
  969. drug.packageName = row.packageName;
  970. drug.secondName = row.secondName;
  971. drug.totalPrice = row.totalPrice;
  972. drug.imgUrl = row.imgUrl;
  973. this.packageList.push(drug);
  974. this.$message({
  975. message: '添加成功',
  976. type: 'success'
  977. });
  978. },
  979. courseProductResult(val) {
  980. this.form.courseProducts = this.form.courseProducts || [];
  981. const productData = {
  982. productId: val.productId, // 确保使用 productId
  983. productName: val.productName,
  984. price: val.price,
  985. stock: val.stock,
  986. stockDesc: val.stock,
  987. id: val.productId, // 添加 id 字段,方便删除操作
  988. onShelfTime: val.onShelfTime || '00:00:00', // 上架时间,默认 00:00:00
  989. cardPopupTime: val.cardPopupTime || '00:00:00', // 卡片弹出时间,默认 00:00:00
  990. cardCloseTime: val.cardCloseTime || '00:00:00', // 卡片关闭时间,默认 00:00:00
  991. offShelfTime: val.offShelfTime || '00:00:00', // 下架时间,默认 00:00:00
  992. hotSaleTags: val.hotSaleTags || '',
  993. };
  994. // 检查商品是否已存在
  995. const exists = this.form.courseProducts.some(item => item.productId === val.productId);
  996. if (exists) {
  997. this.$message.error("当前商品已选择");
  998. return;
  999. }
  1000. // 添加商品到列表,并按规格加载库存
  1001. this.form.courseProducts.push(productData);
  1002. this.loadProductSpecStock(productData);
  1003. this.$message.success("添加成功");
  1004. },
  1005. loadProductSpecStock(product) {
  1006. if (!product || !product.productId) return
  1007. listStoreProductAttrValue({ productId: product.productId, pageNum: 1, pageSize: 100 }).then(res => {
  1008. const rows = res.rows || res.data || []
  1009. if (!rows.length) return
  1010. let total = 0
  1011. const parts = rows.map(r => {
  1012. const s = Number(r.stock || 0)
  1013. total += s
  1014. return (r.sku || '默认规格') + ':' + s
  1015. })
  1016. product.stock = total
  1017. product.stockDesc = parts.join(' / ')
  1018. this.$forceUpdate()
  1019. }).catch(() => {})
  1020. },
  1021. /**
  1022. * 将时间字符串(HH:mm:ss)转换为秒数
  1023. * @param {String} timeStr 时间字符串,格式:HH:mm:ss
  1024. * @returns {Number} 总秒数
  1025. */
  1026. timeToSeconds(timeStr) {
  1027. if (!timeStr || timeStr === '') return 0;
  1028. const parts = timeStr.split(':');
  1029. if (parts.length !== 3) return 0;
  1030. const hours = parseInt(parts[0]) || 0;
  1031. const minutes = parseInt(parts[1]) || 0;
  1032. const seconds = parseInt(parts[2]) || 0;
  1033. return hours * 3600 + minutes * 60 + seconds;
  1034. },
  1035. /**
  1036. * 处理商品时间字段变化,进行验证
  1037. * @param {Number} index 商品索引
  1038. * @param {String} fieldName 字段名称
  1039. * @param {String} value 时间值(HH:mm:ss格式)
  1040. */
  1041. handleProductTimeChange(index, fieldName, value) {
  1042. if (!this.form.duration || this.form.duration <= 0) {
  1043. this.$message.warning('请先上传视频并获取视频时长');
  1044. // 还原为 00:00:00
  1045. this.$nextTick(() => {
  1046. const product = this.form.courseProducts[index];
  1047. if (product) {
  1048. product[fieldName] = '00:00:00';
  1049. }
  1050. });
  1051. return;
  1052. }
  1053. const product = this.form.courseProducts[index];
  1054. if (!product) return;
  1055. const videoDurationSeconds = this.form.duration; // 视频时长(秒)
  1056. const currentTimeSeconds = this.timeToSeconds(value || '00:00:00');
  1057. // 验证:当前时间不能超过视频长度
  1058. if (currentTimeSeconds > videoDurationSeconds) {
  1059. this.$message.error(`${this.getFieldLabel(fieldName)}不能超过视频时长(${this.formatDuration(videoDurationSeconds)})`);
  1060. // 还原为 00:00:00
  1061. this.$nextTick(() => {
  1062. product[fieldName] = '00:00:00';
  1063. });
  1064. return;
  1065. }
  1066. // 验证:关闭时间 - 弹出时间 不能大于视频长度
  1067. if (fieldName === 'cardCloseTime' && product.cardPopupTime) {
  1068. const popupSeconds = this.timeToSeconds(product.cardPopupTime);
  1069. const closeSeconds = this.timeToSeconds(value || '00:00:00');
  1070. const diffSeconds = closeSeconds - popupSeconds;
  1071. if (diffSeconds < 0) {
  1072. this.$message.error('卡片关闭时间不能早于卡片弹出时间');
  1073. this.$nextTick(() => {
  1074. product[fieldName] = '00:00:00';
  1075. });
  1076. return;
  1077. }
  1078. if (diffSeconds > videoDurationSeconds) {
  1079. this.$message.error('卡片关闭时间与弹出时间的差值不能超过视频时长');
  1080. this.$nextTick(() => {
  1081. product[fieldName] = '00:00:00';
  1082. });
  1083. return;
  1084. }
  1085. }
  1086. // 验证:当修改弹出时间时,需要验证关闭时间(若卡片关闭时间为 00:00:00 则不校验关闭>弹出)
  1087. if (fieldName === 'cardPopupTime' && product.cardCloseTime && product.cardCloseTime !== '00:00:00') {
  1088. const popupSeconds = this.timeToSeconds(value || '00:00:00');
  1089. const closeSeconds = this.timeToSeconds(product.cardCloseTime);
  1090. const diffSeconds = closeSeconds - popupSeconds;
  1091. if (diffSeconds < 0) {
  1092. this.$message.error('卡片弹出时间不能晚于卡片关闭时间');
  1093. this.$nextTick(() => {
  1094. product[fieldName] = '00:00:00';
  1095. });
  1096. return;
  1097. }
  1098. if (diffSeconds > videoDurationSeconds) {
  1099. this.$message.error('卡片关闭时间与弹出时间的差值不能超过视频时长');
  1100. this.$nextTick(() => {
  1101. product[fieldName] = '00:00:00';
  1102. });
  1103. return;
  1104. }
  1105. }
  1106. // 验证:下架时间 - 上架时间 不能大于视频长度
  1107. if (fieldName === 'offShelfTime' && product.onShelfTime) {
  1108. const onShelfSeconds = this.timeToSeconds(product.onShelfTime);
  1109. const offShelfSeconds = this.timeToSeconds(value || '00:00:00');
  1110. const diffSeconds = offShelfSeconds - onShelfSeconds;
  1111. if (diffSeconds < 0) {
  1112. this.$message.error('下架时间不能早于上架时间');
  1113. this.$nextTick(() => {
  1114. product[fieldName] = '00:00:00';
  1115. });
  1116. return;
  1117. }
  1118. if (diffSeconds > videoDurationSeconds) {
  1119. this.$message.error('下架时间与上架时间的差值不能超过视频时长');
  1120. this.$nextTick(() => {
  1121. product[fieldName] = '00:00:00';
  1122. });
  1123. return;
  1124. }
  1125. }
  1126. // 验证:当修改上架时间时,需要验证下架时间(若下架时间为 00:00:00 则不校验关闭>开启)
  1127. if (fieldName === 'onShelfTime' && product.offShelfTime && product.offShelfTime !== '00:00:00') {
  1128. const onShelfSeconds = this.timeToSeconds(value || '00:00:00');
  1129. const offShelfSeconds = this.timeToSeconds(product.offShelfTime);
  1130. const diffSeconds = offShelfSeconds - onShelfSeconds;
  1131. if (diffSeconds < 0) {
  1132. this.$message.error('上架时间不能晚于下架时间');
  1133. this.$nextTick(() => {
  1134. product[fieldName] = '00:00:00';
  1135. });
  1136. return;
  1137. }
  1138. if (diffSeconds > videoDurationSeconds) {
  1139. this.$message.error('下架时间与上架时间的差值不能超过视频时长');
  1140. this.$nextTick(() => {
  1141. product[fieldName] = '00:00:00';
  1142. });
  1143. return;
  1144. }
  1145. }
  1146. // 验证:下架时间不能大于视频长度(已在上面验证)
  1147. // 验证:卡片关闭时间不能大于视频长度(已在上面验证)
  1148. // 更新值
  1149. product[fieldName] = value || '00:00:00';
  1150. },
  1151. /**
  1152. * 获取字段中文标签
  1153. * @param {String} fieldName 字段名称
  1154. * @returns {String} 中文标签
  1155. */
  1156. getFieldLabel(fieldName) {
  1157. const labels = {
  1158. onShelfTime: '上架时间',
  1159. cardPopupTime: '卡片弹出时间',
  1160. cardCloseTime: '卡片关闭时间',
  1161. offShelfTime: '下架时间'
  1162. };
  1163. return labels[fieldName] || fieldName;
  1164. },
  1165. //选择结果
  1166. questionBankResult(val) {
  1167. // 确保 questionBankList 是数组
  1168. this.form.questionBankList = this.form.questionBankList || [];
  1169. for (var i = 0; i < this.form.questionBankList.length; i++) {
  1170. if (this.form.questionBankList[i].id == val.id) {
  1171. return this.$message.error("当前课题已选择")
  1172. }
  1173. }
  1174. this.form.questionBankList.push(val);
  1175. this.$message({
  1176. message: '添加成功',
  1177. type: 'success'
  1178. });
  1179. },
  1180. //删除课题
  1181. handleQuestionBankDelete(row) {
  1182. this.form.questionBankList.splice(this.form.questionBankList.findIndex(item => item.id === row.id), 1)
  1183. },
  1184. getHotSaleTagList(row) {
  1185. if (!row || !row.hotSaleTags) {
  1186. return [];
  1187. }
  1188. return row.hotSaleTags.split(/[,,]/).map(t => t.trim()).filter(t => t);
  1189. },
  1190. handleOpenHotSaleTagDialog(row, index) {
  1191. this.hotSaleTagDialog.index = index;
  1192. this.hotSaleTagDialog.productName = row.productName || '';
  1193. this.hotSaleTagDialog.tagInput = row.hotSaleTags || '';
  1194. this.hotSaleTagDialog.visible = true;
  1195. },
  1196. handleClearHotSaleTags(row, index) {
  1197. let idx = index;
  1198. if (idx < 0 || !this.form.courseProducts[idx]) {
  1199. if (row.productId) {
  1200. idx = this.form.courseProducts.findIndex(item => item.productId === row.productId);
  1201. } else if (row.id) {
  1202. idx = this.form.courseProducts.findIndex(item => item.id === row.id);
  1203. }
  1204. }
  1205. if (idx >= 0 && this.form.courseProducts[idx]) {
  1206. this.$set(this.form.courseProducts[idx], 'hotSaleTags', '');
  1207. }
  1208. },
  1209. confirmHotSaleTag() {
  1210. const tags = (this.hotSaleTagDialog.tagInput || '')
  1211. .split(/[,,]/)
  1212. .map(t => t.trim())
  1213. .filter(t => t);
  1214. const index = this.hotSaleTagDialog.index;
  1215. if (index >= 0 && this.form.courseProducts[index]) {
  1216. this.$set(this.form.courseProducts[index], 'hotSaleTags', tags.length ? tags.join(',') : '');
  1217. }
  1218. this.hotSaleTagDialog.visible = false;
  1219. },
  1220. //删除商品
  1221. handleCourseProductDelete(row) {
  1222. // 优先使用 productId 查找
  1223. let index = -1;
  1224. // 方法1:通过 productId 查找
  1225. if (row.productId) {
  1226. index = this.form.courseProducts.findIndex(item => item.productId === row.productId);
  1227. }
  1228. // 方法2:如果 productId 不存在,尝试通过 id 查找
  1229. if (index === -1 && row.id) {
  1230. index = this.form.courseProducts.findIndex(item => item.id === row.id);
  1231. }
  1232. // 方法3:如果以上都不行,通过 productName 查找(最后手段)
  1233. if (index === -1 && row.productName) {
  1234. index = this.form.courseProducts.findIndex(item => item.productName === row.productName);
  1235. }
  1236. if (index > -1) {
  1237. this.form.courseProducts.splice(index, 1);
  1238. this.$message.success('删除成功');
  1239. } else {
  1240. this.$message.warning('未找到对应商品');
  1241. }
  1242. },
  1243. handleVideoChange() {
  1244. if (this.form.uploadType == 1) {
  1245. this.videoUrl = this.form.lineOne;
  1246. } else if (this.form.uploadType == 2) {
  1247. this.videoUrl = this.form.lineTwo;
  1248. } else if (this.form.uploadType == 3) {
  1249. this.videoUrl = this.form.lineThree;
  1250. }
  1251. },
  1252. // 视频库课题
  1253. handleSelectProjects(projectIds) {
  1254. this.form.questionBankList = []
  1255. if (!projectIds || projectIds.length === 0 || this.isPrivate === 0) {
  1256. return
  1257. }
  1258. const params = {ids: projectIds}
  1259. getByIds(params).then(response => {
  1260. if (response.code === 200) {
  1261. response.data.forEach(item => {
  1262. let isExist = this.form.questionBankList.some(q => q.id === item.id)
  1263. if (!isExist) {
  1264. this.form.questionBankList.push(item)
  1265. }
  1266. });
  1267. }
  1268. })
  1269. },
  1270. handleVideoDuration(duration) {
  1271. this.form.duration = duration;
  1272. },
  1273. formatDuration(seconds) {
  1274. if (seconds === null || seconds === undefined) {
  1275. return '未上传视频';
  1276. }
  1277. const hours = Math.floor(seconds / 3600);
  1278. const minutes = Math.floor((seconds % 3600) / 60);
  1279. const remainingSeconds = seconds % 60;
  1280. const formattedHours = hours > 0 ? hours.toString() + ':' : '';
  1281. const formattedMinutes = minutes.toString().padStart(2, '0');
  1282. const formattedSeconds = remainingSeconds.toString().padStart(2, '0');
  1283. return `${formattedHours}${formattedMinutes}:${formattedSeconds}`;
  1284. },
  1285. handleAvatarSuccess(res, file) {
  1286. if (res.code == 200) {
  1287. this.form.thumbnail = res.url;
  1288. this.$forceUpdate()
  1289. } else {
  1290. this.msgError(res.msg);
  1291. }
  1292. },
  1293. beforeAvatarUpload(file) {
  1294. const isLt1M = file.size / 1024 / 1024 < 5;
  1295. if (!isLt1M) {
  1296. this.$message.error('上传图片大小不能超过 5MB!');
  1297. }
  1298. return isLt1M;
  1299. },
  1300. getDetails(courseId, courseName, isPrivate) {
  1301. this.isPrivate = isPrivate
  1302. this.courseName = courseName
  1303. this.courseId = courseId;
  1304. this.queryParams.courseId = courseId;
  1305. this.getList();
  1306. },
  1307. getList() {
  1308. this.loading = true;
  1309. const api = this.usePublicVideoLibrary ? getPublicVideoListByCourseId : getVideoListByCourseId;
  1310. api(this.queryParams).then(response => {
  1311. this.userCourseVideoList = response.rows;
  1312. this.total = response.total;
  1313. this.loading = false;
  1314. });
  1315. },
  1316. // 取消按钮
  1317. cancel() {
  1318. this.open = false;
  1319. this.reset();
  1320. },
  1321. // 表单重置
  1322. reset() {
  1323. this.form = {
  1324. videoId: null,
  1325. title: null,
  1326. description: null,
  1327. url: null,
  1328. thumbnail: null,
  1329. duration: null,
  1330. createTime: null,
  1331. uploadType: null,
  1332. lineOne: null,
  1333. lineTwo: null,
  1334. lineThree: null,
  1335. fileName: null,
  1336. userId: null,
  1337. cateId: null,
  1338. courseId: null,
  1339. likes: null,
  1340. views: null,
  1341. comments: null,
  1342. status: 0,
  1343. courseSort: 1,
  1344. isHot: null,
  1345. isShow: null,
  1346. isAudit: null,
  1347. auditBy: null,
  1348. auditTime: null,
  1349. updateTime: null,
  1350. source: null,
  1351. isDel: null,
  1352. shares: null,
  1353. tags: null,
  1354. productId: null,
  1355. id: null,
  1356. packageJson: null,
  1357. questionBankId: null,
  1358. questionBankList: [],
  1359. redPacketMoney: 0,
  1360. isTranscode: 0,
  1361. transcodeFileKey: null,
  1362. isProduct: 0,
  1363. isFirst: 0,
  1364. isSpeed: 0,
  1365. isOnPut: 0,
  1366. listingStartTime: null,
  1367. listingEndTime: null,
  1368. showProduct: 1, // 默认无商品
  1369. randomRedPacketRules:null,
  1370. randomRedPacketRulesArr:[
  1371. {
  1372. minAmount: 0.01,
  1373. maxAmount: 0.01,
  1374. weight: 100,
  1375. }
  1376. ],
  1377. courseIntroImg: null
  1378. };
  1379. this.videoURL = '';
  1380. this.progress = 0;
  1381. this.resetForm("form");
  1382. },
  1383. /** 搜索按钮操作 */
  1384. handleQuery() {
  1385. this.queryParams.pageNum = 1;
  1386. this.getList();
  1387. },
  1388. /** 重置按钮操作 */
  1389. resetQuery() {
  1390. this.resetForm("queryForm");
  1391. this.handleQuery();
  1392. },
  1393. // 多选框选中数据
  1394. handleSelectionChange(selection) {
  1395. this.ids = selection.map(item => item.videoId)
  1396. this.single = selection.length !== 1
  1397. this.multiple = !selection.length
  1398. },
  1399. // 视频库多选框选中数据(按用户点击顺序记录)
  1400. handVideoleSelectionChange(selection) {
  1401. // 提取当前选中的所有ID
  1402. const selectedIds = selection.map(item => item.id);
  1403. // 处理新增选中项:保留用户点击顺序
  1404. selectedIds.forEach(id => {
  1405. if (!this.addBatchData.select.includes(id)) {
  1406. this.addBatchData.select.push(id);
  1407. }
  1408. });
  1409. // 处理取消选中项:移除已取消的ID
  1410. this.addBatchData.select = this.addBatchData.select.filter(id => selectedIds.includes(id));
  1411. },
  1412. handleAdd() {
  1413. this.reset();
  1414. this.form.courseId = this.courseId;
  1415. this.open = true;
  1416. this.title = "添加课堂视频";
  1417. this.videoUrl = '';
  1418. this.packageList = [];
  1419. getSort(this.courseId).then(response => {
  1420. this.form.courseSort = Number(response.data);
  1421. })
  1422. setTimeout(() => {
  1423. this.$refs.videoUpload.resetUpload();
  1424. }, 500);
  1425. },
  1426. /** 修改按钮操作 */
  1427. handleUpdate(row) {
  1428. this.reset();
  1429. this.form.isOnPut = row.isOnPut;
  1430. this.packageList = [];
  1431. const videoId = row.videoId || this.ids;
  1432. const api = this.usePublicVideoLibrary ? getPublicUserCourseVideo : getUserCourseVideo;
  1433. api(videoId).then(response => {
  1434. console.log(response);
  1435. this.form = response.data;
  1436. this.$set(this.form, 'courseIntroImg', response.data.courseIntroImg || null);
  1437. this.$set(this.form, 'isOnPut', response.data.isOnPut !== undefined ? response.data.isOnPut : 0);
  1438. if (this.form.randomRedPacketRules) {
  1439. this.$set(this.form, 'randomRedPacketRulesArr', JSON.parse(this.form.randomRedPacketRules));
  1440. } else {
  1441. // 处理初始值
  1442. this.form.randomRedPacketRulesArr = [{
  1443. minAmount: 0.01,
  1444. maxAmount: 0.01,
  1445. weight: 100,
  1446. }];
  1447. }
  1448. if (response.data.videoUrl != null && response.data.videoUrl !== '') {
  1449. this.videoUrl = response.data.videoUrl;
  1450. }
  1451. if (this.form.packageJson != null) {
  1452. try {
  1453. // 检查 packageJson 是否包含商品数据,如果是则解析为 courseProducts
  1454. const parsedData = JSON.parse(this.form.packageJson);
  1455. if (Array.isArray(parsedData) && parsedData.length > 0 && parsedData[0].hasOwnProperty('productName')) {
  1456. // 如果是商品数据格式,则设置到 courseProducts,并确保时间字段有默认值
  1457. this.form.courseProducts = parsedData.map(product => ({
  1458. ...product,
  1459. onShelfTime: product.onShelfTime || '00:00:00',
  1460. cardPopupTime: product.cardPopupTime || '00:00:00',
  1461. cardCloseTime: product.cardCloseTime || '00:00:00',
  1462. offShelfTime: product.offShelfTime || '00:00:00',
  1463. hotSaleTags: product.hotSaleTags || '',
  1464. stockDesc: product.stockDesc || product.stock
  1465. }));
  1466. this.form.courseProducts.forEach(p => this.loadProductSpecStock(p));
  1467. } else {
  1468. // 否则按照原有逻辑设置到 packageList
  1469. this.packageList = parsedData;
  1470. }
  1471. } catch (e) {
  1472. // 如果解析失败,按原来的处理方式
  1473. this.packageList = JSON.parse(this.form.packageJson);
  1474. }
  1475. }
  1476. if (response.data.viewStartTime != null && response.data.viewEndTime != null) {
  1477. this.form.timeRange = [response.data.viewStartTime, response.data.viewEndTime];
  1478. }
  1479. // 根据商品数量设置是否关联商品状态
  1480. // if (this.form.courseProducts && this.form.courseProducts.length > 0) {
  1481. // this.form.isProduct = 1; // 有关联商品时设为1(是)
  1482. // } else {
  1483. // this.form.isProduct = 0; // 无关联商品时设为0(否)
  1484. // }
  1485. // 设置商品选择状态
  1486. if (this.form.id) {
  1487. this.form.showProduct = 0; // 有商品时设为0
  1488. } else {
  1489. this.form.showProduct = 1; // 无商品时设为1
  1490. }
  1491. setTimeout(() => {
  1492. this.$refs.videoUpload.resetUpload();
  1493. }, 500);
  1494. this.open = true;
  1495. this.title = "修改课堂视频";
  1496. });
  1497. },
  1498. /** 提交按钮 */
  1499. submitForm() {
  1500. this.$refs["form"].validate(valid => {
  1501. if (valid) {
  1502. this.form.videoUrl = this.videoUrl;
  1503. if (this.form.videoUrl == null || this.form.videoUrl === '') {
  1504. this.$message({
  1505. message: '请上传视频!',
  1506. type: 'warning'
  1507. });
  1508. return
  1509. }
  1510. if (this.form.timeRange && this.form.timeRange.length === 2) {
  1511. this.form.viewStartTime = this.form.timeRange[0];
  1512. this.form.viewEndTime = this.form.timeRange[1];
  1513. }
  1514. if (this.form.duration == null) {
  1515. this.$message({
  1516. message: '未识别到视频时长请稍等。。。',
  1517. type: 'warning'
  1518. });
  1519. return
  1520. }
  1521. if (this.form.isProduct != null && this.form.isProduct == 1 && (this.form.courseProducts == null || this.form.courseProducts.length < 1)) {
  1522. this.$message({
  1523. message: '请选择关联商品',
  1524. type: 'warning'
  1525. });
  1526. return
  1527. }
  1528. if (this.form.questionBankList !== null) {
  1529. this.form.questionBankId = this.form.questionBankList.map(item => item.id).join(',');
  1530. }
  1531. // 处理商品选择逻辑
  1532. if (this.form.courseProducts && this.form.courseProducts.length > 0) {
  1533. this.form.id = this.form.courseProducts.map(item => item.id).join(',');
  1534. this.form.showProduct = 0; // 有商品选择时设为0
  1535. // 将商品数据序列化为JSON字符串传递给packageJson
  1536. this.form.packageJson = JSON.stringify(this.form.courseProducts);
  1537. } else {
  1538. this.form.id = null;
  1539. this.form.showProduct = 1; // 无商品选择时设为1
  1540. this.form.packageJson = "[]"; // 无商品时传递空数组
  1541. }
  1542. if (this.form.randomRedPacketRulesArr) {
  1543. let rulesJson = JSON.stringify(this.form.randomRedPacketRulesArr);
  1544. this.form.randomRedPacketRules = rulesJson;
  1545. }
  1546. if (this.form.videoId != null) {
  1547. updateUserCourseVideo(this.form).then(response => {
  1548. this.msgSuccess("修改成功");
  1549. this.open = false;
  1550. this.getList();
  1551. });
  1552. } else {
  1553. addUserCourseVideo(this.form).then(response => {
  1554. this.msgSuccess("新增成功");
  1555. this.open = false;
  1556. this.getList();
  1557. });
  1558. }
  1559. }
  1560. });
  1561. },
  1562. openUpdates() {
  1563. this.updateBatchData.form = {};
  1564. this.updateBatchData.open = true;
  1565. },
  1566. /** 提交按钮 */
  1567. updateBatch() {
  1568. this.updateBatchData.form.ids = this.ids;
  1569. if (this.updateBatchData.form.timeRange != null && this.updateBatchData.form.timeRange.length === 2) {
  1570. this.updateBatchData.form.viewStartTime = this.updateBatchData.form.timeRange[0];
  1571. this.updateBatchData.form.viewEndTime = this.updateBatchData.form.timeRange[1];
  1572. }
  1573. updates(this.updateBatchData.form).then(response => {
  1574. this.msgSuccess("修改成功");
  1575. this.updateBatchData.open = false;
  1576. this.getList();
  1577. });
  1578. },
  1579. /** 删除按钮操作 */
  1580. handleDelete(row) {
  1581. const videoIds = row.videoId || this.ids;
  1582. this.$confirm('是否确认删除视频编号为"' + videoIds + '"的数据项?', "警告", {
  1583. confirmButtonText: "确定",
  1584. cancelButtonText: "取消",
  1585. type: "warning"
  1586. }).then(function () {
  1587. return delUserCourseVideo(videoIds);
  1588. }).then(() => {
  1589. this.getList();
  1590. this.msgSuccess("删除成功");
  1591. }).catch(() => {
  1592. });
  1593. },
  1594. /** 同步模板数据*/
  1595. handleSync() {
  1596. const courseId = this.courseId;
  1597. this.$confirm('是否同步课程数据至模板', "确认", {
  1598. confirmButtonText: "确定",
  1599. cancelButtonText: "取消",
  1600. type: "warning"
  1601. }).then(function () {
  1602. return syncTemplate(courseId);
  1603. }).then(() => {
  1604. this.getList();
  1605. this.msgSuccess("正在同步模板中!!请稍后!");
  1606. }).catch(() => {
  1607. });
  1608. },
  1609. handleCourseSort() {
  1610. getVideoListByCourseIdAll(this.queryParams.courseId).then(response => {
  1611. response.rows.forEach((item) => item.newCourseSort = item.courseSort);
  1612. this.userCourseVideoSortList = response.rows.sort((a, b) => a.courseSort - b.courseSort);
  1613. if (this.userCourseVideoSortList == null || this.userCourseVideoSortList.length == 0) {
  1614. this.$message.error("暂无课节天数")
  1615. } else {
  1616. this.openVideoSort = true;
  1617. }
  1618. })
  1619. },
  1620. onDragEndDay() {
  1621. this.userCourseVideoSortList.forEach((item, index) => {
  1622. item.newCourseSort = index + 1;
  1623. })
  1624. this.$forceUpdate()
  1625. },
  1626. saveSorts() {
  1627. let list = this.userCourseVideoSortList.filter(e => e.courseSort != e.newCourseSort).map(e => {
  1628. return {courseSort: e.newCourseSort, videoId: e.videoId}
  1629. })
  1630. this.loading3 = true;
  1631. sortCourseVideo(list).then(e => {
  1632. this.getList();
  1633. }).finally(() => {
  1634. this.userCourseVideoSortList = [];
  1635. this.openVideoSort = false;
  1636. })
  1637. },
  1638. openAdds() {
  1639. this.addBatchData.open = true;
  1640. this.getRootTypeList();
  1641. this.addBatchData.form = {
  1642. courseId: this.courseId,
  1643. };
  1644. // 重置选择顺序数组
  1645. this.addBatchData.select = [];
  1646. this.resourceList();
  1647. },
  1648. getRootTypeList() {
  1649. getCatePidList().then(response => {
  1650. this.addBatchData.typeOptions = response.data
  1651. });
  1652. },
  1653. async changeCateType(val) {
  1654. this.addBatchData.queryParams.typeSubId = null
  1655. this.addBatchData.typeSubOptions = []
  1656. if (!val) {
  1657. return
  1658. }
  1659. await getCateListByPid(val).then(response => {
  1660. this.addBatchData.typeSubOptions = response.data
  1661. })
  1662. },
  1663. resourceList() {
  1664. this.addBatchData.loading = true;
  1665. listVideoResource(this.addBatchData.queryParams).then(response => {
  1666. this.addBatchData.loading = false;
  1667. this.addBatchData.list = response.rows;
  1668. this.addBatchData.total = response.total;
  1669. });
  1670. },
  1671. batchVideoSave() {
  1672. if (this.addBatchData.select.length === 0) {
  1673. this.$message({
  1674. message: '请选择视频!!',
  1675. type: 'warning'
  1676. });
  1677. return
  1678. }
  1679. this.addBatchData.form.ids = this.addBatchData.select; // 按用户选择顺序提交
  1680. batchSaveVideo(this.addBatchData.form).then(response => {
  1681. this.addBatchData.open = false;
  1682. this.getList();
  1683. })
  1684. },
  1685. updateRedPageckeOpen() {
  1686. this.redData.open = true;
  1687. this.redData.loading = true;
  1688. this.redData.queryParams.courseId = this.courseId;
  1689. getVideoListByCourseId(this.redData.queryParams).then(response => {
  1690. if(!!response.rows && response.rows.length >0){
  1691. for(let i = 0; i < response.rows.length; i++){
  1692. if(!!response.rows[i].randomRedPacketRules){
  1693. this.$set(response.rows[i], 'randomRedPacketRulesArr', JSON.parse(response.rows[i].randomRedPacketRules)) ;
  1694. }
  1695. }
  1696. }
  1697. this.redData.list = response.rows;
  1698. console.log(this.redData.list);
  1699. this.redData.loading = false;
  1700. });
  1701. },
  1702. batchRedSave() {
  1703. batchUpdateRed(this.redData.list).then(response => {
  1704. this.redData.open = false;
  1705. this.getList();
  1706. })
  1707. },
  1708. /** 查看评论按钮操作 */
  1709. handleComment(row) {
  1710. this.commentDialog.courseId = row.courseId || this.courseId;
  1711. this.commentDialog.videoId = row.videoId;
  1712. this.commentDialog.title = `查看评论 - ${row.title}`;
  1713. this.commentDialog.open = true;
  1714. },
  1715. // 实时过滤金额输入,只允许两位小数
  1716. handleAmountInput(rule, field) {
  1717. let value = rule[field];
  1718. if (value === null || value === undefined) return;
  1719. // 转换为字符串处理
  1720. let str = value.toString();
  1721. // 移除除数字和小数点外的所有字符
  1722. str = str.replace(/[^0-9.]/g, '');
  1723. // 只保留一个小数点
  1724. const dotIndex = str.indexOf('.');
  1725. if (dotIndex !== -1) {
  1726. str = str.substring(0, dotIndex + 1) + str.substring(dotIndex + 1).replace(/\./g, '');
  1727. }
  1728. // 限制小数点后最多两位
  1729. if (dotIndex !== -1 && str.length > dotIndex + 3) {
  1730. str = str.substring(0, dotIndex + 3);
  1731. }
  1732. // 转换回数字并更新
  1733. rule[field] = parseFloat(str) || 0;
  1734. },
  1735. deleteRule(index) {
  1736. this.$confirm("确定要删除这个区间吗?", "提示", {
  1737. confirmButtonText: "确定",
  1738. cancelButtonText: "取消",
  1739. type: "warning",
  1740. }).then(() => {
  1741. this.form.randomRedPacketRulesArr.splice(index, 1);
  1742. this.$message({
  1743. type: "success",
  1744. message: "删除成功!",
  1745. });
  1746. });
  1747. },
  1748. addRule(index) {
  1749. // 在当前行的后面插入一个新行
  1750. this.form.randomRedPacketRulesArr.splice(index + 1, 0, {
  1751. minAmount: 0.01,
  1752. maxAmount: 0.01,
  1753. weight: 100,
  1754. });
  1755. },
  1756. // 自定义校验规则:确保最大金额大于最小金额
  1757. validateMinAmount(rule, value, callback) {
  1758. // debugger;
  1759. // const maxAmount = this.form29.rules[].maxAmount
  1760. const index = rule.index;
  1761. const maxAmount = this.form.randomRedPacketRulesArr[index].maxAmount;
  1762. if (value > maxAmount) {
  1763. callback(new Error("最小金额不能大于最大金额"));
  1764. } else {
  1765. callback();
  1766. }
  1767. },
  1768. validateRules() {
  1769. this.form.randomRedPacketRulesArr.forEach((rule) => {
  1770. if (rule.minAmount === undefined || rule.minAmount < 0.01) {
  1771. rule.minAmount = 0.01;
  1772. }
  1773. if (rule.maxAmount === undefined || rule.maxAmount < rule.minAmount) {
  1774. rule.maxAmount = rule.minAmount;
  1775. }
  1776. if (rule.weight === undefined || rule.weight < 1) {
  1777. rule.weight = 1;
  1778. }
  1779. });
  1780. },
  1781. /** 下架 **/
  1782. handleDown() {
  1783. const videoIds = this.ids;
  1784. this.$confirm('是否确认下架视频编号为"' + videoIds + '"的数据项?', "警告", {
  1785. confirmButtonText: "确定",
  1786. cancelButtonText: "取消",
  1787. type: "warning"
  1788. }).then(function () {
  1789. return batchDownUserCourseVideo(videoIds);
  1790. }).then(() => {
  1791. this.getList();
  1792. this.msgSuccess("下架成功");
  1793. }).catch(() => {
  1794. });
  1795. },
  1796. /** 上架按钮操作 */
  1797. handleUp() {
  1798. const videoIds = this.ids;
  1799. this.$confirm('是否确认上架视频编号为"' + videoIds + '"的数据项?', "警告", {
  1800. confirmButtonText: "确定",
  1801. cancelButtonText: "取消",
  1802. type: "warning"
  1803. }).then(function () {
  1804. return batchUpUserCourseVideo(videoIds);
  1805. }).then(() => {
  1806. this.getList();
  1807. this.msgSuccess("上架成功");
  1808. }).catch(function () {
  1809. });
  1810. },
  1811. /** 修改封面 **/
  1812. handleEditCover() {
  1813. this.batchEditCoverDialog.form = {
  1814. thumbnail: null
  1815. }
  1816. this.batchEditCoverDialog.visible = true
  1817. },
  1818. handleCoverSuccess(res, file) {
  1819. if (res.code === 200) {
  1820. this.batchEditCoverDialog.form.thumbnail = res.url;
  1821. this.$forceUpdate()
  1822. } else {
  1823. this.msgError(res.msg);
  1824. }
  1825. },
  1826. submitEditCoverForm() {
  1827. this.$refs["batchEditCoverDialogForm"].validate(valid => {
  1828. if (!valid) {
  1829. return;
  1830. }
  1831. const thumbnail = this.batchEditCoverDialog.form.thumbnail
  1832. const videoIds = this.ids
  1833. if (!thumbnail || thumbnail === '') {
  1834. this.$message({
  1835. message: '请上传封面!',
  1836. type: 'warning'
  1837. });
  1838. return
  1839. }
  1840. if (!videoIds || videoIds.length === 0) {
  1841. this.$message({
  1842. message: '请选择小节!',
  1843. type: 'warning'
  1844. });
  1845. return
  1846. }
  1847. const params = {
  1848. thumbnail: thumbnail,
  1849. videoIds: videoIds
  1850. }
  1851. batchEditCover(params).then(response => {
  1852. this.msgSuccess("修改成功")
  1853. this.batchEditCoverDialog.visible = false
  1854. this.getList();
  1855. });
  1856. });
  1857. },
  1858. cancelEditCoverForm() {
  1859. this.batchEditCoverDialog.visible = false
  1860. this.batchEditCoverDialog.form = {
  1861. thumbnail: null,
  1862. }
  1863. },
  1864. // 课程介绍图片上传成功
  1865. handleCourseIntroSuccess(res) {
  1866. if (res.code == 200) {
  1867. this.form.courseIntroImg = res.url;
  1868. this.$forceUpdate();
  1869. } else {
  1870. this.$message.error(res.msg || '上传失败');
  1871. }
  1872. },
  1873. // 课程介绍图片上传前校验(10MB限制)
  1874. beforeCourseIntroUpload(file) {
  1875. const isLt10M = file.size / 1024 / 1024 < 10;
  1876. if (!isLt10M) {
  1877. this.$message.error('课程介绍图片大小不能超过10MB!');
  1878. }
  1879. const isImage = file.type.startsWith('image/');
  1880. if (!isImage) {
  1881. this.$message.error('只能上传图片格式文件!');
  1882. }
  1883. return isLt10M && isImage;
  1884. },
  1885. openUserCommentDialog() {
  1886. if (!this.form.courseId || !this.form.videoId) {
  1887. this.$message.warning('请先保存课节后再查看用户留言')
  1888. return
  1889. }
  1890. this.userCommentDialog.courseId = this.form.courseId
  1891. this.userCommentDialog.videoId = this.form.videoId
  1892. this.userCommentDialog.title = `用户留言 - ${this.form.title || ''}`
  1893. this.userCommentDialog.open = true
  1894. },
  1895. onUserCommentDialogOpened() {
  1896. this.$nextTick(() => {
  1897. if (this.$refs.userCourseSectionComment) {
  1898. this.$refs.userCourseSectionComment.handleQuery()
  1899. }
  1900. })
  1901. },
  1902. // 打开精选留言导入弹窗
  1903. openCommentImportDialog() {
  1904. this.commentImportDialog.visible = true;
  1905. this.commentImportDialog.file = null;
  1906. this.commentImportDialog.fileList = [];
  1907. this.commentImportDialog.courseId = this.form.courseId;
  1908. this.commentImportDialog.videoId = this.form.videoId;
  1909. // 加载历史留言
  1910. listFeaturedComments(this.form.courseId, this.form.videoId).then(res => {
  1911. this.commentImportDialog.historyList = res.rows || [];
  1912. });
  1913. },
  1914. // 选择导入文件
  1915. handleCommentFileChange(file) {
  1916. this.commentImportDialog.file = file.raw;
  1917. },
  1918. // 删除精选留言
  1919. handleDeleteComment(row) {
  1920. this.$confirm('确认删除该条精选留言?', '提示', {type: 'warning'}).then(() => {
  1921. delUserCourseComment(row.commentId).then(res => {
  1922. if (res.code === 200) {
  1923. this.$message.success('删除成功');
  1924. // 刷新历史列表
  1925. listFeaturedComments(this.commentImportDialog.courseId, this.commentImportDialog.videoId).then(res2 => {
  1926. this.commentImportDialog.historyList = res2.rows || [];
  1927. });
  1928. } else {
  1929. this.$message.error(res.msg || '删除失败');
  1930. }
  1931. });
  1932. }).catch(() => {});
  1933. },
  1934. // 下载Word导入模板
  1935. handleDownloadTemplate() {
  1936. downloadCommentImportTemplate().then(res => {
  1937. const blob = new Blob([res], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
  1938. const url = window.URL.createObjectURL(blob);
  1939. const link = document.createElement('a');
  1940. link.href = url;
  1941. link.download = '精选留言导入模板.xlsx';
  1942. link.click();
  1943. window.URL.revokeObjectURL(url);
  1944. });
  1945. },
  1946. // 确认导入精选留言
  1947. handleImportComments() {
  1948. if (!this.commentImportDialog.file) {
  1949. this.$message.warning('请先选择要导入的Excel文件');
  1950. return;
  1951. }
  1952. this.commentImportDialog.loading = true;
  1953. importComments(this.commentImportDialog.courseId, this.commentImportDialog.videoId, this.commentImportDialog.file).then(res => {
  1954. if (res.code === 200) {
  1955. this.$message.success(res.msg);
  1956. // 刷新历史列表
  1957. listFeaturedComments(this.commentImportDialog.courseId, this.commentImportDialog.videoId).then(res2 => {
  1958. this.commentImportDialog.historyList = res2.rows || [];
  1959. });
  1960. } else {
  1961. this.$message.error(res.msg);
  1962. }
  1963. }).finally(() => {
  1964. this.commentImportDialog.loading = false;
  1965. });
  1966. },
  1967. }
  1968. }
  1969. </script>
  1970. <style scoped>
  1971. .avatar-uploader-icon {
  1972. display: flex;
  1973. align-items: center;
  1974. justify-content: center;
  1975. }
  1976. /* 商品子表:操作列垂直居中,避免 fixed 列遮挡最后一行 */
  1977. .course-product-table >>> .course-product-op-col .cell {
  1978. display: flex;
  1979. align-items: center;
  1980. justify-content: center;
  1981. overflow: visible;
  1982. padding: 6px 4px;
  1983. line-height: normal;
  1984. }
  1985. .course-product-op-btns {
  1986. display: inline-flex;
  1987. flex-direction: column;
  1988. align-items: center;
  1989. gap: 2px;
  1990. }
  1991. .course-product-op-btns .el-button {
  1992. margin-left: 0 !important;
  1993. padding: 2px 0;
  1994. }
  1995. .course-product-table >>> .el-table__body-wrapper {
  1996. padding-bottom: 8px;
  1997. }
  1998. </style>
  1999. <style>
  2000. .avatar-uploader .el-upload {
  2001. border: 1px dashed #d9d9d9;
  2002. border-radius: 6px;
  2003. cursor: pointer;
  2004. position: relative;
  2005. overflow: hidden;
  2006. }
  2007. .avatar-uploader .el-upload:hover {
  2008. border-color: #409EFF;
  2009. }
  2010. .avatar-uploader-icon {
  2011. font-size: 28px;
  2012. color: #8c939d;
  2013. width: 150px;
  2014. height: 150px;
  2015. line-height: 150px;
  2016. text-align: center;
  2017. }
  2018. .red:hover {
  2019. color: #dbdbdb !important;
  2020. }
  2021. .red {
  2022. background-color: #F56C6C !important;
  2023. color: #fff !important;
  2024. }
  2025. </style>