Compare commits

...

7 Commits

Author SHA1 Message Date
29054c6188 format 2025-11-26 10:37:19 +08:00
dc0d6d5b48 edit not manage file 2025-11-25 23:36:32 +08:00
2b89d07edd Merge remote-tracking branch 'origin/master' 2025-11-25 18:14:08 +08:00
24fdc236b1 1 2025-11-25 18:13:51 +08:00
bbc5957b8c 文件本地上传+知识图谱功能 2025-11-25 18:06:08 +08:00
b5e3cdcd0b Merge remote-tracking branch 'origin/master' 2025-11-25 18:04:33 +08:00
c66439995f 文件本地上传+知识图谱功能 2025-11-25 18:04:27 +08:00
55 changed files with 3473 additions and 363 deletions

View File

@ -12,4 +12,5 @@ import org.springframework.stereotype.Component;
public class AIPlatformConfig {
private String host;
private String api_key;
private String profile;
}

View File

@ -1,5 +1,6 @@
package xyz.playedu.api.controller.backend.jc;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import xyz.playedu.common.annotation.Log;
@ -7,6 +8,7 @@ import xyz.playedu.common.constant.BusinessTypeConstant;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.jc.domain.Knowledge;
import xyz.playedu.jc.param.KnowledgeParam;
import xyz.playedu.jc.service.IKnowledgeService;
import java.util.HashMap;
@ -22,48 +24,52 @@ public class KnowledgeController {
@Autowired
private IKnowledgeService knowledgeService;
/** 分页列表 */
@GetMapping("/index")
public JsonResponse index(@RequestParam HashMap<String, Object> params) {
PaginationResult<Knowledge> result = knowledgeService.paginate(params);
return JsonResponse.data(result);
/**
* 获取知识点卡片列表
*/
@GetMapping("/list")
public JsonResponse list(KnowledgeParam param) {
List<Knowledge> list = knowledgeService.listVo(param);
return JsonResponse.data(list);
}
/** 全量列表 */
@GetMapping("/list")
public JsonResponse list() {
List<Knowledge> list = knowledgeService.list();
/**
* 获取知识点树结构
*/
@GetMapping("/treeList")
public JsonResponse treeList(KnowledgeParam param) {
List<Knowledge> list = knowledgeService.treeList(param);
return JsonResponse.data(list);
}
/** 详情 */
@GetMapping("/{id}")
public JsonResponse detail(@PathVariable("id") Integer id) {
Knowledge one = knowledgeService.getById(id);
return JsonResponse.data(one);
return JsonResponse.data( knowledgeService.getByIdVo(id));
}
/** 新增 */
@Log(title = "新增知识点", businessType = BusinessTypeConstant.INSERT)
@PostMapping
public JsonResponse store(@RequestBody Knowledge knowledge) {
knowledgeService.save(knowledge);
@PostMapping("/saveOrUpdateVo")
public JsonResponse saveOrUpdateVo(@RequestBody Knowledge knowledge) {
knowledgeService.saveOrUpdateVo(knowledge);
return JsonResponse.success();
}
/** 修改 */
@Log(title = "修改知识点", businessType = BusinessTypeConstant.UPDATE)
@PutMapping
public JsonResponse update(@RequestBody Knowledge knowledge) {
knowledgeService.updateById(knowledge);
return JsonResponse.success();
}
/** 删除 */
@Log(title = "删除知识点", businessType = BusinessTypeConstant.DELETE)
@DeleteMapping("/{id}")
public JsonResponse destroy(@PathVariable("id") Integer id) {
knowledgeService.removeById(id);
Knowledge byId = knowledgeService.getById(id);
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.likeRight(Knowledge::getKnowledgeCode, byId.getKnowledgeCode());
knowledgeService.remove(queryWrapper);
return JsonResponse.success();
}
}

View File

@ -9,21 +9,33 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import xyz.playedu.common.annotation.Log;
import xyz.playedu.common.constant.BusinessTypeConstant;
import xyz.playedu.common.constant.CommonConstant;
import xyz.playedu.common.domain.Department;
import xyz.playedu.common.domain.Group;
import xyz.playedu.common.domain.User;
import xyz.playedu.common.domain.UserGroup;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.service.*;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.common.util.StringUtil;
import xyz.playedu.course.constants.CourseConstant;
import xyz.playedu.course.domain.Course;
import xyz.playedu.course.domain.CourseDepartmentUser;
import xyz.playedu.jc.domain.BookDepartmentUser;
import xyz.playedu.jc.domain.JCResource;
import xyz.playedu.jc.domain.Textbook;
import xyz.playedu.jc.domain.dto.TextbookRequestDTO;
import xyz.playedu.jc.service.IBookDepartmentUserService;
import xyz.playedu.jc.service.ITextbookService;
import xyz.playedu.jc.service.JCIResourceService;
import xyz.playedu.knowledge.domain.KnowledgeMessages;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 教材管理 后台接口
@ -38,17 +50,222 @@ public class TextbookController {
@Autowired
private IBookDepartmentUserService bookDepartmentUserService;
@Autowired
private JCIResourceService jciResourceService;
@Autowired private DepartmentService departmentService;
@Autowired private UserGroupService userGroupService;
@Autowired private UserDepartmentService userDepartmentService;
@Autowired private UserService userService;
@Autowired private GroupService groupService;
@GetMapping("/index")
public JsonResponse index(@RequestParam HashMap<String, Object> params) {
/** 调用服务层分页查询方法Service 层已包含完整的分页信息 */
PaginationResult<Textbook> result = textbookService.paginate(params);
HashMap<String, Object> data = new HashMap<>();
data.put("data", result.getData());
data.put("total", result.getTotal());
// 课程封面资源ID
List<Integer> rids = new ArrayList<>();
rids.addAll(result.getData().stream().map(Textbook::getThumb).toList());
List<Integer> bookIds = result.getData().stream().map(Textbook::getId).toList();
// data.put("course_category_ids", courseService.getCategoryIdsGroup(courseIds));
// data.put("categories", categoryService.id2name());
// data.put("departments", departmentService.id2name());
// Map<Integer, Integer> courseIdRecordCountMap = new HashMap<>();
// doGetRecords(courseIds)
// .forEach(
// (key, value) -> {
// courseIdRecordCountMap.put(key, value.size());
// });
// data.put("records", courseIdRecordCountMap);
// 获取签名url
data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids));
// 指派范围
Map<Integer, Integer> book_user_count = new HashMap<>();
List<BookDepartmentUser> courseDepartmentUserList =
bookDepartmentUserService.chunksByBookIds(bookIds);
if (StringUtil.isNotEmpty(courseDepartmentUserList)) {
List<Integer> depIds =
courseDepartmentUserList.stream()
.filter(
courseDepartmentUser ->
CourseConstant.COURSE_TYPE_DEP
== courseDepartmentUser.getType())
.map(BookDepartmentUser::getRangeId)
.distinct()
.toList();
Map<Integer, Department> depMap = new HashMap<>();
if (StringUtil.isNotEmpty(depIds)) {
depMap =
departmentService.chunk(depIds).stream()
.collect(Collectors.toMap(Department::getId, e -> e));
}
Map<Integer, Department> finalDepMap = depMap;
courseDepartmentUserList.stream()
.collect(Collectors.groupingBy(BookDepartmentUser::getBookId))
.forEach(
(key, value) -> {
if (StringUtil.isNotEmpty(value)) {
List<Integer> courseDepIds = new ArrayList<>();
List<Integer> courseUserIds = new ArrayList<>();
value.forEach(
courseDepartmentUser -> {
if (CourseConstant.COURSE_TYPE_DEP
== courseDepartmentUser.getType()) {
Department dep =
finalDepMap.get(
courseDepartmentUser
.getRangeId());
if (StringUtil.isNotNull(dep)) {
courseDepIds.add(dep.getId());
String parentChain = "";
if (StringUtil.isEmpty(
dep.getParentChain())) {
parentChain = dep.getId() + "";
} else {
parentChain =
dep.getParentChain()
+ ","
+ dep.getId();
}
List<Department> childDepartmentList =
departmentService
.getChildDepartmentsByParentChain(
dep.getId(),
parentChain);
if (StringUtil.isNotEmpty(
childDepartmentList)) {
courseDepIds.addAll(
childDepartmentList.stream()
.map(Department::getId)
.toList());
}
}
} else if (CourseConstant.COURSE_TYPE_GROUP
== courseDepartmentUser.getType()) {
List<UserGroup> userGroupList =
userGroupService.chunksByGroupIds(
new ArrayList<>() {
{
add(
courseDepartmentUser
.getRangeId());
}
});
if (StringUtil.isNotEmpty(userGroupList)) {
courseUserIds.addAll(
userGroupList.stream()
.map(UserGroup::getUserId)
.toList());
}
} else {
courseUserIds.add(
courseDepartmentUser.getRangeId());
}
});
if (StringUtil.isNotEmpty(courseDepIds)) {
List<Integer> departmentUserIds =
userDepartmentService.getUserIdsByDepIds(
courseDepIds);
if (StringUtil.isNotEmpty(departmentUserIds)) {
courseUserIds.addAll(departmentUserIds);
}
}
// 过滤本地逻辑删除的学员
List<Integer> userIds =
userService.chunks(courseUserIds).stream()
.filter(
user ->
user.getDeleted().intValue()
== CommonConstant.ZERO
.intValue())
.map(User::getId)
.toList();
book_user_count.put(
key, userIds.stream().distinct().toList().size());
}
});
}
data.put("course_user_count", book_user_count);
/** 直接返回 Service 层的结果 */
return JsonResponse.data(result);
return JsonResponse.data(data);
}
@GetMapping("/{id}")
@Log(title = "线上课-编辑", businessType = BusinessTypeConstant.GET)
public JsonResponse edit(@PathVariable(name = "id") Integer id) throws NotFoundException {
Textbook textbook = textbookService.findOrFail(id);
HashMap<String, Object> data = new HashMap<>();
data.put("textbook", textbook);
List<Integer> rids = new ArrayList<>();
rids.add(textbook.getThumb());
// 获取签名url
data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids));
// 指派范围
List<BookDepartmentUser> courseDepartmentUserList =
bookDepartmentUserService.chunksByCourseId(id);
if (StringUtil.isNotEmpty(courseDepartmentUserList)) {
Map<Integer, String> deps =
departmentService
.chunk(
courseDepartmentUserList.stream()
.filter(
dto ->
CourseConstant.COURSE_TYPE_DEP
== dto.getType())
.map(BookDepartmentUser::getRangeId)
.toList())
.stream()
.collect(Collectors.toMap(Department::getId, Department::getName));
data.put("deps", deps);
Map<Integer, String> users =
userService
.chunks(
courseDepartmentUserList.stream()
.filter(
dto ->
CourseConstant.COURSE_TYPE_USER
== dto.getType())
.map(BookDepartmentUser::getRangeId)
.toList())
.stream()
.filter(
user ->
user.getDeleted().intValue()
== CommonConstant.ZERO.intValue())
.collect(Collectors.toMap(User::getId, User::getName));
data.put("users", users);
Map<Integer, String> groups =
groupService
.chunkByIds(
courseDepartmentUserList.stream()
.filter(
dto ->
CourseConstant.COURSE_TYPE_GROUP
== dto.getType())
.map(BookDepartmentUser::getRangeId)
.toList())
.stream()
.collect(Collectors.toMap(Group::getId, Group::getName));
data.put("groups", groups);
} else {
data.put("deps", new HashMap<>());
data.put("users", new HashMap<>());
data.put("groups", new HashMap<>());
}
return JsonResponse.data(data);
}
@GetMapping("/list")
public JsonResponse list() {
@ -56,11 +273,11 @@ public class TextbookController {
return JsonResponse.data(list);
}
@GetMapping("/{id}")
public JsonResponse detail(@PathVariable("id") Integer id) {
Textbook one = textbookService.getById(id);
return JsonResponse.data(one);
}
// @GetMapping("/{id}")
// public JsonResponse detail(@PathVariable("id") Integer id) {
// Textbook one = textbookService.getById(id);
// return JsonResponse.data(one);
// }
// @Transactional
@PostMapping

View File

@ -0,0 +1,60 @@
package xyz.playedu.api.controller.backend.system;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import xyz.playedu.common.config.PlatformConfig;
import xyz.playedu.common.config.ServerConfig;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.common.util.FileUploadUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* 通用请求处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/backend/v1/localUpload")
public class LocalFileController {
private static final Logger log = LoggerFactory.getLogger(LocalFileController.class);
private static final String FILE_DELIMETER = ",";
@Autowired
private ServerConfig serverConfig;
/**
* 通用上传请求单个
*/
@PostMapping("/upload")
public JsonResponse uploadFile(MultipartFile file) throws Exception {
// 上传文件路径
String filePath = PlatformConfig.getUploadPath();
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
try {
String url = serverConfig.getUrl() + fileName;
JSONObject ajax = new JSONObject();
ajax.put("url", url);
ajax.put("path", fileName);
ajax.put("newFileName", FileUploadUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return JsonResponse.data(ajax);
} catch (Exception e) {
return JsonResponse.error(e.getMessage());
}
}
}

View File

@ -54,6 +54,8 @@ public class UploadController {
@Autowired private AppConfigService appConfigService;
@PostMapping("/minio")
@Log(title = "上传-MinIO", businessType = BusinessTypeConstant.UPLOAD)
public JsonResponse uploadMinio(

View File

@ -0,0 +1,49 @@
package xyz.playedu.api.controller.frontend;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import xyz.playedu.common.annotation.Log;
import xyz.playedu.common.constant.BusinessTypeConstant;
import xyz.playedu.common.types.JsonResponse;
import xyz.playedu.jc.domain.Knowledge;
import xyz.playedu.jc.param.KnowledgeParam;
import xyz.playedu.jc.service.IKnowledgeService;
import java.util.List;
/**
* 知识点管理
*/
@RestController
@RequestMapping("/api/v1/jc/knowledge")
public class KnowledgeController {
@Autowired
private IKnowledgeService knowledgeService;
/**
* 获取知识点卡片列表
*/
@GetMapping("/list")
public JsonResponse list(KnowledgeParam param) {
List<Knowledge> list = knowledgeService.listVo(param);
return JsonResponse.data(list);
}
/**
* 获取知识点树结构
*/
@GetMapping("/treeList")
public JsonResponse treeList(KnowledgeParam param) {
List<Knowledge> list = knowledgeService.treeList(param);
return JsonResponse.data(list);
}
/** 详情 */
@GetMapping("/getDetail")
public JsonResponse detail(@RequestParam("id") Integer id) {
return JsonResponse.data( knowledgeService.stuGetByIdVo(id));
}
}

View File

@ -3,10 +3,14 @@ server:
tomcat:
max-swallow-size: 10000MB
connection-timeout: 600000
ai:
platform:
host: "http://localhost:9380/"
api-key: "ragflow-VlZWVjMDg0ZjAzMTExZWZhZDhkZTU5ZD"
platform:
config:
profile: D:/ruoyi/uploadPath
spring:
profiles:
active: kafka,quartz,dev

View File

@ -0,0 +1,50 @@
package xyz.playedu.common.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "platform.config")
public class PlatformConfig {
private static String profile;
public static String getProfile() {
return profile;
}
public void setProfile(String profile) {
PlatformConfig.profile = profile;
}
/**
* 获取导入上传路径
*/
public static String getImportPath() {
return getProfile() + "/import";
}
/**
* 获取头像上传路径
*/
public static String getAvatarPath() {
return getProfile() + "/avatar";
}
/**
* 获取下载路径
*/
public static String getDownloadPath() {
return getProfile() + "/download/";
}
/**
* 获取上传路径
*/
public static String getUploadPath() {
return getProfile() + "/upload";
}
}

View File

@ -0,0 +1,31 @@
package xyz.playedu.common.config;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import xyz.playedu.common.util.ServletUtils;
/**
* 服务相关配置
*
* @author ruoyi
*/
@Component
public class ServerConfig {
public static String getDomain(HttpServletRequest request) {
StringBuffer url = request.getRequestURL();
String contextPath = request.getServletContext().getContextPath();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
}
/**
* 获取完整的请求路径包括域名端口上下文访问路径
*
* @return 服务地址
*/
public String getUrl() {
HttpServletRequest request = ServletUtils.getRequest();
return getDomain(request);
}
}

View File

@ -0,0 +1,171 @@
package xyz.playedu.common.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.Objects;
import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;
import xyz.playedu.common.config.PlatformConfig;
/**
* 文件上传工具类
*
* @author ruoyi
*/
public class FileUploadUtils {
/**
* 默认大小 50M
*/
public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024L;
/**
* 默认的文件名最大长度 100
*/
public static final int DEFAULT_FILE_NAME_LENGTH = 100;
/**
* 默认上传的地址
*/
private static String defaultBaseDir = PlatformConfig.getProfile();
public static final String RESOURCE_PREFIX = "/profile";
public static String getDefaultBaseDir() {
return defaultBaseDir;
}
public static void setDefaultBaseDir(String defaultBaseDir) {
FileUploadUtils.defaultBaseDir = defaultBaseDir;
}
/**
* 以默认配置进行文件上传
*
* @param file 上传的文件
* @return 文件名称
* @throws Exception
*/
public static final String upload(MultipartFile file) throws IOException {
try {
return upload(getDefaultBaseDir(), file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
} catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* 根据文件路径上传
*
* @param baseDir 相对应用的基目录
* @param file 上传的文件
* @return 文件名称
* @throws IOException
*/
public static final String upload(String baseDir, MultipartFile file) throws IOException {
try {
return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION);
} catch (Exception e) {
throw new IOException(e.getMessage(), e);
}
}
/**
* 文件上传
*
* @param baseDir 相对应用的基目录
* @param file 上传的文件
* @param allowedExtension 上传文件类型
* @return 返回上传成功的文件名
* @throws IOException 比如读写文件出错时
*/
public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension)
throws Exception {
int fileNamelength = Objects.requireNonNull(file.getOriginalFilename()).length();
if (fileNamelength > FileUploadUtils.DEFAULT_FILE_NAME_LENGTH) {
throw new Exception("文件名过长"+FileUploadUtils.DEFAULT_FILE_NAME_LENGTH);
}
String fileName = extractFilename(file);
String absPath = getAbsoluteFile(baseDir, fileName).getAbsolutePath();
file.transferTo(Paths.get(absPath));
return getPathFileName(baseDir, fileName);
}
/**
* 编码文件名
*/
public static final String extractFilename(MultipartFile file) {
return StringUtils.format("{}/{}_{}.{}", DateUtils.datePath(),
FilenameUtils.getBaseName(file.getOriginalFilename()), Seq.getId(Seq.uploadSeqType), getExtension(file));
}
public static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException {
File desc = new File(uploadDir + File.separator + fileName);
if (!desc.exists()) {
if (!desc.getParentFile().exists()) {
desc.getParentFile().mkdirs();
}
}
return desc;
}
public static final String getPathFileName(String uploadDir, String fileName) throws IOException {
int dirLastIndex = PlatformConfig.getProfile().length() + 1;
String currentDir = StringUtils.substring(uploadDir, dirLastIndex);
return RESOURCE_PREFIX + "/" + currentDir + "/" + fileName;
}
/**
* 判断MIME类型是否是允许的MIME类型
*
* @param extension
* @param allowedExtension
* @return
*/
public static final boolean isAllowedExtension(String extension, String[] allowedExtension) {
for (String str : allowedExtension) {
if (str.equalsIgnoreCase(extension)) {
return true;
}
}
return false;
}
/**
* 获取文件名的后缀
*
* @param file 表单文件
* @return 后缀名
*/
public static final String getExtension(MultipartFile file) {
String extension = FilenameUtils.getExtension(file.getOriginalFilename());
if (StringUtils.isEmpty(extension)) {
extension = MimeTypeUtils.getExtension(Objects.requireNonNull(file.getContentType()));
}
return extension;
}
/**
* 获取文件名称 /profile/upload/2022/04/16/ruoyi.png -- ruoyi.png
*
* @param fileName 路径名称
* @return 没有文件路径的名称
*/
public static String getName(String fileName) {
if (fileName == null) {
return null;
}
int lastUnixPos = fileName.lastIndexOf('/');
int lastWindowsPos = fileName.lastIndexOf('\\');
int index = Math.max(lastUnixPos, lastWindowsPos);
return fileName.substring(index + 1);
}
}

View File

@ -0,0 +1,56 @@
package xyz.playedu.common.util;
/**
* 媒体类型工具类
*
* @author ruoyi
*/
public class MimeTypeUtils {
public static final String IMAGE_PNG = "image/png";
public static final String IMAGE_JPG = "image/jpg";
public static final String IMAGE_JPEG = "image/jpeg";
public static final String IMAGE_BMP = "image/bmp";
public static final String IMAGE_GIF = "image/gif";
public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
public static final String[] FLASH_EXTENSION = {"swf", "flv"};
public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
"asf", "rm", "rmvb"};
public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
public static final String[] DEFAULT_ALLOWED_EXTENSION = {
// 图片
"bmp", "gif", "jpg", "jpeg", "png",
// word excel powerpoint
"doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
// 压缩文件
"rar", "zip", "gz", "bz2",
// 视频格式
"mp4", "avi", "rmvb",
// pdf
"pdf"};
public static String getExtension(String prefix) {
switch (prefix) {
case IMAGE_PNG:
return "png";
case IMAGE_JPG:
return "jpg";
case IMAGE_JPEG:
return "jpeg";
case IMAGE_BMP:
return "bmp";
case IMAGE_GIF:
return "gif";
default:
return "";
}
}
}

View File

@ -0,0 +1,76 @@
package xyz.playedu.common.util;
import cn.hutool.core.date.DateUtil;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author ruoyi 序列生成类
*/
public class Seq {
// 通用序列类型
public static final String commSeqType = "COMMON";
// 上传序列类型
public static final String uploadSeqType = "UPLOAD";
// 机器标识
private static final String machineCode = "A";
// 通用接口序列数
private static AtomicInteger commSeq = new AtomicInteger(1);
// 上传接口序列数
private static AtomicInteger uploadSeq = new AtomicInteger(1);
/**
* 获取通用序列号
*
* @return 序列值
*/
public static String getId() {
return getId(commSeqType);
}
/**
* 默认16位序列号 yyMMddHHmmss + 一位机器标识 + 3长度循环递增字符串
*
* @return 序列值
*/
public static String getId(String type) {
AtomicInteger atomicInt = commSeq;
if (uploadSeqType.equals(type)) {
atomicInt = uploadSeq;
}
return getId(atomicInt, 3);
}
/**
* 通用接口序列号 yyMMddHHmmss + 一位机器标识 + length长度循环递增字符串
*
* @param atomicInt 序列数
* @param length 数值长度
* @return 序列值
*/
public static String getId(AtomicInteger atomicInt, int length) {
String result = DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss");
result += machineCode;
result += getSeq(atomicInt, length);
return result;
}
/**
* 序列循环递增字符串[1, 10 (length)幂次方), 用0左补齐length位数
*
* @return 序列值
*/
private synchronized static String getSeq(AtomicInteger atomicInt, int length) {
// 先取值再+1
int value = atomicInt.getAndIncrement();
// 如果更新后值>=10 (length)幂次方则重置为1
int maxSeq = (int) Math.pow(10, length);
if (atomicInt.get() >= maxSeq) {
atomicInt.set(1);
}
// 转字符串用0左补齐
return StringUtils.padl(value, length);
}
}

View File

@ -0,0 +1,72 @@
package xyz.playedu.common.util;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 客户端工具类
*
* @author ruoyi
*/
public class ServletUtils {
/**
* 获取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
/**
* 获取session
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_book_chapter")
public class BookChapter {
public class BookChapter extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -34,23 +35,11 @@ public class BookChapter {
/** 章节名 */
@TableField("name")
private String name;
/** 层级 */
private Integer level;
/** 排序 */
@TableField("sort")
private Integer sort;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("creator")
private String creator;
@TableField("updater")
private String updater;
@TableField("tenant_id")
private String tenantId;
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_book_department_user")
public class BookDepartmentUser {
public class BookDepartmentUser extends TenantBaseDO {
/** 教材ID */
@TableField("book_id")
@ -28,18 +29,4 @@ public class BookDepartmentUser {
@TableField("type")
private Integer type;
@TableField("creator")
private String creator;
@TableField("updater")
private String updater;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("tenant_id")
private String tenantId;
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_book_paper")
public class BookPaper {
public class BookPaper extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -32,18 +33,4 @@ public class BookPaper {
@TableField("extra")
private String extra;
@TableField("creator")
private String creator;
@TableField("updater")
private String updater;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("tenant_id")
private String tenantId;
}

View File

@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -15,7 +16,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_chapter_content")
public class ChapterContent {
public class ChapterContent extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -34,28 +35,9 @@ public class ChapterContent {
@TableField("type")
private String type;
// /** 关联资源ID逗号分隔 */
// @TableField("resource_ids")
// private String resourceIds;
//
// /** 关联知识点ID逗号分隔 */
// @TableField("knowledge_ids")
// private String knowledgeIds;
private String knowledgeCode;
/** 创建人 */
@TableField("creator")
private String creator;
/** 更新人 */
@TableField("updater")
private String updater;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("tenant_id")
private String tenantId;
/** 章节名称 */
@TableField(exist = false)
private String chapterName;
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_discussion")
public class Discussion {
public class Discussion extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -51,20 +52,12 @@ public class Discussion {
@TableField("status")
private Integer status;
/** 创建人(可以是用户名) */
@TableField("creator")
private String creator;
/** 知识点编码 */
@TableField(exist = false)
private String knowledgeCode;
@TableField(exist = false)
private String chapterName;
@TableField(exist = false)
private String orderType;
/** 更新人 */
@TableField("updater")
private String updater;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("tenant_id")
private String tenantId;
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_discussion_detail")
public class DiscussionDetail {
public class DiscussionDetail extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -46,19 +47,4 @@ public class DiscussionDetail {
/** 讨论内容 */
@TableField("content")
private String content;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("creator")
private String creator;
@TableField("updater")
private String updater;
@TableField("tenant_id")
private String tenantId;
}

View File

@ -31,8 +31,8 @@ public class JCResource {
@TableField("name")
private String name;
@TableField("konwledge_code")
private String konwledgeCode;
@TableField("knowledge_code")
private String knowledgeCode;
/** 文本描述 AI分析或自动填入 */

View File

@ -5,8 +5,10 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
import java.util.List;
/**
* 知识点表
@ -14,7 +16,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_knowledge")
public class Knowledge {
public class Knowledge extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -31,9 +33,9 @@ public class Knowledge {
@TableField("name")
private String name;
/** 知识点code字段名拼写为 konwledge_code */
@TableField("konwledge_code")
private String konwledgeCode;
/** 知识点code字段名拼写为 knowledge_code */
@TableField("knowledge_code")
private String knowledgeCode;
/** 知识点介绍 */
@TableField("desc")
@ -41,11 +43,14 @@ public class Knowledge {
/** 层级 */
@TableField("level")
private String level;
private Integer level;
/** 知识点类型 */
@TableField("type")
private String type;
/** 是否是真实知识点 */
@TableField("is_real")
private String isReal;
/** 当前层级排序 */
@TableField("order_num")
@ -54,19 +59,7 @@ public class Knowledge {
/** 预留JSON */
@TableField("extra_json")
private String extraJson;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("creator")
private String creator;
@TableField("updater")
private String updater;
@TableField("tenant_id")
private String tenantId;
//子节点
@TableField(exist = false)
private List<Knowledge> children;
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_note")
public class Note {
public class Note extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -50,19 +51,11 @@ public class Note {
/** 存储笔记信息的JSON位置信息等 */
@TableField("extra_json")
private String extraJson;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("creator")
private String creator;
@TableField("updater")
private String updater;
@TableField("tenant_id")
private String tenantId;
/** 知识点编码 */
@TableField(exist = false)
private String knowledgeCode;
@TableField(exist = false)
private String chapterName;
@TableField(exist = false)
private String orderType;
}

View File

@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import xyz.playedu.framework.tenant.core.db.TenantBaseDO;
import java.util.Date;
@ -14,7 +15,7 @@ import java.util.Date;
*/
@Data
@TableName("jc_textbook")
public class Textbook {
public class Textbook extends TenantBaseDO {
@TableId(type = IdType.AUTO)
private Integer id;
@ -25,7 +26,7 @@ public class Textbook {
/** 封面地址 */
@TableField("thumb")
private String thumb;
private Integer thumb;
/** 简介 */
@TableField("short_desc")
@ -54,20 +55,4 @@ public class Textbook {
@TableField("publish_time")
private Date publishTime;
/** 创建人 */
@TableField("creator")
private String creator;
/** 更新人 */
@TableField("updater")
private String updater;
@TableField("create_time")
private Date createTime;
@TableField("update_time")
private Date updateTime;
@TableField("tenant_id")
private String tenantId;
}

View File

@ -26,7 +26,7 @@ public class TextbookRequestDTO {
/** 封面地址 */
@TableField("thumb")
private String thumb;
private Integer thumb;
/** 简介 */
@TableField("short_desc")

View File

@ -1,10 +1,15 @@
package xyz.playedu.jc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import xyz.playedu.jc.domain.Discussion;
import xyz.playedu.jc.domain.Note;
import java.util.List;
/**
* 讨论 Mapper
*/
public interface DiscussionMapper extends BaseMapper<Discussion> {
List<Discussion> listVo(@Param("param") Discussion note);
}

View File

@ -1,10 +1,14 @@
package xyz.playedu.jc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import xyz.playedu.jc.domain.Note;
import java.util.List;
/**
* 笔记 Mapper
*/
public interface NoteMapper extends BaseMapper<Note> {
List<Note> listVo(@Param("param") Note note);
}

View File

@ -0,0 +1,13 @@
package xyz.playedu.jc.param;
import lombok.Data;
@Data
public class KnowledgeParam {
//书本id
private Integer bookId;
//知识点名称
private String name;
//知识点code
private String knowledgeCode;
}

View File

@ -1,8 +1,15 @@
package xyz.playedu.jc.service;
import com.baomidou.mybatisplus.extension.service.IService;
import xyz.playedu.course.domain.CourseDepartmentUser;
import xyz.playedu.jc.domain.BookDepartmentUser;
import java.util.List;
public interface IBookDepartmentUserService extends IService<BookDepartmentUser> {
void removeByBookId(Integer bookId);
List<BookDepartmentUser> chunksByBookIds(List<Integer> bookIds);
List<BookDepartmentUser> chunksByCourseId(Integer courseId);
}

View File

@ -1,15 +1,24 @@
package xyz.playedu.jc.service;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.jc.domain.Knowledge;
import xyz.playedu.jc.param.KnowledgeParam;
import java.util.HashMap;
import java.util.List;
/**
* 知识点 Service
*/
public interface IKnowledgeService extends IService<Knowledge> {
List<Knowledge> listVo(KnowledgeParam param);
List<Knowledge> treeList(KnowledgeParam param);
void saveOrUpdateVo(Knowledge data);
JSONObject getByIdVo(Integer id);
JSONObject stuGetByIdVo(Integer id);
PaginationResult<Knowledge> paginate(HashMap<String, Object> params);
}

View File

@ -1,7 +1,9 @@
package xyz.playedu.jc.service;
import com.baomidou.mybatisplus.extension.service.IService;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.jc.domain.JCResource;
import xyz.playedu.jc.domain.Textbook;
import java.util.HashMap;
@ -12,4 +14,7 @@ import java.util.HashMap;
public interface ITextbookService extends IService<Textbook> {
PaginationResult<Textbook> paginate(HashMap<String, Object> params);
Textbook findOrFail(Integer id) throws NotFoundException;
}

View File

@ -1,7 +1,14 @@
package xyz.playedu.jc.service;
import com.baomidou.mybatisplus.extension.service.IService;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.course.domain.Course;
import xyz.playedu.jc.domain.JCResource;
import java.util.List;
import java.util.Map;
public interface JCIResourceService extends IService<JCResource> {
Map<Integer, String> chunksPreSignUrlByIds(List<Integer> ids);
}

View File

@ -2,10 +2,13 @@ package xyz.playedu.jc.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import xyz.playedu.course.domain.CourseDepartmentUser;
import xyz.playedu.jc.domain.BookDepartmentUser;
import xyz.playedu.jc.mapper.BookDepartmentUserMapper;
import xyz.playedu.jc.service.IBookDepartmentUserService;
import java.util.List;
@Service
public class BookDepartmentUserServiceImpl
extends ServiceImpl<BookDepartmentUserMapper, BookDepartmentUser>
@ -15,4 +18,15 @@ public class BookDepartmentUserServiceImpl
public void removeByBookId(Integer bookId) {
remove(query().getWrapper().eq("book_id", bookId));
}
@Override
public List<BookDepartmentUser> chunksByBookIds(List<Integer> bookIds) {
return List.of();
}
@Override
public List<BookDepartmentUser> chunksByCourseId(Integer bookId) {
return list(query().getWrapper().eq("book_id", bookId));
}
}

View File

@ -1,13 +1,50 @@
package xyz.playedu.jc.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.service.AppConfigService;
import xyz.playedu.common.util.S3Util;
import xyz.playedu.common.util.StringUtil;
import xyz.playedu.course.domain.Course;
import xyz.playedu.jc.domain.JCResource;
import xyz.playedu.jc.mapper.JCResourceMapper;
import xyz.playedu.jc.service.JCIResourceService;
import xyz.playedu.resource.domain.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class JCResourceServiceImpl
extends ServiceImpl<JCResourceMapper, JCResource>
implements JCIResourceService {
@Autowired
private AppConfigService appConfigService;
@Override
public Map<Integer, String> chunksPreSignUrlByIds(List<Integer> ids) {
S3Util s3Util = new S3Util(appConfigService.getS3Config());
Map<Integer, String> preSignUrlMap = new HashMap<>();
if (StringUtil.isNotEmpty(ids)) {
List<JCResource> resourceList = list(query().getWrapper().in("id", ids));
if (StringUtil.isNotEmpty(resourceList)) {
resourceList.forEach(
resource -> {
String name = resource.getName() + "." + resource.getExtension();
String url = s3Util.generateDownloadUrl(resource.getPath(), name);
if (StringUtil.isNotEmpty(url)) {
preSignUrlMap.put(resource.getId(), url);
}
});
}
}
return preSignUrlMap;
}
}

View File

@ -1,75 +1,255 @@
package xyz.playedu.jc.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.playedu.common.context.FCtx;
import xyz.playedu.common.domain.User;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.jc.domain.Knowledge;
import xyz.playedu.jc.mapper.KnowledgeMapper;
import xyz.playedu.jc.domain.*;
import xyz.playedu.jc.mapper.*;
import xyz.playedu.jc.param.KnowledgeParam;
import xyz.playedu.jc.service.IKnowledgeService;
import xyz.playedu.jc.service.ITextbookService;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* 知识点 Service 实现
*/
@Slf4j
@Service
public class KnowledgeServiceImpl
extends ServiceImpl<KnowledgeMapper, Knowledge>
implements IKnowledgeService {
public class KnowledgeServiceImpl extends ServiceImpl<KnowledgeMapper, Knowledge> implements IKnowledgeService {
@Autowired
private ChapterContentMapper contentMapper;
@Autowired
private JCResourceMapper resourceMapper;
@Autowired
private BookChapterMapper chapterMapper;
@Autowired
private NoteMapper noteMapper;
@Autowired
private DiscussionMapper discussionMapper;
@Override
public PaginationResult<Knowledge> paginate(HashMap<String, Object> params) {
try {
Integer page = MapUtils.getInteger(params, "page", 1);
Integer size = MapUtils.getInteger(params, "size", 10);
public List<Knowledge> listVo(KnowledgeParam param) {
//获取知识点卡片
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Knowledge::getBookId, param.getBookId())
.eq(Knowledge::getIsReal,"1").orderByAsc(Knowledge::getOrderNum);
return list(queryWrapper);
}
@Override
public JSONObject getByIdVo(Integer id) {
JSONObject res = new JSONObject();
Knowledge knowledge = this.getById(id);
res.put("knowledge", knowledge);
//获取各个板块该知识点下的资源
LambdaQueryWrapper<ChapterContent> contentWrapper = new LambdaQueryWrapper<>();
contentWrapper.select(ChapterContent::getId,ChapterContent::getChapterId).like(ChapterContent::getKnowledgeCode, knowledge.getKnowledgeCode()).eq(ChapterContent::getBookId, knowledge.getBookId());
List<ChapterContent> contents = contentMapper.selectList(contentWrapper);
for (ChapterContent content : contents) {
BookChapter bookChapter = chapterMapper.selectById(content.getChapterId());
content.setChapterName(bookChapter.getName());
}
res.put("contents", contents);
LambdaQueryWrapper<JCResource> resourceWrapper = new LambdaQueryWrapper<>();
resourceWrapper.like(JCResource::getKnowledgeCode, knowledge.getKnowledgeCode()).eq(JCResource::getBookId, knowledge.getBookId());
res.put("resources", resourceMapper.selectList(resourceWrapper));
return res;
}
Page<Knowledge> pageParam = new Page<>(page, size);
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
@Override
public JSONObject stuGetByIdVo(Integer id) {
Knowledge knowledge = this.getById(id);
//获取当前学生信息
User user = FCtx.getUser();
JSONObject res = this.getByIdVo(id);
//获取笔记信息
Note param = new Note();
param.setBookId(knowledge.getBookId());
param.setKnowledgeCode(knowledge.getKnowledgeCode());
param.setUserId(user.getId());
List<Note> notes = noteMapper.listVo(param);
res.put("notes", notes);
//获取讨论信息
Discussion discussion = new Discussion();
discussion.setBookId(knowledge.getBookId());
discussion.setKnowledgeCode(knowledge.getKnowledgeCode());
discussion.setUserId(user.getId());
List<Discussion> discussions = discussionMapper.listVo(discussion);
res.put("discussions", discussions);
return res;
}
Integer bookId = MapUtils.getInteger(params, "bookId");
if (bookId != null) {
queryWrapper.eq(Knowledge::getBookId, bookId);
}
@Override
public List<Knowledge> treeList(KnowledgeParam param) {
//获取全部知识点
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Knowledge::getBookId, param.getBookId()).orderByAsc(Knowledge::getOrderNum);
List<Knowledge> allKnowledges = this.list(queryWrapper);
// 处理为树结构
return buildTree(allKnowledges);
}
Integer parentId = MapUtils.getInteger(params, "parentId");
if (parentId != null) {
queryWrapper.eq(Knowledge::getParentId, parentId);
}
String name = MapUtils.getString(params, "name");
if (name != null && !name.isEmpty()) {
queryWrapper.like(Knowledge::getName, name);
}
queryWrapper.orderByAsc(Knowledge::getOrderNum);
IPage<Knowledge> pageResult = this.page(pageParam, queryWrapper);
Long total = pageResult.getTotal();
PaginationResult<Knowledge> result = new PaginationResult<>();
result.setData(pageResult.getRecords());
result.setTotal(total);
result.setCurrent(page);
result.setSize(size);
result.setPages((total + size - 1) / size);
return result;
} catch (Exception e) {
log.error("分页查询知识点失败,参数:{}", params, e);
PaginationResult<Knowledge> emptyResult = new PaginationResult<>();
emptyResult.setData(new ArrayList<>());
emptyResult.setTotal(0L);
emptyResult.setCurrent(MapUtils.getInteger(params, "page", 1));
emptyResult.setSize(MapUtils.getInteger(params, "size", 10));
emptyResult.setPages(0L);
return emptyResult;
@Override
public void saveOrUpdateVo(Knowledge data) {
if (ObjectUtil.isEmpty(data.getId())){
Knowledge knowledge = this.getById(data.getId());
knowledge.setDesc(data.getDesc());
knowledge.setName(data.getName());
knowledge.setType(data.getType());
this.updateById(knowledge);
return;
}
if (ObjectUtil.isNotEmpty(data.getParentId())||data.getParentId()==0){
data.setLevel(1);
data.setKnowledgeCode(generateKnowledgeCode(data.getParentId()));
this.save(data);
}else {
Knowledge byId = this.getById(data.getParentId());
data.setLevel(byId.getLevel()+1);
data.setKnowledgeCode(generateKnowledgeCode(data.getParentId()));
this.save(data);
}
}
private List<Knowledge> buildTree(List<Knowledge> knowledges) {
// 创建一个map来存储id到对象的映射方便快速查找
HashMap<Integer, Knowledge> knowledgeMap = new HashMap<>();
for (Knowledge knowledge : knowledges) {
knowledgeMap.put(knowledge.getId(), knowledge);
}
// 存储最终的根节点
List<Knowledge> rootNodes = new ArrayList<>();
for (Knowledge knowledge : knowledges) {
Integer parentId = knowledge.getParentId();
if (parentId == null || parentId == 0) { // 假设没有父节点的是根节点
rootNodes.add(knowledge);
} else {
Knowledge parent = knowledgeMap.get(parentId);
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(knowledge);
}
}
}
return rootNodes;
}
/**
* 生成知识点编码
* @param parentId 父节点ID0或null代表顶级节点
* @return 知识点编码如A01A01A01等
*/
public String generateKnowledgeCode(Integer parentId) {
if (parentId == null || parentId == 0) {
// 顶级节点编码生成逻辑
// 查询当前最大的顶级节点编码
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.isNull(Knowledge::getParentId).or().eq(Knowledge::getParentId, 0);
queryWrapper.orderByDesc(Knowledge::getId);
List<Knowledge> topKnowledges = this.list(queryWrapper);
if (topKnowledges.isEmpty()) {
return "A01";
}
// 获取最后一个顶级节点的编码
String lastCode = topKnowledges.get(0).getKnowledgeCode();
if (lastCode == null || lastCode.isEmpty()) {
return "A01";
}
// 解析并递增编码
return incrementCode(lastCode, 3); // 顶级节点固定3位编码
} else {
// 子节点编码生成逻辑
Knowledge parent = this.getById(parentId);
if (parent == null || parent.getKnowledgeCode() == null) {
throw new RuntimeException("父节点不存在或无编码");
}
String parentCode = parent.getKnowledgeCode();
// 查询该父节点下的所有子节点
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Knowledge::getParentId, parentId);
queryWrapper.orderByDesc(Knowledge::getId);
List<Knowledge> childKnowledges = this.list(queryWrapper);
if (childKnowledges.isEmpty()) {
// 第一个子节点
return parentCode + "A01";
}
// 获取最后一个子节点的编码
String lastChildCode = childKnowledges.get(0).getKnowledgeCode();
if (lastChildCode == null || lastChildCode.length() <= parentCode.length()) {
return parentCode + "A01";
}
// 提取子节点部分并递增
String childPart = lastChildCode.substring(parentCode.length());
String newChildPart = incrementCode(childPart, 3); // 子节点部分固定3位编码
return parentCode + newChildPart;
}
}
/**
* 递增编码
* @param code 当前编码
* @param length 编码长度
* @return 递增后的编码
*/
private String incrementCode(String code, int length) {
if (code == null || code.isEmpty()) {
return "A01";
}
// 分离字母和数字部分
StringBuilder letters = new StringBuilder();
StringBuilder digits = new StringBuilder();
for (char c : code.toCharArray()) {
if (Character.isLetter(c)) {
letters.append(c);
} else if (Character.isDigit(c)) {
digits.append(c);
}
}
if (digits.isEmpty()) {
return "A01";
}
// 递增数字部分
int num = Integer.parseInt(digits.toString());
num++;
// 格式化回固定长度
String format = "%0" + length + "d";
return letters.toString() + String.format(format, num);
}
}

View File

@ -7,7 +7,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.springframework.stereotype.Service;
import xyz.playedu.common.exception.NotFoundException;
import xyz.playedu.common.types.paginate.PaginationResult;
import xyz.playedu.jc.domain.JCResource;
import xyz.playedu.jc.domain.Textbook;
import xyz.playedu.jc.mapper.TextbookMapper;
import xyz.playedu.jc.service.ITextbookService;
@ -85,4 +87,13 @@ public class TextbookServiceImpl
return emptyResult;
}
}
@Override
public Textbook findOrFail(Integer id) throws NotFoundException {
Textbook textbook = getOne(query().getWrapper().eq("id", id));
if (textbook == null) {
throw new NotFoundException("课程不存在");
}
return textbook;
}
}

View File

@ -5,9 +5,34 @@
<mapper namespace="xyz.playedu.jc.mapper.DiscussionMapper">
<!--
目前使用 MyBatis-Plus 的通用 CRUD这里可以先留空。
如果后续有复杂查询,可以在这里新增 <select> / <update> 等。
-->
<select id="listVo" resultType="xyz.playedu.jc.domain.Discussion">
select discussion.*, chapter.name as chapterName
from jc_discussion discussion
inner join jc_chapter_content content on discussion.chapter_id = content.chapter_id
inner join jc_book_chapter chapter on content.chapter_id = chapter.id
<where>
<if test="param.bookId != null">
and discussion.book_id = #{param.bookId}
</if>
<if test="param.userId != null">
and discussion.user_id = #{param.userId}
</if>
<if test="param.chapterId != null">
and discussion.chapter_id = #{param.chapterId}
</if>
<if test="param.knowledgeCode != null and knowledgeCode!=''">
and content.knowledge_code like concat('%',#{param.knowledgeCode},'%')
</if>
</where>
<choose>
<when test="orderType == 'time'">
order by discussion.create_time desc
</when>
<otherwise>
order by chapter.level asc, chapter.sort asc
</otherwise>
</choose>
</select>
</mapper>

View File

@ -4,40 +4,6 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.playedu.jc.mapper.KnowledgeMapper">
<resultMap id="KnowledgeResultMap" type="xyz.playedu.jc.domain.Knowledge">
<id column="id" property="id"/>
<result column="book_id" property="bookId"/>
<result column="parent_id" property="parentId"/>
<result column="name" property="name"/>
<result column="konwledge_code" property="konwledgeCode"/>
<result column="desc" property="desc"/>
<result column="level" property="level"/>
<result column="type" property="type"/>
<result column="order_num" property="orderNum"/>
<result column="extra_json" property="extraJson"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="creator" property="creator"/>
<result column="updater" property="updater"/>
<result column="tenant_id" property="tenantId"/>
</resultMap>
<sql id="Base_Column_List">
id,
book_id,
parent_id,
name,
konwledge_code,
`desc`,
`level`,
`type`,
order_num,
extra_json,
create_time,
update_time,
creator,
updater,
tenant_id
</sql>
</mapper>

View File

@ -4,38 +4,35 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xyz.playedu.jc.mapper.NoteMapper">
<resultMap id="NoteResultMap" type="xyz.playedu.jc.domain.Note">
<id column="id" property="id"/>
<result column="book_id" property="bookId"/>
<result column="user_id" property="userId"/>
<result column="chapter_id" property="chapterId"/>
<result column="content" property="content"/>
<result column="txt" property="txt"/>
<result column="section_id" property="sectionId"/>
<result column="section_origin_id" property="sectionOriginId"/>
<result column="extra_json" property="extraJson"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
<result column="creator" property="creator"/>
<result column="updater" property="updater"/>
<result column="tenant_id" property="tenantId"/>
</resultMap>
<select id="listVo" resultType="xyz.playedu.jc.domain.Note">
select note.*, chapter.name as chapterName
from jc_note note
inner join jc_chapter_content content on note.chapter_id = content.chapter_id
inner join jc_book_chapter chapter on content.chapter_id = chapter.id
<where>
<if test="param.bookId != null">
and note.book_id = #{param.bookId}
</if>
<if test="param.userId != null">
and note.user_id = #{param.userId}
</if>
<if test="param.chapterId != null">
and note.chapter_id = #{param.chapterId}
</if>
<if test="param.knowledgeCode != null and knowledgeCode!=''">
and content.knowledge_code like concat('%',#{param.knowledgeCode},'%')
</if>
<sql id="Base_Column_List">
id,
book_id,
user_id,
chapter_id,
content,
txt,
section_id,
section_origin_id,
extra_json,
create_time,
update_time,
creator,
updater,
tenant_id
</sql>
</where>
<choose>
<when test="orderType == 'time'">
order by note.create_time desc
</when>
<otherwise>
order by chapter.level asc, chapter.sort asc
</otherwise>
</choose>
</select>
</mapper>

View File

@ -38,7 +38,7 @@ export const knowledgeKeyApi = {
},
validDataSet: () => {
return client.get('/knowledge/dataset/validDataSet', {});
}
},
// // 获取知识库密钥
// getKnowledgeKey: () => {

View File

@ -78,5 +78,5 @@ export function videoUpdate(id: number, params: any) {
}
export function getResourceUrl(id: number) {
return client.get(`/backend/v1/resource/getResourceUrl?id=`+id, {});
return client.get(`/backend/v1/resource/getResourceUrl?id=` + id, {});
}

View File

@ -3,7 +3,6 @@ import client from './internal/httpClient';
export function textbookList(page: number, size: number, title: string) {
return client.get('/backend/v1/jc/textbook/index', {
// return client.get('/backend/v1/course/index', {
page: page,
size: size,
title: title,

View File

@ -185,4 +185,4 @@ const ExamAdministrationDetails = () => {
);
};
export default ExamAdministrationDetails
export default ExamAdministrationDetails;

View File

@ -329,7 +329,8 @@ const ExamAdministrationPage = () => {
}*/
// 根据不同操作类型,调用对应的后端批量接口
switch (currentOperation) {
case 'resetExam': { // 批量重置接口:传递 examIds
case 'resetExam': {
// 批量重置接口:传递 examIds
const resResetExam = (await updateList(ids ?? [], '3', '')) as UpdateListResponse;
if (resResetExam?.code === 0) {
message.success(`成功重置 ${data.records.length} 名学生的考试状态`);

View File

@ -369,7 +369,12 @@ const DictionaryDetailPage = () => {
<Button onClick={enterFullscreen}></Button>
</div>
<div className={styles.iframeContainer}>
<iframe ref={iframeRef} src={accessUrl} className={styles.iframe} title="数字孪生程序" />
<iframe
ref={iframeRef}
src={accessUrl}
className={styles.iframe}
title="数字孪生程序"
/>
</div>
</Card>
@ -472,7 +477,7 @@ const DictionaryDetailPage = () => {
right: 4,
bottom: 4,
borderRadius: 8,
overflow: 'hidden'
overflow: 'hidden',
}}
onError={(error) => handleVideoError(error, item)}
onReady={() => handleVideoReady(item)}
@ -491,7 +496,7 @@ const DictionaryDetailPage = () => {
justifyContent: 'center',
backgroundColor: '#f0f0f0',
color: '#666',
borderRadius: 8
borderRadius: 8,
}}
>
@ -511,7 +516,7 @@ const DictionaryDetailPage = () => {
justifyContent: 'center',
backgroundColor: '#f0f0f0',
color: '#666',
borderRadius: 8
borderRadius: 8,
}}
>

View File

@ -38,7 +38,7 @@ import defaultThumb2 from '../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../assets/thumb/thumb3.png';
import { FileUploader } from '../../compenents/uploadFile';
import { TreeAttachments } from '../offline-course/compenents/attachments';
import {getResourceUrl} from "../../api/resource";
import { getResourceUrl } from '../../api/resource';
//搜索框
type SearchProps = GetProps<typeof Input.Search>;
@ -214,7 +214,7 @@ const Experiment = () => {
interface UploadRes {
data?: string; // 根据后端实际返回结构调整,比如可能是路径字符串
}
const getImageUrl=(id:any)=>{
const getImageUrl = (id: any) => {
getResourceUrl(id).then((res: any) => {
setThumb(res.data.resource_url);
});
@ -356,7 +356,7 @@ const Experiment = () => {
}
// 修复拼写错误并添加空值判断
if (res.data.labCourseImage) {
getImageUrl(res.data.labCourseImage)
getImageUrl(res.data.labCourseImage);
}
setSelectedId(res.data.id);
// 打开弹窗

View File

@ -11,7 +11,6 @@ const HomePage = () => {
return (
<>
<div className={styles['layout-wrap']}>
<div className={styles['left-menu']}>
<LeftMenu />
</div>

View File

@ -81,8 +81,7 @@ const formatDate = (value?: string | number | null): string => {
return '';
}
const date =
value.toString().length === 10 ? new Date(value * 1000) : new Date(value);
const date = value.toString().length === 10 ? new Date(value * 1000) : new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
@ -106,8 +105,7 @@ const normalizeListResponse = (payload: any) => {
root?.items,
Array.isArray(root) ? root : null,
];
const list =
listCandidates.find((candidate) => Array.isArray(candidate)) ?? [];
const list = listCandidates.find((candidate) => Array.isArray(candidate)) ?? [];
const total =
root?.total ??
@ -155,9 +153,7 @@ const ResourceLibraryReviewPage = () => {
});
const [growthTrend, setGrowthTrend] = useState<GrowthRecord[]>([]);
const [distributionData, setDistributionData] = useState<
{ value: number; name: string }[]
>([]);
const [distributionData, setDistributionData] = useState<{ value: number; name: string }[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [selectedQuestion, setSelectedQuestion] = useState<QuestionRecord | null>(null);
@ -320,9 +316,7 @@ const ResourceLibraryReviewPage = () => {
});
const filtered =
kmType !== undefined
? normalizedList.filter(
(item) => Number(item?.kmType) === Number(kmType)
)
? normalizedList.filter((item) => Number(item?.kmType) === Number(kmType))
: normalizedList;
setQuestionList(filtered);
if (kmType !== undefined && filtered.length !== list.length) {
@ -486,15 +480,10 @@ const ResourceLibraryReviewPage = () => {
<div
className={styles['htr-stat-trend']}
style={{
color:
statistics.kmTypeReviewGrowthRate >= 0 ? '#52c41a' : '#ff4d4f',
color: statistics.kmTypeReviewGrowthRate >= 0 ? '#52c41a' : '#ff4d4f',
}}
>
{statistics.kmTypeReviewGrowthRate >= 0 ? (
<RiseOutlined />
) : (
<FallOutlined />
)}
{statistics.kmTypeReviewGrowthRate >= 0 ? <RiseOutlined /> : <FallOutlined />}
<span>
{statistics.kmTypeReviewGrowthRate >= 0 ? '较昨日增长' : '较昨日下降'}{' '}
{Math.abs(statistics.kmTypeReviewGrowthRate)}%
@ -568,8 +557,7 @@ const ResourceLibraryReviewPage = () => {
<div
className={styles['htr-stat-trend']}
style={{
color:
statistics.kmQueryDuplicateRateGrowth >= 0 ? '#52c41a' : '#ff4d4f',
color: statistics.kmQueryDuplicateRateGrowth >= 0 ? '#52c41a' : '#ff4d4f',
}}
>
{statistics.kmQueryDuplicateRateGrowth >= 0 ? (
@ -636,7 +624,9 @@ const ResourceLibraryReviewPage = () => {
<Button
type="primary"
icon={<SearchOutlined />}
onClick={() => fetchQuestionList(1, searchKeyword, statusFilter, experimentTypeFilter)}
onClick={() =>
fetchQuestionList(1, searchKeyword, statusFilter, experimentTypeFilter)
}
>
</Button>
@ -662,15 +652,15 @@ const ResourceLibraryReviewPage = () => {
showTotal: (count, range) => `显示 ${range[0]}-${range[1]} 条,共 ${count}`,
onChange: (current) => setPage(current),
}}
rowKey={(record) =>
record.id ||
record.kmConversationId ||
record.km_conversation_id ||
record.kmId ||
record.km_id ||
record.kmQuery ||
Math.random()
}
rowKey={(record) =>
record.id ||
record.kmConversationId ||
record.km_conversation_id ||
record.kmId ||
record.km_id ||
record.kmQuery ||
Math.random()
}
/>
</Card>
@ -819,12 +809,7 @@ const ResourceLibraryReviewPage = () => {
>
</Button>,
<Button
key="submit"
type="primary"
onClick={handleModalOk}
loading={modalSubmitting}
>
<Button key="submit" type="primary" onClick={handleModalOk} loading={modalSubmitting}>
</Button>,
]}
@ -845,7 +830,9 @@ const ResourceLibraryReviewPage = () => {
<div className={styles['htr-modal-info-item']}>
<Text strong></Text>
<Space>
<Avatar>{selectedQuestion.kmUser ? selectedQuestion.kmUser.charAt(0) : ''}</Avatar>
<Avatar>
{selectedQuestion.kmUser ? selectedQuestion.kmUser.charAt(0) : ''}
</Avatar>
<div>{selectedQuestion.kmUser || '匿名用户'}</div>
</Space>
</div>
@ -915,4 +902,3 @@ const ResourceLibraryReviewPage = () => {
};
export default ResourceLibraryReviewPage;

View File

@ -0,0 +1,150 @@
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { ChapterTree } from './compenents/chapterTree';
import { BackBartment } from '../../compenents';
import styles from './chapter.module.less';
import { department } from '../../api/index';
import EditorTextbookContent from './compenents/TextEditor/EditorToolbar';
import EnhancedTextbookEditor from './compenents/TextEditor/EnhancedTextbookEditor';
export interface ChapterItemModel {
created_at: string;
id: number;
name: string;
from_scene: number;
parent_chain: string;
parent_id: number;
sort: number;
updated_at: string;
}
export interface ChaptersBoxModel {
[key: number]: ChapterItemModel[];
}
export interface Option {
key: string | number;
title: any;
level?: number;
children?: Option[];
}
const ChapterManagementPage = () => {
const { t } = useTranslation();
const params = useParams();
const [selectedChapter, setSelectedChapter] = useState<any>(null);
const [searchParams] = useSearchParams();
const title = searchParams.get('title');
const { bookId } = params;
const [loading, setLoading] = useState<boolean>(false);
const [treeData, setTreeData] = useState<ChaptersBoxModel>([]);
const [selectedChapterId, setSelectedChapterId] = useState<string>();
const [did, setDid] = useState<number>(0);
// 获取数据
const getData = () => {
department.departmentList({ from_scene: 0 }).then((res: any) => {
const resData: ChaptersBoxModel = res.data.departments;
setTreeData(resData);
setLoading(false);
});
};
const getChapterData = () => {
department.departmentList({ from_scene: 0 }).then((res: any) => {
const resData: ChaptersBoxModel = res.data.departments;
setTreeData(resData);
setLoading(false);
});
};
useEffect(() => {
getData();
getChapterData();
}, []);
console.log(bookId, 'bookid');
// 处理章节更新
const handleChapterUpdate = (keys: any, title: any) => {
console.log('选中的章节:', keys, title);
};
// 处理添加章节
const handleAddChapter = (parentId: string | number | null, level: number) => {
console.log('添加章节, 父级ID:', parentId, '级别:', level);
};
// 处理编辑章节
const handleEditChapter = (chapter: any) => {
console.log('编辑章节:', chapter);
};
// 处理删除章节
const handleDeleteChapter = (chapter: any) => {
console.log('删除章节:', chapter);
};
// 处理选中章节
const handleSelectChapter = (chapter: any) => {
console.log('选中章节详情:', chapter);
setSelectedChapter(chapter);
};
// 处理节点拖拽
const handleChangeOrder = (chapters: any[]) => {
console.log(chapters, '<>><><');
};
const onSave = () => {
console.log(selectedChapterId);
};
const onContentChange = () => {
console.log(selectedChapterId);
};
return (
<div className="playedu-main-body">
<BackBartment title={title || '课程名称'} />
<div className={styles['chapter-main-body']}>
<div className={styles['left-box']}>
<ChapterTree
selectedId={selectedChapterId}
isLoading={loading}
chapterTreeData={treeData}
onUpdate={handleChapterUpdate}
onAdd={handleAddChapter}
onEdit={handleEditChapter}
onDelete={handleDeleteChapter}
onSelect={handleSelectChapter}
onOrderChange={handleChangeOrder}
/>
</div>
<div className={styles['right-box']}>
{selectedChapter ? (
<div className="chapter-detail">
<h3></h3>
<p>
<strong>:</strong> {selectedChapter.name}
</p>
<p>
<strong>ID:</strong> {selectedChapter.id}
</p>
</div>
) : (
<div className={styles['chapter-detail']}>
<h3></h3>
<p></p>
</div>
)}
<EnhancedTextbookEditor
chapterId={'1'}
chapterTitle={'xuande'}
initialContent="请编写内容"
onSave={onSave}
onContentChange={onContentChange}
></EnhancedTextbookEditor>
</div>
</div>
</div>
);
};
export default ChapterManagementPage;

View File

@ -347,11 +347,11 @@ export const ChapterTree = (props: PropInterface) => {
onClick={(e) => {
e.stopPropagation();
}}
style={{ padding: '6px', minWidth: 'auto' }}
style={{ padding: '6px', marginRight: 5, minWidth: 'auto' }}
>
<EditFilled
className={styles['icon-hover']}
style={{ fontSize: '18px', color: '#8c8c8c' }}
style={{ fontSize: '18px', color: '#cccccc' }}
/>
</Button>
@ -367,7 +367,7 @@ export const ChapterTree = (props: PropInterface) => {
>
<PlusCircleFilled
className={styles['icon-hover']}
style={{ fontSize: '18px', color: '#8c8c8c' }}
style={{ fontSize: '18px', color: '#cccccc' }}
/>
</Button>
)}

View File

@ -0,0 +1,228 @@
.thumb-item {
width: 80px;
height: 60px;
margin-right: 16px;
border: 2px solid transparent;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
}
}
.thumb-item-avtive {
composes: thumb-item;
border-color: #1890ff;
}
.chapter-item {
margin-bottom: 16px;
border: 1px solid #f0f0f0;
border-radius: 6px;
padding: 16px;
background: #fafafa;
.label {
width: 80px;
font-weight: 500;
color: #333;
line-height: 32px;
}
.input {
width: 200px;
margin-right: 12px;
}
}
.chapter-level-1 {
composes: chapter-item;
background: #fafafa;
}
.chapter-level-2 {
margin-left: 32px;
margin-top: 16px;
padding-left: 16px;
background: #f6ffed;
border: 1px solid #b7eb8f;
.label {
color: #389e0d;
}
}
.chapter-level-3 {
margin-left: 32px;
margin-top: 16px;
padding-left: 16px;
background: #f0f9ff;
border: 1px solid #91d5ff;
.label {
color: #0958d9;
}
}
.chapter-children {
margin-top: 16px;
}
.chapter-actions {
display: flex;
gap: 8px;
margin-left: auto;
align-items: center;
button {
height: 32px;
font-size: 12px;
}
}
.chapter-hous-box {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
border: 1px solid #e8e8e8;
}
.knowledge-points {
margin-top: 12px;
padding: 12px;
background: white;
border-radius: 4px;
border: 1px solid #e8e8e8;
}
.knowledge-title {
font-weight: 500;
margin-bottom: 8px;
color: #666;
font-size: 14px;
}
.knowledge-point-item {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 8px;
background: #fafafa;
border-radius: 4px;
&:last-child {
margin-bottom: 0;
}
input {
flex: 1;
}
button {
flex-shrink: 0;
}
}
// 响应式设计
@media (max-width: 768px) {
.chapter-item {
padding: 12px;
}
.chapter-actions {
flex-direction: column;
gap: 4px;
button {
width: 100%;
}
}
.chapter-level-2,
.chapter-level-3 {
margin-left: 16px;
}
}
// 动画效果
.chapter-item {
transition: all 0.3s ease;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
// 拖拽样式
.drag-handle {
cursor: move;
color: #999;
margin-right: 8px;
&:hover {
color: #1890ff;
}
}
// 空状态样式
.empty-chapter {
text-align: center;
padding: 40px 0;
color: #999;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #d9d9d9;
}
.empty-text {
font-size: 14px;
margin-bottom: 16px;
}
}
// 验证错误样式
.error-border {
border-color: #ff4d4f !important;
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
// 章节统计信息
.chapter-stats {
display: flex;
gap: 16px;
margin-bottom: 16px;
padding: 12px;
background: #f0f9ff;
border-radius: 4px;
border: 1px solid #91d5ff;
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
.stat-value {
font-size: 18px;
font-weight: bold;
color: #1890ff;
}
.stat-label {
font-size: 12px;
color: #666;
}
}
}

View File

@ -0,0 +1,495 @@
import React, { useState, useEffect } from 'react';
import {
Space,
Radio,
Button,
Drawer,
Form,
Input,
Modal,
message,
Image,
Tag,
DatePicker,
} from 'antd';
import styles from './createTextbook.module.less';
import { SelectRange, UploadImageButton } from '../../../compenents';
import defaultThumb1 from '../../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../../assets/thumb/thumb3.png';
import { useTranslation } from 'react-i18next';
import moment from 'moment/moment';
import { createTextbook } from '../../../api/textbook';
const { TextArea } = Input;
const { confirm } = Modal;
// 类型定义
interface KnowledgePoint {
id: number;
name: string;
content?: string;
sortOrder: number;
}
interface Chapter {
id: number;
name: string;
level: 1 | 2 | 3;
parentId: number | null;
sortOrder: number;
children?: Chapter[];
knowledgePoints?: KnowledgePoint[];
hours: CourseHourModel[];
}
interface CourseCreateProps {
cateIds?: any;
open: boolean;
onCancel: () => void;
}
interface Option {
value: string | number;
title: string;
children?: Option[];
}
interface TeacherModel {
label: string;
value: number;
}
interface AttachmentDataModel {
rid: number;
name: string;
type: string;
size: number;
}
interface CourseHourModel {
rid: number;
name: string;
type: string;
duration: number;
extra?: any;
}
// @ts-ignore
export const CreateTextbook: React.FC<CourseCreateProps> = ({ open, onCancel }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
const [thumb, setThumb] = useState<string>(defaultThumb1); // 封面
// 范围指派
const [depIds, setDepIds] = useState<number[]>([]);
const [groupIds, setGroupIds] = useState<number[]>([]);
const [userIds, setUserIds] = useState<number[]>([]);
const [deps, setDeps] = useState<any[]>([]);
const [groups, setGroups] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [idsVisible, setIdsVisible] = useState<boolean>(false);
useEffect(() => {
if (open) {
form.setFieldsValue({
creditType: 0,
title: '',
thumb: -1,
ids: undefined,
isRequired: 1,
short_desc: '',
hasChapter: 0,
drag: 0,
hangUp: 0,
});
setThumb(defaultThumb1);
setDepIds([]);
setDeps([]);
setGroupIds([]);
setGroups([]);
setUserIds([]);
setUsers([]);
}
}, [form, open]);
const onFinish = (values: any) => {
if (loading) return;
const dep_ids: any[] = depIds;
const group_ids: any[] = groupIds;
const user_ids: any[] = userIds;
// 接口位置
setLoading(true);
createTextbook(
values.title,
values.thumb,
values.short_desc,
values.author,
values.major,
dep_ids,
group_ids,
user_ids,
values.publish_time,
values.publish_unit,
values.create_time
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabledDate = (current: any) => {
return current && current >= moment().add(0, 'days'); // 选择时间要大于等于当前天。若今天不能被选择,去掉等号即可。
};
return (
<>
{open ? (
<Drawer
title={t('course.createTextbook')}
onClose={onCancel}
maskClosable={false}
open={true}
footer={
<Space className="j-r-flex">
<Button onClick={() => onCancel()}>{t('commen.drawerCancel')}</Button>
<Button loading={loading} onClick={() => form.submit()} type="primary">
{t('commen.drawerOk')}
</Button>
</Space>
}
width={700}
>
<div className="float-left mt-24">
<SelectRange
defaultDepIds={depIds}
defaultGroupIds={groupIds}
defaultUserIds={userIds}
defaultDeps={deps}
defaultGroups={groups}
defaultUsers={users}
open={idsVisible}
onCancel={() => setIdsVisible(false)}
onSelected={(selDepIds, selDeps, selGroupIds, selGroups, selUserIds, selUsers) => {
setDepIds(selDepIds);
setDeps(selDeps);
setGroupIds(selGroupIds);
setGroups(selGroups);
setUserIds(selUserIds);
setUsers(selUsers);
form.setFieldsValue({
ids: selDepIds.concat(selGroupIds).concat(selUserIds),
});
setIdsVisible(false);
}}
/>
{/* 表单 */}
<Form
form={form}
name="create-basic"
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
{/* 表单字段 */}
<Form.Item
label={t('textbook.create.name')}
name="title"
rules={[{ required: true, message: t('textbook.create.namePlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.namePlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.thumb')}
name="thumb"
rules={[{ required: true, message: t('textbook.create.thumbPlaceholder') }]}
>
<div className="d-flex">
<Image
src={thumb}
width={160}
height={120}
style={{ borderRadius: 6 }}
preview={false}
/>
<div className="c-flex ml-8 flex-1">
<div className="d-flex mb-28">
<div
className={
thumb === defaultThumb1
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb1);
form.setFieldsValue({
thumb: -1,
});
}}
>
<Image
src={defaultThumb1}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb2
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb2);
form.setFieldsValue({
thumb: -2,
});
}}
>
<Image
src={defaultThumb2}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb3
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb3);
form.setFieldsValue({
thumb: -3,
});
}}
>
<Image
src={defaultThumb3}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
</div>
<div className="d-flex">
<UploadImageButton
text={t('course.edit.thumbText')}
isDefault
onSelected={(url, id) => {
setThumb(url);
form.setFieldsValue({ thumb: id });
}}
></UploadImageButton>
<span className="helper-text ml-16">{t('textbook.create.thumbTip')}</span>
</div>
</div>
</div>
</Form.Item>
{/*范围指派*/}
<Form.Item
label={t('textbook.create.assign')}
name="ids"
rules={[{ required: true, message: t('textbook.create.assignPlaceholder') }]}
>
<div
className="d-flex"
style={{ width: '100%', flexWrap: 'wrap', marginBottom: -8 }}
>
<Button
type="default"
style={{ marginBottom: 14 }}
onClick={() => setIdsVisible(true)}
>
{t('course.edit.idsText')}
</Button>
<div
className="d-flex"
style={{
width: '100%',
flexWrap: 'wrap',
marginBottom: -16,
}}
>
{deps.length > 0 &&
deps.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...deps];
const arr2 = [...depIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setDeps(arr);
setDepIds(arr2);
form.setFieldsValue({
ids: arr2.concat(groupIds).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{groups.length > 0 &&
groups.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...groups];
const arr2 = [...groupIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setGroups(arr);
setGroupIds(arr2);
form.setFieldsValue({
ids: depIds.concat(arr2).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{users.length > 0 &&
users.map((item: any, j: number) => (
<Tag
key={j}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...users];
const arr2 = [...userIds];
arr.splice(j, 1);
arr2.splice(j, 1);
setUsers(arr);
setUserIds(arr2);
form.setFieldsValue({
dep_ids: depIds.concat(groupIds).concat(arr2),
});
}}
>
{item.name}
</Tag>
))}
</div>
</div>
</Form.Item>
<Form.Item
label={t('textbook.create.subject')}
name="major"
rules={[{ required: true, message: t('textbook.create.subjectPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.subjectPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.author')}
name="author"
rules={[{ required: true, message: t('textbook.create.authorPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.authorPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.publisher')}
name="publish_unit"
rules={[{ required: true, message: t('textbook.create.publisherPlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('textbook.create.publisherPlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('textbook.create.publishTime')}
name="publish_time"
rules={[{ required: true, message: t('textbook.create.publishTimePlaceholder') }]}
>
<DatePicker
style={{ width: 424 }}
placeholder={t('textbook.create.publishTimePlaceholder')}
allowClear
format="YYYY-MM-DD"
disabledDate={disabledDate}
/>
</Form.Item>
<Form.Item
label={t('textbook.create.desc')}
name="short_desc"
rules={[{ required: true, message: t('textbook.create.descPlaceholder') }]}
>
<TextArea
style={{ width: 424 }}
rows={6}
placeholder={t('textbook.create.descPlaceholder')}
allowClear
maxLength={200}
autoSize={{ minRows: 6, maxRows: 6 }}
/>
</Form.Item>
</Form>
</div>
</Drawer>
) : null}
</>
);
};

View File

@ -0,0 +1,22 @@
.thumb-item {
width: 80px;
height: 60px;
cursor: pointer;
margin-right: 8px;
border-radius: 6px;
&:last-child {
margin-right: 0;
}
}
.thumb-item-avtive {
width: 80px;
height: 60px;
border: 2px solid #ff4d4f;
cursor: pointer;
margin-right: 8px;
border-radius: 8px;
&:last-child {
margin-right: 0;
}
}

View File

@ -0,0 +1,746 @@
import React, { useState, useEffect } from 'react';
import {
Space,
Radio,
Button,
Drawer,
Form,
TreeSelect,
Input,
message,
Image,
Spin,
Select,
DatePicker,
Tag,
Switch,
InputNumber,
} from 'antd';
import styles from './update.module.less';
import { course, teacher } from '../../../api/index';
import { UploadImageButton, SelectRange } from '../../../compenents';
import defaultThumb1 from '../../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../../assets/thumb/thumb3.png';
import { useTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import moment from 'moment';
interface PropInterface {
id: number;
open: boolean;
onCancel: () => void;
}
interface Option {
value: string | number;
title: string;
children?: Option[];
}
type selTeacherModel = {
label: string;
value: number;
};
export const CourseUpdate: React.FC<PropInterface> = ({ id, open, onCancel }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [init, setInit] = useState(true);
const [loading, setLoading] = useState(false);
const [categories, setCategories] = useState<Option[]>([]);
const [thumb, setThumb] = useState('');
const [teachers, setTeachers] = useState<selTeacherModel[]>([]);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [depIds, setDepIds] = useState<number[]>([]);
const [groupIds, setGroupIds] = useState<number[]>([]);
const [userIds, setUserIds] = useState<number[]>([]);
const [deps, setDeps] = useState<any[]>([]);
const [groups, setGroups] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [idsVisible, setIdsVisible] = useState(false);
const [drag, setDrag] = useState(0);
const [credit, setCredit] = useState(0);
useEffect(() => {
setInit(true);
if (id === 0) {
return;
}
if (open) {
form.setFieldsValue({
ids: undefined,
});
setDepIds([]);
setDeps([]);
setGroupIds([]);
setGroups([]);
setUserIds([]);
setUsers([]);
getTeachers();
getCategory();
getDetail();
}
}, [form, id, open]);
const getCategory = () => {
course.createCourse().then((res: any) => {
const categories = res.data.categories;
if (JSON.stringify(categories) !== '{}') {
const new_arr: any = checkArr(categories, 0, null);
setCategories(new_arr);
}
});
};
const getTeachers = () => {
teacher.list(1, 100000, '', '', '').then((res: any) => {
const arr = [];
const roles: any = res.data.result.data;
for (let i = 0; i < roles.length; i++) {
arr.push({
label: roles[i].name,
value: roles[i].id,
});
}
setTeachers(arr);
});
};
const getDetail = () => {
course.course(id).then((res: any) => {
let teacherIds = [];
if (res.data.teachers && res.data.teachers.length > 0) {
teacherIds = res.data.teachers[0].id;
}
const chapterType = res.data.chapters.length > 0 ? 1 : 0;
form.setFieldsValue({
title: res.data.course.title,
thumb: res.data.course.thumb,
category_ids: res.data.category_ids,
isRequired: res.data.course.is_required,
short_desc: res.data.course.short_desc,
hasChapter: chapterType,
teacherIds: teacherIds,
sort_at: res.data.course.sort_at ? dayjs(res.data.course.sort_at) : '',
});
const deps = res.data.deps;
if (deps && JSON.stringify(deps) !== '{}') {
getDepsDetail(deps);
}
const groups = res.data.groups;
if (groups && JSON.stringify(groups) !== '{}') {
getGroupsDetail(groups);
}
const users = res.data.users;
if (users && JSON.stringify(users) !== '{}') {
getUsersDetail(users);
}
if (
(deps && JSON.stringify(deps) !== '{}') ||
(groups && JSON.stringify(groups) !== '{}') ||
(users && JSON.stringify(users) !== '{}')
) {
form.setFieldsValue({ ids: [1, 2] });
}
setResourceUrl(res.data.resource_url);
setThumb(
res.data.course.thumb === -1
? defaultThumb1
: res.data.course.thumb === -2
? defaultThumb2
: res.data.course.thumb === -3
? defaultThumb3
: res.data.resource_url[res.data.course.thumb]
);
if (res.data.course.extra) {
const obj = JSON.parse(res.data.course.extra).rules;
form.setFieldsValue({
drag: Number(obj.drag),
hangUp: Number(obj.hang_up),
});
setDrag(Number(obj.drag));
const key = obj.credit;
if (key && key > 0) {
form.setFieldsValue({
creditType: 1,
credit: key,
});
setCredit(1);
} else {
form.setFieldsValue({
creditType: 0,
});
setCredit(0);
}
} else {
form.setFieldsValue({
creditType: 0,
});
setCredit(0);
}
setInit(false);
});
};
const getDepsDetail = (deps: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(deps).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: deps[v],
},
},
});
});
setDepIds(arr);
setDeps(arr2);
};
const getGroupsDetail = (groups: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(groups).map((v, i) => {
arr.push(Number(v));
arr2.push({
key: Number(v),
title: {
props: {
children: groups[v],
},
},
});
});
setGroupIds(arr);
setGroups(arr2);
};
const getUsersDetail = (users: any) => {
const arr: any = [];
const arr2: any = [];
Object.keys(users).map((v, i) => {
arr.push(Number(v));
arr2.push({
id: Number(v),
name: users[v],
});
});
setUserIds(arr);
setUsers(arr2);
};
const getNewTitle = (title: any, id: number, counts: any) => {
if (counts) {
const value = counts[id] || 0;
return title + '(' + value + ')';
} else {
return title;
}
};
const checkArr = (departments: any[], id: number, counts: any) => {
const arr = [];
for (let i = 0; i < departments[id].length; i++) {
if (!departments[departments[id][i].id]) {
arr.push({
title: getNewTitle(departments[id][i].name, departments[id][i].id, counts),
value: departments[id][i].id,
});
} else {
const new_arr: any = checkArr(departments, departments[id][i].id, counts);
arr.push({
title: getNewTitle(departments[id][i].name, departments[id][i].id, counts),
value: departments[id][i].id,
children: new_arr,
});
}
}
return arr;
};
const onFinish = (values: any) => {
if (loading) {
return;
}
const dep_ids: any[] = depIds;
const user_ids: any[] = userIds;
const group_ids: any[] = groupIds;
const teacherIds: any[] = [];
if (values.teacherIds > 0) {
teacherIds.push(values.teacherIds);
}
values.sort_at = moment(new Date(values.sort_at)).format('YYYY-MM-DD HH:mm:ss');
const extra = {
version: 'v1',
rules: {
drag: values.drag,
hang_up: values.hangUp,
scope: credit === 0 ? 'GLOBAL' : 'COURSE',
credit: credit === 0 ? 0 : values.credit,
},
};
setLoading(true);
course
.updateCourse(
id,
values.title,
values.thumb,
values.short_desc,
1,
values.isRequired,
dep_ids,
group_ids,
user_ids,
values.category_ids,
[],
[],
teacherIds,
values.sort_at,
JSON.stringify(extra)
)
.then((res: any) => {
setLoading(false);
message.success(t('commen.saveSuccess'));
onCancel();
})
.catch((e) => {
setLoading(false);
});
};
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
const disabledDate = (current: any) => {
return current && current >= moment().add(0, 'days'); // 选择时间要大于等于当前天。若今天不能被选择,去掉等号即可。
};
const onDragChange = (checked: boolean) => {
if (checked) {
form.setFieldsValue({ drag: 1 });
setDrag(1);
} else {
form.setFieldsValue({ drag: 0, hangUp: 0 });
setDrag(0);
}
};
const onHangUpChange = (checked: boolean) => {
if (checked) {
form.setFieldsValue({ hangUp: 1 });
} else {
form.setFieldsValue({ hangUp: 0 });
}
};
return (
<>
{open ? (
<Drawer
title={t('course.update')}
onClose={onCancel}
maskClosable={false}
open={true}
footer={
<Space className="j-r-flex">
<Button onClick={() => onCancel()}>{t('commen.drawerCancel')}</Button>
<Button loading={loading} onClick={() => form.submit()} type="primary">
{t('commen.drawerOk')}
</Button>
</Space>
}
width={634}
>
{init && (
<div className="float-left text-center mt-30">
<Spin></Spin>
</div>
)}
<div className="float-left mt-24" style={{ display: init ? 'none' : 'block' }}>
<SelectRange
defaultDepIds={depIds}
defaultGroupIds={groupIds}
defaultUserIds={userIds}
defaultDeps={deps}
defaultGroups={groups}
defaultUsers={users}
open={idsVisible}
onCancel={() => setIdsVisible(false)}
onSelected={(selDepIds, selDeps, selGroupIds, selGroups, selUserIds, selUsers) => {
setDepIds(selDepIds);
setDeps(selDeps);
setGroupIds(selGroupIds);
setGroups(selGroups);
setUserIds(selUserIds);
setUsers(selUsers);
form.setFieldsValue({
ids: selDepIds.concat(selGroupIds).concat(selUserIds),
});
setIdsVisible(false);
}}
/>
<Form
form={form}
name="update-basic"
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
initialValues={{ remember: true }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label={t('course.edit.category')}
name="category_ids"
rules={[
{
required: true,
message: t('course.edit.categoryPlaceholder'),
},
]}
>
<TreeSelect
showCheckedStrategy={TreeSelect.SHOW_ALL}
allowClear
multiple
style={{ width: 424 }}
treeData={categories}
placeholder={t('course.edit.categoryPlaceholder')}
treeDefaultExpandAll
/>
</Form.Item>
<Form.Item
label={t('course.edit.name')}
name="title"
rules={[{ required: true, message: t('course.edit.namePlaceholder') }]}
>
<Input
style={{ width: 424 }}
placeholder={t('course.edit.namePlaceholder')}
allowClear
/>
</Form.Item>
<Form.Item
label={t('course.edit.isRequired')}
name="isRequired"
rules={[
{
required: true,
message: t('course.edit.isRequiredPlaceholder'),
},
]}
>
<Radio.Group>
<Radio value={1}>{t('course.columns.text1')}</Radio>
<Radio value={0} style={{ marginLeft: 22 }}>
{t('course.columns.text2')}
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={t('course.edit.ids')}
name="ids"
rules={[{ required: true, message: t('course.edit.idsPlaceholder') }]}
>
<div
className="d-flex"
style={{ width: '100%', flexWrap: 'wrap', marginBottom: -8 }}
>
<Button
type="default"
style={{ marginBottom: 14 }}
onClick={() => setIdsVisible(true)}
>
{t('course.edit.idsText')}
</Button>
<div
className="d-flex"
style={{
width: '100%',
flexWrap: 'wrap',
marginBottom: -16,
}}
>
{deps.length > 0 &&
deps.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...deps];
const arr2 = [...depIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setDeps(arr);
setDepIds(arr2);
form.setFieldsValue({
ids: arr2.concat(groupIds).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{groups.length > 0 &&
groups.map((item: any, i: number) => (
<Tag
key={i}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...groups];
const arr2 = [...groupIds];
arr.splice(i, 1);
arr2.splice(i, 1);
setGroups(arr);
setGroupIds(arr2);
form.setFieldsValue({
ids: depIds.concat(arr2).concat(userIds),
});
}}
>
{item.title.props.children}
</Tag>
))}
{users.length > 0 &&
users.map((item: any, j: number) => (
<Tag
key={j}
closable
style={{
height: 32,
lineHeight: '32px',
fontSize: 14,
color: '#FF4D4F',
background: 'rgba(255,77,79,0.1)',
marginRight: 16,
marginBottom: 16,
}}
onClose={(e) => {
e.preventDefault();
const arr = [...users];
const arr2 = [...userIds];
arr.splice(j, 1);
arr2.splice(j, 1);
setUsers(arr);
setUserIds(arr2);
form.setFieldsValue({
ids: depIds.concat(groupIds).concat(arr2),
});
}}
>
{item.name}
</Tag>
))}
</div>
</div>
</Form.Item>
<Form.Item
label={t('course.edit.thumb')}
name="thumb"
rules={[
{
required: true,
message: t('course.edit.thumbPlaceholder'),
},
]}
>
<div className="d-flex">
<Image
src={thumb}
width={160}
height={120}
style={{ borderRadius: 6 }}
preview={false}
/>
<div className="c-flex ml-8 flex-1">
<div className="d-flex mb-28">
<div
className={
thumb === defaultThumb1
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb1);
form.setFieldsValue({
thumb: -1,
});
}}
>
<Image
src={defaultThumb1}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb2
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb2);
form.setFieldsValue({
thumb: -2,
});
}}
>
<Image
src={defaultThumb2}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
<div
className={
thumb === defaultThumb3
? styles['thumb-item-avtive']
: styles['thumb-item']
}
onClick={() => {
setThumb(defaultThumb3);
form.setFieldsValue({
thumb: -3,
});
}}
>
<Image
src={defaultThumb3}
width={80}
height={60}
style={{ borderRadius: 6 }}
preview={false}
/>
</div>
</div>
<div className="d-flex">
<UploadImageButton
text={t('course.edit.thumbText')}
isDefault
onSelected={(url, id) => {
setThumb(url);
form.setFieldsValue({ thumb: id });
}}
></UploadImageButton>
<span className="helper-text ml-16">{t('course.edit.thumbTip')}</span>
</div>
</div>
</div>
</Form.Item>
<Form.Item label={t('course.edit.drag')} name="drag">
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="drag" valuePropName="checked">
<Switch onChange={onDragChange} />
</Form.Item>
<div className="helper-text">{t('course.edit.dragPlaceholder')}</div>
</Space>
</Form.Item>
{drag === 1 && (
<Form.Item label={t('course.edit.hangUp')} name="hangUp">
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="hangUp" valuePropName="checked">
<Switch onChange={onHangUpChange} />
</Form.Item>
<div className="helper-text">{t('course.edit.hangUpPlaceholder')}</div>
</Space>
</Form.Item>
)}
<Form.Item label={t('course.edit.credit')}>
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="creditType">
<Radio.Group
onChange={(e) => {
setCredit(Number(e.target.value));
if (Number(e.target.value) > 0) {
form.setFieldsValue({ credit: 0 });
}
}}
>
<Radio value={0}>{t('course.edit.radio5')}</Radio>
<Radio value={1}>{t('course.edit.radio6')}</Radio>
</Radio.Group>
</Form.Item>
{credit > 0 && (
<>
<div className="d-flex">{t('course.edit.text3')}</div>
<Form.Item name="credit">
<InputNumber
min={0}
max={9999}
style={{ width: 56 }}
precision={0}
controls={false}
/>
</Form.Item>
<div className="d-flex">{t('credit.rules.text')}</div>
</>
)}
</Space>
</Form.Item>
<Form.Item label={t('course.edit.teacher')} name="teacherIds">
<Select
style={{ width: 424 }}
allowClear
placeholder={t('course.edit.teacherPlaceholder')}
options={teachers}
/>
</Form.Item>
<Form.Item label={t('course.edit.desc')} name="short_desc">
<Input.TextArea
style={{ width: 424, minHeight: 80 }}
allowClear
placeholder={t('course.edit.descPlaceholder')}
maxLength={200}
/>
</Form.Item>
<Form.Item label={t('course.edit.time')}>
<Space align="baseline" style={{ height: 32 }}>
<Form.Item name="sort_at">
<DatePicker
disabledDate={disabledDate}
format="YYYY-MM-DD HH:mm:ss"
style={{ width: 240 }}
showTime
placeholder={t('course.edit.timePlaceholder')}
/>
</Form.Item>
<div className="helper-text">{t('course.edit.timeTip')}</div>
</Space>
</Form.Item>
</Form>
</div>
</Drawer>
) : null}
</>
);
};

View File

@ -0,0 +1,14 @@
.mainBody {
width: 100%;
box-sizing: border-box;
border-radius: 12px;
background-color: white;
padding: 24px;
}
.contentBox {
width: 100%;
height: auto;
box-sizing: border-box;
padding: 24px;
background-color: white;
}

View File

@ -0,0 +1,495 @@
import { useEffect, useState } from 'react';
import { Button, Modal, Image, Table, Typography, Input, message, Space, Dropdown } from 'antd';
import { PlusOutlined, DownOutlined, ExclamationCircleFilled } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { dateFormatNoTime } from '../../utils';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { PerButton } from '../../compenents';
import { CreateTextbook } from './compenents/createTextbook';
import { TextbookUpdate } from './compenents/updateTextbook';
import defaultThumb1 from '../../assets/thumb/thumb1.png';
import defaultThumb2 from '../../assets/thumb/thumb2.png';
import defaultThumb3 from '../../assets/thumb/thumb3.png';
import { useTranslation } from 'react-i18next';
import { textbook } from '../../api';
const { confirm } = Modal;
interface DataType {
id: React.Key;
charge: number;
class_hour: number;
created_at: string;
is_required: number;
is_show: number;
short_desc: string;
thumb: string;
title: string;
sort_at?: string;
}
type TeacherModel = {
about: string;
avatar: string;
created_at: string;
deleted: number;
id: number;
name: string;
updated_at: string;
};
interface LocalSearchParamsInterface {
page?: number;
size?: number;
title?: string;
label?: string;
did?: string;
category_ids?: any;
}
const CoursePage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams({
page: '1',
size: '10',
title: '',
label: '',
did: '',
category_ids: '[]',
});
const page = parseInt(searchParams.get('page') || '1');
const size = parseInt(searchParams.get('size') || '10');
const [title, setTitle] = useState(searchParams.get('title') || '');
const [list, setList] = useState<DataType[]>([]);
const [refresh, setRefresh] = useState(false);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [createVisible, setCreateVisible] = useState(false);
const [updateVisible, setUpdateVisible] = useState(false);
const [cid, setCid] = useState(0);
const [errorData, setErrorData] = useState<any>({});
const [showDetail, setShowDetail] = useState(false);
const resetLocalSearchParams = (params: LocalSearchParamsInterface) => {
setSearchParams(
(prev) => {
if (typeof params.title !== 'undefined') {
prev.set('title', params.title);
}
if (typeof params.label !== 'undefined') {
prev.set('label', params.label);
}
if (typeof params.did !== 'undefined') {
prev.set('did', params.did);
}
if (typeof params.category_ids !== 'undefined') {
prev.set('category_ids', JSON.stringify(params.category_ids));
}
if (typeof params.page !== 'undefined') {
prev.set('page', params.page + '');
}
if (typeof params.size !== 'undefined') {
prev.set('size', params.size + '');
}
return prev;
},
{ replace: true }
);
};
useEffect(() => {
setList([]);
getList();
}, [refresh, page, size]);
const columns: ColumnsType<DataType> = [
{
title: t('textbook.bookColumns.title1'), // 名称
width: 260,
render: (_, record: any) => (
<div className="d-flex">
<div style={{ width: 80 }}>
<Image
preview={false}
width={80}
height={60}
style={{ borderRadius: 6 }}
src={
record.thumb === -1
? defaultThumb1
: record.thumb === -2
? defaultThumb2
: record.thumb === -3
? defaultThumb3
: resourceUrl[record.thumb]
}
loading="lazy"
alt="thumb"
></Image>
</div>
<span className="ml-8">{record.title}</span>
</div>
),
},
{
title: t('textbook.bookColumns.title2'), //简介
dataIndex: 'shortDesc',
width: 180,
render: (shortDesc: string) => (
<div className="two-lines-clamp" title={shortDesc}>
{shortDesc}
</div>
),
},
{
title: t('textbook.bookColumns.title3'), // 学科专业
width: 120,
dataIndex: 'major',
render: (major: string) => <span>{major}</span>,
},
{
title: t('textbook.bookColumns.title4'), // 作者
dataIndex: 'author',
width: 100,
render: (author: string) => <span>{author}</span>,
},
{
title: t('textbook.bookColumns.title5'), // 出版社
width: 160,
dataIndex: 'publishUnit',
render: (publishUnit: string) => (
<>
<span>{publishUnit}</span>
</>
),
},
{
title: t('textbook.bookColumns.title6'), // 章节总数
width: 120,
dataIndex: 'chapterNum',
render: (chapterNum: number) =>
chapterNum && JSON.stringify(chapterNum) !== '' ? (
<span>{chapterNum}</span>
) : (
<span>-</span>
),
},
{
title: t('textbook.bookColumns.title7'), // 发布时间
width: 120,
dataIndex: 'publishTime',
render: (publishTime: string) => <span>{dateFormatNoTime(publishTime)}</span>,
},
{
title: t('textbook.bookColumns.title8'), // 创建时间
width: 120,
dataIndex: 'createTime',
render: (createTime: string) => (
<span style={{ color: 'gray' }}>{dateFormatNoTime(createTime)}</span>
),
},
{
title: t('textbook.bookColumns.title9'),
key: 'action',
fixed: 'right',
width: 160,
render: (_, record: any) => {
const items: MenuProps['items'] = [
{
key: '1',
label: (
<Button
type="link"
size="small"
className="b-n-link c-red"
onClick={() => {
alert('查看关联资源');
}}
>
{t('textbook.bookColumns.option2')}
</Button>
),
},
{
key: '2',
label: (
<Button
type="link"
size="small"
className="b-n-link c-red"
onClick={() => {
setCid(Number(record.id));
setUpdateVisible(true);
}}
>
{t('textbook.bookColumns.option3')}
</Button>
),
},
{
key: '3',
label: (
<Button
type="link"
size="small"
className="b-n-link c-red"
onClick={() => delTextbook(record.id)}
>
{t('textbook.bookColumns.option4')}
</Button>
),
},
];
return (
<Space size="small">
<PerButton
type="link"
text={t('textbook.bookColumns.option1')}
class="b-link c-red"
icon={null}
p="course" // TODO此处需要权限限定 后期修改为textbook
onClick={() => {
setCid(Number(record.id));
navigate('/textbook/chapter/' + Number(record.id) + '?title=' + record.title);
}}
disabled={null}
/>
<div className="form-column"></div>
<Dropdown menu={{ items }}>
<Button type="link" className="b-link c-red" onClick={(e) => e.preventDefault()}>
<Space size="small" align="center">
{t('commen.more')}
<DownOutlined />
</Space>
</Button>
</Dropdown>
</Space>
);
},
},
];
// 删除课程
const delTextbook = (id: number) => {
if (id === 0) {
return;
}
confirm({
title: t('commen.confirmError'),
icon: <ExclamationCircleFilled />,
content: t('textbook.delTextbook'),
centered: true,
okText: t('commen.okText'),
cancelText: t('commen.cancelText'),
onOk() {
textbook.destroyTextbook(id).then((res: any) => {
if (res.data) {
setErrorData(res.data);
setShowDetail(true);
return;
}
message.success(t('commen.success'));
resetList();
});
},
onCancel() {
console.log('Cancel');
},
});
};
// 获取列表
const getList = () => {
setLoading(true);
textbook
.textbookList(page, size, title)
.then((res: any) => {
setTotal(res.data.total);
setList(res.data.data);
setResourceUrl(res.data.resource_url);
setLoading(false);
})
.catch((err: any) => {
console.log('error', err);
});
};
// 重置列表
const resetList = () => {
resetLocalSearchParams({
page: 1,
size: 10,
title: '',
});
setTitle('');
setList([]);
setRefresh(!refresh);
};
const paginationProps = {
current: page, //当前页码
pageSize: size,
total: total, // 总条数
onChange: (page: number, pageSize: number) => handlePageChange(page, pageSize), //改变页码的函数
showSizeChanger: true,
};
const handlePageChange = (page: number, pageSize: number) => {
resetLocalSearchParams({
page: page,
size: pageSize,
});
};
return (
<div className="playedu-main-body">
<div className="playedu-main-title float-left mb-24">{t('course.label4')}</div>
<div className="float-left j-b-flex mb-24">
<div className="d-flex">
{/*新建教材*/}
<PerButton
type="primary"
text={t('course.createTextbook')}
class="mr-16"
icon={<PlusOutlined />}
p="course" // TODO此处需要权限限定 后期修改为textbook
onClick={() => setCreateVisible(true)}
disabled={null}
/>
</div>
<div className="d-flex">
<div className="d-flex mr-24">
<Typography.Text>{t('course.textbook')}</Typography.Text>
<Input
value={title}
onChange={(e) => {
setTitle(e.target.value);
}}
allowClear
style={{ width: 260 }}
placeholder={t('course.textbookPlaceholder')}
/>
</div>
<div className="d-flex">
<Button className="mr-16" onClick={resetList}>
{t('commen.reset')}
</Button>
<Button
type="primary"
onClick={() => {
resetLocalSearchParams({
page: 1,
title: title,
});
setRefresh(!refresh);
}}
>
{t('commen.search')}
</Button>
</div>
</div>
</div>
<div className="float-left">
<Table
columns={columns}
dataSource={list}
loading={loading}
pagination={paginationProps}
rowKey={(record) => record.id}
/>
<CreateTextbook
open={createVisible}
onCancel={() => {
setCreateVisible(false);
setRefresh(!refresh);
}}
/>
<TextbookUpdate
id={cid}
open={updateVisible}
onCancel={() => {
setUpdateVisible(false);
setRefresh(!refresh);
}}
/>
{/*Error 展示*/}
{showDetail ? (
<Modal
title={
<div className="d-flex">
<ExclamationCircleFilled style={{ color: '#faad14', marginRight: 15 }} />
{t('course.aboutTitle')}
</div>
}
centered
forceRender
closable={false}
open={true}
width={416}
onOk={() => {
setShowDetail(false);
}}
footer={null}
maskClosable={false}
>
<div className="mt-16">
{Object.keys(errorData).map((v, index) => (
<div className="c-flex mb-16" key={index}>
<div
style={{
width: '100%',
height: 22,
fontSize: 14,
color: 'rgba(0,0,0,0.85)',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{errorData[v].course.title}
</div>
{errorData[v].tasks &&
errorData[v].tasks.length > 0 &&
errorData[v].tasks.map((it: any, i: number) => (
<div key={it.id} className="j-b-flex mt-8">
<div
style={{
width: 240,
height: 22,
fontSize: 14,
color: 'rgba(0,0,0,0.45)',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{t('course.task')}-{it.name}
</div>
</div>
))}
</div>
))}
</div>
<div className="r-r-flex mt-36">
<Button
type="primary"
danger
onClick={() => {
setShowDetail(false);
}}
>
{t('commen.complete')}
</Button>
</div>
</Modal>
) : null}
</div>
</div>
);
};
export default CoursePage;