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
-
316chenhai-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
-
188chenhai-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; |
|||
|
|||
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 org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.scheduling.annotation.EnableScheduling; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
/** |
|||
* 证书提醒定时任务 |
|||
* |
|||
* @author ruoyi |
|||
*/ |
|||
import java.util.Calendar; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
|
|||
@Component |
|||
@EnableScheduling |
|||
@Slf4j |
|||
public class CertificateRemindTask { |
|||
|
|||
@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 { |
|||
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) { |
|||
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 { |
|||
// 更新所有证书状态 |
|||
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) { |
|||
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