37 changed files with 3548 additions and 498 deletions
-
12chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java
-
2chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java
-
125chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetKnowledgeController.java
-
140chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetNotificationController.java
-
312chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetQualificationController.java
-
194chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java
-
34chenhai-common/src/main/java/com/chenhai/common/utils/file/FileUtils.java
-
14chenhai-system/pom.xml
-
286chenhai-system/src/main/java/com/chenhai/vet/CertificateRemindTask.java
-
117chenhai-system/src/main/java/com/chenhai/vet/domain/VetKnowledge.java
-
2chenhai-system/src/main/java/com/chenhai/vet/domain/VetNotification.java
-
140chenhai-system/src/main/java/com/chenhai/vet/domain/VetQualification.java
-
26chenhai-system/src/main/java/com/chenhai/vet/domain/VetTrainingVideo.java
-
61chenhai-system/src/main/java/com/chenhai/vet/mapper/VetKnowledgeMapper.java
-
29chenhai-system/src/main/java/com/chenhai/vet/mapper/VetNotificationMapper.java
-
3chenhai-system/src/main/java/com/chenhai/vet/mapper/VetQualificationMapper.java
-
28chenhai-system/src/main/java/com/chenhai/vet/mapper/VetTrainingVideoMapper.java
-
6chenhai-system/src/main/java/com/chenhai/vet/service/IVetCertificateService.java
-
74chenhai-system/src/main/java/com/chenhai/vet/service/IVetKnowledgeService.java
-
1chenhai-system/src/main/java/com/chenhai/vet/service/IVetQualificationService.java
-
51chenhai-system/src/main/java/com/chenhai/vet/service/IVetTrainingVideoService.java
-
15chenhai-system/src/main/java/com/chenhai/vet/service/VetNotificationService.java
-
2chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetCertificateServiceImpl.java
-
134chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetKnowledgeServiceImpl.java
-
78chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetNotificationServiceImpl.java
-
188chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetQualificationServiceImpl.java
-
146chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetTrainingVideoServiceImpl.java
-
96chenhai-system/src/main/resources/mapper/vet/VetKnowledgeMapper.xml
-
51chenhai-system/src/main/resources/mapper/vet/VetNotificationMapper.xml
-
32chenhai-system/src/main/resources/mapper/vet/VetQualificationMapper.xml
-
110chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml
-
4chenhai-ui/package.json
-
60chenhai-ui/src/api/vet/knowledge.js
-
95chenhai-ui/src/api/vet/training.js
-
10chenhai-ui/src/views/vet/certificate/index.vue
-
356chenhai-ui/src/views/vet/knowledge/index.vue
-
960chenhai-ui/src/views/vet/training/TrainingHome.vue
@ -0,0 +1,125 @@ |
|||||
|
package com.chenhai.web.controller.vet; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.PutMapping; |
||||
|
import org.springframework.web.bind.annotation.DeleteMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import com.chenhai.common.annotation.Log; |
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.enums.BusinessType; |
||||
|
import com.chenhai.vet.domain.VetKnowledge; |
||||
|
import com.chenhai.vet.service.IVetKnowledgeService; |
||||
|
import com.chenhai.common.utils.poi.ExcelUtil; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
|
||||
|
/** |
||||
|
* 兽医文章Controller |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
* @date 2026-01-08 |
||||
|
*/ |
||||
|
@RestController |
||||
|
@RequestMapping("/vet/knowledge") |
||||
|
public class VetKnowledgeController extends BaseController |
||||
|
{ |
||||
|
@Autowired |
||||
|
private IVetKnowledgeService vetKnowledgeService; |
||||
|
|
||||
|
/** |
||||
|
* 查询兽医文章列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:list')") |
||||
|
@GetMapping("/list") |
||||
|
public TableDataInfo list(VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
startPage(); |
||||
|
List<VetKnowledge> list = vetKnowledgeService.selectVetKnowledgeList(vetKnowledge); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 导出兽医文章列表 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:export')") |
||||
|
@Log(title = "兽医文章", businessType = BusinessType.EXPORT) |
||||
|
@PostMapping("/export") |
||||
|
public void export(HttpServletResponse response, VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
List<VetKnowledge> list = vetKnowledgeService.selectVetKnowledgeList(vetKnowledge); |
||||
|
ExcelUtil<VetKnowledge> util = new ExcelUtil<VetKnowledge>(VetKnowledge.class); |
||||
|
util.exportExcel(response, list, "兽医文章数据"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取兽医文章详细信息 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:query')") |
||||
|
@GetMapping(value = "/{id}") |
||||
|
public AjaxResult getInfo(@PathVariable("id") Long id) |
||||
|
{ |
||||
|
return success(vetKnowledgeService.selectVetKnowledgeById(id)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增兽医文章 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:add')") |
||||
|
@Log(title = "兽医文章", businessType = BusinessType.INSERT) |
||||
|
@PostMapping |
||||
|
public AjaxResult add(@RequestBody VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
vetKnowledge.setStatus("0"); |
||||
|
return toAjax(vetKnowledgeService.insertVetKnowledge(vetKnowledge)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改兽医文章 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:edit')") |
||||
|
@Log(title = "兽医文章", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping |
||||
|
public AjaxResult edit(@RequestBody VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
return toAjax(vetKnowledgeService.updateVetKnowledge(vetKnowledge)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除兽医文章 |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:remove')") |
||||
|
@Log(title = "兽医文章", businessType = BusinessType.DELETE) |
||||
|
@DeleteMapping("/{ids}") |
||||
|
public AjaxResult remove(@PathVariable Long[] ids) |
||||
|
{ |
||||
|
return toAjax(vetKnowledgeService.deleteVetKnowledgeByIds(ids)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传文章(待审核) |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:upload')") |
||||
|
@Log(title = "兽医文章", businessType = BusinessType.INSERT) |
||||
|
@PostMapping("/upload") |
||||
|
public AjaxResult upload(@RequestBody VetKnowledge vetKnowledge) { |
||||
|
return vetKnowledgeService.uploadVetKnowledge(vetKnowledge); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发布文章(更新状态) |
||||
|
*/ |
||||
|
@PreAuthorize("@ss.hasPermi('vet:knowledge:publish')") |
||||
|
@Log(title = "兽医文章", businessType = BusinessType.UPDATE) |
||||
|
@PutMapping("/publish/{id}") |
||||
|
public AjaxResult publish(@PathVariable Long id) { |
||||
|
return vetKnowledgeService.publishVetKnowledge(id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,194 @@ |
|||||
|
package com.chenhai.web.controller.vet; |
||||
|
|
||||
|
import com.chenhai.common.core.controller.BaseController; |
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.core.page.TableDataInfo; |
||||
|
import com.chenhai.common.utils.SecurityUtils; |
||||
|
import com.chenhai.vet.domain.VetTrainingVideo; |
||||
|
import com.chenhai.vet.service.IVetTrainingVideoService; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.*; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
@RestController |
||||
|
@RequestMapping("/vet/training") |
||||
|
public class VetTrainingVideoController extends BaseController { |
||||
|
|
||||
|
@Autowired |
||||
|
private IVetTrainingVideoService trainingVideoService; |
||||
|
|
||||
|
/** |
||||
|
* 获取当前用户ID |
||||
|
*/ |
||||
|
private Long getCurrentUserId() { |
||||
|
return SecurityUtils.getUserId(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传培训视频 |
||||
|
*/ |
||||
|
@PostMapping("/upload") |
||||
|
public AjaxResult uploadVideo( |
||||
|
@RequestParam String title, |
||||
|
@RequestParam MultipartFile videoFile, |
||||
|
@RequestParam(required = false) String description, |
||||
|
@RequestParam(required = false) String category, |
||||
|
@RequestParam(required = false) String tags, |
||||
|
@RequestParam(required = false, defaultValue = "1") String status, |
||||
|
@RequestParam(required = false) MultipartFile coverImage) { |
||||
|
|
||||
|
Long userId = getCurrentUserId(); |
||||
|
|
||||
|
try { |
||||
|
VetTrainingVideo video = new VetTrainingVideo(); |
||||
|
video.setUserId(userId); // 修正变量名 |
||||
|
video.setTitle(title); |
||||
|
video.setDescription(description); |
||||
|
video.setCategory(category); |
||||
|
video.setTags(tags); |
||||
|
video.setStatus(status); |
||||
|
|
||||
|
// 上传并保存视频 |
||||
|
String result = trainingVideoService.uploadAndSave(video, videoFile, coverImage); |
||||
|
return success(result); |
||||
|
} catch (Exception e) { |
||||
|
return error("上传失败:" + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量上传视频(可选功能) |
||||
|
*/ |
||||
|
@PostMapping("/upload/batch") |
||||
|
public AjaxResult uploadBatch( |
||||
|
@RequestParam String[] titles, |
||||
|
@RequestParam MultipartFile[] videoFiles, |
||||
|
@RequestParam(required = false) String[] categories) { |
||||
|
|
||||
|
Long userId = getCurrentUserId(); |
||||
|
|
||||
|
try { |
||||
|
int successCount = 0; |
||||
|
for (int i = 0; i < videoFiles.length; i++) { |
||||
|
if (i < titles.length) { |
||||
|
VetTrainingVideo video = new VetTrainingVideo(); |
||||
|
video.setUserId(userId); // 修正变量名 |
||||
|
video.setTitle(titles[i]); |
||||
|
video.setCategory(categories != null && i < categories.length ? categories[i] : null); |
||||
|
video.setStatus("1"); |
||||
|
|
||||
|
trainingVideoService.uploadAndSave(video, videoFiles[i], null); |
||||
|
successCount++; |
||||
|
} |
||||
|
} |
||||
|
return success("成功上传 " + successCount + " 个视频"); |
||||
|
} catch (Exception e) { |
||||
|
return error("批量上传失败:" + e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查看我上传的视频 |
||||
|
*/ |
||||
|
@GetMapping("/my-videos") |
||||
|
public TableDataInfo getMyVideos( |
||||
|
@RequestParam(required = false) String title, |
||||
|
@RequestParam(required = false) String category, |
||||
|
@RequestParam(required = false) String status) { |
||||
|
|
||||
|
startPage(); |
||||
|
Long userId = getCurrentUserId(); // 修正变量名 |
||||
|
List<VetTrainingVideo> list = trainingVideoService.getMyVideos(userId, title, category, status); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查看所有公开的视频 |
||||
|
*/ |
||||
|
@GetMapping("/public-videos") |
||||
|
public TableDataInfo getPublicVideos( |
||||
|
@RequestParam(required = false) String title, |
||||
|
@RequestParam(required = false) String category, |
||||
|
@RequestParam(required = false) String userName) { // 参数名改为userName |
||||
|
|
||||
|
startPage(); |
||||
|
List<VetTrainingVideo> list = trainingVideoService.getPublicVideos(title, category, userName); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查看视频详情 |
||||
|
*/ |
||||
|
@GetMapping("/video/{videoId}") |
||||
|
public AjaxResult getVideoDetail(@PathVariable Long videoId) { |
||||
|
Long userId = getCurrentUserId(); // 修正变量名 |
||||
|
VetTrainingVideo video = trainingVideoService.getVideoDetail(videoId, userId); |
||||
|
|
||||
|
if (video == null) { |
||||
|
return error("视频不存在或无权限查看"); |
||||
|
} |
||||
|
|
||||
|
// 如果是公开视频或自己的视频,增加观看次数 |
||||
|
if ("1".equals(video.getStatus()) || userId.equals(video.getUserId())) { |
||||
|
trainingVideoService.incrementViewCount(videoId); |
||||
|
video.setViewCount(video.getViewCount() + 1); |
||||
|
} |
||||
|
|
||||
|
return success(video); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取视频播放地址(带权限校验)- 可选功能 |
||||
|
*/ |
||||
|
@GetMapping("/video/play/{videoId}") |
||||
|
public AjaxResult getPlayUrl(@PathVariable Long videoId) { |
||||
|
Long userId = getCurrentUserId(); // 修正变量名 |
||||
|
String playUrl = trainingVideoService.getVideoPlayUrl(videoId, userId); |
||||
|
|
||||
|
if (playUrl == null) { |
||||
|
return error("无权限播放此视频"); |
||||
|
} |
||||
|
|
||||
|
return success(playUrl); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取热门视频(按观看次数排序) |
||||
|
*/ |
||||
|
@GetMapping("/hot-videos") |
||||
|
public AjaxResult getHotVideos(@RequestParam(defaultValue = "10") Integer limit) { |
||||
|
List<VetTrainingVideo> list = trainingVideoService.getHotVideos(limit); |
||||
|
return success(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 搜索视频 |
||||
|
*/ |
||||
|
@GetMapping("/search") |
||||
|
public TableDataInfo searchVideos(@RequestParam String keyword) { |
||||
|
startPage(); |
||||
|
List<VetTrainingVideo> list = trainingVideoService.searchVideos(keyword); |
||||
|
return getDataTable(list); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除我的视频 |
||||
|
*/ |
||||
|
@DeleteMapping("/{videoId}") |
||||
|
public AjaxResult deleteVideo(@PathVariable Long videoId) { |
||||
|
Long userId = getCurrentUserId(); // 修正变量名 |
||||
|
VetTrainingVideo video = trainingVideoService.getVideoDetail(videoId, userId); |
||||
|
|
||||
|
if (video == null) { |
||||
|
return error("视频不存在"); |
||||
|
} |
||||
|
|
||||
|
if (!userId.equals(video.getUserId())) { |
||||
|
return error("无权删除此视频"); |
||||
|
} |
||||
|
|
||||
|
return toAjax(trainingVideoService.deleteVideoById(videoId)); |
||||
|
} |
||||
|
} |
||||
@ -1,55 +1,285 @@ |
|||||
package com.chenhai.vet; |
package com.chenhai.vet; |
||||
|
|
||||
import com.chenhai.vet.domain.VetCertificate; |
import com.chenhai.vet.domain.VetCertificate; |
||||
import com.chenhai.vet.service.IVetCertificateService; |
|
||||
|
import com.chenhai.vet.domain.VetNotification; |
||||
|
import com.chenhai.vet.mapper.VetCertificateMapper; |
||||
|
import com.chenhai.vet.mapper.VetNotificationMapper; |
||||
import lombok.extern.slf4j.Slf4j; |
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.scheduling.annotation.EnableScheduling; |
import org.springframework.scheduling.annotation.EnableScheduling; |
||||
import org.springframework.scheduling.annotation.Scheduled; |
import org.springframework.scheduling.annotation.Scheduled; |
||||
import org.springframework.stereotype.Component; |
import org.springframework.stereotype.Component; |
||||
|
|
||||
/** |
|
||||
* 证书提醒定时任务 |
|
||||
* |
|
||||
* @author ruoyi |
|
||||
*/ |
|
||||
|
import java.util.Calendar; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
|
||||
@Component |
@Component |
||||
@EnableScheduling |
@EnableScheduling |
||||
@Slf4j |
@Slf4j |
||||
public class CertificateRemindTask { |
public class CertificateRemindTask { |
||||
|
|
||||
@Autowired |
@Autowired |
||||
private IVetCertificateService vetCertificateService; |
|
||||
|
private VetCertificateMapper vetCertificateMapper; |
||||
|
@Autowired |
||||
|
private VetNotificationMapper vetNotificationMapper; |
||||
|
|
||||
|
// 测试:每分钟执行 | 正式:每天上午9点 @Scheduled(cron = "0 0 9 * * ?") |
||||
|
//@Scheduled(cron = "0 */1 * * * ?") |
||||
|
@Scheduled(cron = "0 0 9 * * ?") |
||||
|
public void dailyCertificateCheck() { |
||||
|
/* log.info("开始检查证书过期情况...");*/ |
||||
|
try { |
||||
|
List<VetCertificate> certificates = vetCertificateMapper.selectVetCertificateList(new VetCertificate()); |
||||
|
certificates.forEach(this::checkAndSendReminder); |
||||
|
/* log.info("证书检查完成,共检查{}个证书", certificates.size());*/ |
||||
|
} catch (Exception e) { |
||||
|
/*log.error("证书检查任务执行失败", e);*/ |
||||
|
} |
||||
|
} |
||||
|
|
||||
/** |
/** |
||||
* 每天凌晨2点检查证书过期情况 |
|
||||
|
* 检查单个证书并发送提醒 |
||||
*/ |
*/ |
||||
/* @Scheduled(cron = "0 0 2 * * ?")*/ |
|
||||
@Scheduled(cron = "0 */1 * * * ?") |
|
||||
public void dailyCertificateCheck() { |
|
||||
/*log.info("开始执行每日证书检查任务...");*/ |
|
||||
|
private void checkAndSendReminder(VetCertificate certificate) { |
||||
|
if (certificate == null || certificate.getExpireDate() == null) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
long daysRemaining = calculateDayDifference(new Date(), certificate.getExpireDate()); |
||||
|
|
||||
|
updateCertificateStatus(certificate, daysRemaining); |
||||
|
|
||||
|
if (!shouldSendReminder(certificate, daysRemaining) || hasRecentReminder(certificate, daysRemaining)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
sendReminder(certificate, daysRemaining); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发送提醒 |
||||
|
*/ |
||||
|
private void sendReminder(VetCertificate certificate, long daysRemaining) { |
||||
|
VetNotification notification = createBaseNotification(certificate); |
||||
|
|
||||
|
if (daysRemaining <= 0) { |
||||
|
// 只发送过期当天的提醒 |
||||
|
if (daysRemaining == 0) { |
||||
|
setExpiredContent(notification, certificate, 0); |
||||
|
notification.setRemindLevel(3); // 最高级别 |
||||
|
vetNotificationMapper.insertVetNotification(notification); |
||||
|
} |
||||
|
// 过期后不再提醒 |
||||
|
return; |
||||
|
} else if (daysRemaining <= 7) { |
||||
|
setCountdownContent(notification, certificate, daysRemaining); |
||||
|
notification.setRemindLevel(daysRemaining <= 3 ? 3 : 2); |
||||
|
} else if (daysRemaining == 15 || daysRemaining == 30) { |
||||
|
setPreExpireContent(notification, certificate, daysRemaining); |
||||
|
notification.setRemindLevel(daysRemaining == 30 ? 1 : 2); |
||||
|
} else { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
vetNotificationMapper.insertVetNotification(notification); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 创建基础通知对象 |
||||
|
*/ |
||||
|
private VetNotification createBaseNotification(VetCertificate certificate) { |
||||
|
VetNotification notification = new VetNotification(); |
||||
|
notification.setUserId(certificate.getUserId()); |
||||
|
notification.setRelatedId(certificate.getId().toString()); |
||||
|
notification.setType("CERT_EXPIRE_REMIND"); |
||||
|
notification.setIsRead(0); |
||||
|
notification.setCreateTime(new Date()); |
||||
|
return notification; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置过期内容 |
||||
|
*/ |
||||
|
private void setExpiredContent(VetNotification notification, VetCertificate certificate, long daysExpired) { |
||||
|
if (daysExpired == 0) { |
||||
|
notification.setTitle("🚨 证书今天过期!"); |
||||
|
notification.setContent(String.format("您的《%s》证书今天已过期!请立即更新。", certificate.getCertName())); |
||||
|
} |
||||
|
// 移除过期多天的提醒 |
||||
|
} |
||||
|
/** |
||||
|
* 设置倒计时内容 |
||||
|
*/ |
||||
|
private void setCountdownContent(VetNotification notification, VetCertificate certificate, long daysRemaining) { |
||||
|
notification.setTitle("⚠️ 证书还剩" + daysRemaining + "天过期"); |
||||
|
notification.setContent(String.format("您的《%s》证书还剩%d天过期,请及时更新。", certificate.getCertName(), daysRemaining)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置预过期内容 |
||||
|
*/ |
||||
|
private void setPreExpireContent(VetNotification notification, VetCertificate certificate, long daysRemaining) { |
||||
|
String timeText = daysRemaining == 30 ? "30天" : "15天"; |
||||
|
notification.setTitle("📅 证书还剩" + timeText + "过期"); |
||||
|
notification.setContent(String.format("您的《%s》证书将在%s后过期,请提前准备更新。", certificate.getCertName(), timeText)); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断是否需要发送提醒 |
||||
|
*/ |
||||
|
private boolean shouldSendReminder(VetCertificate certificate, long daysRemaining) { |
||||
|
String status = certificate.getStatus(); |
||||
|
|
||||
|
if ("2".equals(status)) { // 已过期 |
||||
|
// 只在过期的第一天提醒(daysRemaining = 0 或 刚刚过期时) |
||||
|
return daysRemaining == 0; // 只提醒过期当天 |
||||
|
} |
||||
|
|
||||
|
if ("1".equals(status)) { // 即将过期 |
||||
|
return daysRemaining == 30 || daysRemaining == 15 || (daysRemaining >= 1 && daysRemaining <= 7); |
||||
|
} |
||||
|
|
||||
|
if ("0".equals(status)) { // 正常 |
||||
|
return daysRemaining == 30 || daysRemaining == 15; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 检查最近是否已发送过提醒 |
||||
|
*/ |
||||
|
private boolean hasRecentReminder(VetCertificate certificate, long daysRemaining) { |
||||
try { |
try { |
||||
vetCertificateService.checkAndSendCertificateReminders(); |
|
||||
/* log.info("每日证书检查任务执行完成");*/ |
|
||||
|
Calendar cal = Calendar.getInstance(); |
||||
|
cal.set(Calendar.HOUR_OF_DAY, 0); |
||||
|
cal.set(Calendar.MINUTE, 0); |
||||
|
cal.set(Calendar.SECOND, 0); |
||||
|
cal.set(Calendar.MILLISECOND, 0); |
||||
|
Date startOfDay = cal.getTime(); |
||||
|
|
||||
|
cal.add(Calendar.DAY_OF_MONTH, 1); |
||||
|
Date endOfDay = cal.getTime(); |
||||
|
|
||||
|
// 检查今天是否已发送 |
||||
|
VetNotification query = new VetNotification(); |
||||
|
query.setRelatedId(certificate.getId().toString()); |
||||
|
query.setType("CERT_EXPIRE_REMIND"); |
||||
|
|
||||
|
List<VetNotification> notifications = vetNotificationMapper.selectVetNotificationList(query); |
||||
|
|
||||
|
// 获取今天的所有通知 |
||||
|
List<VetNotification> todayNotifications = notifications.stream() |
||||
|
.filter(n -> n.getCreateTime() != null && |
||||
|
n.getCreateTime().after(startOfDay) && |
||||
|
n.getCreateTime().before(endOfDay)) |
||||
|
.toList(); |
||||
|
|
||||
|
// 如果没有今天的通知,直接返回 |
||||
|
if (todayNotifications.isEmpty()) { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
// 生成当前应该发送的通知标题 |
||||
|
String expectedTitle = generateExpectedTitle(certificate, daysRemaining); |
||||
|
|
||||
|
// 检查今天是否已有相同标题的通知 |
||||
|
boolean hasSameTitleToday = todayNotifications.stream() |
||||
|
.anyMatch(n -> expectedTitle.equals(n.getTitle())); |
||||
|
|
||||
|
if (hasSameTitleToday) { |
||||
|
// 如果今天已有相同标题的通知,跳过 |
||||
|
/*log.debug("今天已发送过相同提醒,跳过: 证书ID={}, 标题={}", |
||||
|
certificate.getId(), expectedTitle);*/ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 已过期证书检查最近7天是否有相同标题的通知 |
||||
|
if (daysRemaining <= 0) { |
||||
|
cal.add(Calendar.DAY_OF_MONTH, -7); |
||||
|
Date sevenDaysAgo = cal.getTime(); |
||||
|
|
||||
|
List<VetNotification> recentNotifications = notifications.stream() |
||||
|
.filter(n -> n.getCreateTime() != null && |
||||
|
n.getCreateTime().after(sevenDaysAgo)) |
||||
|
.toList(); |
||||
|
|
||||
|
boolean hasSameTitleRecent = recentNotifications.stream() |
||||
|
.anyMatch(n -> expectedTitle.equals(n.getTitle())); |
||||
|
|
||||
|
if (hasSameTitleRecent) { |
||||
|
/*log.debug("最近7天内已发送过相同提醒,跳过: 证书ID={}", certificate.getId());*/ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
} catch (Exception e) { |
} catch (Exception e) { |
||||
log.error("每日证书检查任务执行失败", e); |
|
||||
|
/*log.warn("检查近期提醒失败: {}", e.getMessage());*/ |
||||
|
return false; |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
/* *//** |
|
||||
* 每小时检查一次(可选,用于及时更新状态) |
|
||||
*//* |
|
||||
@Scheduled(cron = "0 0 * * * ?") |
|
||||
public void hourlyCertificateCheck() { |
|
||||
log.debug("执行每小时证书状态检查..."); |
|
||||
|
/** |
||||
|
* 生成预期的通知标题 |
||||
|
*/ |
||||
|
private String generateExpectedTitle(VetCertificate certificate, long daysRemaining) { |
||||
|
if (daysRemaining <= 0) { |
||||
|
long daysExpired = -daysRemaining; |
||||
|
if (daysExpired == 0) { |
||||
|
return "🚨 证书今天过期!"; |
||||
|
} else { |
||||
|
return "🚨 证书已过期" + daysExpired + "天"; |
||||
|
} |
||||
|
} else if (daysRemaining <= 7) { |
||||
|
return "⚠️ 证书还剩" + daysRemaining + "天过期"; |
||||
|
} else if (daysRemaining == 15 || daysRemaining == 30) { |
||||
|
return "📅 证书还剩" + daysRemaining + "天过期"; |
||||
|
} else { |
||||
|
return ""; // 其他情况不发送 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 更新证书状态 |
||||
|
*/ |
||||
|
private void updateCertificateStatus(VetCertificate certificate, long daysRemaining) { |
||||
try { |
try { |
||||
// 更新所有证书状态 |
|
||||
vetCertificateService.selectVetCertificateList(new VetCertificate()) |
|
||||
.forEach(cert -> { |
|
||||
// 这里可以调用更新状态的方法 |
|
||||
}); |
|
||||
|
String newStatus = daysRemaining <= 0 ? "2" : |
||||
|
daysRemaining <= 30 ? "1" : "0"; |
||||
|
|
||||
|
if (!newStatus.equals(certificate.getStatus())) { |
||||
|
certificate.setStatus(newStatus); |
||||
|
/*log.debug("更新证书状态: 证书ID={}, 新状态={}", certificate.getId(), newStatus);*/ |
||||
|
} |
||||
} catch (Exception e) { |
} catch (Exception e) { |
||||
log.error("每小时证书检查失败", e); |
|
||||
|
/*log.warn("更新证书状态失败: {}", e.getMessage());*/ |
||||
} |
} |
||||
}*/ |
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算天数差(忽略时间部分) |
||||
|
*/ |
||||
|
private long calculateDayDifference(Date startDate, Date endDate) { |
||||
|
Calendar startCal = Calendar.getInstance(); |
||||
|
startCal.setTime(startDate); |
||||
|
resetTime(startCal); |
||||
|
|
||||
|
Calendar endCal = Calendar.getInstance(); |
||||
|
endCal.setTime(endDate); |
||||
|
resetTime(endCal); |
||||
|
|
||||
|
return (endCal.getTimeInMillis() - startCal.getTimeInMillis()) / (1000 * 60 * 60 * 24); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 重置时间为00:00:00 |
||||
|
*/ |
||||
|
private void resetTime(Calendar cal) { |
||||
|
cal.set(Calendar.HOUR_OF_DAY, 0); |
||||
|
cal.set(Calendar.MINUTE, 0); |
||||
|
cal.set(Calendar.SECOND, 0); |
||||
|
cal.set(Calendar.MILLISECOND, 0); |
||||
|
} |
||||
} |
} |
||||
@ -0,0 +1,117 @@ |
|||||
|
package com.chenhai.vet.domain; |
||||
|
|
||||
|
import org.apache.commons.lang3.builder.ToStringBuilder; |
||||
|
import org.apache.commons.lang3.builder.ToStringStyle; |
||||
|
import com.chenhai.common.annotation.Excel; |
||||
|
import com.chenhai.common.core.domain.BaseEntity; |
||||
|
|
||||
|
/** |
||||
|
* 兽医文章对象 vet_knowledge |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
* @date 2026-01-08 |
||||
|
*/ |
||||
|
public class VetKnowledge extends BaseEntity |
||||
|
{ |
||||
|
private static final long serialVersionUID = 1L; |
||||
|
|
||||
|
/** 主键 */ |
||||
|
private Long id; |
||||
|
|
||||
|
/** 文章标题 */ |
||||
|
@Excel(name = "文章标题") |
||||
|
private String title; |
||||
|
|
||||
|
/** 文章内容 */ |
||||
|
@Excel(name = "文章内容") |
||||
|
private String content; |
||||
|
|
||||
|
/** 文章分类(治疗防治/饲养管理/其他) */ |
||||
|
@Excel(name = "文章分类", readConverterExp = "治=疗防治/饲养管理/其他") |
||||
|
private String category; |
||||
|
|
||||
|
/** 检测出的敏感词 */ |
||||
|
@Excel(name = "检测出的敏感词") |
||||
|
private String sensitiveWords; |
||||
|
|
||||
|
/** 状态(0-待审核 1-已发布 2-敏感内容驳回) */ |
||||
|
@Excel(name = "状态", readConverterExp = "0=-待审核,1=-已发布,2=-敏感内容驳回") |
||||
|
private String status; |
||||
|
|
||||
|
public void setId(Long id) |
||||
|
{ |
||||
|
this.id = id; |
||||
|
} |
||||
|
|
||||
|
public Long getId() |
||||
|
{ |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
public void setTitle(String title) |
||||
|
{ |
||||
|
this.title = title; |
||||
|
} |
||||
|
|
||||
|
public String getTitle() |
||||
|
{ |
||||
|
return title; |
||||
|
} |
||||
|
|
||||
|
public void setContent(String content) |
||||
|
{ |
||||
|
this.content = content; |
||||
|
} |
||||
|
|
||||
|
public String getContent() |
||||
|
{ |
||||
|
return content; |
||||
|
} |
||||
|
|
||||
|
public void setCategory(String category) |
||||
|
{ |
||||
|
this.category = category; |
||||
|
} |
||||
|
|
||||
|
public String getCategory() |
||||
|
{ |
||||
|
return category; |
||||
|
} |
||||
|
|
||||
|
public void setSensitiveWords(String sensitiveWords) |
||||
|
{ |
||||
|
this.sensitiveWords = sensitiveWords; |
||||
|
} |
||||
|
|
||||
|
public String getSensitiveWords() |
||||
|
{ |
||||
|
return sensitiveWords; |
||||
|
} |
||||
|
|
||||
|
public void setStatus(String status) |
||||
|
{ |
||||
|
this.status = status; |
||||
|
} |
||||
|
|
||||
|
public String getStatus() |
||||
|
{ |
||||
|
return status; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) |
||||
|
.append("id", getId()) |
||||
|
.append("title", getTitle()) |
||||
|
.append("content", getContent()) |
||||
|
.append("category", getCategory()) |
||||
|
.append("sensitiveWords", getSensitiveWords()) |
||||
|
.append("status", getStatus()) |
||||
|
.append("createBy", getCreateBy()) |
||||
|
.append("createTime", getCreateTime()) |
||||
|
.append("updateBy", getUpdateBy()) |
||||
|
.append("updateTime", getUpdateTime()) |
||||
|
.append("remark", getRemark()) |
||||
|
.toString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
package com.chenhai.vet.domain; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
import java.util.Date; |
||||
|
|
||||
|
@Data |
||||
|
public class VetTrainingVideo { |
||||
|
private Long id; |
||||
|
private Long userId; |
||||
|
private String title; |
||||
|
private String description; |
||||
|
private String videoUrl; |
||||
|
private String coverImage; |
||||
|
private String category; |
||||
|
private String tags; |
||||
|
private Integer duration; // 视频时长(秒) |
||||
|
private Long fileSize; // 文件大小(字节) |
||||
|
private Integer viewCount; |
||||
|
private String status; // 0-私有 1-公开 |
||||
|
private Date createTime; |
||||
|
private Date updateTime; |
||||
|
|
||||
|
// 非数据库字段 |
||||
|
private String userName; // 兽医姓名 |
||||
|
private String durationStr; // 格式化后的时长(如:12:30) |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
package com.chenhai.vet.mapper; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import com.chenhai.vet.domain.VetKnowledge; |
||||
|
|
||||
|
/** |
||||
|
* 兽医文章Mapper接口 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
* @date 2026-01-08 |
||||
|
*/ |
||||
|
public interface VetKnowledgeMapper |
||||
|
{ |
||||
|
/** |
||||
|
* 查询兽医文章 |
||||
|
* |
||||
|
* @param id 兽医文章主键 |
||||
|
* @return 兽医文章 |
||||
|
*/ |
||||
|
public VetKnowledge selectVetKnowledgeById(Long id); |
||||
|
|
||||
|
/** |
||||
|
* 查询兽医文章列表 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 兽医文章集合 |
||||
|
*/ |
||||
|
public List<VetKnowledge> selectVetKnowledgeList(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 新增兽医文章 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int insertVetKnowledge(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 修改兽医文章 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int updateVetKnowledge(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 删除兽医文章 |
||||
|
* |
||||
|
* @param id 兽医文章主键 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int deleteVetKnowledgeById(Long id); |
||||
|
|
||||
|
/** |
||||
|
* 批量删除兽医文章 |
||||
|
* |
||||
|
* @param ids 需要删除的数据主键集合 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int deleteVetKnowledgeByIds(Long[] ids); |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
package com.chenhai.vet.mapper; |
||||
|
|
||||
|
import com.chenhai.vet.domain.VetTrainingVideo; |
||||
|
import org.apache.ibatis.annotations.Param; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface VetTrainingVideoMapper { |
||||
|
|
||||
|
int insertVideo(VetTrainingVideo video); |
||||
|
|
||||
|
List<VetTrainingVideo> selectMyVideos(@Param("userId") Long userId, |
||||
|
@Param("title") String title, |
||||
|
@Param("category") String category, |
||||
|
@Param("status") String status); |
||||
|
|
||||
|
List<VetTrainingVideo> selectPublicVideos(@Param("title") String title, |
||||
|
@Param("category") String category, |
||||
|
@Param("userName") String userName); |
||||
|
|
||||
|
VetTrainingVideo selectVideoById(Long id); |
||||
|
|
||||
|
void incrementViewCount(Long id); |
||||
|
|
||||
|
List<VetTrainingVideo> selectHotVideos(@Param("limit") Integer limit); |
||||
|
|
||||
|
List<VetTrainingVideo> searchVideos(@Param("keyword") String keyword); |
||||
|
int deleteVideoById(@Param("id") Long id); |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
package com.chenhai.vet.service; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.vet.domain.VetKnowledge; |
||||
|
|
||||
|
/** |
||||
|
* 兽医文章Service接口 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
* @date 2026-01-08 |
||||
|
*/ |
||||
|
public interface IVetKnowledgeService |
||||
|
{ |
||||
|
/** |
||||
|
* 查询兽医文章 |
||||
|
* |
||||
|
* @param id 兽医文章主键 |
||||
|
* @return 兽医文章 |
||||
|
*/ |
||||
|
public VetKnowledge selectVetKnowledgeById(Long id); |
||||
|
|
||||
|
/** |
||||
|
* 查询兽医文章列表 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 兽医文章集合 |
||||
|
*/ |
||||
|
public List<VetKnowledge> selectVetKnowledgeList(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 新增兽医文章 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int insertVetKnowledge(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 修改兽医文章 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int updateVetKnowledge(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 批量删除兽医文章 |
||||
|
* |
||||
|
* @param ids 需要删除的兽医文章主键集合 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int deleteVetKnowledgeByIds(Long[] ids); |
||||
|
|
||||
|
/** |
||||
|
* 删除兽医文章信息 |
||||
|
* |
||||
|
* @param id 兽医文章主键 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
public int deleteVetKnowledgeById(Long id); |
||||
|
|
||||
|
/** |
||||
|
* 上传文章(待审核) |
||||
|
*/ |
||||
|
AjaxResult uploadVetKnowledge(VetKnowledge vetKnowledge); |
||||
|
|
||||
|
/** |
||||
|
* 发布文章(更新状态为已发布) |
||||
|
*/ |
||||
|
AjaxResult publishVetKnowledge(Long id); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
package com.chenhai.vet.service; |
||||
|
|
||||
|
import com.chenhai.vet.domain.VetTrainingVideo; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface IVetTrainingVideoService { |
||||
|
|
||||
|
/** |
||||
|
* 上传并保存视频 |
||||
|
*/ |
||||
|
String uploadAndSave(VetTrainingVideo video, MultipartFile videoFile, MultipartFile coverImage); |
||||
|
|
||||
|
/** |
||||
|
* 获取我的视频列表 |
||||
|
*/ |
||||
|
List<VetTrainingVideo> getMyVideos(Long userId, String title, String category, String status); |
||||
|
|
||||
|
/** |
||||
|
* 获取公开视频列表 |
||||
|
*/ |
||||
|
List<VetTrainingVideo> getPublicVideos(String title, String category, String vetName); |
||||
|
|
||||
|
/** |
||||
|
* 获取视频详情(带权限校验) |
||||
|
*/ |
||||
|
VetTrainingVideo getVideoDetail(Long videoId, Long currentVetId); |
||||
|
|
||||
|
/** |
||||
|
* 获取视频播放地址(带权限校验) |
||||
|
*/ |
||||
|
String getVideoPlayUrl(Long videoId, Long currentVetId); |
||||
|
|
||||
|
/** |
||||
|
* 增加观看次数 |
||||
|
*/ |
||||
|
void incrementViewCount(Long videoId); |
||||
|
|
||||
|
/** |
||||
|
* 获取热门视频 |
||||
|
*/ |
||||
|
List<VetTrainingVideo> getHotVideos(Integer limit); |
||||
|
|
||||
|
/** |
||||
|
* 搜索视频 |
||||
|
*/ |
||||
|
List<VetTrainingVideo> searchVideos(String keyword); |
||||
|
|
||||
|
int deleteVideoById(Long videoId); |
||||
|
} |
||||
@ -0,0 +1,134 @@ |
|||||
|
package com.chenhai.vet.service.impl; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
import com.chenhai.common.core.domain.AjaxResult; |
||||
|
import com.chenhai.common.utils.DateUtils; |
||||
|
import com.chenhai.common.utils.SecurityUtils; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import com.chenhai.vet.mapper.VetKnowledgeMapper; |
||||
|
import com.chenhai.vet.domain.VetKnowledge; |
||||
|
import com.chenhai.vet.service.IVetKnowledgeService; |
||||
|
|
||||
|
/** |
||||
|
* 兽医文章Service业务层处理 |
||||
|
* |
||||
|
* @author ruoyi |
||||
|
* @date 2026-01-08 |
||||
|
*/ |
||||
|
@Service |
||||
|
public class VetKnowledgeServiceImpl implements IVetKnowledgeService |
||||
|
{ |
||||
|
@Autowired |
||||
|
private VetKnowledgeMapper vetKnowledgeMapper; |
||||
|
|
||||
|
/** |
||||
|
* 查询兽医文章 |
||||
|
* |
||||
|
* @param id 兽医文章主键 |
||||
|
* @return 兽医文章 |
||||
|
*/ |
||||
|
@Override |
||||
|
public VetKnowledge selectVetKnowledgeById(Long id) |
||||
|
{ |
||||
|
return vetKnowledgeMapper.selectVetKnowledgeById(id); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 查询兽医文章列表 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 兽医文章 |
||||
|
*/ |
||||
|
@Override |
||||
|
public List<VetKnowledge> selectVetKnowledgeList(VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
return vetKnowledgeMapper.selectVetKnowledgeList(vetKnowledge); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 新增兽医文章 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
@Override |
||||
|
public int insertVetKnowledge(VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
vetKnowledge.setCreateTime(DateUtils.getNowDate()); |
||||
|
return vetKnowledgeMapper.insertVetKnowledge(vetKnowledge); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 修改兽医文章 |
||||
|
* |
||||
|
* @param vetKnowledge 兽医文章 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
@Override |
||||
|
public int updateVetKnowledge(VetKnowledge vetKnowledge) |
||||
|
{ |
||||
|
vetKnowledge.setUpdateTime(DateUtils.getNowDate()); |
||||
|
return vetKnowledgeMapper.updateVetKnowledge(vetKnowledge); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 批量删除兽医文章 |
||||
|
* |
||||
|
* @param ids 需要删除的兽医文章主键 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
@Override |
||||
|
public int deleteVetKnowledgeByIds(Long[] ids) |
||||
|
{ |
||||
|
return vetKnowledgeMapper.deleteVetKnowledgeByIds(ids); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 删除兽医文章信息 |
||||
|
* |
||||
|
* @param id 兽医文章主键 |
||||
|
* @return 结果 |
||||
|
*/ |
||||
|
@Override |
||||
|
public int deleteVetKnowledgeById(Long id) |
||||
|
{ |
||||
|
return vetKnowledgeMapper.deleteVetKnowledgeById(id); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 上传文章:默认状态为0-待审核 |
||||
|
*/ |
||||
|
@Override |
||||
|
public AjaxResult uploadVetKnowledge(VetKnowledge vetKnowledge) { |
||||
|
// 填充基础信息 |
||||
|
vetKnowledge.setCreateBy(SecurityUtils.getUsername()); |
||||
|
vetKnowledge.setCreateTime(DateUtils.getNowDate()); |
||||
|
vetKnowledge.setStatus("0"); // 固定为待审核 |
||||
|
|
||||
|
int result = vetKnowledgeMapper.insertVetKnowledge(vetKnowledge); |
||||
|
if (result > 0) { |
||||
|
return AjaxResult.success("文章上传成功,等待审核"); |
||||
|
} |
||||
|
return AjaxResult.error("文章上传失败"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 发布文章:更新状态为1-已发布 |
||||
|
*/ |
||||
|
@Override |
||||
|
public AjaxResult publishVetKnowledge(Long id) { |
||||
|
VetKnowledge vetKnowledge = new VetKnowledge(); |
||||
|
vetKnowledge.setId(id); |
||||
|
vetKnowledge.setStatus("1"); // 已发布 |
||||
|
vetKnowledge.setUpdateBy(SecurityUtils.getUsername()); |
||||
|
vetKnowledge.setUpdateTime(DateUtils.getNowDate()); |
||||
|
|
||||
|
int result = vetKnowledgeMapper.updateVetKnowledge(vetKnowledge); |
||||
|
if (result > 0) { |
||||
|
return AjaxResult.success("文章发布成功"); |
||||
|
} |
||||
|
return AjaxResult.error("文章发布失败"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,146 @@ |
|||||
|
package com.chenhai.vet.service.impl; |
||||
|
|
||||
|
import com.chenhai.vet.domain.VetTrainingVideo; |
||||
|
import com.chenhai.vet.mapper.VetTrainingVideoMapper; |
||||
|
import com.chenhai.vet.service.IVetTrainingVideoService; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.web.multipart.MultipartFile; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.io.IOException; |
||||
|
import java.nio.file.Files; |
||||
|
import java.nio.file.Path; |
||||
|
import java.nio.file.Paths; |
||||
|
import java.util.Date; |
||||
|
import java.util.List; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Service |
||||
|
public class VetTrainingVideoServiceImpl implements IVetTrainingVideoService { |
||||
|
|
||||
|
@Autowired |
||||
|
private VetTrainingVideoMapper videoMapper; |
||||
|
|
||||
|
@Value("${file.upload.path:/uploads}") |
||||
|
private String uploadPath; |
||||
|
|
||||
|
@Override |
||||
|
public String uploadAndSave(VetTrainingVideo video, MultipartFile videoFile, MultipartFile coverImage) { |
||||
|
try { |
||||
|
// 1. 创建上传目录 |
||||
|
File uploadDir = new File(uploadPath); |
||||
|
if (!uploadDir.exists()) { |
||||
|
uploadDir.mkdirs(); |
||||
|
} |
||||
|
|
||||
|
// 2. 生成唯一文件名 |
||||
|
String originalFileName = videoFile.getOriginalFilename(); |
||||
|
String fileExtension = getFileExtension(originalFileName); |
||||
|
String uniqueFileName = UUID.randomUUID().toString() + "." + fileExtension; |
||||
|
|
||||
|
// 3. 保存视频文件 |
||||
|
Path videoPath = Paths.get(uploadPath, uniqueFileName); |
||||
|
Files.write(videoPath, videoFile.getBytes()); |
||||
|
|
||||
|
// 4. 保存封面图(如果有) |
||||
|
String coverImageUrl = null; |
||||
|
if (coverImage != null && !coverImage.isEmpty()) { |
||||
|
String coverExtension = getFileExtension(coverImage.getOriginalFilename()); |
||||
|
String coverFileName = "cover_" + UUID.randomUUID().toString() + "." + coverExtension; |
||||
|
Path coverPath = Paths.get(uploadPath, coverFileName); |
||||
|
Files.write(coverPath, coverImage.getBytes()); |
||||
|
coverImageUrl = "/uploads/" + coverFileName; |
||||
|
} |
||||
|
|
||||
|
// 5. 计算视频时长和大小 |
||||
|
int duration = getVideoDuration(videoFile); // 需要实现这个方法 |
||||
|
long fileSize = videoFile.getSize(); |
||||
|
|
||||
|
// 6. 保存到数据库 |
||||
|
video.setVideoUrl("/uploads/" + uniqueFileName); |
||||
|
video.setCoverImage(coverImageUrl); |
||||
|
video.setDuration(duration); |
||||
|
video.setFileSize(fileSize); |
||||
|
video.setViewCount(0); |
||||
|
video.setCreateTime(new Date()); |
||||
|
video.setUpdateTime(new Date()); |
||||
|
|
||||
|
videoMapper.insertVideo(video); |
||||
|
|
||||
|
return "上传成功!视频ID:" + video.getId(); |
||||
|
|
||||
|
} catch (IOException e) { |
||||
|
throw new RuntimeException("文件保存失败", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<VetTrainingVideo> getMyVideos(Long userId, String title, String category, String status) { |
||||
|
return videoMapper.selectMyVideos(userId, title, category, status); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<VetTrainingVideo> getPublicVideos(String title, String category, String vetName) { |
||||
|
return videoMapper.selectPublicVideos(title, category, vetName); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public VetTrainingVideo getVideoDetail(Long videoId, Long currentVetId) { |
||||
|
VetTrainingVideo video = videoMapper.selectVideoById(videoId); |
||||
|
|
||||
|
if (video == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 权限校验:只能查看公开视频或自己的视频 |
||||
|
boolean canView = "1".equals(video.getStatus()) || currentVetId.equals(video.getUserId()); |
||||
|
return canView ? video : null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public String getVideoPlayUrl(Long videoId, Long currentVetId) { |
||||
|
VetTrainingVideo video = videoMapper.selectVideoById(videoId); |
||||
|
|
||||
|
if (video == null) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
// 权限校验 |
||||
|
boolean canPlay = "1".equals(video.getStatus()) || currentVetId.equals(video.getUserId()); |
||||
|
return canPlay ? video.getVideoUrl() : null; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void incrementViewCount(Long videoId) { |
||||
|
videoMapper.incrementViewCount(videoId); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<VetTrainingVideo> getHotVideos(Integer limit) { |
||||
|
return videoMapper.selectHotVideos(limit); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<VetTrainingVideo> searchVideos(String keyword) { |
||||
|
return videoMapper.searchVideos(keyword); |
||||
|
} |
||||
|
|
||||
|
private String getFileExtension(String fileName) { |
||||
|
return fileName.substring(fileName.lastIndexOf(".") + 1); |
||||
|
} |
||||
|
|
||||
|
private int getVideoDuration(MultipartFile videoFile) { |
||||
|
// 这里需要实现获取视频时长的方法 |
||||
|
// 可以使用 FFmpeg 或 Java 的库来获取 |
||||
|
// 暂时返回一个默认值 |
||||
|
return 60; // 默认60秒 |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int deleteVideoById(Long videoId) { |
||||
|
// 逻辑删除,设置 del_flag = '1' |
||||
|
return videoMapper.deleteVideoById(videoId); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
<mapper namespace="com.chenhai.vet.mapper.VetKnowledgeMapper"> |
||||
|
|
||||
|
<resultMap type="VetKnowledge" id="VetKnowledgeResult"> |
||||
|
<result property="id" column="id" /> |
||||
|
<result property="title" column="title" /> |
||||
|
<result property="content" column="content" /> |
||||
|
<result property="category" column="category" /> |
||||
|
<result property="sensitiveWords" column="sensitive_words" /> |
||||
|
<result property="status" column="status" /> |
||||
|
<result property="createBy" column="create_by" /> |
||||
|
<result property="createTime" column="create_time" /> |
||||
|
<result property="updateBy" column="update_by" /> |
||||
|
<result property="updateTime" column="update_time" /> |
||||
|
<result property="remark" column="remark" /> |
||||
|
</resultMap> |
||||
|
|
||||
|
<sql id="selectVetKnowledgeVo"> |
||||
|
select id, title, content, category, sensitive_words, status, create_by, create_time, update_by, update_time, remark from vet_knowledge |
||||
|
</sql> |
||||
|
|
||||
|
<select id="selectVetKnowledgeList" parameterType="VetKnowledge" resultMap="VetKnowledgeResult"> |
||||
|
<include refid="selectVetKnowledgeVo"/> |
||||
|
<where> |
||||
|
<if test="title != null and title != ''"> and title = #{title}</if> |
||||
|
<if test="content != null and content != ''"> and content = #{content}</if> |
||||
|
<if test="category != null and category != ''"> and category = #{category}</if> |
||||
|
<if test="sensitiveWords != null and sensitiveWords != ''"> and sensitive_words = #{sensitiveWords}</if> |
||||
|
<if test="status != null and status != ''"> and status = #{status}</if> |
||||
|
</where> |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectVetKnowledgeById" parameterType="Long" resultMap="VetKnowledgeResult"> |
||||
|
<include refid="selectVetKnowledgeVo"/> |
||||
|
where id = #{id} |
||||
|
</select> |
||||
|
|
||||
|
<insert id="insertVetKnowledge" parameterType="VetKnowledge" useGeneratedKeys="true" keyProperty="id"> |
||||
|
insert into vet_knowledge |
||||
|
<trim prefix="(" suffix=")" suffixOverrides=","> |
||||
|
<if test="title != null and title != ''">title,</if> |
||||
|
<if test="content != null and content != ''">content,</if> |
||||
|
<if test="category != null and category != ''">category,</if> |
||||
|
<if test="sensitiveWords != null">sensitive_words,</if> |
||||
|
<if test="status != null">status,</if> |
||||
|
<if test="createBy != null">create_by,</if> |
||||
|
<if test="createTime != null">create_time,</if> |
||||
|
<if test="updateBy != null">update_by,</if> |
||||
|
<if test="updateTime != null">update_time,</if> |
||||
|
<if test="remark != null">remark,</if> |
||||
|
</trim> |
||||
|
<trim prefix="values (" suffix=")" suffixOverrides=","> |
||||
|
<if test="title != null and title != ''">#{title},</if> |
||||
|
<if test="content != null and content != ''">#{content},</if> |
||||
|
<if test="category != null and category != ''">#{category},</if> |
||||
|
<if test="sensitiveWords != null">#{sensitiveWords},</if> |
||||
|
<if test="status != null">#{status},</if> |
||||
|
<if test="createBy != null">#{createBy},</if> |
||||
|
<if test="createTime != null">#{createTime},</if> |
||||
|
<if test="updateBy != null">#{updateBy},</if> |
||||
|
<if test="updateTime != null">#{updateTime},</if> |
||||
|
<if test="remark != null">#{remark},</if> |
||||
|
</trim> |
||||
|
</insert> |
||||
|
|
||||
|
<update id="updateVetKnowledge" parameterType="VetKnowledge"> |
||||
|
update vet_knowledge |
||||
|
<trim prefix="SET" suffixOverrides=","> |
||||
|
<if test="title != null and title != ''">title = #{title},</if> |
||||
|
<if test="content != null and content != ''">content = #{content},</if> |
||||
|
<if test="category != null and category != ''">category = #{category},</if> |
||||
|
<if test="sensitiveWords != null">sensitive_words = #{sensitiveWords},</if> |
||||
|
<if test="status != null">status = #{status},</if> |
||||
|
<if test="createBy != null">create_by = #{createBy},</if> |
||||
|
<if test="createTime != null">create_time = #{createTime},</if> |
||||
|
<if test="updateBy != null">update_by = #{updateBy},</if> |
||||
|
<if test="updateTime != null">update_time = #{updateTime},</if> |
||||
|
<if test="remark != null">remark = #{remark},</if> |
||||
|
</trim> |
||||
|
where id = #{id} |
||||
|
</update> |
||||
|
|
||||
|
<delete id="deleteVetKnowledgeById" parameterType="Long"> |
||||
|
delete from vet_knowledge where id = #{id} |
||||
|
</delete> |
||||
|
|
||||
|
<delete id="deleteVetKnowledgeByIds" parameterType="String"> |
||||
|
delete from vet_knowledge where id in |
||||
|
<foreach item="id" collection="array" open="(" separator="," close=")"> |
||||
|
#{id} |
||||
|
</foreach> |
||||
|
</delete> |
||||
|
</mapper> |
||||
@ -0,0 +1,110 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" ?> |
||||
|
<!DOCTYPE mapper |
||||
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
||||
|
<mapper namespace="com.chenhai.vet.mapper.VetTrainingVideoMapper"> |
||||
|
|
||||
|
<resultMap id="VideoResult" type="VetTrainingVideo"> |
||||
|
<id property="id" column="id"/> |
||||
|
<result property="userId" column="user_id"/> |
||||
|
<result property="title" column="title"/> |
||||
|
<result property="description" column="description"/> |
||||
|
<result property="videoUrl" column="video_url"/> |
||||
|
<result property="coverImage" column="cover_image"/> |
||||
|
<result property="category" column="category"/> |
||||
|
<result property="tags" column="tags"/> |
||||
|
<result property="duration" column="duration"/> |
||||
|
<result property="fileSize" column="file_size"/> |
||||
|
<result property="viewCount" column="view_count"/> |
||||
|
<result property="status" column="status"/> |
||||
|
<result property="createTime" column="create_time"/> |
||||
|
<result property="updateTime" column="update_time"/> |
||||
|
<result property="userName" column="user_name"/> |
||||
|
</resultMap> |
||||
|
|
||||
|
<insert id="insertVideo" parameterType="VetTrainingVideo" useGeneratedKeys="true" keyProperty="id"> |
||||
|
INSERT INTO vet_training_video ( |
||||
|
user_id, title, description, video_url, cover_image, |
||||
|
category, tags, duration, file_size, view_count, |
||||
|
status, create_time, update_time |
||||
|
) VALUES ( |
||||
|
#{vetId}, #{title}, #{description}, #{videoUrl}, #{coverImage}, |
||||
|
#{category}, #{tags}, #{duration}, #{fileSize}, #{viewCount}, |
||||
|
#{status}, #{createTime}, #{updateTime} |
||||
|
) |
||||
|
</insert> |
||||
|
|
||||
|
<select id="selectMyVideos" resultMap="VideoResult"> |
||||
|
SELECT v.*, u.nick_name as user_name |
||||
|
FROM vet_training_video v |
||||
|
LEFT JOIN sys_user u ON v.user_id = u.user_id |
||||
|
WHERE v.user_id = #{userId} |
||||
|
<if test="title != null and title != ''"> |
||||
|
AND v.title LIKE CONCAT('%', #{title}, '%') |
||||
|
</if> |
||||
|
<if test="category != null and category != ''"> |
||||
|
AND v.category = #{category} |
||||
|
</if> |
||||
|
<if test="status != null and status != ''"> |
||||
|
AND v.status = #{status} |
||||
|
</if> |
||||
|
ORDER BY v.create_time DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectPublicVideos" resultMap="VideoResult"> |
||||
|
SELECT v.*, u.nick_name as user_name |
||||
|
FROM vet_training_video v |
||||
|
LEFT JOIN sys_user u ON v.user_id = u.user_id |
||||
|
WHERE v.status = '1' |
||||
|
<if test="title != null and title != ''"> |
||||
|
AND v.title LIKE CONCAT('%', #{title}, '%') |
||||
|
</if> |
||||
|
<if test="category != null and category != ''"> |
||||
|
AND v.category = #{category} |
||||
|
</if> |
||||
|
<if test="userName != null and userName != ''"> |
||||
|
AND u.nick_name LIKE CONCAT('%', #{vetName}, '%') |
||||
|
</if> |
||||
|
ORDER BY v.create_time DESC |
||||
|
</select> |
||||
|
|
||||
|
<select id="selectVideoById" resultMap="VideoResult"> |
||||
|
SELECT v.*, u.nick_name as user_name |
||||
|
FROM vet_training_video v |
||||
|
LEFT JOIN sys_user u ON v.user_id = u.user_id |
||||
|
WHERE v.id = #{id} |
||||
|
</select> |
||||
|
|
||||
|
<update id="incrementViewCount"> |
||||
|
UPDATE vet_training_video |
||||
|
SET view_count = view_count + 1 |
||||
|
WHERE id = #{id} |
||||
|
</update> |
||||
|
|
||||
|
<select id="selectHotVideos" resultMap="VideoResult"> |
||||
|
SELECT v.*, u.nick_name as user_name |
||||
|
FROM vet_training_video v |
||||
|
LEFT JOIN sys_user u ON v.user_id = u.user_id |
||||
|
WHERE v.status = '1' |
||||
|
ORDER BY v.view_count DESC |
||||
|
LIMIT #{limit} |
||||
|
</select> |
||||
|
|
||||
|
<select id="searchVideos" resultMap="VideoResult"> |
||||
|
SELECT v.*, u.nick_name as user_name |
||||
|
FROM vet_training_video v |
||||
|
LEFT JOIN sys_user u ON v.user_id = u.user_id |
||||
|
WHERE v.status = '1' |
||||
|
AND (v.title LIKE CONCAT('%', #{keyword}, '%') |
||||
|
OR v.description LIKE CONCAT('%', #{keyword}, '%') |
||||
|
OR v.tags LIKE CONCAT('%', #{keyword}, '%') |
||||
|
OR u.nick_name LIKE CONCAT('%', #{keyword}, '%')) |
||||
|
ORDER BY v.create_time DESC |
||||
|
</select> |
||||
|
|
||||
|
<update id="deleteVideoById"> |
||||
|
UPDATE vet_training_video |
||||
|
SET del_flag = '1' |
||||
|
WHERE id = #{id} |
||||
|
</update> |
||||
|
</mapper> |
||||
@ -0,0 +1,60 @@ |
|||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// 查询兽医文章列表
|
||||
|
export function listKnowledge(query) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge/list', |
||||
|
method: 'get', |
||||
|
params: query |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 查询兽医文章详细
|
||||
|
export function getKnowledge(id) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge/' + id, |
||||
|
method: 'get' |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 新增兽医文章
|
||||
|
export function addKnowledge(data) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge', |
||||
|
method: 'post', |
||||
|
data: data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 修改兽医文章
|
||||
|
export function updateKnowledge(data) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge', |
||||
|
method: 'put', |
||||
|
data: data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 删除兽医文章
|
||||
|
export function delKnowledge(id) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge/' + id, |
||||
|
method: 'delete' |
||||
|
}) |
||||
|
} |
||||
|
// 上传文章(待审核)
|
||||
|
export function uploadKnowledge(data) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge/upload', |
||||
|
method: 'post', |
||||
|
data: data |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// 发布文章
|
||||
|
export function publishKnowledge(id) { |
||||
|
return request({ |
||||
|
url: '/vet/knowledge/publish/' + id, |
||||
|
method: 'put' |
||||
|
}) |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
// src/api/vet/training.js
|
||||
|
import request from '@/utils/request' |
||||
|
|
||||
|
// 兽医培训视频相关接口
|
||||
|
export default { |
||||
|
// 上传视频
|
||||
|
uploadVideo(data) { |
||||
|
const formData = new FormData() |
||||
|
Object.keys(data).forEach(key => { |
||||
|
if (data[key] !== undefined && data[key] !== null) { |
||||
|
if (key === 'videoFile' || key === 'coverImage') { |
||||
|
formData.append(key, data[key]) |
||||
|
} else { |
||||
|
formData.append(key, data[key]) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
return request({ |
||||
|
url: '/vet/training/upload', |
||||
|
method: 'post', |
||||
|
data: formData, |
||||
|
headers: { |
||||
|
'Content-Type': 'multipart/form-data' |
||||
|
}, |
||||
|
timeout: 300000 // 5分钟超时,视频上传需要时间
|
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取公开视频列表
|
||||
|
getPublicVideos(params) { |
||||
|
return request({ |
||||
|
url: '/vet/training/public-videos', |
||||
|
method: 'get', |
||||
|
params |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取我的视频
|
||||
|
getMyVideos(params = {}) { |
||||
|
return request({ |
||||
|
url: '/vet/training/my-videos', |
||||
|
method: 'get', |
||||
|
params: { |
||||
|
pageNum: params.pageNum || 1, |
||||
|
pageSize: params.pageSize || 10, |
||||
|
title: params.title || '', |
||||
|
category: params.category || '', |
||||
|
status: params.status || '' |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取视频详情
|
||||
|
getVideoDetail(id) { |
||||
|
return request({ |
||||
|
url: `/vet/training/video/${id}`, |
||||
|
method: 'get' |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取播放地址(可选)
|
||||
|
getPlayUrl(id) { |
||||
|
return request({ |
||||
|
url: `/vet/training/video/play/${id}`, |
||||
|
method: 'get' |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 删除视频
|
||||
|
deleteVideo(id) { |
||||
|
return request({ |
||||
|
url: `/vet/training/${id}`, |
||||
|
method: 'delete' |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 搜索视频
|
||||
|
searchVideos(keyword) { |
||||
|
return request({ |
||||
|
url: '/vet/training/search', |
||||
|
method: 'get', |
||||
|
params: { keyword } |
||||
|
}) |
||||
|
}, |
||||
|
|
||||
|
// 获取热门视频
|
||||
|
getHotVideos(limit = 10) { |
||||
|
return request({ |
||||
|
url: '/vet/training/hot-videos', |
||||
|
method: 'get', |
||||
|
params: { limit } |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,356 @@ |
|||||
|
<template> |
||||
|
<div class="app-container"> |
||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> |
||||
|
<el-form-item label="文章标题" prop="title"> |
||||
|
<el-input |
||||
|
v-model="queryParams.title" |
||||
|
placeholder="请输入文章标题" |
||||
|
clearable |
||||
|
@keyup.enter.native="handleQuery" |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="文章分类" prop="category"> |
||||
|
<!-- 优化:分类改为下拉选择,避免手动输入不规范 --> |
||||
|
<el-select |
||||
|
v-model="queryParams.category" |
||||
|
placeholder="请选择文章分类" |
||||
|
clearable |
||||
|
@keyup.enter.native="handleQuery" |
||||
|
> |
||||
|
<el-option label="治疗防治" value="治疗防治" /> |
||||
|
<el-option label="饲养管理" value="饲养管理" /> |
||||
|
<el-option label="其他" value="其他" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="状态" prop="status"> |
||||
|
<el-select |
||||
|
v-model="queryParams.status" |
||||
|
placeholder="请选择状态" |
||||
|
clearable |
||||
|
@keyup.enter.native="handleQuery" |
||||
|
> |
||||
|
<el-option label="待审核" value="0" /> |
||||
|
<el-option label="已发布" value="1" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item> |
||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> |
||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
|
||||
|
<el-row :gutter="10" class="mb8"> |
||||
|
<el-col :span="1.5"> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
plain |
||||
|
icon="el-icon-upload2" |
||||
|
size="mini" |
||||
|
@click="handleUpload" |
||||
|
v-hasPermi="['vet:knowledge:upload']" |
||||
|
>上传文章</el-button> |
||||
|
</el-col> |
||||
|
<el-col :span="1.5"> |
||||
|
<el-button |
||||
|
type="success" |
||||
|
plain |
||||
|
icon="el-icon-check" |
||||
|
size="mini" |
||||
|
:disabled="single" |
||||
|
@click="handlePublish" |
||||
|
v-hasPermi="['vet:knowledge:publish']" |
||||
|
>发布文章</el-button> |
||||
|
</el-col> |
||||
|
<el-col :span="1.5"> |
||||
|
<el-button |
||||
|
type="warning" |
||||
|
plain |
||||
|
icon="el-icon-edit" |
||||
|
size="mini" |
||||
|
:disabled="single" |
||||
|
@click="handleUpdate" |
||||
|
v-hasPermi="['vet:knowledge:edit']" |
||||
|
>修改</el-button> |
||||
|
</el-col> |
||||
|
<el-col :span="1.5"> |
||||
|
<el-button |
||||
|
type="danger" |
||||
|
plain |
||||
|
icon="el-icon-delete" |
||||
|
size="mini" |
||||
|
:disabled="multiple" |
||||
|
@click="handleDelete" |
||||
|
v-hasPermi="['vet:knowledge:remove']" |
||||
|
>删除</el-button> |
||||
|
</el-col> |
||||
|
<el-col :span="1.5"> |
||||
|
<el-button |
||||
|
type="info" |
||||
|
plain |
||||
|
icon="el-icon-download" |
||||
|
size="mini" |
||||
|
@click="handleExport" |
||||
|
v-hasPermi="['vet:knowledge:export']" |
||||
|
>导出</el-button> |
||||
|
</el-col> |
||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> |
||||
|
</el-row> |
||||
|
|
||||
|
<el-table v-loading="loading" :data="knowledgeList" @selection-change="handleSelectionChange"> |
||||
|
<el-table-column type="selection" width="55" align="center" /> |
||||
|
<el-table-column label="主键" align="center" prop="id" /> |
||||
|
<el-table-column label="文章标题" align="center" prop="title" min-width="200" /> |
||||
|
<el-table-column label="文章分类" align="center" prop="category" width="120" /> |
||||
|
<!-- 优化:状态显示为标签样式,更直观 --> |
||||
|
<el-table-column label="状态" align="center" prop="status" width="100"> |
||||
|
<template slot-scope="scope"> |
||||
|
<el-tag v-if="scope.row.status === '0'" type="warning">待审核</el-tag> |
||||
|
<el-tag v-if="scope.row.status === '1'" type="success">已发布</el-tag> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
<el-table-column label="创建人" align="center" prop="createBy" width="100" /> |
||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="180" /> |
||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> |
||||
|
<template slot-scope="scope"> |
||||
|
<el-button |
||||
|
size="mini" |
||||
|
type="text" |
||||
|
icon="el-icon-edit" |
||||
|
@click="handleUpdate(scope.row)" |
||||
|
v-hasPermi="['vet:knowledge:edit']" |
||||
|
>修改</el-button> |
||||
|
<el-button |
||||
|
size="mini" |
||||
|
type="text" |
||||
|
icon="el-icon-check" |
||||
|
@click="handlePublish(scope.row.id)" |
||||
|
v-hasPermi="['vet:knowledge:publish']" |
||||
|
v-if="scope.row.status === '0'" |
||||
|
>发布</el-button> |
||||
|
<el-button |
||||
|
size="mini" |
||||
|
type="text" |
||||
|
icon="el-icon-delete" |
||||
|
@click="handleDelete(scope.row)" |
||||
|
v-hasPermi="['vet:knowledge:remove']" |
||||
|
>删除</el-button> |
||||
|
</template> |
||||
|
</el-table-column> |
||||
|
</el-table> |
||||
|
|
||||
|
<pagination |
||||
|
v-show="total>0" |
||||
|
:total="total" |
||||
|
:page.sync="queryParams.pageNum" |
||||
|
:limit.sync="queryParams.pageSize" |
||||
|
@pagination="getList" |
||||
|
/> |
||||
|
|
||||
|
<!-- 添加/上传/修改兽医文章对话框 --> |
||||
|
<el-dialog :title="title" :visible.sync="open" width="700px" append-to-body> |
||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> |
||||
|
<el-form-item label="文章标题" prop="title"> |
||||
|
<el-input v-model="form.title" placeholder="请输入文章标题" /> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="文章内容" prop="content"> |
||||
|
<editor v-model="form.content" :min-height="200"/> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="文章分类" prop="category"> |
||||
|
<!-- 优化:分类下拉选择,固定选项 --> |
||||
|
<el-select v-model="form.category" placeholder="请选择文章分类"> |
||||
|
<el-option label="治疗防治" value="治疗防治" /> |
||||
|
<el-option label="饲养管理" value="饲养管理" /> |
||||
|
<el-option label="其他" value="其他" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
<el-form-item label="备注" prop="remark"> |
||||
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注信息" /> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
<div slot="footer" class="dialog-footer"> |
||||
|
<el-button type="primary" @click="submitForm">确 定</el-button> |
||||
|
<el-button @click="cancel">取 消</el-button> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
// 替换API:新增uploadKnowledge和publishKnowledge接口 |
||||
|
import { listKnowledge, getKnowledge, delKnowledge, addKnowledge, updateKnowledge, uploadKnowledge, publishKnowledge } from "@/api/vet/knowledge" |
||||
|
|
||||
|
export default { |
||||
|
name: "Knowledge", |
||||
|
data() { |
||||
|
return { |
||||
|
// 遮罩层 |
||||
|
loading: true, |
||||
|
// 选中数组 |
||||
|
ids: [], |
||||
|
// 非单个禁用 |
||||
|
single: true, |
||||
|
// 非多个禁用 |
||||
|
multiple: true, |
||||
|
// 显示搜索条件 |
||||
|
showSearch: true, |
||||
|
// 总条数 |
||||
|
total: 0, |
||||
|
// 兽医文章表格数据 |
||||
|
knowledgeList: [], |
||||
|
// 弹出层标题 |
||||
|
title: "", |
||||
|
// 是否显示弹出层 |
||||
|
open: false, |
||||
|
// 查询参数 |
||||
|
queryParams: { |
||||
|
pageNum: 1, |
||||
|
pageSize: 10, |
||||
|
title: null, |
||||
|
category: null, |
||||
|
status: null, // 仅保留状态查询,移除敏感词等无关字段 |
||||
|
}, |
||||
|
// 表单参数 |
||||
|
form: {}, |
||||
|
// 表单校验 |
||||
|
rules: { |
||||
|
title: [ |
||||
|
{ required: true, message: "文章标题不能为空", trigger: "blur" } |
||||
|
], |
||||
|
content: [ |
||||
|
{ required: true, message: "文章内容不能为空", trigger: "blur" } |
||||
|
], |
||||
|
category: [ |
||||
|
{ required: true, message: "文章分类不能为空", trigger: "change" } |
||||
|
], |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
created() { |
||||
|
this.getList() |
||||
|
}, |
||||
|
methods: { |
||||
|
/** 查询兽医文章列表 */ |
||||
|
getList() { |
||||
|
this.loading = true |
||||
|
listKnowledge(this.queryParams).then(response => { |
||||
|
this.knowledgeList = response.rows |
||||
|
this.total = response.total |
||||
|
this.loading = false |
||||
|
}) |
||||
|
}, |
||||
|
// 取消按钮 |
||||
|
cancel() { |
||||
|
this.open = false |
||||
|
this.reset() |
||||
|
}, |
||||
|
// 表单重置 |
||||
|
reset() { |
||||
|
this.form = { |
||||
|
id: null, |
||||
|
title: null, |
||||
|
content: null, |
||||
|
category: null, |
||||
|
status: null, // 状态由后端自动赋值,前端无需维护 |
||||
|
createBy: null, |
||||
|
createTime: null, |
||||
|
updateBy: null, |
||||
|
updateTime: null, |
||||
|
remark: null |
||||
|
} |
||||
|
this.resetForm("form") |
||||
|
}, |
||||
|
/** 搜索按钮操作 */ |
||||
|
handleQuery() { |
||||
|
this.queryParams.pageNum = 1 |
||||
|
this.getList() |
||||
|
}, |
||||
|
/** 重置按钮操作 */ |
||||
|
resetQuery() { |
||||
|
this.resetForm("queryForm") |
||||
|
this.queryParams = { |
||||
|
pageNum: 1, |
||||
|
pageSize: 10, |
||||
|
title: null, |
||||
|
category: null, |
||||
|
status: null, |
||||
|
} |
||||
|
this.handleQuery() |
||||
|
}, |
||||
|
// 多选框选中数据 |
||||
|
handleSelectionChange(selection) { |
||||
|
this.ids = selection.map(item => item.id) |
||||
|
this.single = selection.length!==1 |
||||
|
this.multiple = !selection.length |
||||
|
}, |
||||
|
/** 上传文章按钮操作(替代原新增) */ |
||||
|
handleUpload() { |
||||
|
this.reset() |
||||
|
this.open = true |
||||
|
this.title = "上传兽医文章" |
||||
|
}, |
||||
|
/** 发布文章按钮操作 */ |
||||
|
handlePublish(id) { |
||||
|
// 支持单条/批量发布 |
||||
|
const ids = id || this.ids |
||||
|
if (ids.length === 0) { |
||||
|
this.$modal.msgWarning("请选择需要发布的文章") |
||||
|
return |
||||
|
} |
||||
|
this.$modal.confirm('是否确认发布选中的文章?').then(() => { |
||||
|
return publishKnowledge(ids) |
||||
|
}).then(() => { |
||||
|
this.getList() |
||||
|
this.$modal.msgSuccess("发布成功") |
||||
|
}).catch(() => {}) |
||||
|
}, |
||||
|
/** 修改按钮操作 */ |
||||
|
handleUpdate(row) { |
||||
|
this.reset() |
||||
|
const id = row.id || this.ids |
||||
|
getKnowledge(id).then(response => { |
||||
|
this.form = response.data |
||||
|
this.open = true |
||||
|
this.title = "修改兽医文章" |
||||
|
}) |
||||
|
}, |
||||
|
/** 提交按钮(区分上传/修改) */ |
||||
|
submitForm() { |
||||
|
this.$refs["form"].validate(valid => { |
||||
|
if (valid) { |
||||
|
if (this.form.id != null) { |
||||
|
// 修改文章 |
||||
|
updateKnowledge(this.form).then(response => { |
||||
|
this.$modal.msgSuccess("修改成功") |
||||
|
this.open = false |
||||
|
this.getList() |
||||
|
}) |
||||
|
} else { |
||||
|
// 上传文章(新增) |
||||
|
uploadKnowledge(this.form).then(response => { |
||||
|
this.$modal.msgSuccess(response.msg || "上传成功") |
||||
|
this.open = false |
||||
|
this.getList() |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
}, |
||||
|
/** 删除按钮操作 */ |
||||
|
handleDelete(row) { |
||||
|
const ids = row.id || this.ids |
||||
|
this.$modal.confirm('是否确认删除兽医文章编号为"' + ids + '"的数据项?').then(() => { |
||||
|
return delKnowledge(ids) |
||||
|
}).then(() => { |
||||
|
this.getList() |
||||
|
this.$modal.msgSuccess("删除成功") |
||||
|
}).catch(() => {}) |
||||
|
}, |
||||
|
/** 导出按钮操作 */ |
||||
|
handleExport() { |
||||
|
this.download('vet/knowledge/export', { |
||||
|
...this.queryParams |
||||
|
}, `knowledge_${new Date().getTime()}.xlsx`) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
@ -0,0 +1,960 @@ |
|||||
|
<!-- src/views/vet/training/TrainingHome.vue --> |
||||
|
<template> |
||||
|
<div class="training-home"> |
||||
|
<!-- 顶部导航 --> |
||||
|
<div class="training-header"> |
||||
|
<h2><i class="el-icon-video-camera"></i> 兽医培训中心</h2> |
||||
|
<div class="header-actions"> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
icon="el-icon-upload" |
||||
|
@click="activeTab = 'upload'" |
||||
|
> |
||||
|
上传视频 |
||||
|
</el-button> |
||||
|
<el-button |
||||
|
plain |
||||
|
@click="activeTab = 'list'" |
||||
|
> |
||||
|
返回列表 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 主内容区 --> |
||||
|
<div class="training-main"> |
||||
|
<!-- 左侧菜单 --> |
||||
|
<div class="sidebar"> |
||||
|
<el-menu |
||||
|
:default-active="activeTab" |
||||
|
class="training-menu" |
||||
|
@select="handleMenuSelect" |
||||
|
> |
||||
|
<el-menu-item index="list"> |
||||
|
<i class="el-icon-video-camera"></i> |
||||
|
<span slot="title">视频列表</span> |
||||
|
</el-menu-item> |
||||
|
|
||||
|
<el-menu-item index="upload"> |
||||
|
<i class="el-icon-upload"></i> |
||||
|
<span slot="title">上传视频</span> |
||||
|
</el-menu-item> |
||||
|
|
||||
|
<el-menu-item index="my"> |
||||
|
<i class="el-icon-user"></i> |
||||
|
<span slot="title">我的视频</span> |
||||
|
</el-menu-item> |
||||
|
</el-menu> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 右侧内容区 --> |
||||
|
<div class="content-area"> |
||||
|
<!-- ================= 视频列表页 ================= --> |
||||
|
<div v-if="activeTab === 'list'" class="tab-content"> |
||||
|
<!-- 搜索栏 --> |
||||
|
<div class="search-bar"> |
||||
|
<el-input |
||||
|
v-model="searchKeyword" |
||||
|
placeholder="搜索视频标题" |
||||
|
clearable |
||||
|
@keyup.enter.native="loadPublicVideos" |
||||
|
@clear="loadPublicVideos" |
||||
|
style="width: 300px" |
||||
|
> |
||||
|
<i slot="prefix" class="el-icon-search"></i> |
||||
|
</el-input> |
||||
|
|
||||
|
<el-select |
||||
|
v-model="filterCategory" |
||||
|
placeholder="全部分类" |
||||
|
clearable |
||||
|
@change="loadPublicVideos" |
||||
|
style="width: 120px" |
||||
|
> |
||||
|
<el-option label="手术技巧" value="surgery" /> |
||||
|
<el-option label="疾病诊断" value="diagnosis" /> |
||||
|
<el-option label="药物使用" value="medication" /> |
||||
|
<el-option label="其他" value="other" /> |
||||
|
</el-select> |
||||
|
|
||||
|
<el-button |
||||
|
type="primary" |
||||
|
icon="el-icon-refresh" |
||||
|
@click="loadPublicVideos" |
||||
|
> |
||||
|
刷新 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 视频列表 --> |
||||
|
<h3 style="margin: 20px 0 10px 0; color: #333;"> |
||||
|
培训视频 |
||||
|
<span v-if="videos.length > 0" style="font-size: 14px; color: #999; margin-left: 10px;"> |
||||
|
(共 {{ videos.length }} 个) |
||||
|
</span> |
||||
|
</h3> |
||||
|
|
||||
|
<div v-if="loading" class="loading"> |
||||
|
<el-skeleton :rows="6" animated /> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else-if="videos.length === 0" class="empty"> |
||||
|
<el-empty description="暂无视频"></el-empty> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="video-grid"> |
||||
|
<div |
||||
|
v-for="video in videos" |
||||
|
:key="video.id" |
||||
|
class="video-card" |
||||
|
@click="showVideoDetail(video)" |
||||
|
> |
||||
|
<div class="cover"> |
||||
|
<img v-if="video.coverImage" :src="video.coverImage" :alt="video.title" /> |
||||
|
<div v-else class="no-cover"> |
||||
|
<i class="el-icon-video-camera"></i> |
||||
|
</div> |
||||
|
<div class="duration">{{ formatDuration(video.duration) }}</div> |
||||
|
</div> |
||||
|
<div class="info"> |
||||
|
<h4 class="title">{{ video.title }}</h4> |
||||
|
<div class="meta"> |
||||
|
<span class="author"> |
||||
|
<i class="el-icon-user"></i> |
||||
|
{{ video.nickName || video.userName || '未知' }} |
||||
|
</span> |
||||
|
<span class="views"> |
||||
|
<i class="el-icon-view"></i> |
||||
|
{{ video.viewCount || 0 }}次观看 |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="time">{{ formatTime(video.createTime) }}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ================= 上传视频页 ================= --> |
||||
|
<div v-else-if="activeTab === 'upload'" class="tab-content"> |
||||
|
<h3 style="margin-bottom: 20px; color: #333;">上传培训视频</h3> |
||||
|
|
||||
|
<div class="upload-area" @click="handleUploadAreaClick"> |
||||
|
<input |
||||
|
ref="fileInput" |
||||
|
type="file" |
||||
|
accept="video/*" |
||||
|
style="display: none" |
||||
|
@change="handleFileSelect" |
||||
|
/> |
||||
|
|
||||
|
<div v-if="!selectedFile" class="upload-placeholder"> |
||||
|
<i class="el-icon-upload upload-icon"></i> |
||||
|
<p>点击选择视频文件</p> |
||||
|
<p class="hint">支持 mp4、avi、mov 等格式,最大 1GB</p> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="file-info"> |
||||
|
<i class="el-icon-video-camera file-icon"></i> |
||||
|
<div class="file-details"> |
||||
|
<div class="file-name">{{ selectedFile.name }}</div> |
||||
|
<div class="file-size">{{ formatFileSize(selectedFile.size) }}</div> |
||||
|
</div> |
||||
|
<el-button type="text" @click.stop="removeFile"> |
||||
|
<i class="el-icon-close"></i> |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 视频信息表单 --> |
||||
|
<div class="upload-form"> |
||||
|
<el-form :model="uploadForm" label-width="80px" style="margin-top: 20px;"> |
||||
|
<el-form-item label="视频标题" required> |
||||
|
<el-input |
||||
|
v-model="uploadForm.title" |
||||
|
placeholder="请输入视频标题" |
||||
|
maxlength="50" |
||||
|
show-word-limit |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
|
||||
|
<el-form-item label="分类"> |
||||
|
<el-select v-model="uploadForm.category" placeholder="请选择分类" style="width: 100%"> |
||||
|
<el-option label="手术技巧" value="surgery" /> |
||||
|
<el-option label="疾病诊断" value="diagnosis" /> |
||||
|
<el-option label="药物使用" value="medication" /> |
||||
|
<el-option label="其他" value="other" /> |
||||
|
</el-select> |
||||
|
</el-form-item> |
||||
|
|
||||
|
<el-form-item label="描述"> |
||||
|
<el-input |
||||
|
v-model="uploadForm.description" |
||||
|
type="textarea" |
||||
|
:rows="3" |
||||
|
placeholder="请输入视频描述" |
||||
|
maxlength="200" |
||||
|
show-word-limit |
||||
|
/> |
||||
|
</el-form-item> |
||||
|
|
||||
|
<el-form-item label="视频状态"> |
||||
|
<el-radio-group v-model="uploadForm.status"> |
||||
|
<el-radio label="1">公开(所有人可见)</el-radio> |
||||
|
<el-radio label="0">私有(仅自己可见)</el-radio> |
||||
|
</el-radio-group> |
||||
|
</el-form-item> |
||||
|
|
||||
|
<el-form-item> |
||||
|
<el-button |
||||
|
type="primary" |
||||
|
:loading="uploading" |
||||
|
:disabled="!selectedFile || !uploadForm.title" |
||||
|
@click="handleUpload" |
||||
|
> |
||||
|
{{ uploading ? '上传中...' : '开始上传' }} |
||||
|
</el-button> |
||||
|
<el-button @click="resetUploadForm">重置</el-button> |
||||
|
</el-form-item> |
||||
|
</el-form> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ================= 我的视频页 ================= --> |
||||
|
<div v-else-if="activeTab === 'my'" class="tab-content"> |
||||
|
<div class="my-header"> |
||||
|
<h3 style="margin: 0; color: #333;">我的视频</h3> |
||||
|
<el-input |
||||
|
v-model="mySearchKeyword" |
||||
|
placeholder="搜索我的视频" |
||||
|
clearable |
||||
|
@keyup.enter.native="loadMyVideos" |
||||
|
@clear="loadMyVideos" |
||||
|
style="width: 200px" |
||||
|
> |
||||
|
<i slot="prefix" class="el-icon-search"></i> |
||||
|
</el-input> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="myVideosLoading" class="loading"> |
||||
|
<el-skeleton :rows="3" animated /> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else-if="myVideos.length === 0" class="empty"> |
||||
|
<el-empty description="你还没有上传过视频"> |
||||
|
<el-button type="primary" @click="activeTab = 'upload'"> |
||||
|
去上传 |
||||
|
</el-button> |
||||
|
</el-empty> |
||||
|
</div> |
||||
|
|
||||
|
<div v-else class="my-video-list"> |
||||
|
<div class="my-video-item" v-for="video in myVideos" :key="video.id"> |
||||
|
<div class="video-info" @click="showVideoDetail(video)"> |
||||
|
<div class="left"> |
||||
|
<img v-if="video.coverImage" :src="video.coverImage" class="cover-small" /> |
||||
|
<div v-else class="cover-small-placeholder"> |
||||
|
<i class="el-icon-video-camera"></i> |
||||
|
</div> |
||||
|
<div class="details"> |
||||
|
<div class="title">{{ video.title }}</div> |
||||
|
<div class="meta"> |
||||
|
<el-tag :type="video.status === '1' ? 'success' : 'warning'" size="small"> |
||||
|
{{ video.status === '1' ? '公开' : '私有' }} |
||||
|
</el-tag> |
||||
|
<span class="time">{{ formatTime(video.createTime) }}</span> |
||||
|
<span class="views">{{ video.viewCount }}次观看</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="right"> |
||||
|
<el-button |
||||
|
size="small" |
||||
|
type="danger" |
||||
|
@click.stop="deleteMyVideo(video.id)" |
||||
|
> |
||||
|
删除 |
||||
|
</el-button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- ================= 视频详情弹窗 ================= --> |
||||
|
<el-dialog |
||||
|
:visible.sync="showDetailDialog" |
||||
|
:title="currentVideo ? currentVideo.title : '视频详情'" |
||||
|
width="800px" |
||||
|
@close="handleDialogClose" |
||||
|
> |
||||
|
<div v-if="currentVideo" class="video-detail-dialog"> |
||||
|
<!-- 视频播放器 --> |
||||
|
<div class="video-player"> |
||||
|
<video |
||||
|
v-if="currentVideo.videoUrl" |
||||
|
:src="currentVideo.videoUrl" |
||||
|
controls |
||||
|
autoplay |
||||
|
style="width: 100%; max-height: 400px;" |
||||
|
></video> |
||||
|
<div v-else class="no-video"> |
||||
|
<i class="el-icon-video-camera"></i> |
||||
|
<p>视频加载失败</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 视频信息 --> |
||||
|
<div class="video-info-card"> |
||||
|
<div class="info-row"> |
||||
|
<span class="label">发布者:</span> |
||||
|
<span class="value">{{ currentVideo.nickName || currentVideo.userName || '未知' }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span class="label">发布时间:</span> |
||||
|
<span class="value">{{ formatTime(currentVideo.createTime) }}</span> |
||||
|
</div> |
||||
|
<div class="info-row"> |
||||
|
<span class="label">观看次数:</span> |
||||
|
<span class="value">{{ currentVideo.viewCount || 0 }}</span> |
||||
|
</div> |
||||
|
<div v-if="currentVideo.description" class="info-row"> |
||||
|
<span class="label">视频描述:</span> |
||||
|
<p class="value">{{ currentVideo.description }}</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</el-dialog> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import trainingApi from '@/api/vet/training' |
||||
|
|
||||
|
export default { |
||||
|
name: 'TrainingHome', |
||||
|
data() { |
||||
|
return { |
||||
|
// 当前激活的标签页 |
||||
|
activeTab: 'list', |
||||
|
|
||||
|
// ========== 视频列表相关 ========== |
||||
|
searchKeyword: '', |
||||
|
filterCategory: '', |
||||
|
videos: [], |
||||
|
loading: false, |
||||
|
|
||||
|
// ========== 上传视频相关 ========== |
||||
|
fileInput: null, |
||||
|
selectedFile: null, |
||||
|
uploading: false, |
||||
|
uploadForm: { |
||||
|
title: '', |
||||
|
category: '', |
||||
|
description: '', |
||||
|
status: '1' |
||||
|
}, |
||||
|
|
||||
|
// ========== 我的视频相关 ========== |
||||
|
mySearchKeyword: '', |
||||
|
myVideos: [], |
||||
|
myVideosLoading: false, |
||||
|
|
||||
|
// ========== 视频详情相关 ========== |
||||
|
showDetailDialog: false, |
||||
|
currentVideo: null |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
mounted() { |
||||
|
this.loadPublicVideos() |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
// 菜单选择 |
||||
|
handleMenuSelect(index) { |
||||
|
this.activeTab = index |
||||
|
if (index === 'list') { |
||||
|
this.loadPublicVideos() |
||||
|
} else if (index === 'my') { |
||||
|
this.loadMyVideos() |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 上传区域点击事件 |
||||
|
handleUploadAreaClick() { |
||||
|
if (this.$refs.fileInput) { |
||||
|
this.$refs.fileInput.click() |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 加载公开视频 |
||||
|
async loadPublicVideos() { |
||||
|
try { |
||||
|
this.loading = true |
||||
|
const params = { |
||||
|
title: this.searchKeyword, |
||||
|
category: this.filterCategory |
||||
|
} |
||||
|
const res = await trainingApi.getPublicVideos(params) |
||||
|
this.videos = res.data ? res.data.rows : [] |
||||
|
} catch (error) { |
||||
|
console.error('加载视频失败:', error) |
||||
|
this.$message.error('加载失败') |
||||
|
} finally { |
||||
|
this.loading = false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 加载我的视频 |
||||
|
async loadMyVideos() { |
||||
|
try { |
||||
|
this.myVideosLoading = true |
||||
|
const params = { |
||||
|
title: this.mySearchKeyword |
||||
|
} |
||||
|
const res = await trainingApi.getMyVideos(params) |
||||
|
this.myVideos = res.data ? res.data.rows : [] |
||||
|
} catch (error) { |
||||
|
console.error('加载我的视频失败:', error) |
||||
|
this.$message.error('加载失败') |
||||
|
} finally { |
||||
|
this.myVideosLoading = false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 文件选择 |
||||
|
handleFileSelect(event) { |
||||
|
const input = event.target |
||||
|
if (!input) return |
||||
|
|
||||
|
const file = input.files[0] |
||||
|
if (!file) return |
||||
|
|
||||
|
// 检查文件大小(1GB) |
||||
|
if (file.size > 1024 * 1024 * 1024) { |
||||
|
this.$message.error('文件大小不能超过 1GB') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 检查文件类型 |
||||
|
if (!file.type.startsWith('video/')) { |
||||
|
this.$message.error('请选择视频文件') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
this.selectedFile = file |
||||
|
}, |
||||
|
|
||||
|
// 移除文件 |
||||
|
removeFile() { |
||||
|
this.selectedFile = null |
||||
|
if (this.$refs.fileInput) { |
||||
|
this.$refs.fileInput.value = '' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 上传视频 |
||||
|
async handleUpload() { |
||||
|
if (!this.selectedFile) { |
||||
|
this.$message.error('请选择视频文件') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (!this.uploadForm.title.trim()) { |
||||
|
this.$message.error('请输入视频标题') |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.uploading = true |
||||
|
|
||||
|
const formData = new FormData() |
||||
|
formData.append('title', this.uploadForm.title) |
||||
|
formData.append('videoFile', this.selectedFile) |
||||
|
formData.append('status', this.uploadForm.status) |
||||
|
|
||||
|
if (this.uploadForm.category) { |
||||
|
formData.append('category', this.uploadForm.category) |
||||
|
} |
||||
|
|
||||
|
if (this.uploadForm.description) { |
||||
|
formData.append('description', this.uploadForm.description) |
||||
|
} |
||||
|
|
||||
|
await trainingApi.uploadVideo(formData) |
||||
|
|
||||
|
this.$message.success('上传成功!') |
||||
|
this.resetUploadForm() |
||||
|
this.activeTab = 'my' |
||||
|
this.loadMyVideos() |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.error('上传失败:', error) |
||||
|
this.$message.error(error.message || '上传失败,请重试') |
||||
|
} finally { |
||||
|
this.uploading = false |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 重置上传表单 |
||||
|
resetUploadForm() { |
||||
|
this.selectedFile = null |
||||
|
this.uploadForm.title = '' |
||||
|
this.uploadForm.category = '' |
||||
|
this.uploadForm.description = '' |
||||
|
this.uploadForm.status = '1' |
||||
|
|
||||
|
if (this.$refs.fileInput) { |
||||
|
this.$refs.fileInput.value = '' |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 显示视频详情 |
||||
|
showVideoDetail(video) { |
||||
|
this.currentVideo = video |
||||
|
this.showDetailDialog = true |
||||
|
}, |
||||
|
|
||||
|
// 删除我的视频 |
||||
|
async deleteMyVideo(videoId) { |
||||
|
try { |
||||
|
await this.$confirm('确定要删除这个视频吗?删除后无法恢复。', '提示', { |
||||
|
confirmButtonText: '确定', |
||||
|
cancelButtonText: '取消', |
||||
|
type: 'warning' |
||||
|
}) |
||||
|
|
||||
|
await trainingApi.deleteVideo(videoId) |
||||
|
this.$message.success('删除成功') |
||||
|
this.loadMyVideos() |
||||
|
|
||||
|
} catch (error) { |
||||
|
if (error !== 'cancel') { |
||||
|
console.error('删除失败:', error) |
||||
|
this.$message.error('删除失败') |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// 弹窗关闭处理 |
||||
|
handleDialogClose() { |
||||
|
this.currentVideo = null |
||||
|
}, |
||||
|
|
||||
|
// ========== 工具函数 ========== |
||||
|
formatDuration(seconds) { |
||||
|
if (!seconds) return '00:00' |
||||
|
const min = Math.floor(seconds / 60) |
||||
|
const sec = seconds % 60 |
||||
|
return `${min}:${sec.toString().padStart(2, '0')}` |
||||
|
}, |
||||
|
|
||||
|
formatTime(dateStr) { |
||||
|
if (!dateStr) return '' |
||||
|
const date = new Date(dateStr) |
||||
|
return date.toLocaleDateString('zh-CN') |
||||
|
}, |
||||
|
|
||||
|
formatFileSize(bytes) { |
||||
|
if (bytes === 0) return '0 B' |
||||
|
const k = 1024 |
||||
|
const sizes = ['B', 'KB', 'MB', 'GB'] |
||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k)) |
||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.training-home { |
||||
|
height: calc(100vh - 50px); |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.training-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 16px 20px; |
||||
|
background: white; |
||||
|
border-bottom: 1px solid #e8e8e8; |
||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.training-header h2 { |
||||
|
margin: 0; |
||||
|
color: #1890ff; |
||||
|
font-size: 18px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
.training-main { |
||||
|
flex: 1; |
||||
|
display: flex; |
||||
|
background: #f0f2f5; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.sidebar { |
||||
|
width: 180px; |
||||
|
background: white; |
||||
|
border-right: 1px solid #e8e8e8; |
||||
|
} |
||||
|
|
||||
|
.training-menu { |
||||
|
border-right: none; |
||||
|
} |
||||
|
|
||||
|
.training-menu .el-menu-item { |
||||
|
height: 50px; |
||||
|
line-height: 50px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.content-area { |
||||
|
flex: 1; |
||||
|
padding: 20px; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
|
||||
|
.tab-content { |
||||
|
background: white; |
||||
|
border-radius: 8px; |
||||
|
padding: 20px; |
||||
|
min-height: 500px; |
||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
/* 搜索栏样式 */ |
||||
|
.search-bar { |
||||
|
display: flex; |
||||
|
gap: 12px; |
||||
|
margin-bottom: 20px; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.my-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
/* 视频列表样式 */ |
||||
|
.video-grid { |
||||
|
display: grid; |
||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.video-card { |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 8px; |
||||
|
overflow: hidden; |
||||
|
cursor: pointer; |
||||
|
transition: all 0.3s; |
||||
|
background: white; |
||||
|
} |
||||
|
|
||||
|
.video-card:hover { |
||||
|
transform: translateY(-4px); |
||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.video-card .cover { |
||||
|
position: relative; |
||||
|
height: 140px; |
||||
|
background: #f5f5f5; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.video-card .cover img { |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
.no-cover { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
color: #d9d9d9; |
||||
|
} |
||||
|
|
||||
|
.no-cover .el-icon { |
||||
|
font-size: 40px; |
||||
|
} |
||||
|
|
||||
|
.duration { |
||||
|
position: absolute; |
||||
|
bottom: 8px; |
||||
|
right: 8px; |
||||
|
background: rgba(0, 0, 0, 0.7); |
||||
|
color: white; |
||||
|
padding: 2px 6px; |
||||
|
border-radius: 4px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
|
||||
|
.video-card .info { |
||||
|
padding: 12px; |
||||
|
} |
||||
|
|
||||
|
.video-card .title { |
||||
|
margin: 0 0 8px 0; |
||||
|
font-size: 14px; |
||||
|
font-weight: 500; |
||||
|
line-height: 1.4; |
||||
|
color: #333; |
||||
|
display: -webkit-box; |
||||
|
-webkit-line-clamp: 2; |
||||
|
-webkit-box-orient: vertical; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.video-card .meta { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
font-size: 12px; |
||||
|
color: #8c8c8c; |
||||
|
margin-bottom: 4px; |
||||
|
} |
||||
|
|
||||
|
.video-card .meta span { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 4px; |
||||
|
} |
||||
|
|
||||
|
.video-card .time { |
||||
|
font-size: 12px; |
||||
|
color: #bfbfbf; |
||||
|
} |
||||
|
|
||||
|
/* 上传区域样式 */ |
||||
|
.upload-area { |
||||
|
border: 2px dashed #dcdfe6; |
||||
|
border-radius: 8px; |
||||
|
padding: 40px 20px; |
||||
|
text-align: center; |
||||
|
cursor: pointer; |
||||
|
transition: border-color 0.3s; |
||||
|
background: #fafafa; |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
|
||||
|
.upload-area:hover { |
||||
|
border-color: #409eff; |
||||
|
} |
||||
|
|
||||
|
.upload-placeholder { |
||||
|
color: #909399; |
||||
|
} |
||||
|
|
||||
|
.upload-icon { |
||||
|
font-size: 48px; |
||||
|
margin-bottom: 16px; |
||||
|
color: #409eff; |
||||
|
} |
||||
|
|
||||
|
.hint { |
||||
|
font-size: 12px; |
||||
|
color: #c0c4cc; |
||||
|
margin-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.file-info { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.file-icon { |
||||
|
font-size: 36px; |
||||
|
color: #409eff; |
||||
|
} |
||||
|
|
||||
|
.file-details { |
||||
|
flex: 1; |
||||
|
text-align: left; |
||||
|
} |
||||
|
|
||||
|
.file-name { |
||||
|
font-weight: 500; |
||||
|
margin-bottom: 4px; |
||||
|
word-break: break-all; |
||||
|
} |
||||
|
|
||||
|
.file-size { |
||||
|
font-size: 12px; |
||||
|
color: #909399; |
||||
|
} |
||||
|
|
||||
|
.upload-form { |
||||
|
max-width: 600px; |
||||
|
} |
||||
|
|
||||
|
/* 我的视频列表样式 */ |
||||
|
.my-video-list { |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
|
||||
|
.my-video-item { |
||||
|
border: 1px solid #e8e8e8; |
||||
|
border-radius: 8px; |
||||
|
margin-bottom: 12px; |
||||
|
background: white; |
||||
|
transition: border-color 0.3s; |
||||
|
} |
||||
|
|
||||
|
.my-video-item:hover { |
||||
|
border-color: #1890ff; |
||||
|
} |
||||
|
|
||||
|
.video-info { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
padding: 12px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.video-info .left { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.cover-small { |
||||
|
width: 80px; |
||||
|
height: 45px; |
||||
|
border-radius: 4px; |
||||
|
object-fit: cover; |
||||
|
} |
||||
|
|
||||
|
.cover-small-placeholder { |
||||
|
width: 80px; |
||||
|
height: 45px; |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 4px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
color: #d9d9d9; |
||||
|
} |
||||
|
|
||||
|
.details { |
||||
|
flex: 1; |
||||
|
} |
||||
|
|
||||
|
.details .title { |
||||
|
margin: 0 0 6px 0; |
||||
|
font-weight: 500; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.details .meta { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 12px; |
||||
|
font-size: 12px; |
||||
|
color: #8c8c8c; |
||||
|
} |
||||
|
|
||||
|
/* 视频详情弹窗样式 */ |
||||
|
.video-detail-dialog { |
||||
|
padding: 10px 0; |
||||
|
} |
||||
|
|
||||
|
.video-player { |
||||
|
margin-bottom: 20px; |
||||
|
background: #000; |
||||
|
border-radius: 6px; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
|
||||
|
.no-video { |
||||
|
padding: 60px; |
||||
|
text-align: center; |
||||
|
color: white; |
||||
|
background: #666; |
||||
|
} |
||||
|
|
||||
|
.no-video .el-icon { |
||||
|
font-size: 48px; |
||||
|
margin-bottom: 16px; |
||||
|
} |
||||
|
|
||||
|
.video-info-card { |
||||
|
padding: 16px; |
||||
|
background: #fafafa; |
||||
|
border-radius: 6px; |
||||
|
} |
||||
|
|
||||
|
.info-row { |
||||
|
margin-bottom: 10px; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.info-row .label { |
||||
|
color: #666; |
||||
|
font-weight: 500; |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
.info-row .value { |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
/* 加载和空状态 */ |
||||
|
.loading, .empty { |
||||
|
padding: 60px 0; |
||||
|
text-align: center; |
||||
|
color: #8c8c8c; |
||||
|
} |
||||
|
|
||||
|
/* 响应式设计 */ |
||||
|
@media (max-width: 768px) { |
||||
|
.training-main { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.sidebar { |
||||
|
width: 100%; |
||||
|
border-right: none; |
||||
|
border-bottom: 1px solid #e8e8e8; |
||||
|
} |
||||
|
|
||||
|
.video-grid { |
||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); |
||||
|
gap: 12px; |
||||
|
} |
||||
|
|
||||
|
.search-bar { |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
|
||||
|
.search-bar .el-input { |
||||
|
width: 100% !important; |
||||
|
} |
||||
|
|
||||
|
.my-header { |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
gap: 12px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue