Browse Source

修复个人信息管理的证书详情

master
ChaiNingQi 1 month ago
parent
commit
13fc2a6fd6
  1. 146
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java
  2. 87
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java
  3. 19
      chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java
  4. 4
      chenhai-admin/src/main/resources/application.yml
  5. 14
      chenhai-system/src/main/java/com/chenhai/vet/domain/VetExperienceArticle.java
  6. 36
      chenhai-system/src/main/java/com/chenhai/vet/mapper/VetArticleCategoryMapper.java
  7. 5
      chenhai-system/src/main/java/com/chenhai/vet/mapper/VetExperienceArticleMapper.java
  8. 2
      chenhai-system/src/main/java/com/chenhai/vet/service/IVetExperienceArticleService.java
  9. 14
      chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetExperienceArticleServiceImpl.java
  10. 34
      chenhai-system/src/main/resources/mapper/vet/VetArticleCategoryMapper.xml
  11. 22
      chenhai-system/src/main/resources/mapper/vet/VetExperienceArticleMapper.xml
  12. 11
      chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml
  13. 17
      chenhai-ui/src/api/vet/training.js
  14. 578
      chenhai-ui/src/views/vet/training/TrainingHome.vue

146
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetCertificateController.java

@ -11,7 +11,9 @@ import com.chenhai.vet.domain.VetCertificate;
import com.chenhai.vet.service.IVetCertificateService; import com.chenhai.vet.service.IVetCertificateService;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@ -19,7 +21,7 @@ import java.util.Map;
/** /**
* 兽医执业证书Controller * 兽医执业证书Controller
*
*
* @author ruoyi * @author ruoyi
* @date 2025-12-29 * @date 2025-12-29
*/ */
@ -30,6 +32,35 @@ public class VetCertificateController extends BaseController
@Autowired @Autowired
private IVetCertificateService vetCertificateService; private IVetCertificateService vetCertificateService;
/**
* 初始化数据绑定处理空字符串转换问题
* 防止前端传递空字符串导致Long类型绑定失败
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
// 处理Long类型字段的空字符串问题
binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, true) {
@Override
public void setAsText(String text) throws IllegalArgumentException {
if (text == null || text.trim().isEmpty()) {
setValue(null); // 将空字符串转换为null
} else {
try {
setValue(Long.parseLong(text.trim()));
} catch (NumberFormatException e) {
setValue(null); // 转换失败也设为null
}
}
}
@Override
public String getAsText() {
Object value = getValue();
return (value != null ? value.toString() : "");
}
});
}
/** /**
* 获取当前用户ID * 获取当前用户ID
*/ */
@ -61,9 +92,10 @@ public class VetCertificateController extends BaseController
return certificate != null && currentUserId.equals(certificate.getUserId()); return certificate != null && currentUserId.equals(certificate.getUserId());
} }
/** /**
* 查询兽医执业证书列表
* 查询兽医执业证书列表证书管理页面使用
* 管理员查看所有证书
* 普通用户查看自己的证书
*/ */
@PreAuthorize("@ss.hasPermi('vet:certificate:list')") @PreAuthorize("@ss.hasPermi('vet:certificate:list')")
@GetMapping("/list") @GetMapping("/list")
@ -71,21 +103,52 @@ public class VetCertificateController extends BaseController
{ {
startPage(); startPage();
Long currentUserId = getCurrentUserId(); Long currentUserId = getCurrentUserId();
if (currentUserId == null) { if (currentUserId == null) {
return getDataTable(List.of()); return getDataTable(List.of());
} }
// 如果是管理员可以查看所有证书不过滤用户ID
// 管理员可以查看所有普通用户只能查看自己的
if (!isAdmin()) { if (!isAdmin()) {
// 普通用户只查询自己的证书
vetCertificate.setUserId(currentUserId); vetCertificate.setUserId(currentUserId);
} }
// 管理员不设置userId询所有证书
// 管理员不设置userId看所有
List<VetCertificate> list = vetCertificateService.selectVetCertificateList(vetCertificate); List<VetCertificate> list = vetCertificateService.selectVetCertificateList(vetCertificate);
return getDataTable(list); return getDataTable(list);
} }
/**
* 用户详情页查询证书专用于用户详情页
* 查看指定用户的证书列表
*/
@PreAuthorize("@ss.hasPermi('vet:certificate:list')")
@GetMapping("/listForDetail")
public TableDataInfo listForDetail(@RequestParam(value = "userId", required = false) Long userId)
{
startPage();
Long currentUserId = getCurrentUserId();
if (currentUserId == null) {
return getDataTable(List.of());
}
// 如果没有传userId返回空
if (userId == null) {
return getDataTable(List.of());
}
// 权限检查管理员或查看自己
if (!isAdmin() && !userId.equals(currentUserId)) {
return getDataTable(List.of());
}
VetCertificate query = new VetCertificate();
query.setUserId(userId);
List<VetCertificate> list = vetCertificateService.selectVetCertificateList(query);
return getDataTable(list);
}
/** /**
* 导出兽医执业证书列表 * 导出兽医执业证书列表
*/ */
@ -106,6 +169,10 @@ public class VetCertificateController extends BaseController
@GetMapping(value = "/{id}") @GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) public AjaxResult getInfo(@PathVariable("id") Long id)
{ {
// 权限检查
if (!canAccessCertificate(id)) {
return error("没有权限查看此证书");
}
return success(vetCertificateService.selectVetCertificateById(id)); return success(vetCertificateService.selectVetCertificateById(id));
} }
@ -123,6 +190,13 @@ public class VetCertificateController extends BaseController
return error("用户未登录"); return error("用户未登录");
} }
// 权限检查普通用户只能给自己添加证书
if (!isAdmin() && vetCertificate.getUserId() != null &&
!vetCertificate.getUserId().equals(currentUserId)) {
return error("没有权限为其他用户添加证书");
}
// 设置用户ID和创建人
vetCertificate.setUserId(currentUserId); vetCertificate.setUserId(currentUserId);
vetCertificate.setCreateBy(SecurityUtils.getUsername()); vetCertificate.setCreateBy(SecurityUtils.getUsername());
@ -137,6 +211,20 @@ public class VetCertificateController extends BaseController
@PutMapping @PutMapping
public AjaxResult edit(@RequestBody VetCertificate vetCertificate) public AjaxResult edit(@RequestBody VetCertificate vetCertificate)
{ {
// 权限检查
if (!canAccessCertificate(vetCertificate.getId())) {
return error("没有权限修改此证书");
}
// 如果是普通用户确保不能修改用户ID
if (!isAdmin()) {
Long currentUserId = getCurrentUserId();
if (currentUserId != null && vetCertificate.getUserId() != null &&
!vetCertificate.getUserId().equals(currentUserId)) {
return error("不能修改证书所属用户");
}
}
return toAjax(vetCertificateService.updateVetCertificate(vetCertificate)); return toAjax(vetCertificateService.updateVetCertificate(vetCertificate));
} }
@ -145,19 +233,34 @@ public class VetCertificateController extends BaseController
*/ */
@PreAuthorize("@ss.hasPermi('vet:certificate:remove')") @PreAuthorize("@ss.hasPermi('vet:certificate:remove')")
@Log(title = "兽医执业证书", businessType = BusinessType.DELETE) @Log(title = "兽医执业证书", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) public AjaxResult remove(@PathVariable Long[] ids)
{ {
// 权限检查检查是否所有证书都可以删除
if (!isAdmin()) {
for (Long id : ids) {
if (!canAccessCertificate(id)) {
return error("没有权限删除证书ID: " + id);
}
}
}
return toAjax(vetCertificateService.deleteVetCertificateByIds(ids)); return toAjax(vetCertificateService.deleteVetCertificateByIds(ids));
} }
/** /**
* 根据用户ID获取证书列表
* 根据用户ID获取证书列表兼容旧版本建议前端迁移到listForDetail
*/ */
@GetMapping("/user/{userId}") @GetMapping("/user/{userId}")
public AjaxResult getByUserId(@PathVariable Long userId) public AjaxResult getByUserId(@PathVariable Long userId)
{ {
Long currentUserId = getCurrentUserId();
// 权限检查
if (!isAdmin() && !userId.equals(currentUserId)) {
return error("没有权限查看其他用户的证书");
}
List<VetCertificate> list = vetCertificateService.selectCertificatesByUserId(userId); List<VetCertificate> list = vetCertificateService.selectCertificatesByUserId(userId);
return success(list); return success(list);
} }
@ -168,6 +271,13 @@ public class VetCertificateController extends BaseController
@GetMapping("/expiring/{userId}") @GetMapping("/expiring/{userId}")
public AjaxResult getExpiringCertificates(@PathVariable Long userId) public AjaxResult getExpiringCertificates(@PathVariable Long userId)
{ {
Long currentUserId = getCurrentUserId();
// 权限检查
if (!isAdmin() && !userId.equals(currentUserId)) {
return error("没有权限查看其他用户的证书");
}
List<VetCertificate> list = vetCertificateService.selectExpiringCertificates(userId); List<VetCertificate> list = vetCertificateService.selectExpiringCertificates(userId);
return success(list); return success(list);
} }
@ -178,6 +288,13 @@ public class VetCertificateController extends BaseController
@GetMapping("/statistics/{userId}") @GetMapping("/statistics/{userId}")
public AjaxResult getStatistics(@PathVariable Long userId) public AjaxResult getStatistics(@PathVariable Long userId)
{ {
Long currentUserId = getCurrentUserId();
// 权限检查
if (!isAdmin() && !userId.equals(currentUserId)) {
return error("没有权限查看其他用户的统计信息");
}
Map<String, Object> statistics = vetCertificateService.getCertificateStatistics(userId); Map<String, Object> statistics = vetCertificateService.getCertificateStatistics(userId);
return success(statistics); return success(statistics);
} }
@ -188,7 +305,14 @@ public class VetCertificateController extends BaseController
@PostMapping("/manual-check/{userId}") @PostMapping("/manual-check/{userId}")
public AjaxResult manualCheck(@PathVariable Long userId) public AjaxResult manualCheck(@PathVariable Long userId)
{ {
Long currentUserId = getCurrentUserId();
// 权限检查
if (!isAdmin() && !userId.equals(currentUserId)) {
return error("没有权限为其他用户检查证书");
}
vetCertificateService.manualCheckCertificates(userId); vetCertificateService.manualCheckCertificates(userId);
return success("检查完成");
return success("证书检查完成");
} }
}
}

