You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1323 lines
38 KiB

  1. <template>
  2. <div class="app-container">
  3. <!-- <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">-->
  4. <!-- <el-form-item>-->
  5. <!-- <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>-->
  6. <!-- <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>-->
  7. <!-- </el-form-item>-->
  8. <!-- </el-form>-->
  9. <el-row :gutter="10" class="mb8">
  10. <el-col :span="1.5">
  11. <el-button
  12. type="primary"
  13. plain
  14. icon="el-icon-plus"
  15. size="mini"
  16. @click="handleAdd"
  17. v-hasPermi="['vet:training:add']"
  18. >新增</el-button>
  19. </el-col>
  20. <el-col :span="1.5">
  21. <el-button
  22. type="info"
  23. plain
  24. icon="el-icon-s-promotion"
  25. size="mini"
  26. :disabled="single"
  27. @click="handleSubmitAudit"
  28. v-hasPermi="['vet:training:submit']"
  29. >提交审核</el-button>
  30. </el-col>
  31. <el-col :span="1.5">
  32. <el-button
  33. type="primary"
  34. plain
  35. icon="el-icon-finished"
  36. size="mini"
  37. :disabled="single"
  38. @click="handleAuditDialog"
  39. v-hasPermi="['vet:training:audit']"
  40. >审核</el-button>
  41. </el-col>
  42. <el-col :span="1.5">
  43. <el-button
  44. type="success"
  45. plain
  46. icon="el-icon-check"
  47. size="mini"
  48. :disabled="single"
  49. @click="handlePublish"
  50. v-hasPermi="['vet:training:publish']"
  51. >上架视频</el-button>
  52. </el-col>
  53. <el-col :span="1.5">
  54. <el-button
  55. type="danger"
  56. plain
  57. icon="el-icon-delete"
  58. size="mini"
  59. :disabled="multiple"
  60. @click="handleDelete"
  61. v-hasPermi="['vet:training:remove']"
  62. >删除</el-button>
  63. </el-col>
  64. <el-col :span="1.5">
  65. <el-button
  66. type="warning"
  67. plain
  68. icon="el-icon-download"
  69. size="mini"
  70. :disabled="single"
  71. @click="handleOffline"
  72. v-hasPermi="['vet:training:offline']"
  73. >下架视频</el-button>
  74. </el-col>
  75. <el-col :span="1.5">
  76. <el-button
  77. type="warning"
  78. plain
  79. icon="el-icon-download"
  80. size="mini"
  81. @click="handleExport"
  82. v-hasPermi="['vet:training:export']"
  83. >导出</el-button>
  84. </el-col>
  85. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
  86. </el-row>
  87. <el-table v-loading="loading" :data="trainingList" @selection-change="handleSelectionChange">
  88. <el-table-column type="selection" width="55" align="center" />
  89. <el-table-column label="发布者" align="center" prop="publisherName" width="120">
  90. <template slot-scope="scope">
  91. <span>{{ scope.row.publisherName || '-' }}</span>
  92. </template>
  93. </el-table-column>
  94. <el-table-column label="视频标题" align="center" prop="title" show-overflow-tooltip/>
  95. <el-table-column label="视频描述" align="center" prop="description" show-overflow-tooltip/>
  96. <el-table-column label="视频地址" align="center" prop="videoUrl" width="180" />
  97. <el-table-column label="封面图片" align="center" prop="coverImage" width="100">
  98. <template slot-scope="scope">
  99. <image-preview :src="scope.row.coverImage" :width="50" :height="50"/>
  100. </template>
  101. </el-table-column>
  102. <el-table-column label="分类" align="center" prop="category">
  103. <template slot-scope="scope">
  104. <dict-tag :options="dict.type.video_category" :value="scope.row.category" />
  105. </template>
  106. </el-table-column>
  107. <el-table-column label="观看次数" align="center" prop="viewCount" />
  108. <el-table-column label="发布时间" align="center" prop="publishTime" min-width="200" show-overflow-tooltip />
  109. <!-- 状态列 -->
  110. <el-table-column label="上架状态" align="center" prop="status" width="100">
  111. <template slot-scope="scope">
  112. <el-tag :type="getStatusType(scope.row.status)" size="small">
  113. {{ getStatusText(scope.row.status) }}
  114. </el-tag>
  115. </template>
  116. </el-table-column>
  117. <!-- 审核状态列 -->
  118. <el-table-column label="审核状态" align="center" prop="auditStatus" width="100">
  119. <template slot-scope="scope">
  120. <el-tag :type="getAuditStatusType(scope.row.auditStatus)" size="small">
  121. {{ getAuditStatusText(scope.row.auditStatus) }}
  122. </el-tag>
  123. </template>
  124. </el-table-column>
  125. <el-table-column label="审核意见" align="center" prop="auditOpinion" width="150" show-overflow-tooltip>
  126. <template slot-scope="scope">
  127. <span v-if="scope.row.auditOpinion">{{ truncateText(scope.row.auditOpinion, 15) }}</span>
  128. <span v-else>-</span>
  129. </template>
  130. </el-table-column>
  131. <!-- 操作列 -->
  132. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" fixed="right" width="300">
  133. <template slot-scope="scope">
  134. <!-- 修改按钮 -->
  135. <el-button
  136. size="mini"
  137. type="text"
  138. icon="el-icon-edit"
  139. style="color: #42B983"
  140. @click="handleUpdate(scope.row)"
  141. v-hasPermi="['vet:training:edit']"
  142. v-if="scope.row.status === '0' && (scope.row.auditStatus === '3' || scope.row.auditStatus === '0')"
  143. class="info-btn alter-btn"
  144. >修改</el-button>
  145. <!-- 提交审核按钮审核拒绝或无需审核状态 -->
  146. <el-button
  147. size="mini"
  148. type="text"
  149. icon="el-icon-s-promotion"
  150. style="color: #dab708"
  151. @click="handleSubmitAudit(scope.row.id)"
  152. v-hasPermi="['vet:training:submit']"
  153. v-if="scope.row.status === '0' && scope.row.auditStatus === '0'"
  154. class="info-btn submit-btn"
  155. >提交审核</el-button>
  156. <!-- 审核按钮待审核状态管理员 -->
  157. <el-button
  158. size="mini"
  159. type="text"
  160. icon="el-icon-finished"
  161. @click="handleAuditDialog(scope.row.id)"
  162. style="color: #072eed"
  163. v-hasPermi="['vet:training:audit']"
  164. v-if="scope.row.auditStatus === '0' && isAdmin"
  165. class="info-btn audit-btn"
  166. >审核</el-button>
  167. <!-- 上架按钮审核通过且未上架 -->
  168. <el-button
  169. size="mini"
  170. type="text"
  171. icon="el-icon-top"
  172. @click="handlePublish(scope.row.id)"
  173. style="color: #f46a0c"
  174. v-hasPermi="['vet:training:publish']"
  175. v-if="scope.row.auditStatus === '2' && scope.row.status === '0'"
  176. class="info-btn publish-btn"
  177. >上架</el-button>
  178. <!-- 下架按钮已上架状态 -->
  179. <el-button
  180. size="mini"
  181. type="text"
  182. icon="el-icon-bottom"
  183. style="color: #636361"
  184. @click="handleOffline(scope.row.id)"
  185. v-hasPermi="['vet:training:offline']"
  186. v-if="scope.row.status === '1'"
  187. class="info-btn offline-btn"
  188. >下架</el-button>
  189. <!-- 取消审核按钮 -->
  190. <!-- <el-button-->
  191. <!-- size="mini"-->
  192. <!-- type="text"-->
  193. <!-- icon="el-icon-close"-->
  194. <!-- style="color: #5607b3"-->
  195. <!-- @click="handleCancelAudit(scope.row.id)"-->
  196. <!-- v-hasPermi="['vet:training:edit']"-->
  197. <!-- v-if="scope.row.auditStatus === '1' && !isAdmin"-->
  198. <!-- class="info-btn cancel-btn"-->
  199. <!-- >取消审核</el-button>-->
  200. <el-button
  201. size="mini"
  202. type="text"
  203. icon="el-icon-close"
  204. style="color: #5607b3"
  205. @click="handleCancelAudit(scope.row.id)"
  206. v-hasPermi="['vet:training:edit']"
  207. v-if="scope.row.auditStatus === '1'"
  208. class="info-btn cancel-btn"
  209. >取消审核</el-button>
  210. <el-button
  211. size="mini"
  212. type="text"
  213. icon="el-icon-s-promotion"
  214. style="color: #c108af"
  215. @click="handleResubmitAudit(scope.row)"
  216. class="info-btn resubmit-btn"
  217. v-hasPermi="['vet:training:submit']"
  218. v-if="scope.row.status === '0' && scope.row.auditStatus === '3'"
  219. >
  220. 重新提交
  221. </el-button>
  222. <!-- 删除按钮 -->
  223. <el-button
  224. size="mini"
  225. type="text"
  226. icon="el-icon-delete"
  227. style="color: #f56c6c"
  228. @click="handleDelete(scope.row)"
  229. v-hasPermi="['vet:training:remove']"
  230. class="info-btn delete-btn"
  231. >删除</el-button>
  232. </template>
  233. </el-table-column>
  234. </el-table>
  235. <div class="pagestyle">
  236. <pagination
  237. v-show="total>0"
  238. :total="total"
  239. :page.sync="queryParams.pageNum"
  240. :limit.sync="queryParams.pageSize"
  241. @pagination="getList"
  242. />
  243. </div>
  244. <!-- 添加或修改兽医培训视频对话框 -->
  245. <el-dialog :title="title" :visible.sync="open" width="80%" append-to-body>
  246. <el-form ref="form" :model="form" :rules="rules" label-width="80px">
  247. <el-form-item label="视频标题" prop="title">
  248. <el-input v-model="form.title" placeholder="请输入视频标题" />
  249. </el-form-item>
  250. <el-form-item label="视频描述" prop="description">
  251. <el-input v-model="form.description" type="textarea" placeholder="请输入内容" />
  252. </el-form-item>
  253. <el-form-item label="发布时间" prop="publishTime">
  254. <el-date-picker
  255. v-model="form.publishTime"
  256. type="date"
  257. value-format="yyyy-MM-dd"
  258. placeholder="请选择时间"
  259. style="width: 100%;"
  260. />
  261. </el-form-item>
  262. <el-form-item label="视频" required>
  263. <div style="display: flex; flex-direction: column; gap: 12px;">
  264. <!-- 简单的文件选择方式 -->
  265. <div v-if="!form.videoUrl">
  266. <input
  267. type="file"
  268. ref="videoInput"
  269. accept=".mp4,.avi,.mov,.wmv,.flv,.mkv"
  270. @change="handleFileSelect"
  271. style="display: none"
  272. />
  273. <el-button
  274. type="primary"
  275. @click="$refs.videoInput.click()"
  276. :loading="uploading"
  277. >
  278. <el-icon><Upload /></el-icon>
  279. {{ uploading ? '上传中...' : '点击上传视频' }}
  280. </el-button>
  281. <div class="el-upload__tip" style="margin-top: 7px; color: #909399; font-size: 12px;">
  282. 支持 MP4AVIMOVWMVFLVMKV 格式大小不超过200MB
  283. </div>
  284. </div>
  285. <!-- 预览区域上传后显示 -->
  286. <div v-if="form.videoUrl" class="video-preview">
  287. <div style="display: flex; align-items: flex-start; gap: 10px;">
  288. <video
  289. :src="getVideoUrl(form.videoUrl)"
  290. controls
  291. style="max-width: 300px; max-height: 150px; border-radius: 4px; background: #000;"
  292. ></video>
  293. <div style="display: flex; flex-direction: column; gap: 5px;">
  294. <el-button
  295. type="danger"
  296. size="small"
  297. @click="removeVideo"
  298. circle
  299. >
  300. <el-icon><Delete /></el-icon>
  301. </el-button>
  302. <el-button
  303. type="primary"
  304. size="small"
  305. @click="previewVideo"
  306. >
  307. 预览
  308. </el-button>
  309. </div>
  310. </div>
  311. </div>
  312. <!-- 地址输入框可编辑 -->
  313. <el-form-item prop="videoUrl" style="margin-bottom: 0;">
  314. <el-input
  315. v-model="form.videoUrl"
  316. type="textarea"
  317. :rows="2"
  318. placeholder="请上传视频或直接输入视频地址"
  319. clearable
  320. />
  321. </el-form-item>
  322. </div>
  323. </el-form-item>
  324. <el-form-item label="封面图片" prop="coverImage">
  325. <image-upload v-model="form.coverImage"/>
  326. </el-form-item>
  327. <el-form-item label="分类" prop="category">
  328. <el-select v-model="form.category" placeholder="请选择分类" clearable>
  329. <el-option v-for="dict in dict.type.video_category" :key="dict.value" :label="dict.label" :value="dict.value" />
  330. </el-select>
  331. </el-form-item>
  332. </el-form>
  333. <div slot="footer" class="dialog-footer">
  334. <el-button type="primary" @click="submitForm"> </el-button>
  335. <el-button @click="cancel"> </el-button>
  336. </div>
  337. </el-dialog>
  338. <!-- ================= 审核对话框 ================= -->
  339. <el-dialog
  340. title="视频审核"
  341. :visible.sync="auditOpen"
  342. width="80%"
  343. append-to-body
  344. >
  345. <el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="80px">
  346. <el-form-item label="审核结果" prop="auditStatus" required>
  347. <el-radio-group v-model="auditForm.auditStatus">
  348. <el-radio
  349. v-for="item in auditStatusOptions.filter(item => ['1', '2'].includes(item.dictValue))"
  350. :key="item.dictValue"
  351. :label="item.dictValue"
  352. >
  353. {{ item.dictLabel }}
  354. </el-radio>
  355. </el-radio-group>
  356. </el-form-item>
  357. <el-form-item label="审核意见" prop="auditOpinion">
  358. <el-input
  359. v-model="auditForm.auditOpinion"
  360. type="textarea"
  361. :rows="4"
  362. placeholder="请输入审核意见(审核拒绝时必须填写)"
  363. maxlength="500"
  364. show-word-limit
  365. />
  366. </el-form-item>
  367. </el-form>
  368. <div slot="footer" class="dialog-footer">
  369. <el-button type="primary" @click="submitAudit"> </el-button>
  370. <el-button @click="cancelAudit"> </el-button>
  371. </div>
  372. </el-dialog>
  373. </div>
  374. </template>
  375. <script>
  376. import { listTraining, getTraining, delTraining, addTraining, updateTraining, uploadVideo, resubmitAudit} from "@/api/vet/training"
  377. import {
  378. submitForAudit,
  379. cancelAudit,
  380. auditVideo,
  381. publishVideo,
  382. offlineVideo,
  383. batchSubmitAudit,
  384. batchAuditVideo,
  385. batchPublishVideo,
  386. batchOfflineVideo
  387. } from "@/api/vet/training"
  388. import { getToken } from "@/utils/auth"
  389. export default {
  390. name: "Training",
  391. dicts: ['video_category'],
  392. data() {
  393. return {
  394. // 遮罩层
  395. loading: true,
  396. // 选中数组
  397. ids: [],
  398. // 非单个禁用
  399. single: true,
  400. // 非多个禁用
  401. multiple: true,
  402. // 显示搜索条件
  403. showSearch: true,
  404. // 总条数
  405. total: 0,
  406. // 兽医培训视频表格数据
  407. trainingList: [],
  408. // 弹出层标题
  409. title: "",
  410. // 是否显示弹出层
  411. open: false,
  412. // 上传相关变量
  413. uploading: false,
  414. // 是否为管理员
  415. isAdmin: false,
  416. // 字典数据
  417. statusOptions: [
  418. { dictValue: '0', dictLabel: '未上架', listClass: 'info' },
  419. { dictValue: '1', dictLabel: '已上架', listClass: 'success' }
  420. ],
  421. auditStatusOptions: [
  422. { dictValue: '0', dictLabel: '待审核', listClass: 'info' },
  423. { dictValue: '1', dictLabel: '审核中', listClass: 'warning' },
  424. { dictValue: '2', dictLabel: '审核通过', listClass: 'success' },
  425. { dictValue: '3', dictLabel: '审核驳回', listClass: 'danger' }
  426. ],
  427. // 查询参数
  428. queryParams: {
  429. pageNum: 1,
  430. pageSize: 10,
  431. title: null,
  432. description: null,
  433. videoUrl: null,
  434. coverImage: null,
  435. category: null,
  436. tags: null,
  437. duration: null,
  438. fileSize: null,
  439. viewCount: null,
  440. status: null,
  441. auditStatus: null,
  442. auditOpinion: null,
  443. auditUserId: null,
  444. auditTime: null
  445. },
  446. // 表单参数
  447. form: {
  448. id: undefined,
  449. userId: undefined,
  450. title: "",
  451. description: "",
  452. videoUrl: "",
  453. coverImage: "",
  454. category: "",
  455. tags: "",
  456. duration: undefined,
  457. fileSize: undefined,
  458. viewCount: undefined,
  459. status: "0",
  460. createTime: undefined,
  461. updateTime: undefined,
  462. delFlag: undefined,
  463. auditStatus: "0",
  464. auditOpinion: "",
  465. auditUserId: undefined,
  466. auditTime: undefined
  467. },
  468. // 表单校验
  469. rules: {
  470. title: [
  471. { required: true, message: "视频标题不能为空", trigger: "blur" }
  472. ],
  473. videoUrl: [
  474. { required: true, message: "视频地址不能为空", trigger: "blur" }
  475. ],
  476. category: [
  477. { required: true, message: "分类不能为空", trigger: "blur" }
  478. ]
  479. },
  480. // ========== 审核相关 ==========
  481. auditOpen: false,
  482. auditForm: {
  483. id: null,
  484. auditStatus: '1',
  485. auditOpinion: ''
  486. },
  487. auditRules: {
  488. auditOpinion: [
  489. {
  490. validator: (rule, value, callback) => {
  491. if (this.auditForm.auditStatus === '2' && (!value || value.trim() === '')) {
  492. callback(new Error('审核拒绝时必须填写审核意见'))
  493. } else {
  494. callback()
  495. }
  496. },
  497. trigger: 'blur'
  498. }
  499. ]
  500. }
  501. }
  502. },
  503. created() {
  504. this.getList()
  505. this.checkAdminRole()
  506. },
  507. methods: {
  508. /** 检查是否是管理员 */
  509. checkAdminRole() {
  510. const userInfo = this.$store.getters.userInfo || {}
  511. this.isAdmin = userInfo.roles && userInfo.roles.includes('admin')
  512. },
  513. /** 查询兽医培训视频列表 */
  514. getList() {
  515. this.loading = true
  516. listTraining(this.queryParams).then(response => {
  517. this.trainingList = response.rows
  518. this.total = response.total
  519. this.loading = false
  520. }).catch(() => {
  521. this.loading = false
  522. })
  523. },
  524. /** ============ 字典工具方法 ============ */
  525. /** 通过字典值获取标签 */
  526. getDictLabel(options, value) {
  527. if (!options || !value) return ''
  528. const item = options.find(item => item.dictValue === String(value))
  529. return item ? item.dictLabel : String(value)
  530. },
  531. /** 通过字典值获取样式类型 */
  532. getDictType(options, value) {
  533. if (!options || !value) return 'info'
  534. const item = options.find(item => item.dictValue === String(value))
  535. const type = item ? (item.listClass || 'info') : 'info'
  536. const tagTypeMap = {
  537. 'info': 'info',
  538. 'warning': 'warning',
  539. 'success': 'success',
  540. 'danger': 'danger',
  541. // 'primary': 'primary',
  542. }
  543. return tagTypeMap[type] || 'info'
  544. },
  545. /** 获取上架状态文本 */
  546. getStatusText(status) {
  547. return this.getDictLabel(this.statusOptions, status)
  548. },
  549. /** 获取上架状态类型 */
  550. getStatusType(status) {
  551. return this.getDictType(this.statusOptions, status)
  552. },
  553. /** 获取审核状态文本 */
  554. getAuditStatusText(auditStatus) {
  555. return this.getDictLabel(this.auditStatusOptions, auditStatus)
  556. },
  557. /** 获取审核状态类型 */
  558. getAuditStatusType(auditStatus) {
  559. return this.getDictType(this.auditStatusOptions, auditStatus)
  560. },
  561. /** 截断文本 */
  562. truncateText(text, maxLength) {
  563. if (!text) return ''
  564. if (text.length <= maxLength) return text
  565. return text.substring(0, maxLength) + '...'
  566. },
  567. /** ============ 视频URL处理相关方法 ============ */
  568. // 截短URL显示
  569. truncateUrl(url) {
  570. if (!url) return ''
  571. if (url.includes('/')) {
  572. const parts = url.split('/')
  573. const filename = parts[parts.length - 1]
  574. if (filename.length > 15) {
  575. return filename.substring(0, 12) + '...'
  576. }
  577. return filename
  578. }
  579. if (url.length > 15) {
  580. return url.substring(0, 12) + '...'
  581. }
  582. return url
  583. },
  584. // 表格中的视频预览
  585. previewTableVideo(url) {
  586. if (!url) {
  587. this.$modal.msgWarning("视频地址为空")
  588. return
  589. }
  590. const fullUrl = this.getVideoUrl(url)
  591. const htmlContent = `
  592. <div style="text-align: center; padding: 20px;">
  593. <iframe
  594. src="${fullUrl}"
  595. style="width: 100%; height: 400px; border: none; border-radius: 4px; background: #000;"
  596. frameborder="0"
  597. allowfullscreen
  598. allow="autoplay; encrypted-media"
  599. >
  600. </iframe>
  601. <div style="margin-top: 10px; color: #666; font-size: 12px; word-break: break-all;">
  602. 视频地址: ${url}
  603. </div>
  604. </div>
  605. `
  606. this.$modal.alert({
  607. title: '视频预览',
  608. message: htmlContent,
  609. dangerouslyUseHTMLString: true,
  610. customClass: 'video-preview-modal',
  611. width: '600px',
  612. showConfirmButton: false,
  613. showCancelButton: true,
  614. cancelButtonText: '关闭'
  615. })
  616. },
  617. // 取消按钮
  618. cancel() {
  619. this.open = false
  620. this.reset()
  621. },
  622. // 表单重置
  623. reset() {
  624. this.form = {
  625. id: undefined,
  626. userId: undefined,
  627. title: "",
  628. description: "",
  629. videoUrl: "",
  630. coverImage: "",
  631. category: "",
  632. tags: "",
  633. duration: undefined,
  634. fileSize: undefined,
  635. viewCount: undefined,
  636. status: "0",
  637. createTime: undefined,
  638. updateTime: undefined,
  639. delFlag: undefined,
  640. auditStatus: "0",
  641. auditOpinion: "",
  642. auditUserId: undefined,
  643. auditTime: undefined
  644. }
  645. if (this.$refs.form) {
  646. this.$nextTick(() => {
  647. this.$refs.form.clearValidate()
  648. })
  649. }
  650. this.uploading = false
  651. },
  652. /** ============ 视频上传相关方法 ============ */
  653. // 处理文件选择
  654. handleFileSelect(event) {
  655. const file = event.target.files[0]
  656. if (file) {
  657. this.uploadFile(file)
  658. }
  659. event.target.value = ''
  660. },
  661. // 上传文件
  662. async uploadFile(file) {
  663. const fileName = file.name.toLowerCase()
  664. const validExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
  665. const hasValidExtension = validExtensions.some(ext => fileName.endsWith(ext))
  666. if (!hasValidExtension) {
  667. this.$modal.msgError('请上传 MP4、AVI、MOV、WMV、FLV、MKV 格式的视频文件')
  668. return false
  669. }
  670. const maxSize = 200 * 1024 * 1024
  671. if (file.size > maxSize) {
  672. this.$modal.msgError('视频大小不能超过 200MB')
  673. return false
  674. }
  675. this.uploading = true
  676. try {
  677. const formData = new FormData()
  678. formData.append('file', file)
  679. const response = await uploadVideo(formData)
  680. if (response.code === 200) {
  681. this.form.videoUrl = response.data
  682. this.$modal.msgSuccess("视频上传成功")
  683. return true
  684. } else {
  685. this.$modal.msgError(response.msg || '上传失败')
  686. return false
  687. }
  688. } catch (error) {
  689. console.error('上传失败:', error)
  690. this.$modal.msgError('上传失败,请重试')
  691. return false
  692. } finally {
  693. this.uploading = false
  694. }
  695. },
  696. // 移除视频
  697. removeVideo() {
  698. this.form.videoUrl = ''
  699. this.$modal.msgSuccess("已移除视频")
  700. },
  701. // 预览视频
  702. previewVideo() {
  703. if (this.form.videoUrl) {
  704. let url = this.form.videoUrl
  705. if (url && url.startsWith('/')) {
  706. url = process.env.VUE_APP_BASE_API + url
  707. }
  708. if (url) {
  709. window.open(url, '_blank')
  710. }
  711. } else {
  712. this.$modal.msgWarning("请先上传或输入视频地址")
  713. }
  714. },
  715. // 获取完整的视频URL
  716. getVideoUrl(url) {
  717. if (!url) return ''
  718. if (url.startsWith('/')) {
  719. return process.env.VUE_APP_BASE_API + url
  720. }
  721. return url
  722. },
  723. /** ============ 审核相关方法 ============ */
  724. /** 提交审核(批量或单个) */
  725. async handleSubmitAudit(videoId) {
  726. let id
  727. if (videoId && typeof videoId !== 'object' && videoId !== '') {
  728. id = videoId
  729. } else if (this.ids.length === 1) {
  730. id = this.ids[0]
  731. } else if (typeof videoId === 'object') {
  732. if (this.ids.length === 1) {
  733. id = this.ids[0]
  734. } else {
  735. this.$message.warning('请先选择一个视频')
  736. return
  737. }
  738. } else {
  739. this.$message.warning('请选择需要提交审核的视频')
  740. return
  741. }
  742. if (!id) {
  743. this.$message.warning('请选择需要提交审核的视频')
  744. return
  745. }
  746. try {
  747. await this.$confirm('是否确认提交审核?', '提示', {
  748. confirmButtonText: '确定',
  749. cancelButtonText: '取消',
  750. type: 'warning'
  751. })
  752. await submitForAudit(id)
  753. this.$message.success('提交审核成功')
  754. this.getList()
  755. } catch (error) {
  756. if (error !== 'cancel') {
  757. console.error('提交审核失败:', error)
  758. this.$message.error(error.message || '提交审核失败')
  759. }
  760. }
  761. },
  762. /** 取消审核 */
  763. async handleCancelAudit(videoId) {
  764. const id = videoId || this.ids[0]
  765. if (!id) {
  766. this.$message.warning('请选择需要取消审核的视频')
  767. return
  768. }
  769. try {
  770. await this.$confirm('确定要取消审核吗?取消后可以重新提交审核', '提示', {
  771. confirmButtonText: '确定',
  772. cancelButtonText: '取消',
  773. type: 'warning'
  774. })
  775. await cancelAudit(id)
  776. this.$message.success('取消审核成功')
  777. this.getList()
  778. } catch (error) {
  779. if (error !== 'cancel') {
  780. console.error('取消审核失败:', error)
  781. this.$message.error(error.message || '取消审核失败')
  782. }
  783. }
  784. },
  785. /** 审核(批量或单个) */
  786. handleAuditDialog(videoId) {
  787. let id
  788. if (videoId && typeof videoId !== 'object' && videoId !== '') {
  789. id = videoId
  790. } else if (this.ids.length === 1) {
  791. id = this.ids[0]
  792. } else if (typeof videoId === 'object') {
  793. if (this.ids.length === 1) {
  794. id = this.ids[0]
  795. } else {
  796. this.$message.warning('请先选择一个视频')
  797. return
  798. }
  799. } else {
  800. this.$message.warning('请选择需要审核的视频')
  801. return
  802. }
  803. if (!id) {
  804. this.$message.warning('请选择需要审核的视频')
  805. return
  806. }
  807. this.auditForm.id = id
  808. this.auditForm.auditStatus = '1'
  809. this.auditForm.auditOpinion = ''
  810. this.auditOpen = true
  811. },
  812. /** 取消审核对话框 */
  813. cancelAudit() {
  814. this.auditOpen = false
  815. this.resetAuditForm()
  816. },
  817. /** 重置审核表单 */
  818. resetAuditForm() {
  819. this.auditForm = {
  820. id: null,
  821. auditStatus: '1',
  822. auditOpinion: ''
  823. }
  824. if (this.$refs.auditFormRef) {
  825. this.$refs.auditFormRef.resetFields()
  826. }
  827. },
  828. /** 提交审核(管理员审核) */
  829. async submitAudit() {
  830. try {
  831. const valid = await this.$refs.auditFormRef.validate()
  832. if (!valid) return
  833. const message = this.auditForm.auditStatus === '1'
  834. ? '是否确认审核通过?'
  835. : '是否确认审核拒绝?'
  836. await this.$confirm(message, '提示', {
  837. confirmButtonText: '确定',
  838. cancelButtonText: '取消',
  839. type: 'warning'
  840. })
  841. // 方式1:传递对象参数
  842. const response = await auditVideo({
  843. id: this.auditForm.id,
  844. auditStatus: this.auditForm.auditStatus,
  845. auditOpinion: this.auditForm.auditOpinion || ''
  846. })
  847. // 或者方式2:如果不想改API函数,可以直接调用request
  848. // const response = await request({
  849. // url: `/vet/training/audit/${this.auditForm.id}`,
  850. // method: 'post',
  851. // data: {
  852. // auditStatus: this.auditForm.auditStatus,
  853. // auditOpinion: this.auditForm.auditOpinion || ''
  854. // }
  855. // })
  856. if (response.code === 200) {
  857. this.$message.success('审核成功')
  858. this.auditOpen = false
  859. this.getList()
  860. this.resetAuditForm()
  861. } else {
  862. this.$message.error(response.msg || '审核失败')
  863. }
  864. } catch (error) {
  865. if (error === 'cancel') return
  866. console.error('审核错误:', error)
  867. this.$message.error('审核失败,请检查参数')
  868. }
  869. },
  870. /** ============ 上架/下架相关方法 ============ */
  871. /** 上架视频(批量或单个) */
  872. async handlePublish(videoId) {
  873. let id
  874. if (videoId && typeof videoId !== 'object' && videoId !== '') {
  875. id = videoId
  876. } else if (this.ids.length === 1) {
  877. id = this.ids[0]
  878. } else if (typeof videoId === 'object') {
  879. if (this.ids.length === 1) {
  880. id = this.ids[0]
  881. } else {
  882. this.$message.warning('请先选择一个视频')
  883. return
  884. }
  885. } else {
  886. this.$message.warning('请选择需要上架的视频')
  887. return
  888. }
  889. try {
  890. await this.$confirm('是否确认上架视频?', '提示', {
  891. confirmButtonText: '确定',
  892. cancelButtonText: '取消',
  893. type: 'warning'
  894. })
  895. await publishVideo(id)
  896. this.$message.success('上架成功')
  897. this.getList()
  898. } catch (error) {
  899. if (error !== 'cancel') {
  900. console.error('上架失败:', error)
  901. this.$message.error(error.message || '上架失败')
  902. }
  903. }
  904. },
  905. /** 下架视频(批量或单个) */
  906. async handleOffline(videoId) {
  907. let id
  908. if (videoId && typeof videoId !== 'object' && videoId !== '') {
  909. id = videoId
  910. } else if (this.ids.length === 1) {
  911. id = this.ids[0]
  912. } else if (typeof videoId === 'object') {
  913. if (this.ids.length === 1) {
  914. id = this.ids[0]
  915. } else {
  916. this.$message.warning('请先选择一个视频')
  917. return
  918. }
  919. } else {
  920. this.$message.warning('请选择需要下架的视频')
  921. return
  922. }
  923. try {
  924. await this.$confirm('是否确认下架视频?', '提示', {
  925. confirmButtonText: '确定',
  926. cancelButtonText: '取消',
  927. type: 'warning'
  928. })
  929. await offlineVideo(id)
  930. this.$message.success('下架成功')
  931. this.getList()
  932. } catch (error) {
  933. if (error !== 'cancel') {
  934. console.error('下架失败:', error)
  935. this.$message.error(error.message || '下架失败')
  936. }
  937. }
  938. },
  939. // handleResubmitAudit重新提交审核
  940. handleResubmitAudit(row) {
  941. const id = row.id || this.ids[0];
  942. if (!id) {
  943. this.$modal.msgWarning('请选择需要重新提交审核的视频');
  944. return;
  945. }
  946. this.$confirm('是否确认重新提交审核?', '提示', {
  947. confirmButtonText: '确定',
  948. cancelButtonText: '取消',
  949. type: 'warning'
  950. }).then(async () => {
  951. try {
  952. const response = await resubmitAudit(id);
  953. if (response.code === 200) {
  954. this.$modal.msgSuccess("重新提交审核成功");
  955. this.getList();
  956. } else {
  957. this.$modal.msgError(response.msg || "重新提交审核失败");
  958. }
  959. } catch (error) {
  960. console.error('重新提交审核失败:', error);
  961. this.$modal.msgError('重新提交审核失败:' + (error.message || '网络异常'));
  962. }
  963. }).catch(() => {
  964. });
  965. },
  966. /** ============ 其他方法 ============ */
  967. /** 搜索按钮操作 */
  968. handleQuery() {
  969. this.queryParams.pageNum = 1
  970. this.getList()
  971. },
  972. /** 重置按钮操作 */
  973. resetQuery() {
  974. this.resetForm("queryForm")
  975. this.handleQuery()
  976. },
  977. // 多选框选中数据
  978. handleSelectionChange(selection) {
  979. this.ids = selection.map(item => item.id)
  980. this.single = selection.length !== 1
  981. this.multiple = !selection.length
  982. },
  983. /** 新增按钮操作 */
  984. handleAdd() {
  985. this.reset()
  986. this.open = true
  987. this.title = "添加兽医培训视频"
  988. },
  989. /** 修改按钮操作 */
  990. handleUpdate(row) {
  991. this.reset()
  992. const id = row.id || this.ids
  993. getTraining(id).then(response => {
  994. const data = response.data || {}
  995. this.form = {
  996. id: data.id || undefined,
  997. userId: data.userId || undefined,
  998. title: data.title || "",
  999. description: data.description || "",
  1000. videoUrl: data.videoUrl || "",
  1001. coverImage: data.coverImage || "",
  1002. category: data.category || "",
  1003. tags: data.tags || "",
  1004. duration: data.duration || undefined,
  1005. fileSize: data.fileSize || undefined,
  1006. viewCount: data.viewCount || undefined,
  1007. status: data.status || "0",
  1008. createTime: data.createTime || undefined,
  1009. updateTime: data.updateTime || undefined,
  1010. delFlag: data.delFlag || undefined,
  1011. auditStatus: data.auditStatus || "0",
  1012. auditOpinion: data.auditOpinion || "",
  1013. auditUserId: data.auditUserId || undefined,
  1014. auditTime: data.auditTime || undefined
  1015. }
  1016. this.open = true
  1017. this.title = "修改兽医培训视频"
  1018. }).catch(() => {
  1019. this.$modal.msgError("获取数据失败")
  1020. })
  1021. },
  1022. /** 提交按钮 */
  1023. submitForm() {
  1024. this.$refs["form"].validate(valid => {
  1025. if (valid) {
  1026. const submitData = {
  1027. ...this.form,
  1028. title: (this.form.title || "").trim(),
  1029. category: (this.form.category || "").trim(),
  1030. videoUrl: this.form.videoUrl || "",
  1031. description: this.form.description || "",
  1032. status: this.form.status || "0",
  1033. auditStatus: this.form.auditStatus || "0"
  1034. }
  1035. if (submitData.id) {
  1036. updateTraining(submitData).then(response => {
  1037. this.$modal.msgSuccess("修改成功")
  1038. this.open = false
  1039. this.getList()
  1040. }).catch((error) => {
  1041. console.error('修改失败:', error)
  1042. this.$modal.msgError("修改失败: " + (error.message || "未知错误"))
  1043. })
  1044. } else {
  1045. addTraining(submitData).then(response => {
  1046. this.$modal.msgSuccess("新增成功")
  1047. this.open = false
  1048. this.getList()
  1049. }).catch((error) => {
  1050. console.error('新增失败:', error)
  1051. this.$modal.msgError("新增失败: " + (error.message || "未知错误"))
  1052. })
  1053. }
  1054. } else {
  1055. return false
  1056. }
  1057. })
  1058. },
  1059. /** 删除按钮操作 */
  1060. handleDelete(row) {
  1061. const ids = row.id || this.ids
  1062. this.$modal.confirm('是否确认删除兽医培训视频编号为"' + ids + '"的数据项?').then(() => {
  1063. return delTraining(ids)
  1064. }).then(() => {
  1065. this.getList()
  1066. this.$modal.msgSuccess("删除成功")
  1067. }).catch(() => {})
  1068. },
  1069. /** 导出按钮操作 */
  1070. handleExport() {
  1071. this.download('vet/training/export', {
  1072. ...this.queryParams
  1073. }, `training_${new Date().getTime()}.xlsx`)
  1074. }
  1075. }
  1076. }
  1077. </script>
  1078. <style scoped>
  1079. ::v-deep .pagestyle .el-input{
  1080. width: auto !important;
  1081. }
  1082. </style>
  1083. <style lang="scss" scoped>
  1084. .video-url-cell {
  1085. display: flex;
  1086. align-items: center;
  1087. justify-content: center;
  1088. }
  1089. .url-text {
  1090. max-width: 100px;
  1091. overflow: hidden;
  1092. text-overflow: ellipsis;
  1093. white-space: nowrap;
  1094. }
  1095. // 操作按钮样式
  1096. .info-btn {
  1097. padding: 6px 10px;
  1098. border-radius: 4px;
  1099. margin: 0 10px;
  1100. transition: all 0.3s ease;
  1101. }
  1102. .view-btn:hover {
  1103. background-color: rgb(216, 238, 248);
  1104. transform: translateY(-1px);
  1105. }
  1106. .alter-btn:hover{
  1107. background-color: rgb(230, 255, 238);
  1108. transform: translateY(-1px);
  1109. }
  1110. .delete-btn:hover {
  1111. background-color: rgba(245, 108, 108, 0.1);
  1112. transform: translateY(-1px);
  1113. }
  1114. .submit-btn:hover {
  1115. background-color: rgb(253, 250, 232);
  1116. transform: translateY(-1px);
  1117. }
  1118. .publish-btn:hover {
  1119. background-color: rgb(253, 238, 228);
  1120. transform: translateY(-1px);
  1121. }
  1122. .offline-btn:hover {
  1123. background-color: rgb(237, 237, 235);
  1124. transform: translateY(-1px);
  1125. }
  1126. .cancel-btn:hover {
  1127. background-color: rgb(244, 237, 251);
  1128. transform: translateY(-1px);
  1129. }
  1130. .audit-btn:hover {
  1131. background-color: rgb(215, 223, 246);
  1132. transform: translateY(-1px);
  1133. }
  1134. .resubmit-btn:hover {
  1135. background-color: rgb(248, 232, 250);
  1136. transform: translateY(-1px);
  1137. }
  1138. // 新增/修改的弹窗
  1139. ::v-deep .el-dialog {
  1140. border-radius: 12px;
  1141. overflow: hidden;
  1142. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12);
  1143. animation: dialogFadeIn 0.3s ease;
  1144. }
  1145. ::v-deep .el-dialog__header {
  1146. background: linear-gradient(135deg, #42b983 0%, #83df92 100%);
  1147. padding: 18px 24px;
  1148. border-bottom: none;
  1149. position: relative;
  1150. }
  1151. ::v-deep .el-dialog__title {
  1152. font-size: 17px;
  1153. font-weight: 600;
  1154. color: white;
  1155. letter-spacing: 0.5px;
  1156. }
  1157. ::v-deep .el-dialog__headerbtn:hover .el-dialog__close {
  1158. color: #ffd04b;
  1159. transform: rotate(90deg);
  1160. }
  1161. ::v-deep .el-dialog__body {
  1162. padding: 28px 24px 20px;
  1163. background-color: #f8fafc;
  1164. max-height: 70vh;
  1165. overflow-y: auto;
  1166. }
  1167. ::v-deep .el-form-item {
  1168. margin-bottom: 20px;
  1169. transition: all 0.3s;
  1170. }
  1171. ::v-deep .el-form-item__label {
  1172. font-weight: 500;
  1173. color: #2d3748;
  1174. font-size: 14px;
  1175. transition: color 0.3s;
  1176. }
  1177. ::v-deep .el-input,
  1178. ::v-deep .el-textarea,
  1179. ::v-deep .el-select {
  1180. width: 100%;
  1181. }
  1182. ::v-deep .el-input__inner,
  1183. ::v-deep .el-textarea__inner {
  1184. border-radius: 8px;
  1185. border: 1px solid #dcdfe6;
  1186. font-size: 14px;
  1187. transition: all 0.3s;
  1188. background-color: #fcfdfe;
  1189. }
  1190. ::v-deep .el-input__inner:focus,
  1191. ::v-deep .el-textarea__inner:focus {
  1192. border-color: #42B983;
  1193. box-shadow: 0 0 0 3px rgb(230, 255, 238);
  1194. background-color: white;
  1195. }
  1196. ::v-deep .el-select .el-input__inner {
  1197. padding-right: 35px;
  1198. }
  1199. ::v-deep .el-dialog__footer {
  1200. padding: 20px 24px;
  1201. background-color: #f8fafc;
  1202. border-top: 1px solid #eef2f7;
  1203. border-radius: 0 0 12px 12px;
  1204. }
  1205. </style>