Compare commits
7 Commits
ae7fb942fb
...
29054c6188
| Author | SHA1 | Date | |
|---|---|---|---|
| 29054c6188 | |||
| dc0d6d5b48 | |||
| 2b89d07edd | |||
| 24fdc236b1 | |||
| bbc5957b8c | |||
| b5e3cdcd0b | |||
| c66439995f |
@ -12,4 +12,5 @@ import org.springframework.stereotype.Component;
|
||||
public class AIPlatformConfig {
|
||||
private String host;
|
||||
private String api_key;
|
||||
private String profile;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -54,6 +54,8 @@ public class UploadController {
|
||||
|
||||
@Autowired private AppConfigService appConfigService;
|
||||
|
||||
|
||||
|
||||
@PostMapping("/minio")
|
||||
@Log(title = "上传-MinIO", businessType = BusinessTypeConstant.UPLOAD)
|
||||
public JsonResponse uploadMinio(
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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分析或自动填入 */
|
||||
@ -75,4 +75,4 @@ public class JCResource {
|
||||
|
||||
@TableField("tenant_id")
|
||||
private String tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ public class TextbookRequestDTO {
|
||||
|
||||
/** 封面地址 */
|
||||
@TableField("thumb")
|
||||
private String thumb;
|
||||
private Integer thumb;
|
||||
|
||||
/** 简介 */
|
||||
@TableField("short_desc")
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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 父节点ID,0或null代表顶级节点
|
||||
* @return 知识点编码,如A01、A01A01等
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -38,7 +38,7 @@ export const knowledgeKeyApi = {
|
||||
},
|
||||
validDataSet: () => {
|
||||
return client.get('/knowledge/dataset/validDataSet', {});
|
||||
}
|
||||
},
|
||||
|
||||
// // 获取知识库密钥
|
||||
// getKnowledgeKey: () => {
|
||||
|
||||
@ -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, {});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -185,4 +185,4 @@ const ExamAdministrationDetails = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ExamAdministrationDetails
|
||||
export default ExamAdministrationDetails;
|
||||
|
||||
@ -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} 名学生的考试状态`);
|
||||
|
||||
@ -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,
|
||||
}}
|
||||
>
|
||||
无视频源
|
||||
|
||||
@ -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);
|
||||
// 打开弹窗
|
||||
|
||||
@ -11,7 +11,6 @@ const HomePage = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles['layout-wrap']}>
|
||||
|
||||
<div className={styles['left-menu']}>
|
||||
<LeftMenu />
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
150
app/backend/src/pages/textbook/chapter.tsx
Normal file
150
app/backend/src/pages/textbook/chapter.tsx
Normal 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;
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
495
app/backend/src/pages/textbook/compenents/createTextbook.tsx
Normal file
495
app/backend/src/pages/textbook/compenents/createTextbook.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
22
app/backend/src/pages/textbook/compenents/update.module.less
Normal file
22
app/backend/src/pages/textbook/compenents/update.module.less
Normal 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;
|
||||
}
|
||||
}
|
||||
746
app/backend/src/pages/textbook/compenents/update.tsx
Normal file
746
app/backend/src/pages/textbook/compenents/update.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
14
app/backend/src/pages/textbook/index.module.less
Normal file
14
app/backend/src/pages/textbook/index.module.less
Normal 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;
|
||||
}
|
||||
495
app/backend/src/pages/textbook/index.tsx
Normal file
495
app/backend/src/pages/textbook/index.tsx
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user