87
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetExperienceArticleController.java

@ -7,7 +7,9 @@ import com.chenhai.common.core.page.TableDataInfo;
import com.chenhai.common.enums.BusinessType; import com.chenhai.common.enums.BusinessType;
import com.chenhai.common.utils.SecurityUtils; import com.chenhai.common.utils.SecurityUtils;
import com.chenhai.common.utils.poi.ExcelUtil; import com.chenhai.common.utils.poi.ExcelUtil;
import com.chenhai.vet.domain.VetArticleCategory;
import com.chenhai.vet.domain.VetExperienceArticle; import com.chenhai.vet.domain.VetExperienceArticle;
import com.chenhai.vet.mapper.VetArticleCategoryMapper;
import com.chenhai.vet.service.IVetExperienceArticleService; import com.chenhai.vet.service.IVetExperienceArticleService;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -26,6 +28,9 @@ public class VetExperienceArticleController extends BaseController {
@Autowired @Autowired
private IVetExperienceArticleService vetExperienceArticleService; private IVetExperienceArticleService vetExperienceArticleService;
@Autowired // 确保有这个注解
private VetArticleCategoryMapper vetArticleCategoryMapper;
/** /**
* 查询兽医经验文章列表 * 查询兽医经验文章列表
@ -65,6 +70,53 @@ public class VetExperienceArticleController extends BaseController {
return toAjax(vetExperienceArticleService.deleteVetExperienceArticleByIds(ids)); return toAjax(vetExperienceArticleService.deleteVetExperienceArticleByIds(ids));
} }
@GetMapping("/options")
public AjaxResult getCategoryOptions() {
VetArticleCategory query = new VetArticleCategory();
query.setStatus("0");
List<VetArticleCategory> categories = vetArticleCategoryMapper.selectVetArticleCategoryList(query);
// 排序如果sortOrder为null放到最后
if (categories != null) {
categories.sort((c1, c2) -> {
Integer order1 = c1.getSortOrder() != null ? c1.getSortOrder() : 999;
Integer order2 = c2.getSortOrder() != null ? c2.getSortOrder() : 999;
return order1.compareTo(order2);
});
}
List<Map<String, Object>> options = new ArrayList<>();
for (VetArticleCategory category : categories) {
Map<String, Object> option = new LinkedHashMap<>();
option.put("value", category.getId());
option.put("label", category.getName());
options.add(option);
}
return AjaxResult.success(options);
}
/**
* 根据分类查询文章
*/
@GetMapping("/{categoryId}/articles")
public TableDataInfo getArticlesByCategory(
@PathVariable Long categoryId,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
startPage();
// 创建查询对象
VetExperienceArticle query = new VetExperienceArticle();
query.setStatus("1"); // 只查询已发布的文章
query.setCategoryId(categoryId); // 设置分类ID
List<VetExperienceArticle> list = vetExperienceArticleService.selectVetExperienceArticleList(query);
return getDataTable(list);
}
/** /**
@ -111,13 +163,13 @@ public class VetExperienceArticleController extends BaseController {
// 3. 判断是否是当前用户发布的文章 // 3. 判断是否是当前用户发布的文章
Long currentUserId = getUserId(); Long currentUserId = getUserId();
boolean isOwner = currentUserId != null && currentUserId.equals(article.getVetId());
boolean isOwner = currentUserId != null && currentUserId.equals(article.getUserId());
// 4. 获取相关文章 // 4. 获取相关文章
List<VetExperienceArticle> relatedArticles = vetExperienceArticleService.selectRelatedArticles(id, article.getCategoryId(), 4); List<VetExperienceArticle> relatedArticles = vetExperienceArticleService.selectRelatedArticles(id, article.getCategoryId(), 4);
// 5. 获取作者的其他文章 // 5. 获取作者的其他文章
List<VetExperienceArticle> authorArticles = vetExperienceArticleService.selectArticlesByVetId(article.getVetId(), 5);
List<VetExperienceArticle> authorArticles = vetExperienceArticleService.selectArticlesByVetId(article.getUserId(), 5);
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("article", article); result.put("article", article);
@ -131,7 +183,7 @@ public class VetExperienceArticleController extends BaseController {
/** /**
* 发布新文章论坛发布接口 * 发布新文章论坛发布接口
*/ */
@PreAuthorize("@ss.hasPermi('vet:article:add')")
/*@PreAuthorize("@ss.hasPermi('vet:article:add')")*/
@Log(title = "发布经验文章", businessType = BusinessType.INSERT) @Log(title = "发布经验文章", businessType = BusinessType.INSERT)
@PostMapping("/forum/publish") @PostMapping("/forum/publish")
public AjaxResult publishForumArticle(@RequestBody VetExperienceArticle article) { public AjaxResult publishForumArticle(@RequestBody VetExperienceArticle article) {
@ -139,7 +191,7 @@ public class VetExperienceArticleController extends BaseController {
String currentUsername = SecurityUtils.getUsername(); String currentUsername = SecurityUtils.getUsername();
// 设置作者信息 // 设置作者信息
article.setVetId(currentUserId);
article.setUserId(currentUserId);
article.setVetName(currentUsername); // 暂时用用户名可以根据需要从用户表获取真实姓名 article.setVetName(currentUsername); // 暂时用用户名可以根据需要从用户表获取真实姓名
// 设置文章状态为已发布 // 设置文章状态为已发布
@ -174,7 +226,7 @@ public class VetExperienceArticleController extends BaseController {
/** /**
* 获取当前用户的文章 * 获取当前用户的文章
*/ */
@PreAuthorize("@ss.hasPermi('vet:article:my')")
/* @PreAuthorize("@ss.hasPermi('vet:article:my')")*/
@GetMapping("/forum/myArticles") @GetMapping("/forum/myArticles")
public TableDataInfo getMyForumArticles( public TableDataInfo getMyForumArticles(
@RequestParam(value = "status", required = false) String status) { @RequestParam(value = "status", required = false) String status) {
@ -182,7 +234,7 @@ public class VetExperienceArticleController extends BaseController {
Long currentUserId = getUserId(); Long currentUserId = getUserId();
VetExperienceArticle query = new VetExperienceArticle(); VetExperienceArticle query = new VetExperienceArticle();
query.setVetId(currentUserId);
query.setUserId(currentUserId);
if (status != null && !status.isEmpty()) { if (status != null && !status.isEmpty()) {
query.setStatus(status); query.setStatus(status);
@ -247,10 +299,29 @@ public class VetExperienceArticleController extends BaseController {
return success(hotTags); return success(hotTags);
} }
/**
* 2. 根据标签搜索文章带分页
*/
@GetMapping("/listByTag")
public TableDataInfo listByTag(@RequestParam("tag") String tag,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
startPage();
// 创建查询对象
VetExperienceArticle query = new VetExperienceArticle();
query.setStatus("1"); // 只查已发布的
query.setTags(tag); // 设置标签条件
List<VetExperienceArticle> list = vetExperienceArticleService.selectVetExperienceArticleList(query);
return getDataTable(list);
}
/** /**
* 新增文章 - 调整为论坛发布模式 * 新增文章 - 调整为论坛发布模式
*/ */
@PreAuthorize("@ss.hasPermi('vet:article:add')")
/* @PreAuthorize("@ss.hasPermi('vet:article:add')")*/
@Log(title = "兽医经验文章", businessType = BusinessType.INSERT) @Log(title = "兽医经验文章", businessType = BusinessType.INSERT)
@PostMapping @PostMapping
public AjaxResult add(@RequestBody VetExperienceArticle vetExperienceArticle) { public AjaxResult add(@RequestBody VetExperienceArticle vetExperienceArticle) {
@ -282,7 +353,7 @@ public class VetExperienceArticleController extends BaseController {
/** /**
* 导出兽医经验文章列表 * 导出兽医经验文章列表
*/ */
@PreAuthorize("@ss.hasPermi('vet:article:export')")
/* @PreAuthorize("@ss.hasPermi('vet:article:export')")*/
@Log(title = "兽医经验文章", businessType = BusinessType.EXPORT) @Log(title = "兽医经验文章", businessType = BusinessType.EXPORT)
@PostMapping("/export") @PostMapping("/export")
public void export(HttpServletResponse response, VetExperienceArticle vetExperienceArticle) { public void export(HttpServletResponse response, VetExperienceArticle vetExperienceArticle) {

19
chenhai-admin/src/main/java/com/chenhai/web/controller/vet/VetTrainingVideoController.java

@ -31,29 +31,26 @@ public class VetTrainingVideoController extends BaseController {
*/ */
@PostMapping("/upload") @PostMapping("/upload")
public AjaxResult uploadVideo( 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) {
@RequestParam("title") String title, // 改为 @RequestParam
@RequestParam("videoFile") MultipartFile videoFile, // 文件字段保持 @RequestPart 或改为 @RequestParam
@RequestParam(value = "description", required = false) String description,
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "status", required = false, defaultValue = "1") String status) {
Long userId = getCurrentUserId(); Long userId = getCurrentUserId();
try { try {
VetTrainingVideo video = new VetTrainingVideo(); VetTrainingVideo video = new VetTrainingVideo();
video.setUserId(userId); // 修正变量名
video.setUserId(userId);
video.setTitle(title); video.setTitle(title);
video.setDescription(description); video.setDescription(description);
video.setCategory(category); video.setCategory(category);
video.setTags(tags);
video.setStatus(status); video.setStatus(status);
// 上传并保存视频
String result = trainingVideoService.uploadAndSave(video, videoFile, coverImage);
String result = trainingVideoService.uploadAndSave(video, videoFile, null);
return success(result); return success(result);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
return error("上传失败:" + e.getMessage()); return error("上传失败:" + e.getMessage());
} }
} }

4
chenhai-admin/src/main/resources/application.yml

@ -115,8 +115,8 @@ token:
# 微信小程序配置 # 微信小程序配置
wx: wx:
muhu: muhu:
app-id: wxb5becc8d6d8123a6
app-secret: 74f4211d3985aa782ff9148aa00f824e
app-id: wx9bf7f4e81a1b2d6b
app-secret: b743202d5c1029ab214d94026524c4b7
vet: vet:
app-id: ${WX_MINI_APPID:your_app_id} app-id: ${WX_MINI_APPID:your_app_id}
app-secret: ${WX_MINI_SECRET:your_app_secret} app-secret: ${WX_MINI_SECRET:your_app_secret}

14
chenhai-system/src/main/java/com/chenhai/vet/domain/VetExperienceArticle.java

@ -42,7 +42,7 @@ public class VetExperienceArticle extends BaseEntity
/** 作者(兽医)ID */ /** 作者(兽医)ID */
@Excel(name = "作者", readConverterExp = "兽=医") @Excel(name = "作者", readConverterExp = "兽=医")
private Long vetId;
private Long userId;
/** 兽医姓名 */ /** 兽医姓名 */
@Excel(name = "兽医姓名") @Excel(name = "兽医姓名")
@ -61,7 +61,7 @@ public class VetExperienceArticle extends BaseEntity
private Long categoryId; private Long categoryId;
/** 分类名称 */ /** 分类名称 */
@Excel(name = "分类名称")
@Excel(name = "分类名称",dictType = "vet_experience_category")
private String categoryName; private String categoryName;
/** 标签(逗号分隔) */ /** 标签(逗号分隔) */
@ -190,14 +190,14 @@ public class VetExperienceArticle extends BaseEntity
return images; return images;
} }
public void setVetId(Long vetId)
public void setUserId(Long userId)
{ {
this.vetId = vetId;
this.userId = userId;
} }
public Long getVetId()
public Long getUserId()
{ {
return vetId;
return userId;
} }
public void setVetName(String vetName) public void setVetName(String vetName)
@ -329,7 +329,7 @@ public class VetExperienceArticle extends BaseEntity
.append("summary", getSummary()) .append("summary", getSummary())
.append("coverImage", getCoverImage()) .append("coverImage", getCoverImage())
.append("images", getImages()) .append("images", getImages())
.append("vetId", getVetId())
.append("userId", getUserId())
.append("vetName", getVetName()) .append("vetName", getVetName())
.append("vetAvatar", getVetAvatar()) .append("vetAvatar", getVetAvatar())
.append("vetTitle", getVetTitle()) .append("vetTitle", getVetTitle())

36
chenhai-system/src/main/java/com/chenhai/vet/mapper/VetArticleCategoryMapper.java

@ -4,44 +4,10 @@ import com.chenhai.vet.domain.VetArticleCategory;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
/**
* 文章分类Mapper接口
*/
@Mapper @Mapper
public interface VetArticleCategoryMapper { public interface VetArticleCategoryMapper {
/** /**
* 查询文章分类
*/
VetArticleCategory selectVetArticleCategoryById(Long id);
/**
* 查询文章分类列表
* 这个方法名必须和XML中的id完全一致
*/ */
List<VetArticleCategory> selectVetArticleCategoryList(VetArticleCategory vetArticleCategory); List<VetArticleCategory> selectVetArticleCategoryList(VetArticleCategory vetArticleCategory);
/**
* 查询启用的分类列表
*/
List<VetArticleCategory> selectEnabledCategories();
/**
* 新增文章分类
*/
int insertVetArticleCategory(VetArticleCategory vetArticleCategory);
/**
* 修改文章分类
*/
int updateVetArticleCategory(VetArticleCategory vetArticleCategory);
/**
* 删除文章分类
*/
int deleteVetArticleCategoryById(Long id);
/**
* 批量删除文章分类
*/
int deleteVetArticleCategoryByIds(Long[] ids);
} }

5
chenhai-system/src/main/java/com/chenhai/vet/mapper/VetExperienceArticleMapper.java

@ -86,4 +86,9 @@ public interface VetExperienceArticleMapper
* 增加收藏数 * 增加收藏数
*/ */
int incrementCollectCount(@Param("id") Long id); int incrementCollectCount(@Param("id") Long id);
/**
* 根据标签筛选文章
*/
List<VetExperienceArticle> selectArticlesByTag(@Param("tag") String tag);
} }

2
chenhai-system/src/main/java/com/chenhai/vet/service/IVetExperienceArticleService.java

@ -112,4 +112,6 @@ public interface IVetExperienceArticleService
* 获取热门标签 * 获取热门标签
*/ */
List<Map<String, Object>> selectHotTags(Integer limit); List<Map<String, Object>> selectHotTags(Integer limit);
List<VetExperienceArticle> selectArticlesByTag(String tag);
} }

14
chenhai-system/src/main/java/com/chenhai/vet/service/impl/VetExperienceArticleServiceImpl.java

@ -193,7 +193,7 @@ public class VetExperienceArticleServiceImpl implements IVetExperienceArticleSer
// 获取兽医总数去重 // 获取兽医总数去重
Set<Long> vetIds = new HashSet<>(); Set<Long> vetIds = new HashSet<>();
for (VetExperienceArticle article : allArticles) { for (VetExperienceArticle article : allArticles) {
vetIds.add(article.getVetId());
vetIds.add(article.getUserId());
} }
statistics.put("totalVets", vetIds.size()); statistics.put("totalVets", vetIds.size());
@ -214,6 +214,17 @@ public class VetExperienceArticleServiceImpl implements IVetExperienceArticleSer
return statistics; return statistics;
} }
/**
* 根据标签筛选文章简单实现
*/
@Override
public List<VetExperienceArticle> selectArticlesByTag(String tag) {
if (tag == null || tag.trim().isEmpty()) {
return new ArrayList<>();
}
return vetExperienceArticleMapper.selectArticlesByTag(tag.trim());
}
@Override @Override
public List<Map<String, Object>> selectHotTags(Integer limit) { public List<Map<String, Object>> selectHotTags(Integer limit) {
// 从所有文章中提取标签并统计热度 // 从所有文章中提取标签并统计热度
@ -248,4 +259,5 @@ public class VetExperienceArticleServiceImpl implements IVetExperienceArticleSer
return hotTags; return hotTags;
} }
} }

34
chenhai-system/src/main/resources/mapper/vet/VetArticleCategoryMapper.xml

@ -0,0 +1,34 @@
<?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.VetArticleCategoryMapper">
<resultMap type="com.chenhai.vet.domain.VetArticleCategory" id="VetArticleCategoryResult">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="description" column="description"/>
<result property="icon" column="icon"/>
<result property="sortOrder" column="sort_order"/>
<result property="status" column="status"/>
<result property="articleCount" column="article_count"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<sql id="selectVetArticleCategoryVo">
select id, name, description, icon, sort_order, status, create_time, update_time
from vet_article_category
</sql>
<!-- 这个方法名必须和Mapper接口中的方法名完全一致 -->
<select id="selectVetArticleCategoryList" parameterType="com.chenhai.vet.domain.VetArticleCategory" resultMap="VetArticleCategoryResult">
<include refid="selectVetArticleCategoryVo"/>
<where>
<if test="name != null and name != ''"> and name like concat('%', #{name}, '%')</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
order by sort_order asc
</select>
</mapper>

22
chenhai-system/src/main/resources/mapper/vet/VetExperienceArticleMapper.xml

@ -11,7 +11,7 @@
<result property="summary" column="summary"/> <result property="summary" column="summary"/>
<result property="coverImage" column="cover_image"/> <result property="coverImage" column="cover_image"/>
<result property="images" column="images"/> <result property="images" column="images"/>
<result property="vetId" column="vet_id"/>
<result property="userId" column="user_id"/>
<result property="vetName" column="vet_name"/> <result property="vetName" column="vet_name"/>
<result property="vetAvatar" column="vet_avatar"/> <result property="vetAvatar" column="vet_avatar"/>
<result property="vetTitle" column="vet_title"/> <result property="vetTitle" column="vet_title"/>
@ -32,7 +32,7 @@
</resultMap> </resultMap>
<sql id="selectVetExperienceArticleVo"> <sql id="selectVetExperienceArticleVo">
select id, title, content, summary, cover_image, images, vet_id, vet_name, vet_avatar, vet_title, category_id, category_name, tags, view_count, like_count, collect_count, is_top, is_featured, status, is_sensitive, sensitive_words, publish_time, create_time, update_time
select id, title, content, summary, cover_image, images, user_id, vet_name, vet_avatar, vet_title, category_id, category_name, tags, view_count, like_count, collect_count, is_top, is_featured, status, is_sensitive, sensitive_words, publish_time, create_time, update_time
from vet_experience_article from vet_experience_article
</sql> </sql>
@ -46,7 +46,7 @@
<include refid="selectVetExperienceArticleVo"/> <include refid="selectVetExperienceArticleVo"/>
<where> <where>
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if> <if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
<if test="vetId != null"> and vet_id = #{vetId}</if>
<if test="userId != null"> and user_id = #{userId}</if>
<if test="vetName != null and vetName != ''"> and vet_name like concat('%', #{vetName}, '%')</if> <if test="vetName != null and vetName != ''"> and vet_name like concat('%', #{vetName}, '%')</if>
<if test="categoryId != null"> and category_id = #{categoryId}</if> <if test="categoryId != null"> and category_id = #{categoryId}</if>
<if test="categoryName != null and categoryName != ''"> and category_name like concat('%', #{categoryName}, '%')</if> <if test="categoryName != null and categoryName != ''"> and category_name like concat('%', #{categoryName}, '%')</if>
@ -73,7 +73,7 @@
or tags like concat('%', #{keyword}, '%') or tags like concat('%', #{keyword}, '%')
) )
</if> </if>
<if test="vetId != null"> and vet_id = #{vetId}</if>
<if test="userId != null"> and user_id = #{userId}</if>
<if test="categoryId != null"> and category_id = #{categoryId}</if> <if test="categoryId != null"> and category_id = #{categoryId}</if>
<if test="isFeatured != null"> and is_featured = #{isFeatured}</if> <if test="isFeatured != null"> and is_featured = #{isFeatured}</if>
<if test="isTop != null"> and is_top = #{isTop}</if> <if test="isTop != null"> and is_top = #{isTop}</if>
@ -150,7 +150,7 @@
<if test="summary != null">summary,</if> <if test="summary != null">summary,</if>
<if test="coverImage != null">cover_image,</if> <if test="coverImage != null">cover_image,</if>
<if test="images != null">images,</if> <if test="images != null">images,</if>
<if test="vetId != null">vet_id,</if>
<if test="userId != null">user_id,</if>
<if test="vetName != null and vetName != ''">vet_name,</if> <if test="vetName != null and vetName != ''">vet_name,</if>
<if test="vetAvatar != null">vet_avatar,</if> <if test="vetAvatar != null">vet_avatar,</if>
<if test="vetTitle != null">vet_title,</if> <if test="vetTitle != null">vet_title,</if>
@ -175,7 +175,7 @@
<if test="summary != null">#{summary},</if> <if test="summary != null">#{summary},</if>
<if test="coverImage != null">#{coverImage},</if> <if test="coverImage != null">#{coverImage},</if>
<if test="images != null">#{images},</if> <if test="images != null">#{images},</if>
<if test="vetId != null">#{vetId},</if>
<if test="userId != null">#{userId},</if>
<if test="vetName != null and vetName != ''">#{vetName},</if> <if test="vetName != null and vetName != ''">#{vetName},</if>
<if test="vetAvatar != null">#{vetAvatar},</if> <if test="vetAvatar != null">#{vetAvatar},</if>
<if test="vetTitle != null">#{vetTitle},</if> <if test="vetTitle != null">#{vetTitle},</if>
@ -204,7 +204,7 @@
<if test="summary != null">summary = #{summary},</if> <if test="summary != null">summary = #{summary},</if>
<if test="coverImage != null">cover_image = #{coverImage},</if> <if test="coverImage != null">cover_image = #{coverImage},</if>
<if test="images != null">images = #{images},</if> <if test="images != null">images = #{images},</if>
<if test="vetId != null">vet_id = #{vetId},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="vetName != null and vetName != ''">vet_name = #{vetName},</if> <if test="vetName != null and vetName != ''">vet_name = #{vetName},</if>
<if test="vetAvatar != null">vet_avatar = #{vetAvatar},</if> <if test="vetAvatar != null">vet_avatar = #{vetAvatar},</if>
<if test="vetTitle != null">vet_title = #{vetTitle},</if> <if test="vetTitle != null">vet_title = #{vetTitle},</if>
@ -235,4 +235,12 @@
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<select id="selectArticlesByTag" parameterType="String" resultMap="VetExperienceArticleResult">
<include refid="selectVetExperienceArticleVo"/>
WHERE status = '1' <!-- 只查询已发布的文章 -->
AND tags LIKE CONCAT('%', #{tag}, '%')
ORDER BY publish_time DESC
LIMIT 20 <!-- 限制返回20条 -->
</select>
</mapper> </mapper>

11
chenhai-system/src/main/resources/mapper/vet/VetTrainingVideoMapper.xml

@ -28,7 +28,7 @@
category, tags, duration, file_size, view_count, category, tags, duration, file_size, view_count,
status, create_time, update_time status, create_time, update_time
) VALUES ( ) VALUES (
#{vetId}, #{title}, #{description}, #{videoUrl}, #{coverImage},
#{userId}, #{title}, #{description}, #{videoUrl}, #{coverImage},
#{category}, #{tags}, #{duration}, #{fileSize}, #{viewCount}, #{category}, #{tags}, #{duration}, #{fileSize}, #{viewCount},
#{status}, #{createTime}, #{updateTime} #{status}, #{createTime}, #{updateTime}
) )
@ -63,7 +63,7 @@
AND v.category = #{category} AND v.category = #{category}
</if> </if>
<if test="userName != null and userName != ''"> <if test="userName != null and userName != ''">
AND u.nick_name LIKE CONCAT('%', #{vetName}, '%')
AND u.nick_name LIKE CONCAT('%', #{userName}, '%')
</if> </if>
ORDER BY v.create_time DESC ORDER BY v.create_time DESC
</select> </select>
@ -102,9 +102,8 @@
ORDER BY v.create_time DESC ORDER BY v.create_time DESC
</select> </select>
<update id="deleteVideoById">
UPDATE vet_training_video
SET del_flag = '1'
<delete id="deleteVideoById">
DELETE FROM vet_training_video
WHERE id = #{id} WHERE id = #{id}
</update>
</delete>
</mapper> </mapper>

17
chenhai-ui/src/api/vet/training.js

@ -5,26 +5,17 @@ import request from '@/utils/request'
export default { export default {
// 上传视频 // 上传视频
uploadVideo(data) { 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])
}
}
})
// 注意:这里直接返回请求,不要重新创建 FormData
return request({ return request({
url: '/vet/training/upload', url: '/vet/training/upload',
method: 'post', method: 'post',
data: formData,
data: data, // data 已经是 FormData 对象
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
}, },
timeout: 300000 // 5分钟超时,视频上传需要时间
timeout: 300000
}) })
}, },
// 获取公开视频列表 // 获取公开视频列表

