index.vue 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901
  1. <template>
  2. <div class="app-container">
  3. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
  4. <el-form-item label="素材名称" prop="resourceName">
  5. <el-input
  6. v-model="queryParams.resourceName"
  7. placeholder="请输入素材名称"
  8. clearable
  9. size="small"
  10. @keyup.enter.native="handleQuery"
  11. />
  12. </el-form-item>
  13. <el-form-item label="文件名称" prop="fileName">
  14. <el-input
  15. v-model="queryParams.fileName"
  16. placeholder="请输入素材名称"
  17. clearable
  18. size="small"
  19. @keyup.enter.native="handleQuery"
  20. />
  21. </el-form-item>
  22. <el-form-item label="分类" prop="typeId">
  23. <el-select v-model="queryParams.typeId" clearable placeholder="请选择分类" @change="changeCateType">
  24. <el-option
  25. v-for="item in rootTypeList"
  26. :key="item.dictValue"
  27. :label="item.dictLabel"
  28. :value="item.dictValue">
  29. </el-option>
  30. </el-select>
  31. </el-form-item>
  32. <el-form-item label="子分类" prop="typeSubId">
  33. <el-select v-model="queryParams.typeSubId" clearable placeholder="请选择子分类">
  34. <el-option
  35. v-for="item in subTypeList"
  36. :key="item.dictValue"
  37. :label="item.dictLabel"
  38. :value="item.dictValue">
  39. </el-option>
  40. </el-select>
  41. </el-form-item>
  42. <el-form-item>
  43. <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  44. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  45. </el-form-item>
  46. </el-form>
  47. <el-row :gutter="10" class="mb8">
  48. <el-col :span="1.5">
  49. <el-button
  50. type="primary"
  51. icon="el-icon-plus"
  52. size="mini"
  53. @click="handleAdd"
  54. v-hasPermi="['course:videoResource:add']"
  55. >新增</el-button>
  56. </el-col>
  57. <el-col :span="1.5">
  58. <el-button
  59. type="primary"
  60. icon="el-icon-plus"
  61. size="mini"
  62. @click="handleBatchAdd"
  63. v-hasPermi="['course:videoResource:add']"
  64. >批量新增</el-button>
  65. </el-col>
  66. <el-col :span="1.5">
  67. <el-button
  68. type="danger"
  69. icon="el-icon-delete"
  70. size="mini"
  71. :disabled="multiple"
  72. @click="handleDelete"
  73. v-hasPermi="['course:videoResource:remove']"
  74. >删除</el-button>
  75. </el-col>
  76. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  77. </el-row>
  78. <el-table v-loading="loading" :data="resourceList" @selection-change="handleSelectionChange">
  79. <el-table-column type="selection" width="55" align="center" />
  80. <el-table-column label="序号" width="55" align="center">
  81. <template slot-scope="scope">
  82. {{ scope.$index + 1 }}
  83. </template>
  84. </el-table-column>
  85. <el-table-column label="素材名称" align="center" :show-overflow-tooltip="true" prop="resourceName"/>
  86. <el-table-column label="文件名称" align="center" :show-overflow-tooltip="true" prop="fileName"/>
  87. <el-table-column label="分类" align="center">
  88. <template slot-scope="scope">
  89. <span v-if="scope.row.typeId">{{ getTypeName(scope.row.typeId) }}</span>
  90. </template>
  91. </el-table-column>
  92. <el-table-column label="子分类" align="center">
  93. <template slot-scope="scope">
  94. <span v-if="scope.row.typeSubId">{{ getTypeName(scope.row.typeSubId) }}</span>
  95. </template>
  96. </el-table-column>
  97. <el-table-column label="视频文件" align="center">
  98. <template slot-scope="scope">
  99. <a
  100. @click="handleVideoPreview(scope.row.videoUrl)"
  101. style="background-color: #409EFF; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
  102. 查看文件
  103. </a>
  104. </template>
  105. </el-table-column>
  106. <el-table-column label="CDN" align="center">
  107. <template slot-scope="scope">
  108. <a
  109. @click="copy(scope.row.videoUrl)"
  110. style="color: #409EFF; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
  111. 复制链接
  112. </a>
  113. </template>
  114. </el-table-column>
  115. <el-table-column label="关联题目" align="center">
  116. <template slot-scope="scope">
  117. <a
  118. @click="handleViewProject(scope.row, 3)"
  119. :style="scope.row.projectIds ? {
  120. backgroundColor: '#409EFF',
  121. color: 'white',
  122. border: 'none',
  123. borderRadius: '4px',
  124. padding: '4px 12px',
  125. cursor: 'pointer',
  126. fontSize: '12px',
  127. display: 'inline-block',
  128. textDecoration: 'none'
  129. } : {
  130. backgroundColor: 'rgb(154 156 159)',
  131. color: 'white',
  132. border: 'none',
  133. cursor: 'pointer',
  134. borderRadius: '4px',
  135. padding: '4px 12px',
  136. fontSize: '12px',
  137. display: 'inline-block',
  138. textDecoration: 'none'
  139. }">
  140. {{ scope.row.projectIds ? '查看详情' : '未关联题目' }}
  141. </a>
  142. </template>
  143. </el-table-column>
  144. <el-table-column label="视频时长" align="center">
  145. <template slot-scope="scope">
  146. <div style="padding: 4px 12px;background: linear-gradient(to right, rgb(196 219 255), #409EFF)">{{ formatDuration(scope.row.duration) }}</div>
  147. </template>
  148. </el-table-column>
  149. <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  150. <template slot-scope="scope">
  151. <el-button
  152. size="mini"
  153. type="text"
  154. icon="el-icon-edit"
  155. @click="handleUpdate(scope.row)"
  156. v-hasPermi="['course:videoResource:edit']"
  157. >修改</el-button>
  158. <el-button
  159. size="mini"
  160. type="text"
  161. icon="el-icon-delete"
  162. @click="handleDelete(scope.row)"
  163. v-hasPermi="['course:videoResource:remove']"
  164. >删除</el-button>
  165. </template>
  166. </el-table-column>
  167. </el-table>
  168. <pagination
  169. v-show="total>0"
  170. :total="total"
  171. :page.sync="queryParams.pageNum"
  172. :limit.sync="queryParams.pageSize"
  173. @pagination="getList"
  174. />
  175. <!-- 添加或修改视频素材库对话框 -->
  176. <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body :before-close="cancel">
  177. <el-form ref="form" :model="form" :rules="rules" label-width="80px">
  178. <el-form-item label="素材名称" prop="resourceName" style="margin-top: 20px">
  179. <el-input v-model="form.resourceName" placeholder="请输入" />
  180. </el-form-item>
  181. <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
  182. <el-input v-model="form.fileName" placeholder="请输入" />
  183. </el-form-item>
  184. <el-form-item label="分类" prop="typeId">
  185. <el-select v-model="form.typeId" placeholder="请选择分类" style="width: 100%" @change="changeCateType">
  186. <el-option
  187. v-for="item in rootTypeList"
  188. :key="item.dictValue"
  189. :label="item.dictLabel"
  190. :value="item.dictValue">
  191. </el-option>
  192. </el-select>
  193. </el-form-item>
  194. <el-form-item label="子分类" prop="typeSubId">
  195. <el-select v-model="form.typeSubId" clearable placeholder="请选择子分类" style="width: 100%">
  196. <el-option
  197. v-for="item in subTypeList"
  198. :key="item.dictValue"
  199. :label="item.dictLabel"
  200. :value="item.dictValue">
  201. </el-option>
  202. </el-select>
  203. </el-form-item>
  204. <el-form-item label="关联题目" prop="projectIds">
  205. <el-select
  206. ref="customSelect"
  207. class="custom-select-class"
  208. v-model="form.projectIds"
  209. multiple
  210. placeholder="请选择关联题目"
  211. @click.native.stop="openProjectDialog(form.projectIds, 0)"
  212. style="width: 100%;">
  213. <el-option
  214. v-for="item in projectShowList"
  215. :key="item.id"
  216. :label="item.title"
  217. :value="item.id">
  218. </el-option>
  219. </el-select>
  220. </el-form-item>
  221. <el-form-item label="上传视频" prop="videoUrl" required>
  222. <el-upload
  223. ref="videoUpload"
  224. action="#"
  225. list-type="picture-card"
  226. :http-request="videoUpload"
  227. :file-list="fileList"
  228. :on-change="handleFileChange"
  229. accept=".mp4">
  230. <i slot="default" class="el-icon-plus"></i>
  231. <div slot="file" slot-scope="{file}">
  232. <div
  233. class="el-upload-list__item-thumbnail"
  234. style="display: flex; justify-content: center; align-items: center; background-color: #f5f7fa; height: 146px;"
  235. >
  236. <i class="el-icon-video-camera" style="font-size: 48px;" />
  237. </div>
  238. <span v-if="file.status === 'success'" class="el-upload-list__item-status-label">
  239. <i class="el-icon-upload-success el-icon--check"></i>
  240. </span>
  241. <span v-if="file.status === 'fail'" class="el-upload-list__item-status-label">
  242. <i class="el-icon-circle-close"></i>
  243. </span>
  244. <span class="el-upload-list__item-actions">
  245. <span
  246. class="el-upload-list__item-preview"
  247. @click="handleVideoPreview(form.videoUrl)"
  248. >
  249. <i class="el-icon-zoom-in"></i>
  250. </span>
  251. <span
  252. class="el-upload-list__item-delete"
  253. @click="handleRemove(file)"
  254. >
  255. <i class="el-icon-delete"></i>
  256. </span>
  257. </span>
  258. <div v-if="file.status === 'uploading'" class="el-upload-list__item-progress">
  259. <el-progress
  260. :percentage="file.percentage"
  261. :show-text="false"
  262. :width="52"
  263. ></el-progress>
  264. </div>
  265. </div>
  266. </el-upload>
  267. </el-form-item>
  268. <el-form-item label="时长">
  269. <span>{{ formatDuration(form.duration) }}</span>
  270. </el-form-item>
  271. </el-form>
  272. <div slot="footer" class="dialog-footer">
  273. <el-button @click="cancel">取消</el-button>
  274. <el-button type="primary" @click="submitForm">保存</el-button>
  275. </div>
  276. </el-dialog>
  277. <el-dialog
  278. title="视频预览"
  279. :visible.sync="videoPreviewVisible"
  280. append-to-body
  281. :close-on-click-modal="true"
  282. width="700px"
  283. class="video-preview-dialog"
  284. :modal-append-to-body="false"
  285. :before-close="handleCloseVideoPreview">
  286. <video ref="up-video" id="video" width="100%" height="400px" controls :src="videoPreviewUrl" />
  287. </el-dialog>
  288. <!-- 批量选择视频弹窗 -->
  289. <el-dialog :title="'选择视频'" :visible.sync="batchAddVisible" width="1200px" append-to-body class="batch-dialog" :close-on-click-modal="false" :before-close="cancelBeforeBatch">
  290. <div class="filter-container">
  291. <el-button type="primary" icon="el-icon-plus" size="small" @click="showUploadPanel">上传视频</el-button>
  292. </div>
  293. <el-table
  294. v-loading="batchLoading"
  295. :data="videoList"
  296. height="350">
  297. <el-table-column label="序号" width="60" align="center">
  298. <template slot-scope="scope">
  299. {{ scope.$index + 1 }}
  300. </template>
  301. </el-table-column>
  302. <el-table-column label="素材名称" align="center" prop="resourceName" min-width="120" />
  303. <el-table-column label="文件名称" align="center" prop="fileName" min-width="120" />
  304. <el-table-column label="分类" align="center" min-width="100">
  305. <template slot-scope="scope">
  306. {{ getTypeName(scope.row.typeId) }}
  307. </template>
  308. </el-table-column>
  309. <el-table-column label="关联项目" align="center" min-width="100">
  310. <template slot-scope="scope">
  311. <a
  312. @click="handleViewProject(scope.row, 4)"
  313. :style="scope.row.projectIds.length > 0 ? {
  314. backgroundColor: '#409EFF',
  315. color: 'white',
  316. border: 'none',
  317. borderRadius: '4px',
  318. padding: '4px 12px',
  319. cursor: 'pointer',
  320. fontSize: '12px',
  321. display: 'inline-block',
  322. textDecoration: 'none'
  323. } : {
  324. backgroundColor: 'rgb(154 156 159)',
  325. color: 'white',
  326. border: 'none',
  327. cursor: 'pointer',
  328. borderRadius: '4px',
  329. padding: '4px 12px',
  330. fontSize: '12px',
  331. display: 'inline-block',
  332. textDecoration: 'none'
  333. }">
  334. {{ scope.row.projectIds.length > 0 ? '查看详情' : '未关联题目' }}
  335. </a>
  336. </template>
  337. </el-table-column>
  338. <el-table-column label="视频文件" align="center" prop="fileName" min-width="120">
  339. <template slot-scope="scope">
  340. <a
  341. @click="handleVideoPreview(scope.row.videoUrl)"
  342. style="background-color: #409EFF; color: white; border: none; border-radius: 4px; padding: 4px 12px; cursor: pointer; font-size: 12px; display: inline-block; text-decoration: none;">
  343. 查看文件
  344. </a>
  345. </template>
  346. </el-table-column>
  347. <el-table-column label="视频时长" align="center" width="80">
  348. <template slot-scope="scope">
  349. {{ formatDuration(scope.row.duration) }}
  350. </template>
  351. </el-table-column>
  352. <el-table-column label="上传进度" align="center" width="120">
  353. <template slot-scope="scope">
  354. <el-progress :percentage="scope.row.progress" :status="scope.row.progress === 100 ? 'success' : undefined"></el-progress>
  355. </template>
  356. </el-table-column>
  357. <el-table-column label="操作" align="center" width="150">
  358. <template slot-scope="scope">
  359. <el-button
  360. size="mini"
  361. type="text"
  362. icon="el-icon-edit"
  363. @click="handleEditVideo(scope.row)">编辑</el-button>
  364. <el-button
  365. size="mini"
  366. type="text"
  367. icon="el-icon-delete"
  368. @click="handleDeleteVideo(scope.row)">删除</el-button>
  369. </template>
  370. </el-table-column>
  371. </el-table>
  372. <div class="dialog-footer">
  373. <el-button @click="cancelBatch">取 消</el-button>
  374. <el-button type="primary" @click="submitBatchAdd">保 存</el-button>
  375. </div>
  376. <!-- 批量上传视频弹窗 -->
  377. <el-dialog
  378. title="批量上传视频"
  379. :visible.sync="showUpload"
  380. width="500px"
  381. append-to-body
  382. :close-on-click-modal="false"
  383. class="upload-dialog">
  384. <el-form :model="batchUploadForm" ref="batchUploadForm" label-width="80px">
  385. <el-form-item style="margin-top: 20px" label="分类" prop="typeId" :rules="[{ required: true, message: '请选择分类', trigger: 'blur' }]">
  386. <el-select v-model="batchUploadForm.typeId" placeholder="请选择分类" style="width: 100%" @change="changeCateType">
  387. <el-option
  388. v-for="item in rootTypeList"
  389. :key="item.dictValue"
  390. :label="item.dictLabel"
  391. :value="item.dictValue">
  392. </el-option>
  393. </el-select>
  394. </el-form-item>
  395. <el-form-item label="子分类" prop="typeSubId" :rules="[{ required: true, message: '请选择子分类', trigger: 'blur' }]">
  396. <el-select v-model="batchUploadForm.typeSubId" clearable placeholder="请选择子分类" style="width: 100%">
  397. <el-option
  398. v-for="item in subTypeList"
  399. :key="item.dictValue"
  400. :label="item.dictLabel"
  401. :value="item.dictValue">
  402. </el-option>
  403. </el-select>
  404. </el-form-item>
  405. <el-form-item label="关联题目" prop="projectIds" style="display: none">
  406. <el-select
  407. ref="customSelect"
  408. class="custom-select-class"
  409. v-model="batchUploadForm.projectIds"
  410. multiple
  411. placeholder="请选择关联题目"
  412. @click.native.stop="openProjectDialog(batchUploadForm.projectIds, 1)"
  413. style="width: 100%;">
  414. <el-option
  415. v-for="item in projectShowList"
  416. :key="item.id"
  417. :label="item.title"
  418. :value="item.id">
  419. </el-option>
  420. </el-select>
  421. </el-form-item>
  422. <el-form-item label="上传视频" prop="files">
  423. <el-upload
  424. ref="batchVideoUpload"
  425. action="#"
  426. :http-request="batchVideoUpload"
  427. :file-list="batchFileList"
  428. :on-change="handleBatchFileChange"
  429. multiple
  430. accept=".mp4">
  431. <el-button type="primary" :disabled="batchUploadForm.typeSubId === null">选择文件</el-button>
  432. <div slot="tip" class="el-upload__tip">选择分类后才可以上传视频</div>
  433. </el-upload>
  434. </el-form-item>
  435. </el-form>
  436. </el-dialog>
  437. <!-- 添加或修改视频素材库对话框 -->
  438. <el-dialog :title="batchEditDialog.title" :visible.sync="batchEditDialog.open" width="600px" append-to-body :before-close="batchEditCancel">
  439. <el-form ref="batchEditDialogForm" :model="batchEditDialog.form" :rules="batchEditDialog.rules" label-width="80px">
  440. <el-form-item label="素材名称" prop="resourceName" style="margin-top: 20px">
  441. <el-input v-model="batchEditDialog.form.resourceName" placeholder="请输入" />
  442. </el-form-item>
  443. <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
  444. <el-input v-model="batchEditDialog.form.fileName" placeholder="请输入" />
  445. </el-form-item>
  446. <el-form-item label="关联题目" prop="projectIds">
  447. <el-select
  448. ref="customSelect"
  449. class="custom-select-class"
  450. v-model="batchEditDialog.form.projectIds"
  451. multiple
  452. placeholder="请选择关联题目"
  453. @click.native.stop="openProjectDialog(batchEditDialog.form.projectIds, 2)"
  454. style="width: 100%;">
  455. <el-option
  456. v-for="item in projectShowList"
  457. :key="item.id"
  458. :label="item.title"
  459. :value="item.id">
  460. </el-option>
  461. </el-select>
  462. </el-form-item>
  463. </el-form>
  464. <div slot="footer" class="dialog-footer">
  465. <el-button @click="batchEditCancel">取消</el-button>
  466. <el-button type="primary" @click="batchEditSubmitForm">保存</el-button>
  467. </div>
  468. </el-dialog>
  469. </el-dialog>
  470. <!-- 项目选择弹窗 -->
  471. <el-dialog
  472. title="选择题目"
  473. :visible.sync="projectDialogVisible"
  474. width="1000px"
  475. append-to-body
  476. @close="cancelSelectProject"
  477. :close-on-click-modal="false">
  478. <div class="project-container">
  479. <!-- 左侧分类树 -->
  480. <div class="category-tree">
  481. <div class="tree-fixed-header">
  482. <el-input
  483. placeholder="请输入分类名称"
  484. v-model="categoryFilterText"
  485. size="small"
  486. clearable
  487. prefix-icon="el-icon-search">
  488. </el-input>
  489. </div>
  490. <div class="tree-content">
  491. <el-tree
  492. ref="categoryTree"
  493. :data="categoryTreeData"
  494. node-key="cateId"
  495. highlight-current
  496. :filter-node-method="filterNode"
  497. :props="{ label: 'cateName', children: 'children' }"
  498. @node-click="handleCategoryClick"
  499. :expand-on-click-node="false"
  500. default-expand-all>
  501. <span class="custom-tree-node" slot-scope="{ node, data }">
  502. <span>{{ node.label }}</span>
  503. </span>
  504. </el-tree>
  505. </div>
  506. </div>
  507. <!-- 右侧题目列表 -->
  508. <div class="project-list">
  509. <div class="filter-container">
  510. <el-form :inline="true" :model="projectQueryParams" ref="projectForm">
  511. <el-form-item>
  512. <el-input prefix-icon="el-icon-search" @input="searchProjects" v-model="projectQueryParams.title" placeholder="请输入题目标题" clearable size="small" />
  513. </el-form-item>
  514. </el-form>
  515. </div>
  516. <el-table
  517. ref="projectTable"
  518. height="350"
  519. :data="projectList"
  520. row-key="id"
  521. v-loading="projectLoading"
  522. border
  523. @select="handleProjectSelect"
  524. @select-all="handleProjectSelect">
  525. <el-table-column type="selection" reserve-selection width="55" align="center" />
  526. <el-table-column label="序号" width="60" align="center">
  527. <template slot-scope="scope">
  528. {{ scope.$index + 1 }}
  529. </template>
  530. </el-table-column>
  531. <el-table-column label="题目标题" align="center" prop="title" min-width="200" show-overflow-tooltip />
  532. </el-table>
  533. <div class="table-footer">
  534. <el-pagination
  535. style="text-align: right"
  536. v-show="projectListTotal>0"
  537. :pager-count="5"
  538. background
  539. @size-change="handleProjectPageSizeChange"
  540. @current-change="handleProjectPageChange"
  541. :current-page="projectQueryParams.pageNum"
  542. :page-sizes="[10, 20, 30, 50]"
  543. :page-size="projectQueryParams.pageSize"
  544. layout="total, sizes, prev, pager, next, jumper"
  545. :total="projectListTotal">
  546. </el-pagination>
  547. </div>
  548. </div>
  549. </div>
  550. <div slot="footer" class="dialog-footer">
  551. <span style="float: left; color: #606266; font-size: 13px;">
  552. 共选择 <span style="color: #409EFF; font-weight: bold;">{{ selectedProjectIds.length }}</span> 个题目
  553. </span>
  554. <el-button @click="projectDialogVisible = false">取消</el-button>
  555. <el-button type="primary" @click="confirmSelectProject">确定</el-button>
  556. </div>
  557. </el-dialog>
  558. <!-- 项目列表弹窗 -->
  559. <el-dialog
  560. title="题目列表"
  561. :visible.sync="projectListDialogVisible"
  562. width="600px"
  563. append-to-body
  564. :close-on-click-modal="false">
  565. <div class="project-list-container" style="max-height: 500px; overflow-y: auto; padding: 10px;">
  566. <!-- 题目列表 -->
  567. <div v-for="(item, index) in projectShowList" :key="index" class="question-card">
  568. <!-- 题目标题 -->
  569. <div class="question-header">
  570. <span class="question-index">{{ index + 1 }}</span>
  571. <span class="question-title">{{ item.title }}</span>
  572. </div>
  573. <!-- 题目类型 -->
  574. <div class="question-type">
  575. <el-tag size="small" type="primary">{{ item.type === 1 ? '单选' : '多选' }}</el-tag>
  576. </div>
  577. <!-- 题目内容 -->
  578. <div class="question-content" v-if="item.question && item.question.length > 0">
  579. <div class="content-title">题目内容:</div>
  580. <div v-for="(q, qIndex) in JSON.parse(item.question)" :key="qIndex" class="question-item"
  581. :style=" q.isAnswer === 1 ? 'background-color: rgb(234 245 251)' : ''">
  582. <div class="question-item-header">
  583. <span class="item-index">{{ convertToLetter(qIndex) }}</span>
  584. <span class="item-name">{{ q.name }}</span>
  585. </div>
  586. </div>
  587. </div>
  588. <!-- 答案 -->
  589. <div class="question-answer" v-if="item.answer">
  590. <span class="answer-label">答案:</span>
  591. <span class="answer-content">{{ item.answer }}</span>
  592. </div>
  593. </div>
  594. </div>
  595. </el-dialog>
  596. </div>
  597. </template>
  598. <script>
  599. import {
  600. addVideoResource,
  601. deleteVideoResource,
  602. getVideoResource,
  603. listVideoResource,
  604. updateVideoResource,
  605. batchAddVideoResource
  606. } from '@/api/course/videoResource'
  607. import {listUserCourseCategory,getCatePidList,getCateListByPid} from '@/api/course/userCourseCategory'
  608. import {getByIds, listCourseQuestionBank} from '@/api/course/courseQuestionBank'
  609. import {getThumbnail} from "@/api/course/userVideo";
  610. import {uploadObject} from "@/utils/cos.js";
  611. import {uploadToOBS} from "@/utils/obs.js";
  612. export default {
  613. name: 'VideoResource',
  614. data() {
  615. return {
  616. // 遮罩层
  617. loading: true,
  618. // 选中数组
  619. ids: [],
  620. // 非单个禁用
  621. single: true,
  622. // 非多个禁用
  623. multiple: true,
  624. // 显示搜索条件
  625. showSearch: true,
  626. // 总条数
  627. total: 0,
  628. // 视频素材库表格数据
  629. resourceList: [],
  630. // 弹出层标题
  631. title: "",
  632. // 是否显示弹出层
  633. open: false,
  634. // 查询参数
  635. queryParams: {
  636. pageNum: 1,
  637. pageSize: 10,
  638. resourceName: null,
  639. fileName: null,
  640. typeId: null
  641. },
  642. // 表单参数
  643. form: {
  644. id: null,
  645. resourceName: null,
  646. fileName: null,
  647. thumbnail: null,
  648. line1: null,
  649. line2: null,
  650. line3: null,
  651. duration: null,
  652. fileSize: null,
  653. fileKey: null,
  654. videoUrl: null,
  655. typeId: null,
  656. typeSubId: null,
  657. projectIds: []
  658. },
  659. // 表单校验
  660. rules: {
  661. resourceName: [
  662. { required: true, message: "素材名称不能为空", trigger: "blur" }
  663. ],
  664. fileName: [
  665. { required: true, message: "文件名称不能为空", trigger: "blur" }
  666. ],
  667. typeId: [
  668. { required: true, message: "请选择分类", trigger: "change" }
  669. ],
  670. typeSubId: [
  671. { required: true, message: "请选择子分类", trigger: "change" }
  672. ],
  673. videoUrl: [
  674. { required: true, message: "请上传视频", trigger: "change" }
  675. ]
  676. },
  677. // 课程列表数据
  678. projectList: [],
  679. projectShowList: [],
  680. // 分类
  681. typeList: [],
  682. rootTypeList: [],
  683. subTypeList: [],
  684. // 视频上传
  685. videoDisabled: false,
  686. videoPreviewVisible: false,
  687. videoPreviewUrl: '',
  688. fileList: [],
  689. // 批量添加相关
  690. batchAddVisible: false,
  691. batchLoading: false,
  692. videoList: [],
  693. // 批量上传相关
  694. showUpload: false,
  695. batchUploadForm: {
  696. typeId: null,
  697. projectIds: [],
  698. files: []
  699. },
  700. batchFileList: [],
  701. // 弹窗选择项目相关
  702. projectDialogVisible: false,
  703. projectQueryParams: {
  704. pageNum: 1,
  705. pageSize: 10,
  706. questionType: null,
  707. title: null
  708. },
  709. projectLoading: false,
  710. projectListTotal: 0,
  711. selectedProjectIds: [],
  712. selectedType: 0,
  713. currentRow: null,
  714. // 分类树相关数据
  715. categoryFilterText: '',
  716. categoryTreeData: [],
  717. add: false,
  718. // 题目列表
  719. projectListDialogVisible: false,
  720. // 修改视频记录
  721. batchEditDialog: {
  722. title: '修改视频',
  723. open: false,
  724. form: {},
  725. rules: {
  726. resourceName: [
  727. { required: true, message: "素材名称不能为空", trigger: "blur" }
  728. ],
  729. fileName: [
  730. { required: true, message: "文件名称不能为空", trigger: "blur" }
  731. ]
  732. },
  733. },
  734. }
  735. },
  736. watch: {
  737. categoryFilterText(val) {
  738. this.$refs.categoryTree.filter(val);
  739. }
  740. },
  741. created() {
  742. this.getTypeList();
  743. this.getRootTypeList()
  744. this.getList();
  745. },
  746. methods: {
  747. /** 将数字索引转换为字母序号 (0->A, 1->B, 等) */
  748. convertToLetter(index) {
  749. // 确保索引是数字
  750. let numIndex = parseInt(index, 10);
  751. // 如果无法转换成数字,返回原始值
  752. if (isNaN(numIndex)) {
  753. return index;
  754. }
  755. // 处理负数情况
  756. if (numIndex < 0) {
  757. return index;
  758. }
  759. // 直接使用索引计算ASCII码(0对应A,1对应B,以此类推)
  760. return String.fromCharCode(65 + numIndex); // 65 是大写字母 A 的ASCII码
  761. },
  762. /** 查询视频素材库列表 */
  763. getList() {
  764. this.loading = true;
  765. listVideoResource(this.queryParams).then(response => {
  766. this.resourceList = response.rows;
  767. this.total = response.total;
  768. this.loading = false;
  769. });
  770. },
  771. // 取消按钮
  772. cancel() {
  773. this.$refs.videoUpload.clearFiles()
  774. this.open = false;
  775. this.reset();
  776. this.changeCateType(this.queryParams.typeId)
  777. },
  778. // 表单重置
  779. reset() {
  780. // 初始化表单对象
  781. this.form = {
  782. id: null,
  783. resourceName: null,
  784. fileName: null,
  785. thumbnail: null,
  786. line1: null,
  787. line2: null,
  788. line3: null,
  789. duration: null,
  790. fileSize: null,
  791. fileKey: null,
  792. videoUrl: null,
  793. typeId: null,
  794. projectIds: []
  795. };
  796. // 重置表单验证状态
  797. this.resetForm("form");
  798. this.add = false
  799. },
  800. /** 搜索按钮操作 */
  801. handleQuery() {
  802. this.queryParams.pageNum = 1;
  803. this.getList();
  804. },
  805. /** 重置按钮操作 */
  806. resetQuery() {
  807. this.resetForm("queryForm");
  808. this.handleQuery();
  809. },
  810. // 多选框选中数据
  811. handleSelectionChange(selection) {
  812. this.ids = selection.map(item => item.id)
  813. this.single = selection.length!==1
  814. this.multiple = !selection.length
  815. },
  816. /** 新增按钮操作 */
  817. handleAdd() {
  818. // 先清空文件列表
  819. this.fileList = [];
  820. this.projectShowList = [];
  821. // 先重置表单
  822. this.reset();
  823. this.subTypeList = []
  824. // 重置上传组件
  825. if (this.$refs.videoUpload) {
  826. this.$refs.videoUpload.clearFiles();
  827. }
  828. // 所有重置完成后再打开弹窗
  829. this.open = true;
  830. this.title = "添加视频素材库";
  831. },
  832. /** 修改按钮操作 */
  833. handleUpdate(row) {
  834. // 先清空文件列表
  835. this.fileList = [];
  836. this.projectShowList = [];
  837. // 先重置表单
  838. this.reset();
  839. this.subTypeList = []
  840. const id = row.id
  841. // 获取数据并设置表单
  842. getVideoResource(id).then(async response => {
  843. this.form = response.data;
  844. await this.changeCateType(this.form.typeId)
  845. // 处理projectIds,确保是数组格式
  846. if (this.form.projectIds && typeof this.form.projectIds === 'string') {
  847. this.form.projectIds = this.form.projectIds.split(',').map(id => parseInt(id));
  848. } else if (!this.form.projectIds) {
  849. this.form.projectIds = [];
  850. }
  851. // 如果存在关联项目,获取项目详情用于回显
  852. if (this.form.projectIds && this.form.projectIds.length > 0) {
  853. // 加载项目列表信息用于回显
  854. await getByIds({ids: this.form.projectIds.join(',')}).then(reponse => {
  855. this.projectShowList = reponse.data
  856. });
  857. }
  858. // 如果存在视频URL,设置fileList
  859. if (this.form.videoUrl) {
  860. this.fileList = [{
  861. name: this.form.fileName || '视频文件',
  862. url: this.form.videoUrl,
  863. thumbnail: this.form.thumbnail,
  864. status: 'success' // 设置为成功状态
  865. }];
  866. }
  867. // 所有设置完成后再打开弹窗
  868. this.open = true;
  869. this.title = "修改视频素材库";
  870. });
  871. },
  872. /** 提交按钮 */
  873. submitForm() {
  874. this.$refs["form"].validate(valid => {
  875. if (valid) {
  876. if (this.add){
  877. this.$message.warning("文件上传中,请稍后再试")
  878. return
  879. }
  880. const params = Object.assign({}, this.form);
  881. params.projectIds = this.form.projectIds.join(',');
  882. if (this.form.id != null) {
  883. updateVideoResource(params).then(response => {
  884. if (response.code === 200) {
  885. this.msgSuccess("修改成功");
  886. this.open = false;
  887. this.getList();
  888. }
  889. });
  890. } else {
  891. addVideoResource(params).then(response => {
  892. if (response.code === 200) {
  893. this.msgSuccess("新增成功");
  894. this.open = false;
  895. this.getList();
  896. }
  897. });
  898. }
  899. }
  900. });
  901. },
  902. /** 删除按钮操作 */
  903. handleDelete(row) {
  904. const ids = row.id || this.ids;
  905. this.$confirm('是否确认删除视频素材库编号为"' + ids + '"的数据项?', "警告", {
  906. confirmButtonText: "确定",
  907. cancelButtonText: "取消",
  908. type: "warning"
  909. }).then(function() {
  910. return deleteVideoResource(ids);
  911. }).then(() => {
  912. this.getList();
  913. this.msgSuccess("删除成功");
  914. }).catch(function() {});
  915. },
  916. /** 查询视频分类列表 */
  917. getTypeList() {
  918. listUserCourseCategory().then(response => {
  919. this.typeList = response.data;
  920. });
  921. },
  922. getRootTypeList() {
  923. getCatePidList().then(response => {
  924. this.rootTypeList = response.data
  925. });
  926. },
  927. async changeCateType(val) {
  928. if (!val) {
  929. this.subTypeList = []
  930. return
  931. }
  932. await getCateListByPid(val).then(response => {
  933. this.subTypeList = response.data
  934. })
  935. },
  936. /** 预览视频 */
  937. handleVideoPreview(url) {
  938. this.videoPreviewVisible = true;
  939. this.videoPreviewUrl = url || this.form.videoUrl;
  940. },
  941. /** 格式化视频时长 */
  942. formatDuration(seconds) {
  943. if (!seconds) return '00:00';
  944. const minutes = Math.floor(seconds / 60);
  945. const remainingSeconds = seconds % 60;
  946. return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
  947. },
  948. /** 移除视频 */
  949. handleRemove(file) {
  950. this.fileList.splice(this.fileList.indexOf(file), 1);
  951. this.form.videoUrl = '';
  952. this.form.thumbnail = '';
  953. this.form.duration = 0;
  954. this.form.fileSize = 0;
  955. this.form.fileName = '';
  956. this.form.line1 = '';
  957. this.form.line_2 = '';
  958. this.form.line_3 = '';
  959. this.uploadType = null
  960. this.fileSize = null
  961. this.fileKey = null
  962. },
  963. //获取第一帧封面
  964. async getFirstThumbnail(file, form){
  965. getThumbnail(file).then(response => {
  966. form.thumbnail = response.url;
  967. })
  968. },
  969. //上传腾讯云Pcdn
  970. async uploadVideoToTxPcdn(file, form, onProgress) {
  971. try {
  972. const data = await uploadObject(file, (progress) => {
  973. const progressEvent = {
  974. percent: Math.floor(progress.percent * 100 / 2), // COS SDK 百分比是 0-1,el-upload 需要 0-100
  975. loaded: progress.loaded,
  976. total: progress.total,
  977. lengthComputable: true // 文件上传通常总大小可知
  978. };
  979. onProgress(progressEvent);
  980. }, 1);
  981. let line_1 = `${process.env.VIDEO_LINE_1}${data.urlPath}`;
  982. form.fileKey = data.urlPath.substring(1);
  983. form.videoUrl = line_1;
  984. form.line1 = line_1;
  985. this.$message.success("线路一上传成功");
  986. } catch (error) {
  987. this.$message.error("线路一上传失败");
  988. }
  989. },
  990. //上传华为云Obs
  991. async uploadVideoToHwObs(file, form, onProgress) {
  992. try {
  993. const data = await uploadToOBS(file, (progress) => {
  994. const progressEvent = {
  995. percent: Math.floor(progress / 2) + 50,
  996. loaded: progress,
  997. total: progress,
  998. lengthComputable: true // 文件上传通常总大小可知
  999. };
  1000. onProgress(progressEvent);
  1001. }, 1);
  1002. form.line2 = `${process.env.VIDEO_LINE_1}/${data.urlPath}`;
  1003. this.$message.success("线路二上传成功");
  1004. } catch (error) {
  1005. this.$message.error("线路二上传失败");
  1006. }
  1007. },
  1008. // 上传视频
  1009. async videoUpload(options) {
  1010. this.add = true
  1011. const file = options.file;
  1012. this.getMediaDuration(file)
  1013. // 获取第一帧图片
  1014. await this.getFirstThumbnail(file, this.form);
  1015. // 上传腾讯云pcdn
  1016. await this.uploadVideoToTxPcdn(file, this.form, options.onProgress);
  1017. // 上传华为obs
  1018. await this.uploadVideoToHwObs(file, this.form, options.onProgress);
  1019. this.form.fileName = file.name;
  1020. this.form.fileSize = file.size;
  1021. this.add = false
  1022. },
  1023. // 获取媒体文件时长
  1024. getMediaDuration(file) {
  1025. // 处理视频
  1026. const video = document.createElement('video');
  1027. video.preload = 'metadata';
  1028. video.onloadedmetadata = () => {
  1029. this.form.duration = Math.round(video.duration);
  1030. };
  1031. video.src = URL.createObjectURL(file);
  1032. },
  1033. handleFileChange(file, files) {
  1034. // 保留最后一个文件
  1035. this.fileList = files.slice(-1);
  1036. },
  1037. /** 批量新增 */
  1038. handleBatchAdd() {
  1039. this.batchAddVisible = true;
  1040. this.videoList = []; // 清空之前的视频列表
  1041. },
  1042. cancelBeforeBatch(done, cancel) {
  1043. if (!this.videoList || this.videoList.length === 0) {
  1044. done()
  1045. return
  1046. }
  1047. this.$confirm('关闭窗口视频需要重新上传,确定关闭吗?', '提示', {
  1048. confirmButtonText: '确定',
  1049. cancelButtonText: '取消',
  1050. type: 'warning'
  1051. }).then(() => {
  1052. done()
  1053. }).catch(() => {
  1054. cancel()
  1055. });
  1056. },
  1057. /** 取消批量 */
  1058. cancelBatch() {
  1059. if (!this.videoList || this.videoList.length === 0) {
  1060. this.batchAddVisible = false
  1061. return
  1062. }
  1063. this.$confirm('关闭窗口视频需要重新上传,确定关闭吗?', '提示', {
  1064. confirmButtonText: '确定',
  1065. cancelButtonText: '取消',
  1066. type: 'warning'
  1067. }).then(() => {
  1068. this.batchAddVisible = false
  1069. this.changeCateType(this.queryParams.typeId)
  1070. }).catch(() => {
  1071. });
  1072. },
  1073. /** 提交批量添加 */
  1074. submitBatchAdd() {
  1075. // 检查是否所有选中的视频都已上传完成
  1076. const incompleteVideos = this.videoList.filter(item => (item.progress || 0) < 100);
  1077. if (incompleteVideos.length > 0) {
  1078. this.$message.warning('有未完成上传的视频,请先完成上传');
  1079. return;
  1080. }
  1081. // 调用批量添加API
  1082. const videoList = JSON.parse(JSON.stringify(this.videoList));
  1083. videoList.forEach(item => {
  1084. item.projectIds = item.projectIds.join(",");
  1085. });
  1086. batchAddVideoResource(videoList).then(response => {
  1087. if (response.code === 200) {
  1088. this.$message.success('批量添加成功');
  1089. this.batchAddVisible = false;
  1090. this.getList();
  1091. }
  1092. });
  1093. },
  1094. /** 获取分类名称 */
  1095. getTypeName(typeId) {
  1096. const type = this.typeList.find(item => item.cateId === typeId);
  1097. return type ? type.cateName : '';
  1098. },
  1099. /** 编辑视频信息 */
  1100. handleEditVideo(row) {
  1101. this.batchEditDialog.form = Object.assign({}, row)
  1102. this.changeCateType(row.typeId)
  1103. this.batchEditDialog.open = true
  1104. },
  1105. batchEditCancel() {
  1106. this.resetForm('batchEditDialogForm')
  1107. this.batchEditDialog.open = false
  1108. },
  1109. batchEditSubmitForm() {
  1110. let temp = this.videoList.find(item => item.tempId === this.batchEditDialog.form.tempId)
  1111. Object.assign(temp, this.batchEditDialog.form)
  1112. this.batchEditDialog.open = false
  1113. },
  1114. /** 删除视频 */
  1115. handleDeleteVideo(row) {
  1116. this.$confirm('确认要从列表中删除该视频吗?', '提示', {
  1117. confirmButtonText: '确定',
  1118. cancelButtonText: '取消',
  1119. type: 'warning'
  1120. }).then(() => {
  1121. // 从视频列表中删除该项
  1122. const index = this.videoList.findIndex(item => {
  1123. // 通过文件名和大小确定唯一性,因为临时视频可能没有id
  1124. return item.tempId === row.tempId
  1125. });
  1126. if (index !== -1) {
  1127. this.videoList.splice(index, 1);
  1128. this.$message({
  1129. type: 'success',
  1130. message: '已从列表中移除该视频'
  1131. });
  1132. }
  1133. }).catch(() => {
  1134. // 取消删除
  1135. });
  1136. },
  1137. /** 显示上传面板 */
  1138. showUploadPanel() {
  1139. this.showUpload = true;
  1140. this.batchUploadForm = {
  1141. typeId: null,
  1142. typeSubId: null,
  1143. projectIds: [],
  1144. files: []
  1145. };
  1146. this.batchFileList = [];
  1147. this.subTypeList = []
  1148. if (this.$refs.batchVideoUpload) {
  1149. this.$refs.batchVideoUpload.clearFiles();
  1150. }
  1151. },
  1152. /** 批量文件变更 */
  1153. handleBatchFileChange(file, fileList) {
  1154. this.batchFileList = fileList;
  1155. },
  1156. /** 批量上传视频 */
  1157. async batchVideoUpload(options) {
  1158. const file = options.file;
  1159. // 创建临时视频对象添加到列表中
  1160. const tempVideo = {
  1161. tempId: Math.random().toString(36).substring(2, 15),
  1162. resourceName: file.name.split('.')[0], // 使用文件名作为视频名称
  1163. fileName: file.name,
  1164. thumbnail: null,
  1165. line1: null,
  1166. line2: null,
  1167. line3: null,
  1168. duration: 0, // 先默认为0,后续获取真实时长
  1169. fileSize: file.size,
  1170. fileKey: null,
  1171. videoUrl: null,
  1172. typeId: this.batchUploadForm.typeId, // 使用选择的分类
  1173. typeSubId: this.batchUploadForm.typeSubId, // 使用选择的子分类
  1174. projectIds: this.batchUploadForm.projectIds, // 使用选择的项目
  1175. progress: 0, // 上传进度
  1176. file: file // 保存文件引用
  1177. };
  1178. // 添加到视频列表
  1179. this.videoList.unshift(tempVideo);
  1180. // 获取视频时长
  1181. const video = document.createElement('video');
  1182. video.preload = 'metadata';
  1183. video.onloadedmetadata = () => {
  1184. const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
  1185. if (index !== -1) {
  1186. tempVideo.duration = Math.round(video.duration);
  1187. }
  1188. };
  1189. video.src = URL.createObjectURL(file);
  1190. // 关闭上传弹窗,返回批量选择视频弹窗
  1191. this.showUpload = false;
  1192. try {
  1193. // 获取封面
  1194. await this.getFirstThumbnail(file, tempVideo);
  1195. // 上传到第一个服务器
  1196. await this.uploadVideoToTxPcdn(file, tempVideo, (event) => {
  1197. tempVideo.progress = event.percent
  1198. });
  1199. // 上传到第二个服务器
  1200. await this.uploadVideoToHwObs(file, tempVideo, (event) => {
  1201. tempVideo.progress = event.percent
  1202. });
  1203. } catch (error) {
  1204. this.$message.error(`文件 ${file.name} 上传失败: ${error.message || '未知错误'}`);
  1205. }
  1206. if (this.$refs.batchVideoUpload) {
  1207. this.$refs.batchVideoUpload.clearFiles();
  1208. }
  1209. },
  1210. /** 复制视频链接到剪贴板 */
  1211. copy(url) {
  1212. // 创建一个临时的input元素
  1213. const input = document.createElement('input');
  1214. // 设置input的值为要复制的视频链接
  1215. input.value = url;
  1216. // 将input添加到DOM中
  1217. document.body.appendChild(input);
  1218. // 选中input的值
  1219. input.select();
  1220. // 执行复制命令
  1221. document.execCommand('copy');
  1222. // 从DOM中移除input元素
  1223. document.body.removeChild(input);
  1224. // 提示用户复制成功
  1225. this.$message({
  1226. message: '已复制到剪贴板',
  1227. type: 'success',
  1228. duration: 1500
  1229. });
  1230. },
  1231. getProjectList() {
  1232. this.projectLoading = true
  1233. listCourseQuestionBank(this.projectQueryParams).then(response => {
  1234. this.projectList = response.rows;
  1235. this.projectListTotal = response.total;
  1236. // 如果存在已选择的项目,预选中表格中对应的行
  1237. this.$nextTick(() => {
  1238. // 获取表格组件实例
  1239. const projectTable = this.$refs.projectTable;
  1240. if (projectTable) {
  1241. if (this.selectedProjectIds.length > 0) {
  1242. // 遍历项目列表,找到匹配的ID并选中对应行
  1243. const selectedIds = this.selectedProjectIds;
  1244. this.projectList.forEach(row => {
  1245. if (selectedIds.includes(row.id)) {
  1246. projectTable.toggleRowSelection(row, true);
  1247. }
  1248. });
  1249. }
  1250. }
  1251. });
  1252. this.projectLoading = false
  1253. });
  1254. },
  1255. /** 打开项目选择弹窗 */
  1256. openProjectDialog(projectIds, type) {
  1257. this.$nextTick(() => {
  1258. if (this.$refs.customSelect) {
  1259. this.$refs.customSelect.blur();
  1260. }
  1261. });
  1262. // 重置查询参数
  1263. this.projectQueryParams = {
  1264. pageNum: 1,
  1265. pageSize: 10,
  1266. questionType: null,
  1267. title: null
  1268. };
  1269. this.selectedType = type
  1270. // 设置选中的项目IDs
  1271. if (projectIds) {
  1272. if (typeof projectIds === 'string') {
  1273. this.selectedProjectIds = projectIds.split(',').map(id => parseInt(id));
  1274. } else if (Array.isArray(projectIds)) {
  1275. this.selectedProjectIds = [...projectIds];
  1276. } else if (typeof projectIds === 'number') {
  1277. this.selectedProjectIds = [projectIds];
  1278. }
  1279. } else {
  1280. this.selectedProjectIds = [];
  1281. }
  1282. // 显示弹窗
  1283. this.projectDialogVisible = true;
  1284. // 加载分类树数据
  1285. this.initCategoryTree();
  1286. // 加载项目列表
  1287. this.getProjectList();
  1288. },
  1289. /** 确认选择项目 */
  1290. confirmSelectProject() {
  1291. // 更新表单中的项目ID
  1292. if (this.selectedType === 0) {
  1293. this.form.projectIds = this.selectedProjectIds;
  1294. }
  1295. else if (this.selectedType === 1) {
  1296. this.batchUploadForm.projectIds = this.selectedProjectIds;
  1297. }
  1298. else if (this.selectedType === 2) {
  1299. this.batchEditDialog.form.projectIds = this.selectedProjectIds;
  1300. }
  1301. else if (this.selectedType === 3) {
  1302. const params = {
  1303. id: this.currentRow.id,
  1304. projectIds: this.selectedProjectIds.join(",")
  1305. }
  1306. updateVideoResource(params).then(response => {
  1307. if (response.code === 200) {
  1308. this.msgSuccess("修改成功");
  1309. this.open = false;
  1310. this.getList();
  1311. }
  1312. });
  1313. }
  1314. else if (this.selectedType === 4) {
  1315. this.currentRow.projectIds = this.selectedProjectIds
  1316. }
  1317. this.projectDialogVisible = false;
  1318. },
  1319. /** 取消选择项目 */
  1320. cancelSelectProject() {
  1321. const projectTable = this.$refs.projectTable;
  1322. if (projectTable) {
  1323. // 清空表格数据
  1324. projectTable.clearSelection();
  1325. }
  1326. this.projectDialogVisible = false;
  1327. },
  1328. handleProjectSelect(selection) {
  1329. this.selectedProjectIds = selection.map(item => item.id);
  1330. selection.forEach(item => {
  1331. // 检查是否已存在该项目,不存在则添加
  1332. if (!this.projectShowList.some(p => p.id === item.id)) {
  1333. this.projectShowList.push(item);
  1334. }
  1335. });
  1336. },
  1337. /** 项目列表页码变更 */
  1338. handleProjectPageChange(page) {
  1339. this.projectQueryParams.pageNum = page;
  1340. this.getProjectList();
  1341. },
  1342. /** 项目列表每页条数变更 */
  1343. handleProjectPageSizeChange(size) {
  1344. this.projectQueryParams.pageNum = 1;
  1345. this.projectQueryParams.pageSize = size;
  1346. this.getProjectList();
  1347. },
  1348. /** 处理查看项目 */
  1349. handleViewProject(row, type) {
  1350. // 保存当前选择的行,以便后续操作
  1351. this.currentRow = row;
  1352. // 设置form对象的projectIds为当前行的项目IDs
  1353. this.projectShowList = [];
  1354. if (!row.projectIds || row.projectIds.length === 0) {
  1355. this.openProjectDialog(null, type)
  1356. return;
  1357. }
  1358. let projectIds = row.projectIds
  1359. if (Array.isArray(row.projectIds)) {
  1360. projectIds = projectIds.join(',');
  1361. }
  1362. getByIds({ids: projectIds}).then(response => {
  1363. this.projectShowList = response.data;
  1364. })
  1365. // 打开弹窗展示列表
  1366. this.projectListDialogVisible = true;
  1367. },
  1368. /** 初始化分类树 */
  1369. initCategoryTree() {
  1370. // 获取分类列表
  1371. listUserCourseCategory().then(response => {
  1372. const treeDate = this.handleTree(response.data, "cateId", "pid");
  1373. this.categoryTreeData = [{
  1374. cateId: 0,
  1375. cateName: '全部',
  1376. children: treeDate
  1377. }];
  1378. });
  1379. },
  1380. filterNode(value, data) {
  1381. if (!value) return true;
  1382. return data.cateName.indexOf(value) !== -1;
  1383. },
  1384. /** 处理分类点击 */
  1385. handleCategoryClick(data) {
  1386. // 更新查询参数
  1387. this.projectQueryParams.pageNum = 1;
  1388. // 如果是全部分类,则清空分类过滤
  1389. if (data.cateId === 0) {
  1390. this.projectQueryParams.questionType = null;
  1391. } else {
  1392. this.projectQueryParams.questionType = data.cateId;
  1393. }
  1394. // 重新加载项目列表
  1395. this.getProjectList();
  1396. },
  1397. /** 搜索项目 */
  1398. searchProjects() {
  1399. this.projectQueryParams.pageNum = 1;
  1400. this.getProjectList();
  1401. },
  1402. /** 视频预览弹窗关闭前的处理函数 */
  1403. handleCloseVideoPreview(done) {
  1404. // 停止视频播放
  1405. const video = document.getElementById('video');
  1406. if (video) {
  1407. video.pause();
  1408. video.currentTime = 0;
  1409. }
  1410. // 关闭弹窗
  1411. this.videoPreviewVisible = false;
  1412. done();
  1413. },
  1414. }
  1415. }
  1416. </script>
  1417. <style scoped>
  1418. /* 自定义表格样式 */
  1419. .el-table .video-cell {
  1420. padding: 5px;
  1421. }
  1422. /* 设置删除和修改按钮的间距 */
  1423. .el-button + .el-button {
  1424. margin-left: 5px;
  1425. }
  1426. ::v-deep .upload-icon {
  1427. font-size: 28px;
  1428. color: #8c939d;
  1429. }
  1430. ::v-deep .upload-text {
  1431. font-size: 12px;
  1432. color: #606266;
  1433. margin-top: 10px;
  1434. }
  1435. /* 表单样式 */
  1436. ::v-deep .el-form-item {
  1437. margin-bottom: 22px;
  1438. }
  1439. /* 批量选择弹窗样式 */
  1440. ::v-deep .batch-dialog .el-dialog__body {
  1441. padding: 0 20px 20px;
  1442. max-height: 70vh;
  1443. overflow-y: auto;
  1444. }
  1445. ::v-deep .el-dialog .el-dialog__body {
  1446. padding: 0 20px 20px;
  1447. max-height: 70vh;
  1448. overflow-y: auto;
  1449. }
  1450. .filter-container {
  1451. padding: 15px 0;
  1452. background-color: #fff;
  1453. border-bottom: 1px solid #ebeef5;
  1454. position: relative;
  1455. text-align: right;
  1456. }
  1457. .dialog-footer {
  1458. text-align: right;
  1459. margin-top: 20px;
  1460. padding-right: 20px;
  1461. }
  1462. ::v-deep .batch-dialog .el-table {
  1463. margin-top: 10px;
  1464. }
  1465. /* 按钮样式 */
  1466. ::v-deep .el-button--primary {
  1467. background-color: #1890ff;
  1468. border-color: #1890ff;
  1469. }
  1470. ::v-deep .el-button--primary:hover {
  1471. background-color: #40a9ff;
  1472. border-color: #40a9ff;
  1473. }
  1474. ::v-deep .el-button--primary:focus {
  1475. background-color: #40a9ff;
  1476. border-color: #40a9ff;
  1477. }
  1478. /* 表格头部样式 */
  1479. ::v-deep .batch-dialog .el-table__header-wrapper th {
  1480. background-color: #f5f7fa;
  1481. color: #606266;
  1482. font-weight: 500;
  1483. padding: 8px 0;
  1484. }
  1485. ::v-deep .el-table__header-wrapper th {
  1486. background-color: #f5f7fa;
  1487. color: #606266;
  1488. font-weight: 500;
  1489. padding: 8px 0;
  1490. }
  1491. /* 确保弹窗中表格内容垂直居中 */
  1492. ::v-deep .batch-dialog .el-table .cell {
  1493. line-height: 23px;
  1494. }
  1495. ::v-deep .el-table .cell {
  1496. line-height: 23px;
  1497. }
  1498. /* 自定义滚动条样式 */
  1499. ::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar {
  1500. width: 6px;
  1501. height: 6px;
  1502. }
  1503. ::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar {
  1504. width: 6px;
  1505. height: 6px;
  1506. }
  1507. ::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar-thumb {
  1508. background: #c0c4cc;
  1509. border-radius: 3px;
  1510. }
  1511. ::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar-thumb {
  1512. background: #c0c4cc;
  1513. border-radius: 3px;
  1514. }
  1515. ::v-deep .batch-dialog .el-dialog__body::-webkit-scrollbar-track {
  1516. background: #f5f7fa;
  1517. }
  1518. ::v-deep .el-dialog .el-dialog__body::-webkit-scrollbar-track {
  1519. background: #f5f7fa;
  1520. }
  1521. /* 批量上传视频弹窗样式 */
  1522. .upload-dialog .el-dialog__body {
  1523. padding: 20px;
  1524. }
  1525. ::v-deep .upload-dialog .el-dialog__header {
  1526. padding: 15px 20px;
  1527. background-color: #f9f9f9;
  1528. border-bottom: 1px solid #ebeef5;
  1529. }
  1530. ::v-deep .upload-dialog .el-dialog__title {
  1531. font-size: 16px;
  1532. font-weight: 500;
  1533. color: #303133;
  1534. }
  1535. /* 项目选择弹窗样式 */
  1536. ::v-deep .el-dialog__wrapper {
  1537. z-index: 2001 !important;
  1538. }
  1539. ::v-deep .el-dialog__header {
  1540. padding: 15px 20px;
  1541. background-color: #f9f9f9;
  1542. border-bottom: 1px solid #ebeef5;
  1543. }
  1544. ::v-deep .el-dialog__title {
  1545. font-size: 16px;
  1546. font-weight: 500;
  1547. color: #303133;
  1548. }
  1549. ::v-deep .el-pagination {
  1550. text-align: center;
  1551. margin-top: 10px;
  1552. }
  1553. /* 项目选择弹窗的样式 */
  1554. .project-container {
  1555. display: flex;
  1556. height: 500px;
  1557. }
  1558. .category-tree {
  1559. width: 250px;
  1560. height: 100%;
  1561. border-right: 1px solid #ebeef5;
  1562. padding: 0;
  1563. display: flex;
  1564. flex-direction: column;
  1565. overflow: hidden;
  1566. flex-shrink: 0;
  1567. }
  1568. .tree-fixed-header {
  1569. padding: 10px;
  1570. background-color: #fff;
  1571. border-bottom: 1px solid #ebeef5;
  1572. z-index: 10;
  1573. box-shadow: 0 1px 4px rgba(0,0,0,0.05);
  1574. }
  1575. .tree-content {
  1576. flex: 1;
  1577. overflow-y: auto;
  1578. padding: 10px 10px 10px 10px;
  1579. }
  1580. .project-list {
  1581. flex: 1;
  1582. padding: 10px;
  1583. height: 100%;
  1584. display: flex;
  1585. flex-direction: column;
  1586. width: calc(100% - 250px); /* 固定宽度为剩余空间 */
  1587. }
  1588. .project-list .filter-container {
  1589. padding: 0 0 15px 0;
  1590. text-align: left;
  1591. }
  1592. .project-list .table-footer {
  1593. margin-top: 15px;
  1594. }
  1595. ::v-deep .el-tree-node__content {
  1596. height: 36px;
  1597. border-radius: 4px;
  1598. margin-bottom: 2px;
  1599. }
  1600. ::v-deep .el-tree-node:focus > .el-tree-node__content {
  1601. background-color: #e6f7ff;
  1602. }
  1603. ::v-deep .el-tree-node__content:hover {
  1604. background-color: #f0f7ff;
  1605. }
  1606. ::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
  1607. background-color: #e6f7ff;
  1608. color: #1890ff;
  1609. }
  1610. ::v-deep .custom-tree-node {
  1611. flex: 1;
  1612. display: flex;
  1613. align-items: center;
  1614. justify-content: space-between;
  1615. font-size: 14px;
  1616. padding: 0 8px;
  1617. }
  1618. /* 视频预览弹窗样式 */
  1619. ::v-deep .video-preview-dialog {
  1620. z-index: 3000 !important;
  1621. }
  1622. ::v-deep .video-preview-dialog .el-dialog {
  1623. margin-top: 10vh !important;
  1624. }
  1625. ::v-deep .video-preview-dialog .el-dialog__header {
  1626. padding: 15px 20px;
  1627. background-color: #f9f9f9;
  1628. border-bottom: 1px solid #ebeef5;
  1629. }
  1630. ::v-deep .video-preview-dialog .el-dialog__body {
  1631. padding: 15px;
  1632. background-color: #000;
  1633. display: flex;
  1634. justify-content: center;
  1635. align-items: center;
  1636. }
  1637. ::v-deep .video-preview-dialog video {
  1638. max-width: 100%;
  1639. max-height: 70vh;
  1640. }
  1641. /* 题目列表样式 */
  1642. .project-list-container {
  1643. background-color: #f5f7fa;
  1644. border-radius: 4px;
  1645. }
  1646. .question-card {
  1647. background-color: #ffffff;
  1648. border-radius: 4px;
  1649. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
  1650. padding: 15px;
  1651. margin-bottom: 15px;
  1652. position: relative;
  1653. }
  1654. .question-header {
  1655. display: flex;
  1656. align-items: flex-start;
  1657. margin-bottom: 10px;
  1658. border-bottom: 1px solid #ebeef5;
  1659. padding-bottom: 10px;
  1660. }
  1661. .question-index {
  1662. display: flex;
  1663. justify-content: center;
  1664. align-items: center;
  1665. width: 24px;
  1666. height: 24px;
  1667. background-color: #409EFF;
  1668. color: white;
  1669. border-radius: 50%;
  1670. font-size: 14px;
  1671. margin-right: 10px;
  1672. flex-shrink: 0;
  1673. }
  1674. .question-title {
  1675. font-size: 16px;
  1676. font-weight: 500;
  1677. color: #303133;
  1678. line-height: 1.5;
  1679. word-break: break-all;
  1680. }
  1681. .question-type {
  1682. margin-bottom: 15px;
  1683. }
  1684. .content-title {
  1685. font-weight: 500;
  1686. color: #606266;
  1687. margin-bottom: 10px;
  1688. }
  1689. .question-content {
  1690. margin-bottom: 15px;
  1691. }
  1692. .question-item {
  1693. background-color: #f8f8f8;
  1694. border-left: 3px solid #409EFF;
  1695. padding: 10px;
  1696. margin-bottom: 10px;
  1697. border-radius: 0 4px 4px 0;
  1698. }
  1699. .question-item-header {
  1700. display: flex;
  1701. align-items: center;
  1702. margin-bottom: 5px;
  1703. }
  1704. .item-index {
  1705. display: inline-block;
  1706. margin-right: 10px;
  1707. font-weight: 500;
  1708. color: #409EFF;
  1709. }
  1710. .item-name {
  1711. color: #303133;
  1712. }
  1713. .question-answer {
  1714. background-color: #f0f9eb;
  1715. padding: 10px;
  1716. border-radius: 4px;
  1717. border-left: 3px solid #67c23a;
  1718. }
  1719. .answer-label {
  1720. font-weight: 500;
  1721. color: #67c23a;
  1722. margin-right: 10px;
  1723. }
  1724. .answer-content {
  1725. color: #606266;
  1726. }
  1727. ::v-deep .custom-select-class .el-select__tags {
  1728. display: flex;
  1729. flex-direction: column;
  1730. align-items: flex-start;
  1731. flex-wrap: nowrap;
  1732. max-height: 200px;
  1733. overflow-y: auto;
  1734. }
  1735. ::v-deep .custom-select-class .el-tag {
  1736. margin-bottom: 4px;
  1737. margin-right: 0;
  1738. }
  1739. </style>