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.

1277 lines
37 KiB

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