578
chenhai-ui/src/views/vet/training/TrainingHome.vue

@ -110,22 +110,41 @@
@click="showVideoDetail(video)" @click="showVideoDetail(video)"
> >
<div class="cover"> <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>
<!-- 封面图片 -->
<img
v-if="video.coverImage && video.coverImage !== 'null'"
:src="getCoverImage(video)"
:alt="video.title"
@load="handleCoverImageLoad(video.id)"
@error="handleCoverImageError(video)"
style="width: 100%; height: 100%; object-fit: cover;"
/>
<!-- 默认封面 -->
<div
v-else
class="default-cover"
:style="{ background: getCategoryColor(video.category) }"
>
<div class="cover-content">
<i class="el-icon-video-camera cover-icon"></i>
<span class="cover-text">{{ getCategoryText(video.category) }}</span>
</div>
</div> </div>
<div class="duration">{{ formatDuration(video.duration) }}</div> <div class="duration">{{ formatDuration(video.duration) }}</div>
</div> </div>
<div class="info"> <div class="info">
<h4 class="title">{{ video.title }}</h4>
<div class="title">{{ video.title }}</div>
<div class="meta"> <div class="meta">
<span class="author">
<span>
<i class="el-icon-user"></i> <i class="el-icon-user"></i>
{{ video.nickName || video.userName || '未知' }}
{{ video.userName || '未知' }}
</span> </span>
<span class="views">
<span>
<i class="el-icon-view"></i> <i class="el-icon-view"></i>
{{ video.viewCount || 0 }}次观看
{{ video.viewCount || 0 }}
</span> </span>
</div> </div>
<div class="time">{{ formatTime(video.createTime) }}</div> <div class="time">{{ formatTime(video.createTime) }}</div>
@ -251,9 +270,14 @@
<div class="my-video-item" v-for="video in myVideos" :key="video.id"> <div class="my-video-item" v-for="video in myVideos" :key="video.id">
<div class="video-info" @click="showVideoDetail(video)"> <div class="video-info" @click="showVideoDetail(video)">
<div class="left"> <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 class="cover-small-container">
<img v-if="video.coverImage && video.coverImage !== 'null'"
:src="getCoverImage(video)"
class="cover-small"
@error="handleCoverImageError(video)" />
<div v-else class="cover-small-placeholder" :style="{ background: getCategoryColor(video.category) }">
<i class="el-icon-video-camera"></i>
</div>
</div> </div>
<div class="details"> <div class="details">
<div class="title">{{ video.title }}</div> <div class="title">{{ video.title }}</div>
@ -262,7 +286,7 @@
{{ video.status === '1' ? '公开' : '私有' }} {{ video.status === '1' ? '公开' : '私有' }}
</el-tag> </el-tag>
<span class="time">{{ formatTime(video.createTime) }}</span> <span class="time">{{ formatTime(video.createTime) }}</span>
<span class="views">{{ video.viewCount }}次观看</span>
<span class="views">{{ video.viewCount || 0 }}次观看</span>
</div> </div>
</div> </div>
</div> </div>
@ -287,29 +311,66 @@
:visible.sync="showDetailDialog" :visible.sync="showDetailDialog"
:title="currentVideo ? currentVideo.title : '视频详情'" :title="currentVideo ? currentVideo.title : '视频详情'"
width="800px" width="800px"
@open="handleDialogOpen"
@close="handleDialogClose" @close="handleDialogClose"
> >
<div v-if="currentVideo" class="video-detail-dialog"> <div v-if="currentVideo" class="video-detail-dialog">
<!-- 视频播放器 --> <!-- 视频播放器 -->
<div class="video-player"> <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">
<div v-if="!videoPlayerReady && !videoLoadFailed" class="video-loading">
<i class="el-icon-loading"></i>
<p>加载视频中...</p>
</div>
<div v-else-if="videoLoadFailed" class="video-error">
<i class="el-icon-video-camera"></i> <i class="el-icon-video-camera"></i>
<p>视频加载失败</p> <p>视频加载失败</p>
<p class="error-tip">可能的原因</p>
<ul>
<li>视频文件不存在</li>
<li>网络连接问题</li>
<li>视频格式不支持</li>
</ul>
<div class="error-actions">
<el-button type="primary" @click="retryLoadVideo" size="small">
<i class="el-icon-refresh"></i> 重试加载
</el-button>
<el-button @click="openVideoInNewTab" size="small">
<i class="el-icon-link"></i> 在新标签页打开
</el-button>
<el-button @click="showDetailDialog = false" size="small">
关闭
</el-button>
</div>
</div> </div>
<video
v-else
ref="videoPlayer"
:src="currentVideo.videoUrl"
controls
preload="metadata"
style="width: 100%; max-height: 400px; border-radius: 6px;"
@error="handleVideoError"
@loadeddata="handleVideoLoaded"
@canplay="handleVideoCanPlay"
crossorigin="anonymous"
>
<source :src="currentVideo.videoUrl" type="video/mp4">
<source :src="currentVideo.videoUrl" type="video/webm">
您的浏览器不支持视频播放
</video>
</div> </div>
<!-- 视频信息 --> <!-- 视频信息 -->
<div class="video-info-card"> <div class="video-info-card">
<div class="info-row"> <div class="info-row">
<span class="label">发布者</span> <span class="label">发布者</span>
<span class="value">{{ currentVideo.nickName || currentVideo.userName || '未知' }}</span>
<span class="value">{{ currentVideo.userName || '未知' }}</span>
</div>
<div class="info-row">
<span class="label">分类</span>
<span class="value">{{ getCategoryText(currentVideo.category) }}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="label">发布时间</span> <span class="label">发布时间</span>
@ -321,7 +382,15 @@
</div> </div>
<div v-if="currentVideo.description" class="info-row"> <div v-if="currentVideo.description" class="info-row">
<span class="label">视频描述</span> <span class="label">视频描述</span>
<p class="value">{{ currentVideo.description }}</p>
<p class="value description-text">{{ currentVideo.description }}</p>
</div>
<div class="info-row">
<span class="label">视频地址</span>
<p class="value">
<el-link :href="currentVideo.videoUrl" target="_blank" type="primary">
{{ currentVideo.videoUrl }}
</el-link>
</p>
</div> </div>
</div> </div>
</div> </div>
@ -363,7 +432,14 @@ export default {
// ========== ========== // ========== ==========
showDetailDialog: false, showDetailDialog: false,
currentVideo: null
currentVideo: null,
videoPlayerReady: false,
videoLoadFailed: false,
// ========== ==========
coverLoadingStates: new Map(),
coverErrorStates: new Map(),
defaultCoverCache: new Map(),
} }
}, },
@ -393,14 +469,28 @@ export default {
async loadPublicVideos() { async loadPublicVideos() {
try { try {
this.loading = true this.loading = true
console.log('🔄 加载公开视频...')
const params = { const params = {
title: this.searchKeyword, title: this.searchKeyword,
category: this.filterCategory
category: this.filterCategory,
pageNum: 1,
pageSize: 100
} }
const res = await trainingApi.getPublicVideos(params) const res = await trainingApi.getPublicVideos(params)
this.videos = res.data ? res.data.rows : []
console.log('✅ 公开视频响应:', res)
if (res && res.rows) {
console.log(`📊 找到 ${res.rows.length} 个视频`)
this.videos = res.rows
} else {
console.warn('⚠️ 公开视频响应数据格式不正确')
this.videos = []
}
} catch (error) { } catch (error) {
console.error('加载视频失败:', error)
console.error('加载公开视频失败:', error)
this.$message.error('加载失败') this.$message.error('加载失败')
} finally { } finally {
this.loading = false this.loading = false
@ -411,13 +501,27 @@ export default {
async loadMyVideos() { async loadMyVideos() {
try { try {
this.myVideosLoading = true this.myVideosLoading = true
console.log('🔄 加载我的视频...')
const params = { const params = {
title: this.mySearchKeyword
title: this.mySearchKeyword,
pageNum: 1,
pageSize: 100
} }
const res = await trainingApi.getMyVideos(params) const res = await trainingApi.getMyVideos(params)
this.myVideos = res.data ? res.data.rows : []
console.log('✅ 我的视频响应:', res)
if (res && res.rows) {
console.log(`📊 找到 ${res.rows.length} 个我的视频`)
this.myVideos = res.rows
} else {
console.warn('⚠️ 我的视频响应数据格式不正确')
this.myVideos = []
}
} catch (error) { } catch (error) {
console.error('加载我的视频失败:', error)
console.error('加载我的视频失败:', error)
this.$message.error('加载失败') this.$message.error('加载失败')
} finally { } finally {
this.myVideosLoading = false this.myVideosLoading = false
@ -511,10 +615,141 @@ export default {
} }
}, },
// ========== ==========
// //
showVideoDetail(video) { showVideoDetail(video) {
this.currentVideo = video
this.showDetailDialog = true
console.log('🔍 查看视频详情,原始数据:', video)
if (!video || !video.videoUrl) {
this.$message.error('视频信息不完整')
return
}
// URL
const fullVideoUrl = this.getVideoUrl(video.videoUrl)
console.log('📹 视频URL分析:')
console.log(' 原始URL:', video.videoUrl)
console.log(' 完整URL:', fullVideoUrl)
console.log(' 是否有效URL:', fullVideoUrl.startsWith('http') || fullVideoUrl.startsWith('/'))
// URL访
this.testVideoUrl(fullVideoUrl).then(isAccessible => {
if (!isAccessible) {
console.log('⚠️ 视频URL可能无法访问')
}
this.currentVideo = {
...video,
videoUrl: fullVideoUrl,
coverImage: this.getCoverImage(video)
}
this.showDetailDialog = true
}).catch(() => {
this.currentVideo = {
...video,
videoUrl: fullVideoUrl,
coverImage: this.getCoverImage(video)
}
this.showDetailDialog = true
})
},
// URL访
testVideoUrl(url) {
return new Promise((resolve) => {
const testVideo = document.createElement('video')
testVideo.preload = 'metadata'
testVideo.onloadedmetadata = () => {
console.log('✅ 视频URL可访问,时长:', testVideo.duration)
resolve(true)
}
testVideo.onerror = () => {
console.log('❌ 视频URL不可访问:', url)
resolve(false)
}
//
setTimeout(() => {
console.log('⏰ 视频加载超时')
resolve(false)
}, 3000)
testVideo.src = url
})
},
//
handleDialogOpen() {
this.videoPlayerReady = false
this.videoLoadFailed = false
//
this.$nextTick(() => {
if (this.$refs.videoPlayer) {
this.$refs.videoPlayer.load()
}
})
},
//
handleDialogClose() {
//
if (this.$refs.videoPlayer) {
this.$refs.videoPlayer.pause()
}
this.currentVideo = null
this.videoPlayerReady = false
this.videoLoadFailed = false
},
//
handleVideoCanPlay() {
console.log('🎬 视频可以播放了')
this.videoPlayerReady = true
this.videoLoadFailed = false
},
//
handleVideoLoaded() {
console.log('✅ 视频加载完成')
},
//
handleVideoError(event) {
console.error('❌ 视频加载错误:', event)
this.videoLoadFailed = true
this.$message.error('视频加载失败,请检查视频文件')
},
//
retryLoadVideo() {
console.log('🔄 重试加载视频')
this.videoPlayerReady = false
this.videoLoadFailed = false
if (this.currentVideo && this.currentVideo.videoUrl) {
//
const videoUrl = this.currentVideo.videoUrl +
(this.currentVideo.videoUrl.includes('?') ? '&' : '?') +
't=' + Date.now()
this.currentVideo.videoUrl = videoUrl
this.$nextTick(() => {
const videoElement = this.$refs.videoPlayer
if (videoElement) {
videoElement.load()
}
})
}
},
//
openVideoInNewTab() {
if (this.currentVideo && this.currentVideo.videoUrl) {
window.open(this.currentVideo.videoUrl, '_blank')
}
}, },
// //
@ -528,19 +763,144 @@ export default {
await trainingApi.deleteVideo(videoId) await trainingApi.deleteVideo(videoId)
this.$message.success('删除成功') this.$message.success('删除成功')
//
this.loadMyVideos() this.loadMyVideos()
//
if (this.activeTab === 'list') {
this.loadPublicVideos()
}
} catch (error) { } catch (error) {
if (error !== 'cancel') { if (error !== 'cancel') {
console.error('删除失败:', error) console.error('删除失败:', error)
this.$message.error('删除失败')
this.$message.error('删除失败: ' + (error.message || '未知错误'))
} }
} }
}, },
//
handleDialogClose() {
this.currentVideo = null
// ========== ==========
//
getCoverImage(video) {
// URL
if (video.coverImage &&
video.coverImage !== 'null' &&
video.coverImage !== 'undefined' &&
video.coverImage !== '') {
return this.getFullUrl(video.coverImage)
}
//
return this.getCategoryDefaultCover(video.category || 'other')
},
//
getCategoryDefaultCover(category) {
//
if (this.defaultCoverCache.has(category)) {
return this.defaultCoverCache.get(category)
}
// SVG
const color = this.getCategoryColor(category)
const text = this.getCategoryText(category)
// SVG
const svgString = `<svg width="320" height="180" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="${color}" />
<text x="50%" y="50%" font-family="Arial, sans-serif" font-size="16"
fill="white" text-anchor="middle" dy=".3em">
${text}
</text>
</svg>`
// base64
const base64 = btoa(unescape(encodeURIComponent(svgString)))
const dataUrl = `data:image/svg+xml;base64,${base64}`
//
this.defaultCoverCache.set(category, dataUrl)
return dataUrl
},
//
getCategoryColor(category) {
const colors = {
surgery: '#667eea', //
diagnosis: '#67cf77', // 绿
medication: '#ff6b6b', //
other: '#ffb366' //
}
return colors[category] || colors.other
},
//
getCategoryText(category) {
const texts = {
surgery: '手术技巧',
diagnosis: '疾病诊断',
medication: '药物使用',
other: '其他'
}
return texts[category] || '其他'
},
//
handleCoverImageLoad(videoId) {
console.log(`✅ 封面图片加载成功: ${videoId}`)
this.coverLoadingStates.set(videoId, true)
this.coverErrorStates.set(videoId, false)
},
//
handleCoverImageError(video) {
const videoId = video.id
console.log(`❌ 封面图片加载失败: ${videoId}`)
//
this.coverErrorStates.set(videoId, true)
// 使
this.$nextTick(() => {
const imgElements = document.querySelectorAll(`[data-video-id="${videoId}"] img`)
imgElements.forEach(img => {
img.src = this.getCategoryDefaultCover(video.category || 'other')
})
})
},
// ========== URL ==========
// URL
getFullUrl(url) {
if (!url) return ''
//
url = url.trim()
// URLbase64
if (url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('data:') ||
url.startsWith('blob:')) {
return url
}
// /
if (url.startsWith('/')) {
return window.location.origin + url
}
// uploads
return window.location.origin + '/uploads/' + url
},
// URL
getVideoUrl(videoUrl) {
return this.getFullUrl(videoUrl)
}, },
// ========== ========== // ========== ==========
@ -548,13 +908,20 @@ export default {
if (!seconds) return '00:00' if (!seconds) return '00:00'
const min = Math.floor(seconds / 60) const min = Math.floor(seconds / 60)
const sec = seconds % 60 const sec = seconds % 60
return `${min}:${sec.toString().padStart(2, '0')}`
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
}, },
formatTime(dateStr) { formatTime(dateStr) {
if (!dateStr) return '' if (!dateStr) return ''
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN')
try {
const date = new Date(dateStr)
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
} catch (error) {
return dateStr
}
}, },
formatFileSize(bytes) { formatFileSize(bytes) {
@ -563,7 +930,7 @@ export default {
const sizes = ['B', 'KB', 'MB', 'GB'] const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
},
} }
} }
</script> </script>
@ -594,8 +961,6 @@ export default {
gap: 8px; gap: 8px;
} }
.training-main { .training-main {
flex: 1; flex: 1;
display: flex; display: flex;
@ -680,19 +1045,33 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block;
transition: opacity 0.3s;
} }
.no-cover {
/* 默认封面样式 */
.default-cover {
width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 100%;
height: 100%;
color: #d9d9d9;
} }
.no-cover .el-icon {
font-size: 40px;
.cover-content {
text-align: center;
color: white;
}
.cover-icon {
font-size: 36px;
margin-bottom: 8px;
display: block;
}
.cover-text {
font-size: 14px;
font-weight: 500;
} }
.duration { .duration {
@ -704,6 +1083,7 @@ export default {
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 12px;
z-index: 2;
} }
.video-card .info { .video-card .info {
@ -837,22 +1217,28 @@ export default {
flex: 1; flex: 1;
} }
.cover-small {
.cover-small-container {
position: relative;
width: 80px; width: 80px;
height: 45px; height: 45px;
flex-shrink: 0;
}
.cover-small {
width: 100%;
height: 100%;
border-radius: 4px; border-radius: 4px;
object-fit: cover; object-fit: cover;
} }
.cover-small-placeholder { .cover-small-placeholder {
width: 80px;
height: 45px;
background: #f5f5f5;
width: 100%;
height: 100%;
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #d9d9d9;
color: white;
} }
.details { .details {
@ -863,6 +1249,10 @@ export default {
margin: 0 0 6px 0; margin: 0 0 6px 0;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
} }
.details .meta { .details .meta {
@ -878,25 +1268,79 @@ export default {
padding: 10px 0; padding: 10px 0;
} }
.video-player {
margin-bottom: 20px;
background: #000;
/* 视频加载状态 */
.video-loading {
padding: 60px;
text-align: center;
color: #666;
background: #f5f5f5;
border-radius: 6px; border-radius: 6px;
overflow: hidden;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
} }
.no-video {
padding: 60px;
.video-loading .el-icon-loading {
font-size: 36px;
margin-bottom: 16px;
color: #409eff;
}
/* 视频错误状态 */
.video-error {
padding: 40px;
text-align: center; text-align: center;
color: white;
background: #666;
color: #f56c6c;
background: #fff5f5;
border-radius: 6px;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
} }
.no-video .el-icon {
.video-error .el-icon-video-camera {
font-size: 48px; font-size: 48px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.error-tip {
margin: 16px 0 8px 0;
color: #333;
font-weight: 500;
}
.error-tip ul {
text-align: left;
display: inline-block;
margin: 8px auto;
color: #666;
}
.error-tip li {
margin: 4px 0;
}
.error-actions {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 12px;
}
.video-player {
margin-bottom: 20px;
border-radius: 6px;
overflow: hidden;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
}
.video-info-card { .video-info-card {
padding: 16px; padding: 16px;
background: #fafafa; background: #fafafa;
@ -904,18 +1348,28 @@ export default {
} }
.info-row { .info-row {
margin-bottom: 10px;
margin-bottom: 12px;
font-size: 14px; font-size: 14px;
display: flex;
} }
.info-row .label { .info-row .label {
color: #666; color: #666;
font-weight: 500; font-weight: 500;
margin-right: 8px; margin-right: 8px;
flex-shrink: 0;
width: 80px;
} }
.info-row .value { .info-row .value {
color: #333; color: #333;
flex: 1;
}
.description-text {
white-space: pre-wrap;
line-height: 1.5;
margin: 0;
} }
/* 加载和空状态 */ /* 加载和空状态 */

Loading…
Cancel
Save