mirror of
https://gitee.com/blossom-editor/blossom.git
synced 2025-12-06 16:58:26 +08:00
refactor: 文档菜单重构
1. 无限层级菜单 2. 文档拖拽排序
This commit is contained in:
parent
a246b0f055
commit
64a8d730a6
@ -1,6 +1,5 @@
|
||||
package com.blossom.backend.base.search.message.consumer;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.blossom.backend.base.search.SearchProperties;
|
||||
import com.blossom.backend.base.search.message.IndexMsg;
|
||||
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||
@ -51,7 +50,6 @@ public class IndexMsgConsumer {
|
||||
final Long userId = indexMsg.getUserId();
|
||||
final Long id = indexMsg.getId();
|
||||
if (userId == null || id == null) {
|
||||
log.error("消费异常. 获取用户id为空");
|
||||
continue;
|
||||
}
|
||||
if (IndexMsgTypeEnum.ADD == indexMsg.getType()) {
|
||||
@ -62,11 +60,11 @@ public class IndexMsgConsumer {
|
||||
// 查询最新的消息
|
||||
ArticleEntity article = this.articleService.selectById(id, false, true, false, userId);
|
||||
Document document = new Document();
|
||||
document.add(new StringField("id", Convert.toStr(id), Field.Store.YES));
|
||||
document.add(new StringField("id", String.valueOf(id), Field.Store.YES));
|
||||
document.add(new TextField("name", article.getName(), Field.Store.YES));
|
||||
document.add(new TextField("tags", article.getTags(), Field.Store.YES));
|
||||
document.add(new TextField("markdown", article.getMarkdown(), Field.Store.YES));
|
||||
indexWriter.updateDocument(new Term("id", Convert.toStr(id)), document);
|
||||
indexWriter.updateDocument(new Term("id", String.valueOf(id)), document);
|
||||
indexWriter.flush();
|
||||
indexWriter.commit();
|
||||
}
|
||||
@ -74,7 +72,7 @@ public class IndexMsgConsumer {
|
||||
// 删除索引
|
||||
try (Directory directory = FSDirectory.open(this.searchProperties.getUserIndexDirectory(userId));
|
||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
|
||||
indexWriter.deleteDocuments(new Term("id", Convert.toStr(id)));
|
||||
indexWriter.deleteDocuments(new Term("id", String.valueOf(id)));
|
||||
indexWriter.flush();
|
||||
indexWriter.commit();
|
||||
}
|
||||
|
||||
@ -1,11 +1,33 @@
|
||||
package com.blossom.backend.config;
|
||||
|
||||
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
public class Test {
|
||||
public static void main(String[] args) {
|
||||
|
||||
Set<FolderEntity> set = new HashSet<>();
|
||||
|
||||
FolderEntity f1 = new FolderEntity();
|
||||
f1.setId(1L);
|
||||
f1.setName("1");
|
||||
|
||||
|
||||
FolderEntity f2 = new FolderEntity();
|
||||
f2.setId(1L);
|
||||
f2.setName("2");
|
||||
|
||||
set.add(f1);
|
||||
set.add(f2);
|
||||
|
||||
System.out.println(set.size());
|
||||
System.out.println(Arrays.toString(set.toArray()));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -12,8 +12,10 @@ import com.blossom.backend.server.article.draft.pojo.*;
|
||||
import com.blossom.backend.server.article.open.ArticleOpenService;
|
||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
||||
import com.blossom.backend.server.doc.DocService;
|
||||
import com.blossom.backend.server.doc.DocSortChecker;
|
||||
import com.blossom.backend.server.doc.DocTypeEnum;
|
||||
import com.blossom.backend.server.folder.FolderService;
|
||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||
import com.blossom.backend.server.utils.ArticleUtil;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
@ -50,13 +52,14 @@ import java.util.List;
|
||||
@AllArgsConstructor
|
||||
@RequestMapping("/article")
|
||||
public class ArticleController {
|
||||
|
||||
private final ArticleService baseService;
|
||||
private final ArticleOpenService openService;
|
||||
private final FolderService folderService;
|
||||
private final UserService userService;
|
||||
private final ArticleTempVisitService tempVisitService;
|
||||
private final DocService docService;
|
||||
private final DocSortChecker docSortChecker;
|
||||
private final ImportManager importManager;
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
@ -122,9 +125,9 @@ public class ArticleController {
|
||||
ArticleEntity article = req.to(ArticleEntity.class);
|
||||
article.setTags(DocUtil.toTagStr(req.getTags()));
|
||||
article.setUserId(AuthContext.getUserId());
|
||||
// 如果新增到顶部, 获取最小的
|
||||
// 如果新增到顶部, 获取最小的排序
|
||||
if (BooleanUtil.isTrue(req.getAddToLast())) {
|
||||
article.setSort(docService.selectMinSortByPid(req.getPid()) + 1);
|
||||
article.setSort(docService.selectMaxSortByPid(req.getPid(), AuthContext.getUserId(), FolderTypeEnum.ARTICLE) + 1);
|
||||
}
|
||||
return R.ok(baseService.insert(article));
|
||||
}
|
||||
@ -136,10 +139,19 @@ public class ArticleController {
|
||||
* @apiNote 该接口只能修改文章的基本信息, 正文及版本修改请使用 "/upd/content" 接口,或者 {@link ArticleService#updateContentById(ArticleEntity)}
|
||||
*/
|
||||
@PostMapping("/upd")
|
||||
public R<Long> insert(@Validated @RequestBody ArticleUpdReq req) {
|
||||
public R<Long> update(@Validated @RequestBody ArticleUpdReq req) {
|
||||
ArticleEntity article = req.to(ArticleEntity.class);
|
||||
article.setTags(DocUtil.toTagStr(req.getTags()));
|
||||
article.setUserId(AuthContext.getUserId());
|
||||
// 检查排序是否重复
|
||||
// if (req.getSort() != null && req.getPid() != null) {
|
||||
// final long newPid = req.getPid();
|
||||
// docSortChecker.checkUnique(CollUtil.newArrayList(newPid),
|
||||
// null,
|
||||
// CollUtil.newArrayList(article),
|
||||
// FolderTypeEnum.ARTICLE,
|
||||
// AuthContext.getUserId());
|
||||
// }
|
||||
return R.ok(baseService.update(article));
|
||||
}
|
||||
|
||||
@ -263,11 +275,11 @@ public class ArticleController {
|
||||
* @param pid 上级菜单
|
||||
*/
|
||||
@PostMapping("import")
|
||||
public R<?> upload(@RequestParam("file") MultipartFile file, @RequestParam(value = "pid") Long pid) {
|
||||
public R<?> upload(@RequestParam("file") MultipartFile file, @RequestParam(value = "pid") Long pid, @RequestParam(value = "batchId") String batchId) {
|
||||
try {
|
||||
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
|
||||
if (!"txt".equals(suffix) && !"md".equals(suffix)) {
|
||||
throw new XzException404("不支持的文件类型: [" + suffix + "]");
|
||||
throw new XzException400("不支持的文件类型: [" + suffix + "]");
|
||||
}
|
||||
FolderEntity folder = folderService.selectById(pid);
|
||||
XzException404.throwBy(ObjUtil.isNull(folder), "上级文件夹不存在");
|
||||
@ -278,10 +290,12 @@ public class ArticleController {
|
||||
article.setPid(pid);
|
||||
article.setUserId(AuthContext.getUserId());
|
||||
article.setName(FileUtil.getPrefix(file.getOriginalFilename()));
|
||||
article.setWords(ArticleUtil.statWords(content));
|
||||
// article.setWords(ArticleUtil.statWords(content));
|
||||
article.setSort(importManager.getSort(batchId, pid, AuthContext.getUserId()));
|
||||
baseService.insert(article);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new XzException400("上传失败");
|
||||
}
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package com.blossom.backend.server.article.draft;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.base.search.EnableIndex;
|
||||
@ -166,7 +167,9 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
||||
public Long update(ArticleEntity req) {
|
||||
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
||||
baseMapper.updById(req);
|
||||
referenceService.updateInnerName(req.getUserId(), req.getId(), req.getName());
|
||||
if(StrUtil.isNotBlank(req.getName())) {
|
||||
referenceService.updateInnerName(req.getUserId(), req.getId(), req.getName());
|
||||
}
|
||||
return req.getId();
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
package com.blossom.backend.server.article.draft;
|
||||
|
||||
import com.blossom.backend.server.doc.DocService;
|
||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.RemovalCause;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class ImportManager {
|
||||
|
||||
private final DocService docService;
|
||||
private final ReentrantLock LOCK = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* 批量导入时的批次缓存, 一个批次的导入只会从数据库获取一次排序, 后续排序从缓存中递增
|
||||
*/
|
||||
private final Cache<String, AtomicInteger> batchCache = Caffeine.newBuilder()
|
||||
.initialCapacity(50)
|
||||
.expireAfterWrite(120, TimeUnit.MINUTES)
|
||||
.removalListener((String location, AtomicInteger i, RemovalCause cause) ->
|
||||
log.info("batch import [" + location + "] has been deleted")
|
||||
)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 并发导入时的排序获取
|
||||
*/
|
||||
public Integer getSort(String batchId, Long pid, Long userId) {
|
||||
AtomicInteger sort = batchCache.getIfPresent(batchId);
|
||||
if (null == sort) {
|
||||
try {
|
||||
LOCK.tryLock(1000, TimeUnit.MILLISECONDS);
|
||||
sort = batchCache.getIfPresent(batchId);
|
||||
if (null == sort) {
|
||||
sort = initBatchCount(batchId, pid, userId);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOCK.unlock();
|
||||
}
|
||||
}
|
||||
if (sort == null) {
|
||||
throw new XzException500("导入失败");
|
||||
}
|
||||
return sort.getAndIncrement();
|
||||
}
|
||||
|
||||
private AtomicInteger initBatchCount(String batchId, Long pid, Long userId) {
|
||||
System.out.println("初始化导入排序");
|
||||
AtomicInteger i = new AtomicInteger(docService.selectMaxSortByPid(pid, userId, FolderTypeEnum.ARTICLE) + 1);
|
||||
batchCache.put(batchId, i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,8 @@ import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.config.BlConstants;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeReq;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeUpdSortReq;
|
||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@ -29,7 +31,7 @@ public class DocController {
|
||||
/**
|
||||
* 文档列表
|
||||
*
|
||||
* @return 文件夹列表
|
||||
* @return 文档列表
|
||||
* @apiNote 文档包含文章和文件夹, 文件夹分为图片文件夹和文章文件夹 {@link DocTypeEnum}
|
||||
*/
|
||||
@GetMapping("/trees")
|
||||
@ -56,4 +58,21 @@ public class DocController {
|
||||
open.setUserId(userId);
|
||||
return R.ok(docService.listTree(open));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改排序
|
||||
* <p/>todo 返回列表, 兼容列表查询条件
|
||||
*
|
||||
* @param tree 需要修改排序的文档列表
|
||||
* @return 文档列表
|
||||
* @since 1.14.0
|
||||
*/
|
||||
@PostMapping("/upd/sort")
|
||||
public R<List<DocTreeRes>> updSort(@RequestBody DocTreeUpdSortReq tree) {
|
||||
docService.updSort(tree.getDocs(), AuthContext.getUserId(), FolderTypeEnum.getType(tree.getFolderType()));
|
||||
DocTreeReq req = new DocTreeReq();
|
||||
req.setUserId(AuthContext.getUserId());
|
||||
req.setOnlyPicture(tree.getOnlyPicture());
|
||||
return R.ok(docService.listTree(req));
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,9 +8,10 @@ public interface DocMapper {
|
||||
|
||||
/**
|
||||
* 查询 PID 下的最小排序
|
||||
*
|
||||
* @param pid PID
|
||||
* @return 最小排序
|
||||
*/
|
||||
Integer selectMaxSortByPid(@Param("pid") Long pid);
|
||||
Integer selectMaxSortByPid(@Param("pid") Long pid, @Param("userId") Long userId, @Param("type") Integer type);
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
package com.blossom.backend.server.doc;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.server.article.TagEnum;
|
||||
import com.blossom.backend.server.article.draft.ArticleMapper;
|
||||
import com.blossom.backend.server.article.draft.ArticleService;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeReq;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeUpdSortReq;
|
||||
import com.blossom.backend.server.folder.FolderMapper;
|
||||
import com.blossom.backend.server.folder.FolderService;
|
||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||
import com.blossom.backend.server.folder.pojo.FolderQueryReq;
|
||||
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||
import com.blossom.backend.server.picture.PictureService;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.backend.server.utils.PictureUtil;
|
||||
@ -16,6 +21,7 @@ import com.blossom.common.base.enums.YesNo;
|
||||
import com.blossom.common.base.util.SortUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -29,23 +35,28 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@Service
|
||||
public class DocService {
|
||||
private FolderService folderService;
|
||||
private ArticleService articleService;
|
||||
private PictureService pictureService;
|
||||
|
||||
private FolderService folderService;
|
||||
@Autowired
|
||||
private DocMapper baseMapper;
|
||||
|
||||
@Autowired
|
||||
public void setFolderService(FolderService folderService) {
|
||||
this.folderService = folderService;
|
||||
}
|
||||
private FolderMapper folderMapper;
|
||||
@Autowired
|
||||
private ArticleMapper articleMapper;
|
||||
@Autowired
|
||||
private DocSortChecker docSortChecker;
|
||||
|
||||
@Autowired
|
||||
public void setArticleService(ArticleService articleService) {
|
||||
this.articleService = articleService;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setFolderService(FolderService folderService) {
|
||||
this.folderService = folderService;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setPictureService(PictureService pictureService) {
|
||||
this.pictureService = pictureService;
|
||||
@ -64,10 +75,10 @@ public class DocService {
|
||||
* 只查询文件夹
|
||||
* =============================================================================================== */
|
||||
if (req.getOnlyFolder()) {
|
||||
FolderQueryReq where = req.to(FolderQueryReq.class);
|
||||
List<DocTreeRes> folder = folderService.listTree(where);
|
||||
FolderEntity where = req.to(FolderEntity.class);
|
||||
List<FolderEntity> folder = folderMapper.listAll(where);
|
||||
all.addAll(CollUtil.newArrayList(PictureUtil.getDefaultFolder(req.getUserId())));
|
||||
all.addAll(folder);
|
||||
all.addAll(DocUtil.toTreeRes(folder));
|
||||
priorityType = true;
|
||||
}
|
||||
/* ===============================================================================================
|
||||
@ -75,16 +86,16 @@ public class DocService {
|
||||
* =============================================================================================== */
|
||||
else if (req.getOnlyPicture()) {
|
||||
// 1. 所有图片文件夹
|
||||
FolderQueryReq folder = req.to(FolderQueryReq.class);
|
||||
folder.setType(FolderTypeEnum.PICTURE.getType());
|
||||
List<DocTreeRes> picFolder = folderService.listTree(folder);
|
||||
all.addAll(picFolder);
|
||||
FolderEntity where = req.to(FolderEntity.class);
|
||||
where.setType(FolderTypeEnum.PICTURE.getType());
|
||||
List<FolderEntity> picFolder = folderMapper.listAll(where);
|
||||
all.addAll(DocUtil.toTreeRes(picFolder));
|
||||
|
||||
// 2. 有图片的图片或文章文件夹
|
||||
List<Long> pids = pictureService.listDistinctPid(req.getUserId());
|
||||
if (CollUtil.isNotEmpty(pids)) {
|
||||
List<DocTreeRes> articleTopFolder = folderService.recursiveToParentTree(pids);
|
||||
all.addAll(articleTopFolder);
|
||||
List<FolderEntity> articleTopFolder = folderMapper.recursiveToParent(pids);
|
||||
all.addAll(DocUtil.toTreeRes(articleTopFolder));
|
||||
}
|
||||
|
||||
Optional<DocTreeRes> min = all.stream().min((f1, f2) -> SortUtil.intSort.compare(f1.getS(), f2.getS()));
|
||||
@ -105,24 +116,24 @@ public class DocService {
|
||||
all.addAll(articles);
|
||||
if (CollUtil.isNotEmpty(articles)) {
|
||||
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
||||
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
|
||||
all.addAll(folders);
|
||||
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
|
||||
all.addAll(DocUtil.toTreeRes(folders));
|
||||
}
|
||||
}
|
||||
/* ===============================================================================================
|
||||
* 只查询专题的文章和文件夹
|
||||
* =============================================================================================== */
|
||||
else if (req.getOnlySubject()) {
|
||||
FolderQueryReq folderWhere = req.to(FolderQueryReq.class);
|
||||
FolderEntity folderWhere = req.to(FolderEntity.class);
|
||||
folderWhere.setTags(TagEnum.subject.name());
|
||||
folderWhere.setType(FolderTypeEnum.ARTICLE.getType());
|
||||
List<DocTreeRes> subjects = folderService.listTree(folderWhere);
|
||||
List<FolderEntity> subjects = folderMapper.listAll(folderWhere);
|
||||
if (CollUtil.isNotEmpty(subjects)) {
|
||||
List<Long> subjectIds = subjects.stream().map(DocTreeRes::getI).collect(Collectors.toList());
|
||||
List<DocTreeRes> foldersTop = folderService.recursiveToParentTree(subjectIds);
|
||||
List<DocTreeRes> foldersBottom = folderService.recursiveToChildrenTree(subjectIds);
|
||||
all.addAll(foldersTop);
|
||||
all.addAll(foldersBottom);
|
||||
List<Long> subjectIds = subjects.stream().map(FolderEntity::getId).collect(Collectors.toList());
|
||||
List<FolderEntity> foldersTop = folderMapper.recursiveToParent(subjectIds);
|
||||
List<FolderEntity> foldersBottom = folderMapper.recursiveToChildren(subjectIds);
|
||||
all.addAll(DocUtil.toTreeRes(foldersTop));
|
||||
all.addAll(DocUtil.toTreeRes(foldersBottom));
|
||||
}
|
||||
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
|
||||
all.addAll(articles);
|
||||
@ -137,8 +148,8 @@ public class DocService {
|
||||
all.addAll(articles);
|
||||
if (CollUtil.isNotEmpty(articles)) {
|
||||
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
||||
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
|
||||
all.addAll(folders);
|
||||
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
|
||||
all.addAll(DocUtil.toTreeRes(folders));
|
||||
}
|
||||
}
|
||||
/* ===============================================================================================
|
||||
@ -151,19 +162,19 @@ public class DocService {
|
||||
all.addAll(articles);
|
||||
if (CollUtil.isNotEmpty(articles)) {
|
||||
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
||||
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
|
||||
all.addAll(folders);
|
||||
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
|
||||
all.addAll(DocUtil.toTreeRes(folders));
|
||||
}
|
||||
}
|
||||
/* ===============================================================================================
|
||||
* 默认查询文章文件夹
|
||||
* =============================================================================================== */
|
||||
else {
|
||||
FolderQueryReq folder = req.to(FolderQueryReq.class);
|
||||
FolderEntity folder = req.to(FolderEntity.class);
|
||||
folder.setType(FolderTypeEnum.ARTICLE.getType());
|
||||
List<DocTreeRes> folders = folderService.listTree(folder);
|
||||
List<FolderEntity> folders = folderMapper.listAll(folder);
|
||||
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
|
||||
all.addAll(folders);
|
||||
all.addAll(DocUtil.toTreeRes(folders));
|
||||
all.addAll(articles);
|
||||
}
|
||||
|
||||
@ -177,8 +188,64 @@ public class DocService {
|
||||
* @return 最大排序
|
||||
* @since 1.10.0
|
||||
*/
|
||||
public int selectMinSortByPid(Long pid) {
|
||||
return baseMapper.selectMaxSortByPid(pid);
|
||||
public int selectMaxSortByPid(Long pid, Long userId, FolderTypeEnum type) {
|
||||
return baseMapper.selectMaxSortByPid(pid, userId, type.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改排序
|
||||
*
|
||||
* @param docs 需要修改的文档
|
||||
* @since 1.14.0
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updSort(List<DocTreeUpdSortReq.Doc> docs, Long userId, FolderTypeEnum folderType) {
|
||||
// 提取所有需要修改的文档的父文档
|
||||
List<Long> pids = docs.stream().map(DocTreeUpdSortReq.Doc::getP).collect(Collectors.toList());
|
||||
|
||||
List<ArticleEntity> articles = new ArrayList<>();
|
||||
List<FolderEntity> folders = new ArrayList<>();
|
||||
|
||||
// 一次拖拽所修改的文档可能包含文章和文件夹, 需要归类和转为 Entity
|
||||
for (DocTreeUpdSortReq.Doc doc : docs) {
|
||||
if (DocTypeEnum.A.getType().equals(doc.getTy())) {
|
||||
ArticleEntity a = new ArticleEntity();
|
||||
a.setId(doc.getI());
|
||||
a.setPid(doc.getP());
|
||||
a.setName(doc.getN());
|
||||
a.setSort(doc.getS());
|
||||
articles.add(a);
|
||||
}
|
||||
if (DocTypeEnum.FA.getType().equals(doc.getTy())) {
|
||||
FolderEntity f = new FolderEntity();
|
||||
f.setId(doc.getI());
|
||||
f.setPid(doc.getP());
|
||||
f.setName(doc.getN());
|
||||
f.setSort(doc.getS());
|
||||
folders.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
docSortChecker.checkUnique(pids, folders, articles, folderType, userId);
|
||||
|
||||
for (DocTreeUpdSortReq.Doc tree : docs) {
|
||||
if (DocTypeEnum.FA.getType().equals(tree.getTy()) || DocTypeEnum.FP.getType().equals(tree.getTy())) {
|
||||
FolderEntity f = new FolderEntity();
|
||||
f.setId(tree.getI());
|
||||
f.setPid(tree.getP());
|
||||
f.setSort(tree.getS());
|
||||
f.setUserId(AuthContext.getUserId());
|
||||
f.setStorePath(tree.getSp());
|
||||
folderService.update(f);
|
||||
} else if (DocTypeEnum.A.getType().equals(tree.getTy())) {
|
||||
ArticleEntity a = new ArticleEntity();
|
||||
a.setId(tree.getI());
|
||||
a.setPid(tree.getP());
|
||||
a.setSort(tree.getS());
|
||||
a.setUserId(AuthContext.getUserId());
|
||||
articleService.update(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
package com.blossom.backend.server.doc;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.blossom.backend.server.article.draft.ArticleMapper;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
||||
import com.blossom.backend.server.folder.FolderMapper;
|
||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DocSortChecker {
|
||||
|
||||
@Autowired
|
||||
private FolderMapper folderMapper;
|
||||
|
||||
@Autowired
|
||||
private ArticleMapper articleMapper;
|
||||
|
||||
/**
|
||||
* 检查 pid 下是否有重复的文档排序
|
||||
*
|
||||
* @param pids pid
|
||||
* @param newFs 新文件夹
|
||||
* @param newAs 新文章
|
||||
* @return
|
||||
*/
|
||||
public boolean checkUnique(List<Long> pids,
|
||||
List<FolderEntity> newFs,
|
||||
List<ArticleEntity> newAs,
|
||||
FolderTypeEnum folderType,
|
||||
Long userId) {
|
||||
|
||||
Map<Long, FolderEntity> newFMap = CollUtil.isEmpty(newFs) ? new HashMap<>() : newFs.stream().collect(Collectors.toMap(FolderEntity::getId, doc -> doc));
|
||||
Map<Long, ArticleEntity> newAMap = CollUtil.isEmpty(newAs) ? new HashMap<>() : newAs.stream().collect(Collectors.toMap(ArticleEntity::getId, doc -> doc));
|
||||
|
||||
List<DocTreeRes> allDoc = new ArrayList<>();
|
||||
|
||||
// 获取文件夹排序
|
||||
FolderEntity fByPid = new FolderEntity();
|
||||
fByPid.setPids(pids);
|
||||
fByPid.setType(folderType.getType());
|
||||
fByPid.setUserId(userId);
|
||||
List<FolderEntity> folders = folderMapper.listAll(fByPid);
|
||||
|
||||
if (CollUtil.isNotEmpty(folders)) {
|
||||
for (FolderEntity f : folders) {
|
||||
FolderEntity newF = newFMap.get(f.getId());
|
||||
if (newF != null) {
|
||||
f.setPid(newF.getPid());
|
||||
f.setSort(newF.getSort());
|
||||
newFMap.remove(f.getId());
|
||||
}
|
||||
allDoc.add(DocUtil.toDocTree(f));
|
||||
}
|
||||
}
|
||||
|
||||
if (MapUtil.isNotEmpty(newFMap)) {
|
||||
for (FolderEntity f : newFMap.values()) {
|
||||
allDoc.add(DocUtil.toDocTree(f));
|
||||
}
|
||||
}
|
||||
|
||||
// 只有处理文章排序时, 才需要获取文章排序
|
||||
if(FolderTypeEnum.ARTICLE.equals(folderType)) {
|
||||
ArticleEntity aByPid = new ArticleEntity();
|
||||
aByPid.setPids(pids);
|
||||
aByPid.setUserId(userId);
|
||||
List<ArticleEntity> articles = articleMapper.listAll(aByPid);
|
||||
if (CollUtil.isNotEmpty(articles)) {
|
||||
for (ArticleEntity a : articles) {
|
||||
ArticleEntity newA = newAMap.get(a.getId());
|
||||
if (newA != null) {
|
||||
a.setPid(newA.getPid());
|
||||
a.setSort(newA.getSort());
|
||||
newAMap.remove(a.getId());
|
||||
}
|
||||
allDoc.add(DocUtil.toDocTree(a));
|
||||
}
|
||||
}
|
||||
|
||||
if (MapUtil.isNotEmpty(newAMap)) {
|
||||
for (ArticleEntity a : newAMap.values()) {
|
||||
allDoc.add(DocUtil.toDocTree(a));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 按 pid 分组, 依次校验各组下数据
|
||||
Map<Long, List<DocTreeRes>> map = allDoc.stream().collect(Collectors.groupingBy(DocTreeRes::getP));
|
||||
|
||||
map.forEach((pid, list) -> {
|
||||
// 获取不重复的排序
|
||||
long distinct = list.stream().map(DocTreeRes::getS).distinct().count();
|
||||
// 不重复的个数小于总数则有重复
|
||||
if (distinct != list.size()) {
|
||||
throw new XzException400("排序重复");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package com.blossom.backend.server.doc.pojo;
|
||||
|
||||
import com.blossom.backend.server.doc.DocTypeEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 修改文档排序
|
||||
*
|
||||
* @since 1.14.0
|
||||
*/
|
||||
@Data
|
||||
public class DocTreeUpdSortReq {
|
||||
|
||||
/**
|
||||
* 需要修改排序的文档列表
|
||||
*/
|
||||
private List<Doc> docs;
|
||||
/**
|
||||
* 文档类型 1:文章文件夹; 2:图片文件夹
|
||||
*
|
||||
* @see com.blossom.backend.server.folder.FolderTypeEnum
|
||||
*/
|
||||
private Integer folderType;
|
||||
/**
|
||||
* [Picture + Article] 只查询图片文件夹, 以及含有图片的文章文件夹
|
||||
*/
|
||||
private Boolean onlyPicture;
|
||||
|
||||
public Boolean getOnlyPicture() {
|
||||
if (onlyPicture == null) {
|
||||
return false;
|
||||
}
|
||||
return onlyPicture;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Doc {
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long i;
|
||||
/**
|
||||
* 父id
|
||||
*/
|
||||
private Long p;
|
||||
/**
|
||||
* 名称, 文件夹名称或文章名称
|
||||
*/
|
||||
private String n;
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer s;
|
||||
/**
|
||||
* 类型 {@link DocTypeEnum}
|
||||
*
|
||||
* @see com.blossom.backend.server.doc.DocTypeEnum
|
||||
*/
|
||||
private Integer ty;
|
||||
/**
|
||||
* 存储路径
|
||||
*/
|
||||
private String sp;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -5,10 +5,9 @@ import cn.hutool.core.util.ObjUtil;
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||
import com.blossom.backend.config.BlConstants;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleStarReq;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleUpdTagReq;
|
||||
import com.blossom.backend.server.doc.DocService;
|
||||
import com.blossom.backend.server.doc.DocSortChecker;
|
||||
import com.blossom.backend.server.folder.pojo.*;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
@ -34,6 +33,7 @@ import java.util.List;
|
||||
public class FolderController {
|
||||
private final FolderService baseService;
|
||||
private final DocService docService;
|
||||
private final DocSortChecker docSortChecker;
|
||||
|
||||
/**
|
||||
* 查询专题列表 [OP]
|
||||
@ -96,9 +96,9 @@ public class FolderController {
|
||||
FolderEntity folder = req.to(FolderEntity.class);
|
||||
folder.setTags(DocUtil.toTagStr(req.getTags()));
|
||||
folder.setUserId(AuthContext.getUserId());
|
||||
// 如果新增到顶部, 获取最小的
|
||||
// 如果新增到底部, 获取最大的排序
|
||||
if (BooleanUtil.isTrue(req.getAddToLast())) {
|
||||
folder.setSort(docService.selectMinSortByPid(req.getPid()) + 1);
|
||||
folder.setSort(docService.selectMaxSortByPid(req.getPid(), AuthContext.getUserId(), FolderTypeEnum.PICTURE) + 1);
|
||||
}
|
||||
return R.ok(baseService.insert(folder));
|
||||
}
|
||||
@ -112,6 +112,15 @@ public class FolderController {
|
||||
public R<Long> update(@Validated @RequestBody FolderUpdReq req) {
|
||||
FolderEntity folder = req.to(FolderEntity.class);
|
||||
folder.setTags(DocUtil.toTagStr(req.getTags()));
|
||||
// 检查排序是否重复
|
||||
// if (folder.getSort() != null && folder.getPid() != null) {
|
||||
// final long newPid = folder.getPid();
|
||||
// docSortChecker.checkUnique(CollUtil.newArrayList(newPid),
|
||||
// CollUtil.newArrayList(folder),
|
||||
// null,
|
||||
// FolderTypeEnum.ARTICLE,
|
||||
// AuthContext.getUserId());
|
||||
// }
|
||||
return R.ok(baseService.update(folder));
|
||||
}
|
||||
|
||||
|
||||
@ -5,17 +5,13 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.blossom.backend.server.article.TagEnum;
|
||||
import com.blossom.backend.server.article.draft.ArticleService;
|
||||
import com.blossom.backend.server.article.draft.ArticleMapper;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
|
||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
||||
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||
import com.blossom.backend.server.folder.pojo.FolderQueryReq;
|
||||
import com.blossom.backend.server.folder.pojo.FolderSubjectRes;
|
||||
import com.blossom.backend.server.picture.PictureService;
|
||||
import com.blossom.backend.server.picture.PictureMapper;
|
||||
import com.blossom.backend.server.picture.pojo.PictureEntity;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import com.blossom.common.base.enums.YesNo;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
@ -29,7 +25,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -40,19 +35,12 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
private ArticleService articleService;
|
||||
private PictureService pictureService;
|
||||
|
||||
@Autowired
|
||||
public void setArticleService(ArticleService articleService) {
|
||||
this.articleService = articleService;
|
||||
}
|
||||
private PictureMapper picMapper;
|
||||
|
||||
@Autowired
|
||||
public void setPictureService(PictureService pictureService) {
|
||||
this.pictureService = pictureService;
|
||||
}
|
||||
|
||||
private ArticleMapper articleMapper;
|
||||
|
||||
|
||||
/**
|
||||
@ -69,9 +57,11 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
FolderEntity where = new FolderEntity();
|
||||
where.setTags(TagEnum.subject.name());
|
||||
where.setUserId(userId);
|
||||
if (Objects.nonNull(starStatus) &&
|
||||
(starStatus.equals(1) || starStatus.equals(0))) { where.setStarStatus(starStatus); }
|
||||
else {where.setStarStatus(0);}
|
||||
if (null != starStatus && (starStatus.equals(1) || starStatus.equals(0))) {
|
||||
where.setStarStatus(starStatus);
|
||||
} else {
|
||||
where.setStarStatus(0);
|
||||
}
|
||||
List<FolderEntity> allOpenSubject = baseMapper.listAll(where);
|
||||
if (CollUtil.isEmpty(allOpenSubject)) {
|
||||
return new ArrayList<>();
|
||||
@ -85,12 +75,10 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
allOpenSubjectIds.addAll(allOpenSubjectChildFolders.stream().map(FolderEntity::getId).collect(Collectors.toList()));
|
||||
|
||||
// 3. 查询这些文件夹下的所有文章
|
||||
ArticleQueryReq articleWhere = new ArticleQueryReq();
|
||||
ArticleEntity articleWhere = new ArticleEntity();
|
||||
articleWhere.setPids(allOpenSubjectIds);
|
||||
articleWhere.setUserId(userId);
|
||||
// 统计专题信息时, 会包含非公开文章
|
||||
// articleWhere.setOpenStatus(YesNo.YES.getValue());
|
||||
List<ArticleEntity> articles = articleService.listAll(articleWhere);
|
||||
List<ArticleEntity> articles = articleMapper.listAll(articleWhere);
|
||||
|
||||
List<FolderSubjectRes> results = new ArrayList<>();
|
||||
|
||||
@ -123,34 +111,6 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部文件夹, 并转换成 {@link DocTreeRes}
|
||||
*/
|
||||
public List<DocTreeRes> listTree(FolderQueryReq req) {
|
||||
List<FolderEntity> folders = baseMapper.listAll(req.to(FolderEntity.class));
|
||||
return DocUtil.toTreeRes(folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取传入ID的所有的父文件夹, 并转换成 {@link DocTreeRes}, 结果会包含自己
|
||||
*
|
||||
* @param ids ID 集合
|
||||
*/
|
||||
public List<DocTreeRes> recursiveToParentTree(List<Long> ids) {
|
||||
List<FolderEntity> folders = baseMapper.recursiveToParent(ids);
|
||||
return DocUtil.toTreeRes(folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取传入ID的所有的子文件夹, 并转换成 {@link DocTreeRes}, 结果会包含自己
|
||||
*
|
||||
* @param ids ID 集合
|
||||
*/
|
||||
public List<DocTreeRes> recursiveToChildrenTree(List<Long> ids) {
|
||||
List<FolderEntity> folders = baseMapper.recursiveToChildren(ids);
|
||||
return DocUtil.toTreeRes(folders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询
|
||||
*
|
||||
@ -200,9 +160,52 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long update(FolderEntity folder) {
|
||||
updateParamValid(folder);
|
||||
updateStorePath(folder);
|
||||
baseMapper.updById(folder);
|
||||
return folder.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹
|
||||
* <p>1. 文件夹下有子文件夹时, 无法删除</p>
|
||||
* <p>2. 文件夹下有文章时, 无法删除</p>
|
||||
*
|
||||
* @param folderId 文件夹ID
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long folderId) {
|
||||
// 文件夹下有文件夹, 无法删除
|
||||
if (baseMapper.recursiveToChildren(CollUtil.newArrayList(folderId)).stream().anyMatch(d -> !d.getId().equals(folderId))) {
|
||||
throw new XzException500("文件夹下有子文件夹, 无法删除, 请先删除子文件夹");
|
||||
}
|
||||
|
||||
// 文件夹下有文章, 无法删除
|
||||
ArticleEntity articleWhere = new ArticleEntity();
|
||||
articleWhere.setPids(CollUtil.newArrayList(folderId));
|
||||
if (CollUtil.isNotEmpty(articleMapper.listAll(articleWhere))) {
|
||||
throw new XzException500("文件夹下有文章, 无法删除, 请先删除下属文章");
|
||||
}
|
||||
|
||||
// 文件夹下有图片, 无法删除
|
||||
PictureEntity picReq = new PictureEntity();
|
||||
picReq.setPid(folderId);
|
||||
if (CollUtil.isNotEmpty(picMapper.listAll(picReq))) {
|
||||
throw new XzException500("文件夹下有图片, 无法删除, 请先删除下属图片");
|
||||
}
|
||||
|
||||
baseMapper.deleteById(folderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改文件夹的存储地址
|
||||
*
|
||||
* @param folder
|
||||
*/
|
||||
private void updateStorePath(FolderEntity folder) {
|
||||
// 处理文件夹的存储地址
|
||||
XzException404.throwBy(folder.getId() == null, "ID不得为空");
|
||||
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
|
||||
if (Objects.isNull(folder.getStarStatus())) {folder.setStarStatus(0);}
|
||||
// 如果
|
||||
if (StrUtil.isNotBlank(folder.getStorePath())) {
|
||||
final FolderEntity oldFolder = selectById(folder.getId());
|
||||
@ -221,9 +224,6 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
}
|
||||
}
|
||||
folder.setStorePath(formatStorePath(folder.getStorePath()));
|
||||
if (Objects.isNull(folder.getStarStatus())) { folder.setStarStatus(0);}
|
||||
baseMapper.updById(folder);
|
||||
return folder.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -253,34 +253,12 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件夹
|
||||
* <p>1. 文件夹下有子文件夹时, 无法删除</p>
|
||||
* <p>2. 文件夹下有文章时, 无法删除</p>
|
||||
* 检查修改是否有效
|
||||
*
|
||||
* @param folderId 文件夹ID
|
||||
* @param folder 文件夹
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long folderId) {
|
||||
// 文件夹下有文件夹, 无法删除
|
||||
if (recursiveToChildrenTree(CollUtil.newArrayList(folderId)).stream().anyMatch(d -> !d.getI().equals(folderId))) {
|
||||
throw new XzException500("文件夹下有子文件夹, 无法删除, 请先删除子文件夹");
|
||||
}
|
||||
|
||||
// 文件夹下有文章, 无法删除
|
||||
ArticleQueryReq articleReq = new ArticleQueryReq();
|
||||
articleReq.setPids(CollUtil.newArrayList(folderId));
|
||||
if (CollUtil.isNotEmpty(articleService.listTree(articleReq))) {
|
||||
throw new XzException500("文件夹下有文章, 无法删除, 请先删除下属文章");
|
||||
}
|
||||
|
||||
// 文件夹下有图片, 无法删除
|
||||
PictureEntity picReq = new PictureEntity();
|
||||
picReq.setPid(folderId);
|
||||
if (CollUtil.isNotEmpty(pictureService.listAll(picReq))) {
|
||||
throw new XzException500("文件夹下有图片, 无法删除, 请先删除下属图片");
|
||||
}
|
||||
|
||||
baseMapper.deleteById(folderId);
|
||||
private void updateParamValid(FolderEntity folder) {
|
||||
XzException404.throwBy(folder.getId() == null, "ID不得为空");
|
||||
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
|
||||
}
|
||||
|
||||
}
|
||||
@ -28,4 +28,14 @@ public enum FolderTypeEnum {
|
||||
this.type = type;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public static FolderTypeEnum getType(Integer type) {
|
||||
for (FolderTypeEnum value : FolderTypeEnum.values()) {
|
||||
if (value.getType().equals(type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
package com.blossom.backend.server.folder.pojo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
@ -18,7 +15,6 @@ import java.util.List;
|
||||
*
|
||||
* @author xzzz
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@TableName("blossom_folder")
|
||||
public class FolderEntity extends AbstractPOJO implements Serializable {
|
||||
@ -106,6 +102,26 @@ public class FolderEntity extends AbstractPOJO implements Serializable {
|
||||
@TableField(exist = false)
|
||||
private List<Long> ids;
|
||||
|
||||
/**
|
||||
* 父ID集合
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<Long> pids;
|
||||
|
||||
//endregion
|
||||
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Long.hashCode(this.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof FolderEntity) {
|
||||
return this.id.equals(((FolderEntity)obj).getId());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ public class DocUtil {
|
||||
tree.setTy(DocTypeEnum.A.getType());
|
||||
|
||||
// 判断文章的版本与公开版本是否有差异
|
||||
if (article.getOpenStatus().equals(YesNo.YES.getValue()) && article.getVersion() > article.getOpenVersion()) {
|
||||
if (YesNo.YES.getValue().equals(article.getOpenStatus()) && article.getVersion() > article.getOpenVersion()) {
|
||||
tree.setVd(YesNo.YES.getValue());
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
spring:
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/blossom?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8
|
||||
url: jdbc:mysql://192.168.31.99:3306/blossom?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8
|
||||
username: root
|
||||
password: jasmine888
|
||||
hikari:
|
||||
@ -9,7 +9,7 @@ spring:
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.blossom: debug
|
||||
com.blossom: info
|
||||
com.blossom.expand.tracker: info
|
||||
com.blossom.backend.base.auth: info
|
||||
org.springframework.boot.web.embedded.tomcat.TomcatWebServer: warn
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
<select id="selectMaxSortByPid" resultType="java.lang.Integer">
|
||||
select IFNULL(max(h.sort), 0)
|
||||
from (
|
||||
select sort from blossom_article where pid = #{pid}
|
||||
select sort from blossom_article where pid = #{pid} and user_id = #{userId}
|
||||
union
|
||||
select sort from blossom_folder where pid = #{pid}
|
||||
select sort from blossom_folder where pid = #{pid} and user_id = #{userId} and type = #{type}
|
||||
) h;
|
||||
</select>
|
||||
</mapper>
|
||||
@ -26,6 +26,10 @@
|
||||
<if test="openStatus != null ">and open_status = #{openStatus}</if>
|
||||
<if test="type != null ">and type = #{type}</if>
|
||||
<if test="tags != null and tags != ''">and tags like concat('%',#{tags},'%')</if>
|
||||
<if test="pid != null">and pid = #{pid}</if>
|
||||
<if test="pids != null and pids.size() != 0">and pid in
|
||||
<foreach collection="pids" item="item" open="(" close=")" separator=",">#{item}</foreach>
|
||||
</if>
|
||||
<if test="userId != null">and user_id = #{userId}</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElConfigProvider } from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@ -94,6 +94,15 @@ export const docTreeApi = (params?: object): Promise<R<any>> => {
|
||||
return rq.get<R<any>>('/doc/trees', { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改文档的排序
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const docUpdSortApi = (data: object): Promise<R<any>> => {
|
||||
return rq.post<R<any>>('/doc/upd/sort', data)
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region ====================================================< folder >====================================================
|
||||
@ -440,16 +449,6 @@ export const articleBackupListApi = (): Promise<R<BackupFile[]>> => {
|
||||
return rq.get<BackupFile[]>('/article/backup/list')
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载备份文件
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export const articleBackupDownloadApi = (params?: object): Promise<any> => {
|
||||
let config = { params: params, responseType: 'blob' }
|
||||
return rq.get('/article/backup/download', config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载文件信息
|
||||
* @param data
|
||||
|
||||
@ -90,7 +90,7 @@ export class Request {
|
||||
/* 其他接口报错, 直接拒绝并提示错误信息 */
|
||||
let errorResponse = data
|
||||
errorResponse['url'] = res.config.url
|
||||
Notify.error(data.msg, '请求失败')
|
||||
Notify.error(data.msg, '处理失败')
|
||||
return Promise.reject(res)
|
||||
}
|
||||
},
|
||||
@ -106,22 +106,23 @@ export class Request {
|
||||
}
|
||||
let code = err.code
|
||||
let resp = err.response
|
||||
if (resp && resp.data) {
|
||||
Notify.error(resp.data.msg, '请求失败')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
console.log("🚀 ~ Request ~ constructor ~ resp:123123123", resp)
|
||||
if (code === 'ERR_NETWORK') {
|
||||
Notify.error('网络错误,请检查您的网络是否通畅', '请求失败')
|
||||
Notify.error('网络错误, 请检查您的网络是否通畅', '请求失败')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (err.request && err.request.status === 404) {
|
||||
Notify.error('未找到您的请求, 请您检查服务器地址!', '请求失败(404)')
|
||||
Notify.error('未找到您的请求', '请求失败(404)')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (err.request && err.request.status === 405) {
|
||||
Notify.error(`您的请求地址可能有误, 请检查请求地址${url}`, '请求失败(405)')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
if (resp && resp.data) {
|
||||
Notify.error(resp.data.msg, '请求失败')
|
||||
return Promise.reject(err)
|
||||
}
|
||||
return Promise.reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
@ -12,4 +12,4 @@
|
||||
@import './bl-tip.scss';
|
||||
@import './bl-tooltip.scss';
|
||||
@import './bl-tree.scss';
|
||||
@import '../../views/doc/tree-docs-right-menu.scss';
|
||||
@import '../../views/doc/doc-tree-right-menu.scss';
|
||||
9
blossom-editor/src/renderer/src/assets/utils/test.ts
Normal file
9
blossom-editor/src/renderer/src/assets/utils/test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const uuid = (): string => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
console.log(uuid())
|
||||
@ -433,3 +433,11 @@ export const isBase64Img = (image: string) => {
|
||||
let prefix = image.substring(0, Math.max(image.indexOf(','), 0))
|
||||
return prefix.startsWith('data:image') && prefix.endsWith('base64')
|
||||
}
|
||||
|
||||
export const uuid = (): string => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = (Math.random() * 16) | 0,
|
||||
v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
@ -127,6 +127,9 @@ export const useUserStore = defineStore('userStore', {
|
||||
userinfo: (Local.get(userinfoKey) as Userinfo) || initUserinfo()
|
||||
}),
|
||||
getters: {
|
||||
currentUserId(state) {
|
||||
return state.userinfo.id
|
||||
},
|
||||
/**
|
||||
* 是否登录
|
||||
*/
|
||||
|
||||
@ -8,14 +8,14 @@
|
||||
|
||||
<div class="content">
|
||||
<el-upload
|
||||
multiple
|
||||
class="article-upload"
|
||||
ref="uploadRef"
|
||||
name="file"
|
||||
multiple
|
||||
:action="serverStore.serverUrl + articleImportApiUrl"
|
||||
:data="{ pid: porps.doc.i }"
|
||||
:data="{ pid: porps.doc.i, batchId: importBatch }"
|
||||
:headers="{ Authorization: 'Bearer ' + userStore.auth.token }"
|
||||
:on-change="onChange"
|
||||
:show-file-list="true"
|
||||
:before-upload="beforeUpload"
|
||||
:on-success="onUploadSeccess"
|
||||
:on-error="onError"
|
||||
@ -38,6 +38,7 @@ import type { UploadInstance } from 'element-plus'
|
||||
import { articleImportApiUrl } from '@renderer/api/blossom'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import { useServerStore } from '@renderer/stores/server'
|
||||
import { uuid } from '@renderer/assets/utils/util'
|
||||
import { onChange, beforeUpload, onUploadSeccess, onError } from './scripts/article-import'
|
||||
|
||||
const userStore = useUserStore()
|
||||
@ -50,6 +51,8 @@ const porps = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const importBatch = ref(uuid())
|
||||
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
const submitUpload = () => {
|
||||
@ -66,21 +69,21 @@ const submitUpload = () => {
|
||||
.content {
|
||||
padding: 20px;
|
||||
|
||||
.upload-tip {
|
||||
border: 1px solid var(--el-border-color);
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
color: rgb(188, 55, 55);
|
||||
}
|
||||
// .upload-tip {
|
||||
// border: 1px solid var(--el-border-color);
|
||||
// padding: 5px 10px;
|
||||
// border-radius: 5px;
|
||||
// color: rgb(188, 55, 55);
|
||||
// }
|
||||
|
||||
.article-upload {
|
||||
:deep(.el-upload-list) {
|
||||
li {
|
||||
transition: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// .article-upload {
|
||||
// :deep(.el-upload-list) {
|
||||
// li {
|
||||
// transition: none;
|
||||
// margin-bottom: 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -222,7 +222,7 @@ import { ref, nextTick, inject, computed, watch, Ref } from 'vue'
|
||||
import { ElInput, ElMessageBox, FormInstance } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { provideKeyDocTree, getCDocsByPid, getDocById, checkLevel } from '@renderer/views/doc/doc'
|
||||
import { provideKeyDocTree, getCDocsByPid, getDocById } from '@renderer/views/doc/doc'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import {
|
||||
folderInfoApi,
|
||||
@ -388,6 +388,10 @@ const formatStorePath = () => {
|
||||
}
|
||||
|
||||
const showStorePathWarning = ref(false)
|
||||
|
||||
/**
|
||||
* 填充文件夹路径
|
||||
*/
|
||||
const fillStorePath = (id: string, path: string = ''): void => {
|
||||
let doc = getDocById(id, docTreeData!.value)
|
||||
if (!doc) {
|
||||
@ -542,13 +546,9 @@ const saveDoc = async (formEl: FormInstance | undefined) => {
|
||||
await formEl.validate((valid, _fields) => {
|
||||
if (valid) {
|
||||
saveLoading.value = true
|
||||
if (!checkLevel(docForm.value.pid, docTreeData!.value)) {
|
||||
saveLoading.value = false
|
||||
return
|
||||
}
|
||||
const handleResp = (_: any) => {
|
||||
Notify.success(curDocDialogType.value === 'upd' ? `修改《${docForm.value.name}》成功` : `新增《${docForm.value.name}》成功`)
|
||||
emits('saved', curDocDialogType.value)
|
||||
emits('saved', curDocDialogType.value, docForm.value)
|
||||
}
|
||||
const handleFinally = () => setTimeout(() => (saveLoading.value = false), 300)
|
||||
// 新增文件夹
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div :class="[viewStyle.isShowSubjectStyle ? (isSubject ? 'subject-title' : 'doc-title') : 'doc-title']">
|
||||
<bl-tag class="sort" v-show="props.trees.showSort" :bgColor="levelColor">
|
||||
<bl-tag class="sort" v-show="props.trees.showSort">
|
||||
{{ props.trees.s }}
|
||||
</bl-tag>
|
||||
<div class="doc-name">
|
||||
@ -30,9 +30,6 @@
|
||||
{{ tag.content }}
|
||||
</bl-tag>
|
||||
</div>
|
||||
<div v-if="level >= 2" class="folder-level-line" style="left: -20px"></div>
|
||||
<div v-if="level >= 3" class="folder-level-line" style="left: -30px"></div>
|
||||
<div v-if="level >= 4" class="folder-level-line" style="left: -40px"></div>
|
||||
<div
|
||||
v-if="viewStyle.isShowArticleType"
|
||||
v-for="(line, index) in tagLins"
|
||||
@ -47,19 +44,13 @@ import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { useConfigStore } from '@renderer/stores/config'
|
||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
import { computedDocTitleColor } from '@renderer/views/doc/doc'
|
||||
import { articleUpdNameApi, folderUpdNameApi } from '@renderer/api/blossom'
|
||||
|
||||
const { viewStyle } = useConfigStore()
|
||||
|
||||
const props = defineProps({
|
||||
trees: { type: Object as PropType<DocTree>, default: {} },
|
||||
size: { type: Number, default: 14 },
|
||||
level: { type: Number, required: true }
|
||||
})
|
||||
|
||||
const levelColor = computed(() => {
|
||||
return computedDocTitleColor(props.level)
|
||||
size: { type: Number, default: 14 }
|
||||
})
|
||||
|
||||
const nameWrapperStyle = computed(() => {
|
||||
@ -137,8 +128,8 @@ $icon-size: 17px;
|
||||
.doc-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
width: 100%;
|
||||
padding-bottom: 1px;
|
||||
position: relative;
|
||||
padding: 1px 0;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@ -147,12 +138,13 @@ $icon-size: 17px;
|
||||
font-size: inherit;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 5px;
|
||||
margin-top: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.menu-icon-img {
|
||||
@ -160,7 +152,8 @@ $icon-size: 17px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
@include ellipsis();
|
||||
min-height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,27 +164,18 @@ $icon-size: 17px;
|
||||
top: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
.folder-level-line {
|
||||
@include box(1px, 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.folder-level-line {
|
||||
background-color: var(--el-border-color);
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
// 专题样式, 包括边框和文字样式
|
||||
.subject-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include themeShadow(2px 2px 10px 1px var(--el-color-primary-light-8), 2px 2px 10px 1px #131313);
|
||||
@include themeShadow(2px 2px 8px 1px var(--el-color-primary-light-8), 2px 2px 10px 1px #131313);
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-7), var(--el-color-primary-light-8), var(--bl-html-color));
|
||||
max-width: calc(100% - 15px);
|
||||
min-width: calc(100% - 15px);
|
||||
padding: 2px 5px;
|
||||
margin: 5px 0 10px 0;
|
||||
min-height: 44px;
|
||||
max-width: 300px;
|
||||
width: calc(100% - 10px);
|
||||
padding: 4px 5px;
|
||||
margin: 5px 0 5px 0;
|
||||
border-radius: 7px;
|
||||
position: relative;
|
||||
|
||||
@ -206,7 +190,6 @@ $icon-size: 17px;
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@ -215,7 +198,6 @@ $icon-size: 17px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
@include ellipsis();
|
||||
max-width: calc(100% - 25px);
|
||||
min-width: calc(100% - 25px);
|
||||
}
|
||||
@ -223,13 +205,9 @@ $icon-size: 17px;
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
right: -10px;
|
||||
}
|
||||
|
||||
.folder-level-line {
|
||||
@include box(1px, calc(100% + 15px));
|
||||
top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
// 在左侧显示
|
||||
@ -238,8 +216,8 @@ $icon-size: 17px;
|
||||
.sync-line {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 60%;
|
||||
top: 20%;
|
||||
height: 70%;
|
||||
top: 15%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
@ -1,154 +1,26 @@
|
||||
<template>
|
||||
<div class="doc-workbench-root">
|
||||
<bl-col class="workbench-name" just="flex-start" align="flex-end" height="46px" v-show="curArticle !== undefined">
|
||||
<span>《{{ curArticle?.name }}》</span>
|
||||
<span style="font-size: 9px; padding-right: 5px">{{ curArticle?.id }}</span>
|
||||
</bl-col>
|
||||
|
||||
<bl-row class="wb-page-container">
|
||||
<bl-row class="wb-page-item" just="flex-start" align="flex-end" width="calc(100% - 16px)" height="44px">
|
||||
<el-tooltip
|
||||
content="文章引用网络"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="-5"
|
||||
:hide-after="0">
|
||||
<bl-row class="wb-page-item" just="flex-start" align="flex-end" height="44px">
|
||||
<el-tooltip content="文章引用网络" effect="light" popper-class="is-small" placement="top" :offset="-5" :hide-after="0">
|
||||
<div class="iconbl bl-correlation-line" @click="openArticleReferenceWindow()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="新增文件夹或文章"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<div class="iconbl bl-a-fileadd-line" @click="handleShowAddDocInfoDialog()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="刷新"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<div class="iconbl bl-a-cloudrefresh-line" @click="refreshDocTree()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="全文搜索"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="9"
|
||||
:hide-after="0">
|
||||
<el-tooltip content="全文搜索" effect="light" popper-class="is-small" placement="top" :offset="9" :hide-after="0">
|
||||
<div class="iconbl bl-search-line" @click="showSearch()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" popper-class="is-small" transition="none" placement="top" :show-arrow="false" :offset="5" :hide-after="0">
|
||||
<div class="iconbl bl-a-leftdirection-line" @click="emits('show-sort')"></div>
|
||||
<template #content>
|
||||
显示排序<br />
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="SortLevelColor.ONE">一级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.TWO">二级</bl-tag>
|
||||
</bl-row>
|
||||
<bl-row style="padding-bottom: 5px">
|
||||
<bl-tag :bgColor="SortLevelColor.THREE">三级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.FOUR">四级</bl-tag>
|
||||
</bl-row>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="备份记录"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<el-tooltip content="备份记录" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
|
||||
<div class="iconbl bl-a-cloudstorage-line" @click="handleShowBackupDialog"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="文章回收站"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<el-tooltip content="文章回收站" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
|
||||
<div class="iconbl bl-delete-line" @click="handleShowRecycleDialog"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="查看收藏"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<div v-if="props.showStar">
|
||||
<div v-if="onlyStars" class="iconbl bl-star-fill" @click="changeOnlyStar()"></div>
|
||||
<div v-else class="iconbl bl-star-line" @click="changeOnlyStar()"></div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="查看专题"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<div v-if="props.showSubject">
|
||||
<div v-if="onlySubject" class="iconbl bl-a-lowerrightpage-fill" @click="changeOnlySubject()"></div>
|
||||
<div v-else class="iconbl bl-a-lowerrightpage-line" @click="changeOnlySubject()"></div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
content="查看公开"
|
||||
effect="light"
|
||||
popper-class="is-small"
|
||||
transition="none"
|
||||
placement="top"
|
||||
:show-arrow="false"
|
||||
:offset="8"
|
||||
:hide-after="0">
|
||||
<div v-if="props.showOpen">
|
||||
<div v-if="onlyOpen" class="iconbl bl-cloud-fill" @click="changeOnlyOpen()"></div>
|
||||
<div v-else class="iconbl bl-cloud-line" @click="changeOnlyOpen()"></div>
|
||||
</div>
|
||||
<el-tooltip content="文档快速编辑" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
|
||||
<div class="iconbl bl-article-line" @click=""></div>
|
||||
</el-tooltip>
|
||||
</bl-row>
|
||||
|
||||
<bl-col width="12px" height="30px" just="end" class="workbench-more" style="">
|
||||
<div class="iconbl bl-a-morevertical-line" @click="showMoreMenu"></div>
|
||||
</bl-col>
|
||||
</bl-row>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="isShowDocInfoDialog"
|
||||
width="535"
|
||||
top="100px"
|
||||
style="margin-left: 320px"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
draggable>
|
||||
<ArticleInfo ref="ArticleInfoRef" @saved="savedCallback"></ArticleInfo>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
class="bl-dialog-fixed-body"
|
||||
v-model="isShowBackupDialog"
|
||||
@ -174,25 +46,13 @@
|
||||
draggable>
|
||||
<ArticleRecycle ref="ArticleRecycleRef"></ArticleRecycle>
|
||||
</el-dialog>
|
||||
|
||||
<Teleport to="body">
|
||||
<div v-show="moreMenu.show" class="tree-menu" :style="{ left: moreMenu.clientX + 'px', top: moreMenu.clientY + 'px', width: '120px' }">
|
||||
<div class="menu-content" style="border: none">
|
||||
<div @click="changeOnlyOpen"><span class="iconbl bl-cloud-line"></span>查看公开</div>
|
||||
<div @click="changeOnlySubject"><span class="iconbl bl-a-lowerrightpage-line"></span>查看专题</div>
|
||||
<div @click="changeOnlyStar"><span class="iconbl bl-star-line"></span>查看收藏</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, inject, onDeactivated } from 'vue'
|
||||
import { provideKeyCurArticleInfo, SortLevelColor } from '@renderer/views/doc/doc'
|
||||
import { ref, nextTick, onDeactivated } from 'vue'
|
||||
import { openNewArticleReferenceWindow } from '@renderer/assets/utils/electron'
|
||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import ArticleInfo from './ArticleInfo.vue'
|
||||
import ArticleBackup from './ArticleBackup.vue'
|
||||
import ArticleRecycle from './ArticleRecycle.vue'
|
||||
|
||||
@ -205,43 +65,9 @@ onDeactivated(() => {
|
||||
unbindKeys()
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
showOpen: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showSubject: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showStar: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
//#region --------------------------------------------------< 控制台更多选项 >--------------------------------------------------
|
||||
const moreMenu = ref<RightMenu>({ show: false, clientX: 0, clientY: 0 })
|
||||
|
||||
/**
|
||||
* 显示右键菜单
|
||||
* @param doc 文档
|
||||
* @param event 事件
|
||||
*/
|
||||
const showMoreMenu = (event: MouseEvent) => {
|
||||
moreMenu.value.show = true
|
||||
nextTick(() => {
|
||||
let y = event.clientY
|
||||
if (document.body.clientHeight - event.clientY < 50) {
|
||||
y = event.clientY - 50
|
||||
}
|
||||
moreMenu.value = { show: true, clientX: event.clientX, clientY: y }
|
||||
setTimeout(() => {
|
||||
document.body.addEventListener('click', closeMoreMenu)
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
const closeMoreMenu = (event: MouseEvent) => {
|
||||
if (event.target) {
|
||||
let isPrevent = (event.target as HTMLElement).getAttribute('data-bl-prevet')
|
||||
@ -257,36 +83,6 @@ const closeMoreMenu = (event: MouseEvent) => {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region --------------------------------------------------< 查询 >--------------------------------------------------
|
||||
const curArticle = inject(provideKeyCurArticleInfo)
|
||||
|
||||
const onlyOpen = ref<boolean>(false) // 只显示公开
|
||||
const onlySubject = ref<boolean>(false) // 只显示专题
|
||||
const onlyStars = ref<boolean>(false) // 只显示 star
|
||||
|
||||
const changeOnlyOpen = () => {
|
||||
onlyOpen.value = !onlyOpen.value
|
||||
onlySubject.value = false
|
||||
onlyStars.value = false
|
||||
refreshDocTree()
|
||||
}
|
||||
|
||||
const changeOnlySubject = () => {
|
||||
onlyOpen.value = false
|
||||
onlySubject.value = !onlySubject.value
|
||||
onlyStars.value = false
|
||||
refreshDocTree()
|
||||
}
|
||||
|
||||
const changeOnlyStar = () => {
|
||||
onlyOpen.value = false
|
||||
onlySubject.value = false
|
||||
onlyStars.value = !onlyStars.value
|
||||
refreshDocTree()
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region --------------------------------------------------< 新增窗口 >--------------------------------------------------
|
||||
const ArticleInfoRef = ref()
|
||||
const isShowDocInfoDialog = ref<boolean>(false)
|
||||
@ -302,23 +98,6 @@ const openArticleReferenceWindow = () => {
|
||||
openNewArticleReferenceWindow()
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台刷新文档列表
|
||||
*/
|
||||
const refreshDocTree = () => {
|
||||
emits('refreshDocTree', onlyOpen.value, onlySubject.value, onlyStars.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存后的回调
|
||||
* 1. 刷新菜单列表
|
||||
* 2. 关闭 dialog 页面
|
||||
*/
|
||||
const savedCallback = () => {
|
||||
isShowDocInfoDialog.value = false
|
||||
emits('refreshDocTree', onlyOpen.value, onlySubject.value, onlyStars.value)
|
||||
}
|
||||
|
||||
const showSearch = () => {
|
||||
emits('show-search')
|
||||
}
|
||||
@ -363,7 +142,7 @@ const unbindKeys = () => {
|
||||
}
|
||||
|
||||
//#endregion
|
||||
const emits = defineEmits(['refreshDocTree', 'show-sort', 'show-search'])
|
||||
const emits = defineEmits(['show-sort', 'show-search'])
|
||||
defineExpose({ handleShowBackupDialog })
|
||||
</script>
|
||||
|
||||
@ -387,14 +166,6 @@ defineExpose({ handleShowBackupDialog })
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
// 排序
|
||||
.bl-a-leftdirection-line {
|
||||
font-size: 27px;
|
||||
padding-bottom: 3px;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
// 搜索
|
||||
.bl-search-line {
|
||||
font-size: 23px;
|
||||
@ -408,14 +179,6 @@ defineExpose({ handleShowBackupDialog })
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
// 刷新图标
|
||||
.bl-a-cloudrefresh-line,
|
||||
.bl-a-fileadd-line {
|
||||
&:active {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.bl-correlation-line {
|
||||
font-size: 40px;
|
||||
padding-bottom: 0px;
|
||||
|
||||
@ -1,21 +1,12 @@
|
||||
<template>
|
||||
<div class="editor-status-root">
|
||||
<bl-row width="calc(100% - 240px)" height="100%" class="status-item-container">
|
||||
<div>
|
||||
《{{ curDoc?.name }}》
|
||||
</div>
|
||||
<div>
|
||||
版本:{{ curDoc?.version }}
|
||||
</div>
|
||||
<div>
|
||||
字数:{{ curDoc?.words }}
|
||||
</div>
|
||||
<div>
|
||||
最近修改:{{ curDoc?.updTime }}
|
||||
</div>
|
||||
<div v-if="curDoc?.openTime">
|
||||
发布:{{ curDoc?.openTime }}
|
||||
</div>
|
||||
<div>《{{ curDoc?.name }}》</div>
|
||||
<div>ID:{{ curDoc?.id }}</div>
|
||||
<div>版本:{{ curDoc?.version }}</div>
|
||||
<div>字数:{{ curDoc?.words }}</div>
|
||||
<div>最近修改:{{ curDoc?.updTime }}</div>
|
||||
<div v-if="curDoc?.openTime">发布:{{ curDoc?.openTime }}</div>
|
||||
</bl-row>
|
||||
<bl-row just="flex-end" width="240px" height="100%" class="status-item-container">
|
||||
<div @click="openArticleLogWindow">
|
||||
@ -26,9 +17,7 @@
|
||||
<span class="iconbl bl-correlation-line"></span>
|
||||
引用网络
|
||||
</div>
|
||||
<bl-col width="100px" just="center">
|
||||
渲染用时: {{ props.renderInterval }}ms
|
||||
</bl-col>
|
||||
<bl-col width="100px" just="center"> 渲染用时: {{ props.renderInterval }}ms </bl-col>
|
||||
</bl-row>
|
||||
</div>
|
||||
</template>
|
||||
@ -37,7 +26,7 @@
|
||||
import { inject, toRaw } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { provideKeyCurArticleInfo } from '@renderer/views/doc/doc'
|
||||
import { openNewArticleReferenceWindow, openNewArticleLogWindow } from "@renderer/assets/utils/electron"
|
||||
import { openNewArticleReferenceWindow, openNewArticleLogWindow } from '@renderer/assets/utils/electron'
|
||||
|
||||
const props = defineProps({
|
||||
renderInterval: {
|
||||
@ -73,7 +62,7 @@ const openArticleLogWindow = () => {
|
||||
overflow-x: overlay;
|
||||
white-space: nowrap;
|
||||
|
||||
&>div {
|
||||
& > div {
|
||||
height: 100%;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
@ -96,4 +85,4 @@ const openArticleLogWindow = () => {
|
||||
@include flex(row, flex-start, center);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -378,7 +378,6 @@ const handleShowHotKeyDialog = () => {
|
||||
.editor-tools-root {
|
||||
@include box(calc(100% - 20px), 35px);
|
||||
@include themeShadow(0 1px 4px 1px #d3d3d3, 0 1px 4px 1px rgb(20, 20, 20));
|
||||
margin: 5px 10px 10px 10px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@ -426,7 +425,7 @@ const handleShowHotKeyDialog = () => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--
|
||||
<!--
|
||||
快捷键说明为弹出框 需要设置全局的样式
|
||||
-->
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -5,9 +5,9 @@ import Notify from '@renderer/scripts/notify'
|
||||
* 添加文件的检查, 当前支持如下格式:
|
||||
* .txt
|
||||
* .md
|
||||
*
|
||||
* @param _file
|
||||
* @param files
|
||||
*
|
||||
* @param _file
|
||||
* @param files
|
||||
*/
|
||||
export const onChange: UploadProps['onChange'] = (_file: UploadUserFile, files: UploadUserFile[]) => {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
@ -19,11 +19,10 @@ export const onChange: UploadProps['onChange'] = (_file: UploadUserFile, files:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rawFile
|
||||
* @returns
|
||||
*
|
||||
* @param rawFile
|
||||
* @returns
|
||||
*/
|
||||
export const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
if (rawFile.size / 1024 / 1024 > 10) {
|
||||
@ -34,14 +33,19 @@ export const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件成功
|
||||
* @param resp
|
||||
* @param _file
|
||||
* 文件上传成功时的钩子, 成功代表 http 成功, 不代表后台处理成功, 后台处理成功的返回 20000 之外的状态的, 并在这里重置为其他状态
|
||||
* 'ready' | 'uploading' | 'success' | 'fail'
|
||||
*
|
||||
* @param resp
|
||||
* @param _file
|
||||
*/
|
||||
export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file: UploadFile, _files: UploadFiles) => {
|
||||
const result = handleUploadSeccess(resp)
|
||||
console.log(_file)
|
||||
if (result) {
|
||||
_file.status = 'ready'
|
||||
_file.status = 'success'
|
||||
} else {
|
||||
_file.status = 'fail'
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +56,7 @@ export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file: UploadFil
|
||||
*/
|
||||
export const handleUploadSeccess = (resp: any): boolean => {
|
||||
if (resp.code === '20000') {
|
||||
Notify.success('上传成功')
|
||||
// Notify.success('上传成功')
|
||||
return true
|
||||
} else {
|
||||
Notify.error(resp.msg, '上传失败')
|
||||
@ -61,23 +65,24 @@ export const handleUploadSeccess = (resp: any): boolean => {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error
|
||||
* @param _file
|
||||
* @param _files
|
||||
*
|
||||
* @param error
|
||||
* @param _file
|
||||
* @param _files
|
||||
*/
|
||||
export const onError: UploadProps['onError'] = (error, _file, _files) => {
|
||||
handleUploadError(error)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error
|
||||
* 上传失败时的处理, 失败是根据 http 响应码决定, 通常是网络失败等原因
|
||||
* @param error
|
||||
*/
|
||||
export const handleUploadError = (error: Error) => {
|
||||
console.log('🚀 ~ handleUploadError ~ error:', error)
|
||||
if (error.message != undefined) {
|
||||
try {
|
||||
let resp = JSON.parse(error.message);
|
||||
let resp = JSON.parse(error.message)
|
||||
if (resp != undefined) {
|
||||
Notify.error(resp.msg, '上传失败')
|
||||
}
|
||||
@ -85,4 +90,4 @@ export const handleUploadError = (error: Error) => {
|
||||
Notify.error(error.message, '上传失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,4 +94,27 @@ export const parseTocAsync = async (ele: HTMLElement): Promise<Toc[]> => {
|
||||
tocs.push(toc)
|
||||
}
|
||||
return tocs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载返回对象
|
||||
*
|
||||
* @param resp
|
||||
*/
|
||||
export const downloadTextPlain = (resp: any) => {
|
||||
let filename: string = resp.headers.get('content-disposition')
|
||||
let filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
|
||||
let matches = filenameRegex.exec(filename)
|
||||
if (matches != null && matches[1]) {
|
||||
filename = decodeURI(matches[1].replace(/['"]/g, ''))
|
||||
}
|
||||
filename = decodeURI(filename)
|
||||
let a = document.createElement('a')
|
||||
let blob = new Blob([resp.data], { type: 'text/plain' })
|
||||
let objectUrl = URL.createObjectURL(blob)
|
||||
a.setAttribute('href', objectUrl)
|
||||
a.setAttribute('download', filename)
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
a.remove()
|
||||
}
|
||||
|
||||
@ -93,18 +93,19 @@
|
||||
position: relative;
|
||||
background-color: #ffffff00;
|
||||
|
||||
$heightTools: 45px;
|
||||
$heightTools: 50px;
|
||||
$heightStatus: 28px;
|
||||
$heightEP: calc(100% - 5px - #{$heightStatus} - #{$heightTools});
|
||||
$heightEP: calc(100% - #{$heightStatus} - #{$heightTools});
|
||||
|
||||
.editor-tools {
|
||||
@include box(100%, $heightTools);
|
||||
padding: 5px 10px 10px 10px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.editor-preview {
|
||||
@include box(100%, $heightEP);
|
||||
@include flex(row, flex-start, center);
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
position: relative;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
@ -137,6 +138,11 @@
|
||||
overflow: overlay;
|
||||
z-index: 2;
|
||||
|
||||
/* 定义滑块 内阴影+圆角 */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(*) {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
@ -234,6 +240,11 @@
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
/* 定义滑块 内阴影+圆角 */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.katex > *) {
|
||||
font-size: 1.2em !important;
|
||||
font-family: 'KaTeX_Size1', sans-serif !important;
|
||||
|
||||
107
blossom-editor/src/renderer/src/views/doc/doc-tree-detail.scss
Normal file
107
blossom-editor/src/renderer/src/views/doc/doc-tree-detail.scss
Normal file
@ -0,0 +1,107 @@
|
||||
$icon-size: 17px;
|
||||
|
||||
.doc-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 1px 0;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include themeBrightness(100%, 90%);
|
||||
@include ellipsis();
|
||||
font-size: inherit;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
min-height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
padding: 0 2px;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 专题样式, 包括边框和文字样式
|
||||
.subject-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include themeShadow(2px 2px 8px 1px var(--el-color-primary-light-8), 2px 2px 10px 1px #131313);
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-7), var(--el-color-primary-light-8), var(--bl-html-color));
|
||||
min-height: 44px;
|
||||
max-width: 300px;
|
||||
width: calc(100% - 10px);
|
||||
padding: 4px 5px;
|
||||
margin: 5px 0 5px 0;
|
||||
border-radius: 7px;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include themeBrightness(100%, 100%);
|
||||
color: var(--el-color-primary);
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
max-width: calc(100% - 25px);
|
||||
min-width: calc(100% - 25px);
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// 在左侧显示
|
||||
.open-line,
|
||||
.star-line,
|
||||
.sync-line {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 70%;
|
||||
top: 15%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.star-line {
|
||||
@include themeBg(rgb(237, 204, 11), rgba(228, 195, 5, 0.724));
|
||||
}
|
||||
|
||||
.open-line {
|
||||
background: #79c20c71;
|
||||
}
|
||||
|
||||
.sync-line {
|
||||
background: #e8122479;
|
||||
}
|
||||
68
blossom-editor/src/renderer/src/views/doc/doc-tree-detail.ts
Normal file
68
blossom-editor/src/renderer/src/views/doc/doc-tree-detail.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/* ======================================================================
|
||||
* 文章树状列表自定义节点
|
||||
* ====================================================================== */
|
||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
|
||||
/**
|
||||
* 计算标签, 并返回便签对象集合
|
||||
* @param doc 文章内容
|
||||
* @param viewStyle 页面样式
|
||||
*/
|
||||
export const tags = (doc: DocTree, viewStyle: { isShowArticleTocTag: boolean; isShowArticleCustomTag: boolean; isShowFolderOpenTag: boolean }) => {
|
||||
let icons: any = []
|
||||
doc.t.forEach((tag) => {
|
||||
if (tag.toLocaleLowerCase() === 'subject') {
|
||||
icons.unshift({ content: '专题', bgColor: 'var(--bl-tag-color-subject)', icon: 'bl-a-lowerrightpage-line' })
|
||||
} else if (tag.toLocaleLowerCase() === 'toc') {
|
||||
if (!viewStyle.isShowArticleTocTag) {
|
||||
return
|
||||
}
|
||||
icons.push({ content: 'TOC', bgColor: 'var(--bl-tag-color-toc)' })
|
||||
} else if (viewStyle.isShowArticleCustomTag) {
|
||||
icons.push({ content: tag })
|
||||
}
|
||||
})
|
||||
if (doc.o === 1 && doc.ty != 3) {
|
||||
if (viewStyle.isShowFolderOpenTag) {
|
||||
icons.unshift({ bgColor: 'var(--bl-tag-color-open)', icon: 'bl-cloud-line' })
|
||||
}
|
||||
}
|
||||
return icons
|
||||
}
|
||||
|
||||
/**
|
||||
* 竖条状态标识
|
||||
*/
|
||||
export const tagLins = (doc: DocTree) => {
|
||||
let lines: string[] = []
|
||||
if (doc.star === 1) {
|
||||
lines.push('star-line')
|
||||
}
|
||||
if (doc.o === 1 && doc.ty === 3) {
|
||||
lines.push('open-line')
|
||||
}
|
||||
if (doc.vd === 1) {
|
||||
lines.push('sync-line')
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章图标是否是链接
|
||||
* @param doc
|
||||
* @param viewStyle
|
||||
* @returns
|
||||
*/
|
||||
export const isShowImg = (doc: DocTree, viewStyle: { isShowArticleIcon: boolean }) => {
|
||||
return viewStyle.isShowArticleIcon && isNotBlank(doc.icon) && (doc.icon.startsWith('http') || doc.icon.startsWith('https')) && !doc?.updn
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章图标是否是内置 svg
|
||||
* @param doc
|
||||
* @param viewStyle
|
||||
* @returns
|
||||
*/
|
||||
export const isShowSvg = (doc: DocTree, viewStyle: { isShowArticleIcon: boolean }) => {
|
||||
return viewStyle.isShowArticleIcon && isNotBlank(doc.icon) && !doc?.updn
|
||||
}
|
||||
174
blossom-editor/src/renderer/src/views/doc/doc-tree.scss
Normal file
174
blossom-editor/src/renderer/src/views/doc/doc-tree.scss
Normal file
@ -0,0 +1,174 @@
|
||||
.doc-workbench {
|
||||
@include box(100%, 50px);
|
||||
}
|
||||
|
||||
// 操作树状菜单的工具栏
|
||||
.doc-tree-operator {
|
||||
@include box(100%, 28px);
|
||||
@include flex(row, center, center);
|
||||
@include themeColor(#909399, #969696);
|
||||
position: relative;
|
||||
|
||||
.iconbl {
|
||||
font-size: 18px;
|
||||
margin: 0 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.bl-a-leftdirection-line {
|
||||
font-size: 20px;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.bl-a-folderon-line {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.bl-refresh-line {
|
||||
font-size: 16px;
|
||||
margin: 0 2px;
|
||||
|
||||
&:active {
|
||||
color: var(--el-color-primary-light-3);
|
||||
}
|
||||
}
|
||||
|
||||
.doc-tree-search {
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
right: 10px;
|
||||
z-index: 2000;
|
||||
|
||||
.el-input {
|
||||
border-radius: 4px;
|
||||
background-color: var(--bl-html-color);
|
||||
@include themeShadow(0 0 7px 1px #4a4a4a2f, 0 0 7px 1px #000000);
|
||||
}
|
||||
|
||||
:deep(.el-input--suffix) {
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input-group__append) {
|
||||
padding: 0;
|
||||
:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
div {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
.el-icon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-trees-container {
|
||||
@include box(100%, calc(100% - 78px));
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
* {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.menu-item-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.doc-trees-placeholder {
|
||||
width: 100%;
|
||||
font-size: 13.5px;
|
||||
text-align: right;
|
||||
color: var(--bl-text-color-light);
|
||||
padding-top: 10px;
|
||||
|
||||
.iconbl {
|
||||
font-size: 16px;
|
||||
margin: 0 4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-tree {
|
||||
@include box(100%, 100%);
|
||||
padding-left: 5px;
|
||||
// --el-transition-duration: .1s; // 折叠展开的动画效果
|
||||
|
||||
.menu-divider {
|
||||
width: calc(100%);
|
||||
height: 15px;
|
||||
margin-left: -20px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
margin-bottom: 15px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.menu-item-wrapper) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__children) {
|
||||
transition: none; // 关闭折叠展开动画
|
||||
}
|
||||
|
||||
:deep(.is-drop-inner) {
|
||||
box-shadow: inset 0 0 1px 2px var(--el-color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-tree__drop-indicator) {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
color: var(--bl-text-doctree-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
:deep(.is-current) {
|
||||
& > .el-tree-node__content {
|
||||
border-radius: 5px;
|
||||
// background-color: var(--el-color-primary-light-8);
|
||||
|
||||
&:has(.menu-divider) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
/* 定义滑块 内阴影+圆角 */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
241
blossom-editor/src/renderer/src/views/doc/doc-tree.ts
Normal file
241
blossom-editor/src/renderer/src/views/doc/doc-tree.ts
Normal file
@ -0,0 +1,241 @@
|
||||
/* ======================================================================
|
||||
* 文章树状列表
|
||||
* ====================================================================== */
|
||||
import Node from 'element-plus/es/components/tree/src/model/node'
|
||||
import { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
|
||||
import { NodeDropType } from 'element-plus/es/components/tree/src/tree.type'
|
||||
import { Ref } from 'vue'
|
||||
|
||||
export const getColor = (node: Node) => {
|
||||
if (node.level === 1) {
|
||||
return '#878787AF'
|
||||
}
|
||||
if (node.level === 2) {
|
||||
return '#89A319AA'
|
||||
}
|
||||
if (node.level === 3) {
|
||||
return '#A37E19AA'
|
||||
}
|
||||
if (node.level === 4) {
|
||||
return '#A33B19AA'
|
||||
}
|
||||
if (node.level === 5) {
|
||||
return '#19A383AA'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
export interface NeedUpd {
|
||||
i: string
|
||||
p: string
|
||||
s: number
|
||||
n: string
|
||||
ty: DocType
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖拽后处理各个节点排序
|
||||
* @param drag 拖拽的节点
|
||||
* @param enter 放入的节点
|
||||
* @param dropType 拖拽的类型
|
||||
* @param _event 事件
|
||||
* @param DocTreeRef 树状列表对象
|
||||
* @param docTreeData 树状类表数据
|
||||
*/
|
||||
export const handleTreeDrop = (
|
||||
drag: Node,
|
||||
enter: Node,
|
||||
dropType: NodeDropType,
|
||||
_event: DragEvents,
|
||||
DocTreeRef: Ref,
|
||||
docTreeData: Ref<DocTree[]>,
|
||||
updateFn: (needUpd: NeedUpd[]) => void
|
||||
) => {
|
||||
// 是否同级别
|
||||
const isSame = drag.data.p === enter.data.p
|
||||
const dragSourceSort = drag.data.s
|
||||
const enterSourceSort = enter.data.s
|
||||
// 记录最终需要修改的数据
|
||||
const needUpd: NeedUpd[] = []
|
||||
|
||||
// 保存需要传入后台修改的节点
|
||||
const addUpd = (node: Node) => {
|
||||
needUpd.push({
|
||||
i: node.data.i,
|
||||
p: node.data.p,
|
||||
s: node.data.s,
|
||||
n: node.data.n,
|
||||
ty: node.data.ty
|
||||
})
|
||||
}
|
||||
|
||||
console.log(`same: ${isSame}, dropType: ${dropType}, drag: ${dragSourceSort}, enter: ${enterSourceSort}`)
|
||||
if (isSame) {
|
||||
// drag 在 enter 前
|
||||
if (dropType === 'before') {
|
||||
// 下方的拖动到上方
|
||||
if (dragSourceSort > enterSourceSort) {
|
||||
drag.data.s = enterSourceSort
|
||||
addUpd(drag)
|
||||
for (let i = 0; i < enter.parent.childNodes.length; i++) {
|
||||
const node = enter.parent.childNodes[i]
|
||||
if (node.data.s >= enterSourceSort && node.data.s < dragSourceSort && node.data.i != drag.data.i) {
|
||||
node.data.s += 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 上方的拖动到下方
|
||||
else if (isSame) {
|
||||
drag.data.s = enterSourceSort - 1
|
||||
addUpd(drag)
|
||||
for (let i = 0; i < enter.parent.childNodes.length; i++) {
|
||||
const node = enter.parent.childNodes[i]
|
||||
if (node.data.s < enterSourceSort && node.data.s > dragSourceSort && node.data.i != drag.data.i) {
|
||||
node.data.s -= 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drag 在 enter 后
|
||||
else if (dropType === 'after') {
|
||||
// 下方拖动到上方, 即拖拽的节点排序大于放置的节点排序
|
||||
if (dragSourceSort > enterSourceSort) {
|
||||
drag.data.s = enterSourceSort + 1
|
||||
addUpd(drag)
|
||||
for (let i = 0; i < enter.parent.childNodes.length; i++) {
|
||||
const node = enter.parent.childNodes[i]
|
||||
if (node.data.s > enterSourceSort && node.data.s < dragSourceSort && node.data.i != drag.data.i) {
|
||||
node.data.s += 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drag.data.s = enterSourceSort
|
||||
addUpd(drag)
|
||||
for (let i = 0; i < enter.parent.childNodes.length; i++) {
|
||||
const node = enter.parent.childNodes[i]
|
||||
if (node.data.s <= enterSourceSort && node.data.s > dragSourceSort && node.data.i != drag.data.i) {
|
||||
node.data.s -= 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drag 在 enter 内部
|
||||
if (dropType === 'inner') {
|
||||
const dragSourcePid = drag.data.p
|
||||
const dragSourceParent = DocTreeRef.value.getNode(dragSourcePid)
|
||||
if (dragSourceParent) {
|
||||
for (let i = 0; i < dragSourceParent.childNodes.length; i++) {
|
||||
const node = dragSourceParent.childNodes[i]
|
||||
if (node.data.s > dragSourceSort) {
|
||||
node.data.s -= 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drag.data.p = enter.data.i
|
||||
|
||||
let maxSort: number = 0
|
||||
if (!enter.childNodes || enter.childNodes.length == 0) {
|
||||
maxSort = 0
|
||||
} else if (enter.childNodes.length == 1 && enter.childNodes[0].data.i === drag.data.i) {
|
||||
maxSort = 0
|
||||
} else {
|
||||
maxSort = getMaxSort(drag, enter.childNodes)
|
||||
}
|
||||
drag.data.s = maxSort + 1
|
||||
addUpd(drag)
|
||||
}
|
||||
}
|
||||
// 不同的父级
|
||||
else {
|
||||
const dragSourcePid = drag.data.p
|
||||
// pid 为 0 时需要特殊处理, 无法通过 node 获取, 只能通过 docTreeData 获取
|
||||
if (dragSourcePid === '0') {
|
||||
for (let i = 0; i < docTreeData.value.length; i++) {
|
||||
const node = docTreeData.value[i]
|
||||
if (node.s > dragSourceSort) {
|
||||
node.s -= 1
|
||||
needUpd.push({ i: node.i, p: node.p, n: node.n, s: node.s, ty: node.ty })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const dragSourceParent = DocTreeRef.value.getNode(dragSourcePid)
|
||||
if (dragSourceParent) {
|
||||
for (let i = 0; i < dragSourceParent.childNodes.length; i++) {
|
||||
const node = dragSourceParent.childNodes[i]
|
||||
if (node.data.s > dragSourceSort) {
|
||||
node.data.s -= 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dropType === 'before') {
|
||||
drag.data.p = enter.data.p
|
||||
drag.data.s = enterSourceSort
|
||||
addUpd(drag)
|
||||
for (let i = 0; i < enter.parent.childNodes.length; i++) {
|
||||
const node = enter.parent.childNodes[i]
|
||||
if (node.data.s >= enterSourceSort && node.data.i != drag.data.i) {
|
||||
node.data.s += 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dropType === 'after') {
|
||||
drag.data.p = enter.data.p
|
||||
drag.data.s = enterSourceSort + 1
|
||||
addUpd(drag)
|
||||
for (let i = 0; i < enter.parent.childNodes.length; i++) {
|
||||
const node = enter.parent.childNodes[i]
|
||||
if (node.data.s > enterSourceSort && node.data.i != drag.data.i) {
|
||||
node.data.s += 1
|
||||
addUpd(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dropType === 'inner') {
|
||||
drag.data.p = enter.data.i
|
||||
let maxSort: number = 0
|
||||
if (!enter.childNodes || enter.childNodes.length == 0) {
|
||||
maxSort = 0
|
||||
} else if (enter.childNodes.length == 1 && enter.childNodes[0].data.i === drag.data.i) {
|
||||
maxSort = 0
|
||||
} else {
|
||||
maxSort = getMaxSort(drag, enter.childNodes)
|
||||
}
|
||||
drag.data.s = maxSort + 1
|
||||
addUpd(drag)
|
||||
}
|
||||
}
|
||||
if (needUpd.length > 0) {
|
||||
updateFn(needUpd)
|
||||
}
|
||||
console.log(needUpd)
|
||||
console.log('==========================================')
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 nodes 中获取最大排序, 排除掉 drag
|
||||
*/
|
||||
export const getMaxSort = (drag: Node, nodes: Node[]): number => {
|
||||
const maxSort = Math.max.apply(
|
||||
Math,
|
||||
nodes
|
||||
.filter((n) => n.data.i != drag.data.i)
|
||||
.map((item) => {
|
||||
return item.data.s
|
||||
})
|
||||
)
|
||||
return maxSort
|
||||
}
|
||||
@ -75,13 +75,14 @@ export const computedDocTitleColor = (level: number) => {
|
||||
* @param pid 上级ID
|
||||
* @param trees 树状列表
|
||||
* @returns
|
||||
* @deprecated 1.14.0 支持无限菜单后不需要控制层级
|
||||
*/
|
||||
export const checkLevel = (pid: string, trees: DocTree[]): boolean => {
|
||||
let parents = getPDocsByPid(pid, trees)
|
||||
if (parents.length >= 4) {
|
||||
Notify.error('最多仅支持4级层级关系', '菜单层级错误')
|
||||
return false
|
||||
}
|
||||
// let parents = getPDocsByPid(pid, trees)
|
||||
// if (parents.length >= 4) {
|
||||
// Notify.error('最多仅支持4级层级关系', '菜单层级错误')
|
||||
// return false
|
||||
// }
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
.doc-workbench {
|
||||
@include box(100%, 90px);
|
||||
}
|
||||
|
||||
.doc-trees-container {
|
||||
@include box(100%, calc(100% - 90px));
|
||||
// 图片文档菜单中需要有 padding-top
|
||||
padding-top: 8px;
|
||||
&:hover {
|
||||
:deep(.folder-level-line) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.menu-item-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.doc-trees-placeholder {
|
||||
width: 100%;
|
||||
padding-right: 35px;
|
||||
color: var(--bl-text-color-light);
|
||||
font-size: 13.5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.doc-trees {
|
||||
@include box(100%, 100%);
|
||||
font-weight: 200;
|
||||
border: 0;
|
||||
overflow-y: overlay;
|
||||
// 基础的 padding
|
||||
--el-menu-base-level-padding: 25px;
|
||||
// 每级别的的缩进
|
||||
--el-menu-level-padding: 10px;
|
||||
// ------------------- sub-item 的样式
|
||||
// sub-item 菜单的高度
|
||||
--el-menu-sub-item-height: 25px;
|
||||
|
||||
// ------------------- item 的样式
|
||||
// 菜单每个 item 的高度
|
||||
--el-menu-item-height: 25px;
|
||||
--el-transition-duration: 0.1s;
|
||||
--el-menu-hover-bg-color: #0000000b;
|
||||
// item 的 字体大小
|
||||
// --el-menu-item-font-size: inherit;
|
||||
// --el-font-size-base: inherit;
|
||||
|
||||
.menu-divider {
|
||||
width: 100%;
|
||||
height: 15px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu) {
|
||||
.el-sub-menu__title {
|
||||
height: auto;
|
||||
min-height: 25px;
|
||||
padding-right: 0;
|
||||
overflow: hidden;
|
||||
color: var(--bl-text-doctree-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: inherit;
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: var(--bl-text-color-light);
|
||||
left: calc(10px + (var(--el-menu-level) * 10px));
|
||||
font-size: 12px;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
font-weight: 300;
|
||||
color: var(--bl-text-doctree-color);
|
||||
height: auto;
|
||||
min-height: 25px;
|
||||
padding-right: 0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
@include themeText(0px 1px 3px rgba(107, 104, 104, 1), 0px 1px 3px rgb(145, 145, 145));
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--bl-html-color) 0%,
|
||||
var(--el-color-primary-light-7) 40%,
|
||||
var(--el-color-primary-light-7) 60%,
|
||||
var(--bl-html-color) 100%
|
||||
);
|
||||
color: #ffffff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.el-badge__content) {
|
||||
top: 7px;
|
||||
transform: translateY(-50%) translateX(100%) scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
@ -142,7 +142,7 @@ import { ref, nextTick, inject, computed, watch, Ref } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
import type { FormRules } from 'element-plus'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { checkLevel, getCDocsByPid, provideKeyDocTree, getDocById } from '@renderer/views/doc/doc'
|
||||
import { getCDocsByPid, provideKeyDocTree, getDocById } from '@renderer/views/doc/doc'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import { folderInfoApi, folderAddApi, folderUpdApi } from '@renderer/api/blossom'
|
||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
@ -272,14 +272,10 @@ const fillStorePath = (id: string, path: string = ''): void => {
|
||||
const saveLoading = ref<boolean>(false)
|
||||
const saveDoc = () => {
|
||||
saveLoading.value = true
|
||||
if (!checkLevel(docForm.value.pid, docTreeData!.value)) {
|
||||
saveLoading.value = false
|
||||
return
|
||||
}
|
||||
// then 回调
|
||||
const handleResp = (_: any) => {
|
||||
Notify.success(curDocDialogType === 'upd' ? `修改《${docForm.value.name}》成功` : `新增《${docForm.value.name}》成功`)
|
||||
emits('saved')
|
||||
emits('saved', curDocDialogType, docForm.value)
|
||||
}
|
||||
const handleFinally = () => setTimeout(() => (saveLoading.value = false), 300)
|
||||
if (curDocDialogType == 'add')
|
||||
|
||||
@ -1,93 +1,92 @@
|
||||
<template>
|
||||
<!-- 文件夹操作 -->
|
||||
<div class="doc-workbench">
|
||||
<Workbench @refresh-doc-tree="getDocTree" @show-sort="handleShowSort"></Workbench>
|
||||
<Workbench></Workbench>
|
||||
</div>
|
||||
<div class="doc-tree-operator">
|
||||
<el-tooltip effect="light" popper-class="is-small" placement="top" :offset="4" :hide-after="0" content="显示排序">
|
||||
<div class="iconbl bl-a-leftdirection-line" @click="handleShowSort"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" popper-class="is-small" placement="top" :offset="4" :hide-after="0" content="根目录下新建图片文件夹">
|
||||
<div class="iconbl bl-a-folderon-line" @click="addFolderToRoot()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" popper-class="is-small" placement="top" :offset="4" :hide-after="0" :show-after="1000" content="搜索">
|
||||
<div class="iconbl bl-search-line" @click="showTreeFilter()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" popper-class="is-small" placement="top" :offset="4" :hide-after="0" :show-after="1000" content="刷新">
|
||||
<div class="iconbl bl-refresh-line" @click="refreshDocTree()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" popper-class="is-small" placement="top" :offset="4" :hide-after="0" :show-after="1000" content="折叠所有文件夹">
|
||||
<div class="iconbl bl-collapse" @click="collapseAll"></div>
|
||||
</el-tooltip>
|
||||
<div class="doc-tree-search" ref="DocTreeSearch" v-show="isShowTreeFilter">
|
||||
<el-input v-model="treeFilterText" style="width: 180px" ref="DocTreeSearchInput">
|
||||
<template #append>
|
||||
<div ref="DocTreeSearchMove" style="cursor: move; border-right: 1px solid var(--el-border-color)">
|
||||
<el-icon><Rank /></el-icon>
|
||||
</div>
|
||||
<div style="cursor: pointer" @click="showTreeFilter()">
|
||||
<el-icon size="14"><Close /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="DocTreeContainer"
|
||||
class="doc-trees-container"
|
||||
v-loading="docTreeLoading"
|
||||
element-loading-text="正在读取文档..."
|
||||
:style="{ fontSize: configStore.viewStyle.treeDocsFontSize }">
|
||||
<!-- 文件夹 -->
|
||||
<el-menu v-if="!isEmpty(docTreeData)" ref="DocTreeRef" class="doc-trees" :unique-opened="configStore.viewStyle.isMenuUniqueOpened">
|
||||
<!-- ================================================ L1 ================================================ -->
|
||||
<div v-for="L1 in docTreeData" :key="L1.i">
|
||||
<div v-if="L1.ty == 11" class="menu-divider" />
|
||||
|
||||
<!-- L1无下级 -->
|
||||
<el-menu-item v-else-if="isEmpty(L1.children)" :index="L1.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L1)" @click.right="handleClickRight(L1, $event)">
|
||||
<PictureTitle :trees="L1" :level="1" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- L1有下级 -->
|
||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L1.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L1)" @click.right="handleClickRight(L1, $event)">
|
||||
<PictureTitle :trees="L1" :level="1" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================================================ L2 ================================================ -->
|
||||
<div v-for="L2 in L1.children" :key="L2.i">
|
||||
<!-- L2无下级 -->
|
||||
<el-menu-item v-if="isEmpty(L2.children)" :index="L2.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L2)" @click.right="handleClickRight(L2, $event)">
|
||||
<PictureTitle :trees="L2" :level="2" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- L2有下级 -->
|
||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L2.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L2)" @click.right="handleClickRight(L2, $event)">
|
||||
<PictureTitle :trees="L2" :level="2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================================================ L3 ================================================ -->
|
||||
<div v-for="L3 in L2.children" :key="L3.i">
|
||||
<!-- L3无下级 -->
|
||||
<el-menu-item v-if="isEmpty(L3.children)" :index="L3.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L3)" @click.right="handleClickRight(L3, $event)">
|
||||
<PictureTitle :trees="L3" :level="3" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- L3有下级 -->
|
||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L3.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L3)" @click.right="handleClickRight(L3, $event)">
|
||||
<PictureTitle :trees="L3" :level="3" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================================================ L4 ================================================ -->
|
||||
<div v-for="L4 in L3.children" :key="L4.i">
|
||||
<!-- L4 不允许有下级, 只允许4级 -->
|
||||
<el-menu-item v-if="isEmpty(L4.children)" :index="L4.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L4)" style="width: 100%" @click.right="handleClickRight(L4, $event)">
|
||||
<PictureTitle :trees="L4" :level="4" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</div>
|
||||
</el-sub-menu>
|
||||
:style="{ fontSize: viewStyle.treeDocsFontSize }">
|
||||
<el-tree
|
||||
v-if="docTreeData.length > 0"
|
||||
ref="DocTreeRef"
|
||||
class="doc-tree"
|
||||
:data="docTreeData"
|
||||
:allow-drag="handleAllowDrag"
|
||||
:allow-drop="handleAllowDrop"
|
||||
:highlight-current="true"
|
||||
:indent="14"
|
||||
:icon="ArrowRightBold"
|
||||
:accordion="false"
|
||||
:default-expanded-keys="Array.from(docTreeCurrentExpandId)"
|
||||
:filter-node-method="filterNode"
|
||||
:draggable="isBlank(treeFilterText)"
|
||||
node-key="i"
|
||||
@nodeClick="clickCurDoc"
|
||||
@nodeExpand="handleNodeExpand"
|
||||
@nodeCollapse="handleNodeCollapse"
|
||||
@nodeDrop="handleDrop">
|
||||
<template #default="{ node, data }">
|
||||
<div v-if="data.ty === 11" class="menu-divider"></div>
|
||||
<div v-else class="menu-item-wrapper" @click.right="handleClickRightMenu($event, data)">
|
||||
<div class="doc-title">
|
||||
<bl-tag v-if="isShowSort && data.ty === 2" class="sort" :bgColor="getColor(node)">
|
||||
{{ data.s }}
|
||||
</bl-tag>
|
||||
<div class="doc-name">
|
||||
<img class="menu-icon-img" v-if="isShowImg(data, viewStyle)" :src="data.icon" />
|
||||
<svg v-else-if="isShowSvg(data, viewStyle)" class="icon menu-icon" aria-hidden="true">
|
||||
<use :xlink:href="'#' + data.icon"></use>
|
||||
</svg>
|
||||
<el-input
|
||||
v-if="data?.updn"
|
||||
v-model="data.n"
|
||||
:id="'article-doc-name-' + data.i"
|
||||
@blur="blurArticleNameInput(data)"
|
||||
@keyup.enter="blurArticleNameInput(data)"
|
||||
style="width: 95%"></el-input>
|
||||
<div v-else class="name-wrapper" :style="{ maxWidth: isNotBlank(data.icon) ? 'calc(100% - 25px)' : '100%' }">
|
||||
{{ data.n }}
|
||||
</div>
|
||||
</el-sub-menu>
|
||||
<bl-tag v-for="tag in tags(data, viewStyle)" style="margin-top: 5px" :bg-color="tag.bgColor" :icon="tag.icon">
|
||||
{{ tag.content }}
|
||||
</bl-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单, 添加到 body 下 -->
|
||||
@ -96,14 +95,14 @@
|
||||
<div class="doc-name">{{ curDoc.n }}</div>
|
||||
<div class="menu-content">
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 ? 'disabled' : '']" @click="rename"><span class="iconbl bl-pen"></span>重命名</div>
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 || curDoc.ty != 2 ? 'disabled' : '']" @click="handleShowDocInfoDialog('upd')">
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 ? 'disabled' : '']" @click="handleShowDocInfoDialog('upd')">
|
||||
<span class="iconbl bl-a-fileedit-line"></span>编辑详情
|
||||
</div>
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 || curDoc.ty != 2 ? 'disabled' : '']" @click="addFolder">
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 ? 'disabled' : '']" @click="addFolderToDoc()">
|
||||
<span class="iconbl bl-a-fileadd-line"></span>新增文件夹
|
||||
</div>
|
||||
<div class="menu-item-divider"></div>
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 || curDoc.ty != 2 ? 'disabled' : '']" @click="delDoc()">
|
||||
<div :class="['menu-item', Number(curDoc.i) <= 0 ? 'disabled' : '']" @click="delDoc()">
|
||||
<span class="iconbl bl-a-fileprohibit-line"></span>删除文件夹
|
||||
</div>
|
||||
</div>
|
||||
@ -122,116 +121,355 @@
|
||||
draggable>
|
||||
<PictureInfo ref="PictureInfoRef" @saved="savedCallback"></PictureInfo>
|
||||
</el-dialog>
|
||||
|
||||
<div style="position: absolute; top: 5px; left: 5px; z-index: 100; font-size: 12px; color: black; font-weight: 700; text-align: left; width: 700px">
|
||||
<div>当前选中:{{ docTreeCurrentId }}</div>
|
||||
<div>所有展开:{{ Array.from(docTreeCurrentExpandId) + '' }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, provide, nextTick, watch } from 'vue'
|
||||
import { useConfigStore } from '@renderer/stores/config'
|
||||
import { ref, provide, nextTick } from 'vue'
|
||||
import { ArrowDownBold, ArrowRightBold } from '@element-plus/icons-vue'
|
||||
import Workbench from './PictureTreeWorkbench.vue'
|
||||
import { docTreeApi, folderAddApi, folderDelApi } from '@renderer/api/blossom'
|
||||
import { checkLevel, provideKeyDocTree } from '@renderer/views/doc/doc'
|
||||
import { isEmpty } from 'lodash'
|
||||
import PictureTitle from './PictureTreeTitle.vue'
|
||||
import PictureInfo from '@renderer/views/picture/PictureInfo.vue'
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
import { ElMessageBox, MenuInstance } from 'element-plus'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
// element plus
|
||||
import { ElMessageBox, TreeNode } from 'element-plus'
|
||||
import { NodeDropType } from 'element-plus/es/components/tree/src/tree.type'
|
||||
import { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
|
||||
import { ArrowRightBold, Rank, Close } from '@element-plus/icons-vue'
|
||||
import Node from 'element-plus/es/components/tree/src/model/node'
|
||||
// ts
|
||||
import { docTreeApi, docUpdSortApi, folderAddApi, folderDelApi, folderUpdNameApi } from '@renderer/api/blossom'
|
||||
import { provideKeyDocTree } from '@renderer/views/doc/doc'
|
||||
import { isShowImg, isShowSvg, tags } from '@renderer/views/doc/doc-tree-detail'
|
||||
import { getColor, handleTreeDrop } from '@renderer/views/doc/doc-tree'
|
||||
import { useDraggable } from '@renderer/scripts/draggable'
|
||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||
// util
|
||||
import { isEmpty } from 'lodash'
|
||||
import { isBlank, isNotBlank } from '@renderer/assets/utils/obj'
|
||||
// components
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
import Workbench from './PictureTreeWorkbench.vue'
|
||||
import PictureInfo from '@renderer/views/picture/PictureInfo.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const user = useUserStore()
|
||||
const { viewStyle } = useConfigStore()
|
||||
|
||||
useLifecycle(
|
||||
() => getDocTree(),
|
||||
() => getDocTree()
|
||||
)
|
||||
watch(
|
||||
() => user.currentUserId,
|
||||
(_newVal, _oldVal) => getDocTree()
|
||||
)
|
||||
|
||||
//#region ----------------------------------------< 树状列表 >--------------------------------------
|
||||
let editorLoadingTimeout: NodeJS.Timeout
|
||||
const DocTreeRef = ref<MenuInstance>()
|
||||
const DocTreeRef = ref()
|
||||
const docTreeLoading = ref(true) // 文档菜单的加载动画
|
||||
const showSort = ref(false) // 是否显示文档排序
|
||||
const isShowSort = ref(false) // 是否显示文档排序
|
||||
const docTreeData = ref<DocTree[]>([]) // 文档菜单
|
||||
provide(provideKeyDocTree, docTreeData)
|
||||
|
||||
/**
|
||||
* 获取文档树状列表
|
||||
* 1. 初始化是全否调用
|
||||
* 2. 在 workbench 中点击按钮调用, 每个按钮是单选的
|
||||
* 刷新文档, 并在渲染结束后选中最后一次选中项
|
||||
*/
|
||||
const getDocTree = () => {
|
||||
editorLoadingTimeout = setTimeout(() => (docTreeLoading.value = true), 100)
|
||||
docTreeApi({ onlyPicture: true })
|
||||
.then((resp) => {
|
||||
const docTree: DocTree[] = resp.data
|
||||
// 两种类型的交界位置
|
||||
let lastPicIndex: number = docTree.length - 1
|
||||
// 循环一级文件夹,第一个文章文件夹即是最后一个图片文件夹的位置
|
||||
for (let i = 0; i < docTree.length; i++) {
|
||||
let doc = docTree[i]
|
||||
if (doc.ty === 1) {
|
||||
lastPicIndex = i
|
||||
break
|
||||
}
|
||||
const refreshDocTree = () => {
|
||||
getDocTree(() => {
|
||||
nextTick(() => {
|
||||
if (!isEmpty(docTreeData.value) && isNotBlank(docTreeCurrentId.value)) {
|
||||
DocTreeRef.value.setCurrentKey(docTreeCurrentId.value)
|
||||
}
|
||||
|
||||
// 插入分割符
|
||||
docTree.splice(Math.max(lastPicIndex, 1), 0, {
|
||||
i: (Number(docTree[0].i) - 100000).toString(),
|
||||
p: '0',
|
||||
n: '',
|
||||
o: 0,
|
||||
t: [],
|
||||
s: 0,
|
||||
icon: '',
|
||||
ty: 11,
|
||||
star: 0
|
||||
})
|
||||
|
||||
docTreeData.value = docTree
|
||||
concatSort(docTreeData.value)
|
||||
})
|
||||
.finally(() => {
|
||||
if (editorLoadingTimeout) {
|
||||
clearTimeout(editorLoadingTimeout)
|
||||
}
|
||||
docTreeLoading.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 在名称中显式排序
|
||||
* @param trees
|
||||
* 获取文档树状列表
|
||||
*
|
||||
* @param callback 获取文档后的自定义回调
|
||||
*/
|
||||
const concatSort = (trees: DocTree[]) => {
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
if (!isEmpty(trees[i].children)) {
|
||||
concatSort(trees[i].children as DocTree[])
|
||||
}
|
||||
trees[i].showSort = showSort.value
|
||||
}
|
||||
const getDocTree = (callback?: () => void) => {
|
||||
startLoading()
|
||||
docTreeApi({ onlyPicture: true })
|
||||
.then((resp) => {
|
||||
addTreeDivider(resp.data)
|
||||
endLoading()
|
||||
if (callback) callback()
|
||||
})
|
||||
.finally(() => endLoading())
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击菜单
|
||||
*
|
||||
* @param tree 点击的菜单节点数据
|
||||
* @param node 树状菜单 node
|
||||
* @param treeNode TreeNode
|
||||
* @param event 点击事件
|
||||
*/
|
||||
const clickCurDoc = (tree: DocTree, node: Node, treeNode: TreeNode, event: MouseEvent) => {
|
||||
closeTreeDocsMenuShow(event)
|
||||
setCurrentKey(tree, node, treeNode, event)
|
||||
emits('clickDoc', tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否显示排序
|
||||
*/
|
||||
const handleShowSort = () => {
|
||||
showSort.value = !showSort.value
|
||||
concatSort(docTreeData.value)
|
||||
const handleShowSort = () => (isShowSort.value = !isShowSort.value)
|
||||
|
||||
/** 开始加载 */
|
||||
const startLoading = () => {
|
||||
if (!editorLoadingTimeout) {
|
||||
editorLoadingTimeout = setTimeout(() => (docTreeLoading.value = true), 100)
|
||||
}
|
||||
}
|
||||
|
||||
/** 结束加载 */
|
||||
const endLoading = () => {
|
||||
if (editorLoadingTimeout) {
|
||||
clearTimeout(editorLoadingTimeout)
|
||||
}
|
||||
docTreeLoading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 为树状列表插入图片文件夹和文章文件夹的分割线
|
||||
*/
|
||||
const addTreeDivider = (data: any) => {
|
||||
const docTree: DocTree[] = data
|
||||
// 两种类型的交界位置
|
||||
let lastPicIndex: number = docTree.length - 1
|
||||
// 循环一级文件夹,第一个文章文件夹即是最后一个图片文件夹的位置
|
||||
for (let i = 0; i < docTree.length; i++) {
|
||||
let doc = docTree[i]
|
||||
if (doc.ty === 1) {
|
||||
lastPicIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 插入分割符
|
||||
docTree.splice(Math.max(lastPicIndex, 1), 0, {
|
||||
i: (Number(docTree[0].i) - 100000).toString(),
|
||||
p: '0',
|
||||
n: '',
|
||||
o: 0,
|
||||
t: [],
|
||||
s: 0,
|
||||
icon: '',
|
||||
ty: 11,
|
||||
star: 0
|
||||
})
|
||||
docTreeData.value = docTree
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ----------------------------------------< 树状列表管理 >--------------------------------------
|
||||
// 文档的最后选中项, 用于外部跳转后选中菜单
|
||||
const docTreeCurrentId = ref('')
|
||||
// 所有展开的节点
|
||||
const docTreeCurrentExpandId = ref<Set<string>>(new Set())
|
||||
// 搜索内容
|
||||
const treeFilterText = ref('')
|
||||
const isShowTreeFilter = ref(false)
|
||||
// 搜索框
|
||||
const DocTreeContainer = ref()
|
||||
const DocTreeSearch = ref()
|
||||
const DocTreeSearchMove = ref()
|
||||
const DocTreeSearchInput = ref()
|
||||
// 禁止拖拽的节点, 正在重命名的节点不允许进行拖拽
|
||||
let notAllowDragKey: string = ''
|
||||
|
||||
useDraggable(DocTreeSearch, DocTreeSearchMove, DocTreeContainer)
|
||||
|
||||
watch(treeFilterText, (val) => DocTreeRef.value!.filter(val))
|
||||
|
||||
/**
|
||||
* 设置选中项, 并展开所有上级
|
||||
*
|
||||
* @param tree 当前选中的文档
|
||||
*/
|
||||
const setCurrentKey = (tree: { i: string; p: string; ty: DocType }, node?: Node, _treeNode?: any, _event?: MouseEvent) => {
|
||||
if (tree.ty === 1 || tree.ty === 2) {
|
||||
docTreeCurrentId.value = tree.i
|
||||
if (node && node.expanded) {
|
||||
docTreeCurrentExpandId.value.add(tree.i)
|
||||
}
|
||||
} else if (tree.ty === 3) {
|
||||
docTreeCurrentId.value = tree.i
|
||||
docTreeCurrentExpandId.value.add(tree.p)
|
||||
}
|
||||
DocTreeRef.value.setCurrentKey(tree.i)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示过滤框
|
||||
*/
|
||||
const showTreeFilter = () => {
|
||||
isShowTreeFilter.value = !isShowTreeFilter.value
|
||||
if (isShowTreeFilter.value) {
|
||||
DocTreeSearchInput.value.focus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤节点名称和标签
|
||||
* @param value 搜索内容
|
||||
* @param data 列表
|
||||
* @return 返回节点是否保留
|
||||
*/
|
||||
const filterNode = (value: string, data: DocTree): boolean => {
|
||||
if (!value) return true
|
||||
return data.n.includes(value) || data.t.toString().includes(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否允许被拖拽
|
||||
* 1. 文章文件夹不允许拖拽
|
||||
* 2. 正在重命名的节点不允许被拖拽
|
||||
*
|
||||
* @param node 拖动的节点
|
||||
* @return boolean 节点是否允许被拖动
|
||||
*/
|
||||
const handleAllowDrag = (node: Node): boolean => {
|
||||
return node.data.ty === 2 && notAllowDragKey !== node.data.i
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否允许被节点放置
|
||||
*
|
||||
* @param _draggingNode 拖动的节点
|
||||
* @param dropNode 被防止的节点
|
||||
* @param type 放置的类型
|
||||
* @return boolean 是否允许被放置
|
||||
*/
|
||||
const handleAllowDrop = (_draggingNode: Node, dropNode: Node, _type: NodeDropType): boolean => {
|
||||
if (dropNode.data.ty !== 2) {
|
||||
return false
|
||||
}
|
||||
if (dropNode.data.i < 0) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 折叠全部, 清空当前选中状态, 并刷新列表
|
||||
*/
|
||||
const collapseAll = () => {
|
||||
docTreeCurrentExpandId.value.clear()
|
||||
getDocTree()
|
||||
}
|
||||
|
||||
/**
|
||||
* 折叠所有无子菜单的文件夹
|
||||
*/
|
||||
const collapseNoChild = () => {
|
||||
nextTick(() => {
|
||||
for (let i = 0; i < docTreeData.value.length; i++) {
|
||||
const doc = docTreeData.value[i]
|
||||
collapseChild(doc)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归折叠所有子文件夹
|
||||
*
|
||||
* @param doc
|
||||
*/
|
||||
const collapseChild = (doc: DocTree) => {
|
||||
if (doc.ty === 1 || doc.ty === 2) {
|
||||
if (isEmpty(doc.children)) {
|
||||
docTreeCurrentExpandId.value.delete(doc.i)
|
||||
} else {
|
||||
for (let i = 0; i < doc.children!.length; i++) {
|
||||
const cdoc = doc.children![i]
|
||||
collapseChild(cdoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点展开
|
||||
*/
|
||||
const handleNodeExpand = (tree: DocTree, _node: Node) => {
|
||||
docTreeCurrentExpandId.value.add(tree.i)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点缩起, 同时清除所有子节点的展开状态
|
||||
*/
|
||||
const handleNodeCollapse = (tree: DocTree, node: Node) => {
|
||||
docTreeCurrentExpandId.value.delete(tree.i)
|
||||
collapseChilds(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归缩起所有子节点
|
||||
*/
|
||||
const collapseChilds = (node: Node) => {
|
||||
for (let i = 0; i < node.childNodes.length; i++) {
|
||||
const child = node.childNodes[i]
|
||||
if (child.isLeaf) {
|
||||
} else {
|
||||
child.expanded = false
|
||||
docTreeCurrentExpandId.value.delete(child.data.i)
|
||||
collapseChilds(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果父节点没有子节点时, 关闭父节点的展开状态
|
||||
* @param pid 父ID
|
||||
*/
|
||||
const closeParentIfNoChild = (pid: string) => {
|
||||
let node: Node = DocTreeRef.value.getNode(pid)
|
||||
if (node && isEmpty(node.childNodes)) {
|
||||
docTreeCurrentExpandId.value.delete(pid)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拖拽后处理各个节点排序
|
||||
*/
|
||||
const handleDrop = (drag: Node, enter: Node, dropType: NodeDropType, _event: DragEvents) => {
|
||||
handleTreeDrop(drag, enter, dropType, _event, DocTreeRef, docTreeData, (needUpd) => {
|
||||
docUpdSortApi({ docs: needUpd, folderType: 2, onlyPicture: true })
|
||||
.then((resp) => {
|
||||
addTreeDivider(resp.data)
|
||||
collapseNoChild()
|
||||
})
|
||||
.catch(() => getDocTree())
|
||||
})
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region ----------------------------------------< 右键菜单 >--------------------------------------
|
||||
const curDoc = ref<DocTree>({ i: '0', p: '0', n: '选择菜单', o: 0, t: [], s: 0, icon: '', ty: 1, star: 0 })
|
||||
const rMenu = ref<RightMenu>({ show: false, clientX: 0, clientY: 0 })
|
||||
const rMenuHeight = 151 // 固定的菜单高度, 每次增加右键菜单项时需要修改该值
|
||||
|
||||
/**
|
||||
* 显示有检查菜单
|
||||
* 显示右键菜单
|
||||
* 文章文件夹不显示右键菜单, 文章文件夹的管理一律在文章编辑功能中
|
||||
*
|
||||
* @param doc 文档
|
||||
* @param event 事件
|
||||
*/
|
||||
const handleClickRight = (doc: DocTree, event: MouseEvent) => {
|
||||
const handleClickRightMenu = (event: MouseEvent, doc: DocTree) => {
|
||||
event.preventDefault()
|
||||
if (!doc) {
|
||||
return
|
||||
}
|
||||
docTreeCurrentExpandId.value.add(doc.p)
|
||||
if (!doc) return
|
||||
if (doc.ty !== 2) return
|
||||
|
||||
curDoc.value = doc
|
||||
rMenu.value = { show: false, clientX: 0, clientY: 0 }
|
||||
let y = event.clientY
|
||||
@ -244,20 +482,25 @@ const handleClickRight = (doc: DocTree, event: MouseEvent) => {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const closeTreeDocsMenuShow = () => {
|
||||
removeListenerTreeDocsRightMenu()
|
||||
/**
|
||||
* 关闭右键菜单, 有子菜单时, 点击菜单不会关闭
|
||||
*
|
||||
* @param event 点击事件
|
||||
*/
|
||||
const closeTreeDocsMenuShow = (event?: MouseEvent) => {
|
||||
if (event && event?.target) {
|
||||
let isPrevent = (event.target as HTMLElement).getAttribute('data-bl-prevet')
|
||||
if (isPrevent === 'true') {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
rMenu.value.show = false
|
||||
}
|
||||
|
||||
const removeListenerTreeDocsRightMenu = () => {
|
||||
document.body.removeEventListener('click', closeTreeDocsMenuShow)
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 右键菜单功能
|
||||
/**
|
||||
* 删除文档
|
||||
* 删除文档, 删除后将文档从树状节点中删除
|
||||
*/
|
||||
const delDoc = () => {
|
||||
ElMessageBox.confirm(`是否确定删除文件夹: <span style="color:#C02B2B;text-decoration: underline;">${curDoc.value.n}</span>?删除后将不可恢复!`, {
|
||||
@ -269,28 +512,53 @@ const delDoc = () => {
|
||||
}).then(() => {
|
||||
folderDelApi({ id: curDoc.value.i }).then((_resp) => {
|
||||
Notify.success(`删除文件夹成功`)
|
||||
getDocTree()
|
||||
DocTreeRef.value.remove(curDoc.value.i)
|
||||
closeParentIfNoChild(curDoc.value.p)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 重命名文章 */
|
||||
/**
|
||||
* 重命名文章, 重命名时该节点无法拖拽
|
||||
*/
|
||||
const rename = () => {
|
||||
curDoc.value.updn = true
|
||||
notAllowDragKey = curDoc.value.i
|
||||
nextTick(() => {
|
||||
let ele = document.getElementById('article-doc-name-' + curDoc.value.i)
|
||||
if (ele) ele.focus()
|
||||
})
|
||||
}
|
||||
|
||||
/** 在末尾新增文件夹 */
|
||||
const addFolder = () => {
|
||||
if (!checkLevel(curDoc.value.i, docTreeData.value)) {
|
||||
return
|
||||
}
|
||||
/**
|
||||
* 重命名文章失去焦点
|
||||
*/
|
||||
const blurArticleNameInput = (doc: DocTree) => {
|
||||
folderUpdNameApi({ id: doc.i, name: doc.n }).then((_resp) => {
|
||||
doc.updn = false
|
||||
notAllowDragKey = ''
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 在文件夹下新增文件夹
|
||||
*/
|
||||
const addFolderToDoc = () => addFolder(curDoc.value.i)
|
||||
|
||||
/**
|
||||
* 在根目录添加文件夹
|
||||
*/
|
||||
const addFolderToRoot = () => addFolder('0')
|
||||
|
||||
/**
|
||||
* 在指定 pid 的末尾新增文件夹
|
||||
*
|
||||
* @param pid 父ID
|
||||
*/
|
||||
const addFolder = (pid: string) => {
|
||||
// 将文件夹新增至尾部
|
||||
folderAddApi({ pid: curDoc.value.i, name: '新文件夹', storePath: '/', type: 2, icon: 'wl-folder', sort: 0, addToLast: true }).then((resp) => {
|
||||
let doc: DocTree = {
|
||||
folderAddApi({ pid: pid, name: '新文件夹', storePath: '/', type: 2, icon: 'wl-folder', sort: 0, addToLast: true }).then((resp) => {
|
||||
let newFolder: DocTree = {
|
||||
i: resp.data.id,
|
||||
p: resp.data.pid,
|
||||
n: resp.data.name,
|
||||
@ -303,21 +571,34 @@ const addFolder = () => {
|
||||
star: 0,
|
||||
showSort: curDoc.value.showSort
|
||||
}
|
||||
if (isEmpty(curDoc.value.children)) {
|
||||
curDoc.value.children = [doc]
|
||||
} else {
|
||||
curDoc.value.children!.push(doc)
|
||||
}
|
||||
nextTick(() => {
|
||||
DocTreeRef.value!.open(curDoc.value.i.toString())
|
||||
nextTick(() => {
|
||||
let ele = document.getElementById('article-doc-name-' + doc.i)
|
||||
if (ele) ele.focus()
|
||||
})
|
||||
})
|
||||
addDocToTail(newFolder)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文档添加至末尾并选中
|
||||
*/
|
||||
const addDocToTail = (doc: DocTree) => {
|
||||
if (doc.p !== '0') {
|
||||
// 插入到根目录
|
||||
DocTreeRef.value.append(doc, DocTreeRef.value.getNode(doc.p))
|
||||
docTreeCurrentExpandId.value.add(doc.p)
|
||||
} else {
|
||||
const picRootFolder: DocTree[] = docTreeData.value.filter((n) => n.ty === 2)
|
||||
const lastFolder = picRootFolder[picRootFolder.length - 1]
|
||||
console.log(lastFolder)
|
||||
DocTreeRef.value.insertAfter(doc, lastFolder.i)
|
||||
}
|
||||
nextTick(() => {
|
||||
let ele = document.getElementById('article-doc-name-' + doc.i) as HTMLInputElement
|
||||
setTimeout(() => {
|
||||
if (ele) ele.select()
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region ----------------------------------------< 新增修改详情弹框 >--------------------------------------
|
||||
const PictureInfoRef = ref()
|
||||
const isShowDocInfoDialog = ref<boolean>(false)
|
||||
@ -332,39 +613,41 @@ const handleShowDocInfoDialog = (dialogType: DocDialogType, pid?: number) => {
|
||||
Notify.info('当前文档为系统默认文档, 无法操作', '操作无效')
|
||||
return
|
||||
}
|
||||
|
||||
if (dialogType === 'upd' && (curDoc.value == undefined || curDoc.value?.i == undefined)) {
|
||||
Notify.info('请先选则要修改的文件夹或文档')
|
||||
return
|
||||
}
|
||||
isShowDocInfoDialog.value = true
|
||||
if (dialogType === 'add') {
|
||||
nextTick(() => {
|
||||
PictureInfoRef.value.reload(dialogType, undefined, pid)
|
||||
})
|
||||
}
|
||||
if (dialogType === 'upd') {
|
||||
nextTick(() => {
|
||||
PictureInfoRef.value.reload(dialogType, curDoc.value.i)
|
||||
})
|
||||
}
|
||||
if (dialogType === 'add') nextTick(() => PictureInfoRef.value.reload(dialogType, undefined, pid))
|
||||
if (dialogType === 'upd') nextTick(() => PictureInfoRef.value.reload(dialogType, curDoc.value.i))
|
||||
}
|
||||
|
||||
const savedCallback = () => {
|
||||
/**
|
||||
* 保存后回调
|
||||
*
|
||||
* @param dialogType 保存类型
|
||||
* @param doc 文档
|
||||
*/
|
||||
const savedCallback = (_dialogType: DocDialogType, doc: DocInfo) => {
|
||||
isShowDocInfoDialog.value = false
|
||||
getDocTree()
|
||||
const oldDoc: DocTree = DocTreeRef.value.getNode(doc.id).data
|
||||
oldDoc.n = doc.name
|
||||
oldDoc.t = doc.tags
|
||||
oldDoc.icon = doc.icon!
|
||||
if (oldDoc.s !== doc.sort || oldDoc.p !== doc.pid) {
|
||||
getDocTree(() => {
|
||||
collapseNoChild()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion 右键菜单
|
||||
|
||||
const clickCurDoc = (tree: DocTree) => {
|
||||
emits('clickDoc', tree)
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const emits = defineEmits(['clickDoc'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '../doc/tree-docs.scss';
|
||||
@import '../doc/tree-docs-right-menu.scss';
|
||||
@import '../doc/doc-tree.scss';
|
||||
@import '../doc/doc-tree-detail.scss';
|
||||
@import '../doc/doc-tree-right-menu.scss';
|
||||
</style>
|
||||
|
||||
@ -1,149 +0,0 @@
|
||||
<template>
|
||||
<div class="doc-title">
|
||||
<bl-tag class="sort" v-show="props.trees.showSort" :bgColor="levelColor" style="padding: 0 2px">
|
||||
{{ props.trees.s }}
|
||||
</bl-tag>
|
||||
<span class="doc-name">
|
||||
<img
|
||||
class="menu-icon-img"
|
||||
v-if="
|
||||
viewStyle.isShowArticleIcon &&
|
||||
isNotBlank(props.trees.icon) &&
|
||||
(props.trees.icon.startsWith('http') || props.trees.icon.startsWith('https')) &&
|
||||
!props.trees.updn
|
||||
"
|
||||
:src="props.trees.icon" />
|
||||
<svg v-else-if="viewStyle.isShowArticleIcon && isNotBlank(props.trees.icon) && !props.trees.updn" class="icon menu-icon" aria-hidden="true">
|
||||
<use :xlink:href="'#' + props.trees.icon"></use>
|
||||
</svg>
|
||||
|
||||
<el-input
|
||||
v-if="$props.trees?.updn"
|
||||
v-model="$props.trees.n"
|
||||
:id="'article-doc-name-' + props.trees.i"
|
||||
@blur="blurArticleNameInput"
|
||||
@keyup.enter="blurArticleNameInput"
|
||||
style="width: 95%"></el-input>
|
||||
<div v-else class="name-wrapper" :style="nameWrapperStyle">
|
||||
{{ props.trees.n }}
|
||||
</div>
|
||||
<bl-tag v-for="tag in tags" :bg-color="tag.bgColor" style="margin-top: 5px" :icon="tag.icon">
|
||||
{{ tag.content }}
|
||||
</bl-tag>
|
||||
</span>
|
||||
<div v-if="level >= 2" class="folder-level-line" style="left: -20px"></div>
|
||||
<div v-if="level >= 3" class="folder-level-line" style="left: -30px"></div>
|
||||
<div v-if="level >= 4" class="folder-level-line" style="left: -40px"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
import { computedDocTitleColor } from '@renderer/views/doc/doc'
|
||||
import { folderUpdNameApi } from '@renderer/api/blossom'
|
||||
import { useConfigStore } from '@renderer/stores/config'
|
||||
|
||||
const { viewStyle } = useConfigStore()
|
||||
|
||||
//#region ----------------------------------------< 标题信息 >--------------------------------------
|
||||
|
||||
const props = defineProps({
|
||||
trees: { type: Object as PropType<DocTree>, default: {} },
|
||||
level: { type: Number, required: true }
|
||||
})
|
||||
|
||||
const levelColor = computed(() => {
|
||||
return computedDocTitleColor(props.level)
|
||||
})
|
||||
|
||||
const nameWrapperStyle = computed(() => {
|
||||
return {
|
||||
maxWidth: isNotBlank(props.trees.icon) ? 'calc(100% - 25px)' : '100%'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算标签, 并返回便签对象集合
|
||||
*/
|
||||
const tags = computed(() => {
|
||||
let icons: any = []
|
||||
props.trees.t?.forEach((tag) => {
|
||||
if (tag === 'subject') {
|
||||
icons.unshift({ content: '专题', bgColor: 'var(--bl-tag-color-subject)', icon: 'bl-a-lowerrightpage-line' })
|
||||
} else if (tag.toLocaleLowerCase() === 'toc') {
|
||||
if (!viewStyle.isShowArticleTocTag) {
|
||||
return
|
||||
}
|
||||
icons.push({ content: 'TOC', bgColor: 'var(--bl-tag-color-toc)' })
|
||||
} else if (viewStyle.isShowArticleCustomTag) {
|
||||
icons.push({ content: tag })
|
||||
}
|
||||
})
|
||||
if (props.trees.o === 1 && props.trees.ty != 3) {
|
||||
if (viewStyle.isShowFolderOpenTag) {
|
||||
icons.unshift({ bgColor: 'var(--bl-tag-color-open)', icon: 'bl-cloud-line' })
|
||||
}
|
||||
}
|
||||
return icons
|
||||
})
|
||||
|
||||
/**
|
||||
* 重命名文章
|
||||
*/
|
||||
const blurArticleNameInput = () => {
|
||||
folderUpdNameApi({ id: props.trees.i, name: props.trees.n }).then((_resp) => {
|
||||
props.trees.updn = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$icon-size: 17px;
|
||||
|
||||
.doc-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
width: 100%;
|
||||
padding-bottom: 1px;
|
||||
position: relative;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include themeBrightness(100%, 90%);
|
||||
@include ellipsis();
|
||||
font-size: inherit;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
@include ellipsis();
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.folder-level-line {
|
||||
@include box(1px, 100%);
|
||||
background-color: var(--el-border-color);
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,82 +1,17 @@
|
||||
<template>
|
||||
<div class="doc-workbench-root">
|
||||
<bl-col class="workbench-name" just="flex-start" align="flex-end" height="46px" v-show="curFolder !== undefined">
|
||||
<span>《{{ curFolder?.name }}》</span>
|
||||
<span style="font-size: 9px; padding-right: 5px">{{ curFolder?.id }}</span>
|
||||
</bl-col>
|
||||
|
||||
<!-- -->
|
||||
<bl-row just="flex-end" align="flex-end">
|
||||
<el-tooltip content="显示排序" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-a-leftdirection-line" @click="emits('show-sort')"></div>
|
||||
<template #content>
|
||||
显示排序<br />
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="SortLevelColor.ONE">一级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.TWO">二级</bl-tag>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="SortLevelColor.THREE">三级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.FOUR">四级</bl-tag>
|
||||
</bl-row>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新列表" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-a-cloudrefresh-line" @click="refreshDocTree()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新增图片文件夹" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-a-fileadd-line" @click="handleShowAddDocInfoDialog()"></div>
|
||||
</el-tooltip>
|
||||
</bl-row>
|
||||
<bl-row just="flex-end" align="flex-end"> </bl-row>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="isShowDocInfoDialog"
|
||||
width="535"
|
||||
top="100px"
|
||||
style="margin-left: 320px"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
draggable>
|
||||
<PictureInfo ref="PictureInfoRef" @saved="savedCallback"></PictureInfo>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, inject } from 'vue'
|
||||
import { provideKeyDocInfo, SortLevelColor } from '@renderer/views/doc/doc'
|
||||
import PictureInfo from '@renderer/views/picture/PictureInfo.vue'
|
||||
|
||||
// 当前菜单中选择的文档
|
||||
const curFolder = inject(provideKeyDocInfo)
|
||||
import { ref, nextTick } from 'vue'
|
||||
|
||||
// ---------- 新增修改按钮 ----------
|
||||
const PictureInfoRef = ref()
|
||||
// 显示编辑 dialog
|
||||
const isShowDocInfoDialog = ref<boolean>(false)
|
||||
const handleShowAddDocInfoDialog = () => {
|
||||
isShowDocInfoDialog.value = true
|
||||
nextTick(() => {
|
||||
PictureInfoRef.value.reload('add')
|
||||
})
|
||||
}
|
||||
|
||||
const refreshDocTree = () => {
|
||||
emits('refreshDocTree')
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存后的回调
|
||||
* 1. [暂无] 刷新菜单列表
|
||||
* 2. 关闭 dialog 页面
|
||||
*/
|
||||
const savedCallback = () => {
|
||||
isShowDocInfoDialog.value = false
|
||||
refreshDocTree()
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const emits = defineEmits(['refreshDocTree', 'show-sort'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -126,7 +126,7 @@ export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file?) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件结果处理
|
||||
* 上传文件结果处理, 失败是根据
|
||||
* @param resp 接口响应
|
||||
* @returns 是否成功
|
||||
*/
|
||||
|
||||
BIN
blossom-web/src/assets/fonts/JetBrainsMono-Regular.woff2
Normal file
BIN
blossom-web/src/assets/fonts/JetBrainsMono-Regular.woff2
Normal file
Binary file not shown.
6
blossom-web/src/assets/fonts/config.scss
Normal file
6
blossom-web/src/assets/fonts/config.scss
Normal file
@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Jetbrains Mono';
|
||||
src: url('@/assets/fonts/JetBrainsMono-Regular.woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
@import '../fonts/config.scss';
|
||||
|
||||
@import './element/dialog';
|
||||
@import './element/drawer';
|
||||
@import './element/popper';
|
||||
|
||||
@ -39,11 +39,10 @@ body {
|
||||
|
||||
/* 定义滑块 内阴影+圆角 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: #c7c7c7;
|
||||
}
|
||||
|
||||
/* 解决滚动条右下角出现白色方块 */
|
||||
/* 滚动条右下角出现白色方块 */
|
||||
::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
:root {
|
||||
/* 数字越大, 颜色越淡 */
|
||||
--el-color-primary: #c1992f;
|
||||
/* --el-color-primary: #c1992f;
|
||||
--el-color-primary-dark-2: rgba(250, 195, 56, 0.8);
|
||||
--el-color-primary-light-1: rgba(193, 153, 47, 0.9);
|
||||
--el-color-primary-light-2: rgba(193, 153, 47, 0.8);
|
||||
@ -9,7 +9,20 @@
|
||||
--el-color-primary-light-6: rgba(193, 153, 47, 0.4);
|
||||
--el-color-primary-light-7: rgba(193, 153, 47, 0.3);
|
||||
--el-color-primary-light-8: rgba(193, 153, 47, 0.2);
|
||||
--el-color-primary-light-9: rgba(232, 219, 190, 0.505);
|
||||
--el-color-primary-light-9: rgba(232, 219, 190, 0.505); */
|
||||
|
||||
--el-color-primary: rgb(104, 104, 104);
|
||||
--el-color-primary-dark: rgb(104, 104, 104);
|
||||
--el-color-primary-dark-2: rgba(104, 104, 104, 0.8);
|
||||
--el-color-primary-light-1: rgba(104, 104, 104, 0.9);
|
||||
--el-color-primary-light-2: rgba(104, 104, 104, 0.8);
|
||||
--el-color-primary-light-3: rgba(104, 104, 104, 0.7);
|
||||
--el-color-primary-light-4: rgba(104, 104, 104, 0.6);
|
||||
--el-color-primary-light-5: rgba(104, 104, 104, 0.5);
|
||||
--el-color-primary-light-6: rgba(104, 104, 104, 0.4);
|
||||
--el-color-primary-light-7: rgba(104, 104, 104, 0.3);
|
||||
--el-color-primary-light-8: rgba(104, 104, 104, 0.2);
|
||||
--el-color-primary-light-9: rgba(104, 104, 104, 0.1);
|
||||
|
||||
/* 文本颜色 */
|
||||
--el-text-color-primary: #606266;
|
||||
|
||||
@ -53,8 +53,8 @@ const props = defineProps({
|
||||
@include flex(row, center, center);
|
||||
box-shadow: 2px 2px 3px 0 #999999;
|
||||
border-radius: 4px;
|
||||
padding: 0px 4px;
|
||||
margin: 3px;
|
||||
padding: 0 4px;
|
||||
margin: 4px;
|
||||
height: 15px;
|
||||
min-height: 15px;
|
||||
max-height: 15px;
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus, Minus } from '@element-plus/icons-vue'
|
||||
import { increase, decrease, getFontSizeValue } from './article-setting'
|
||||
import { increase, decrease, getFontSizeValue } from './scripts/article-setting'
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -13,83 +13,44 @@
|
||||
</bl-row>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="menu" :style="menuStyle">
|
||||
<el-menu
|
||||
v-if="docTreeData != undefined && docTreeData.length > 0"
|
||||
class="doc-trees"
|
||||
:default-active="docTreeDefaultActive"
|
||||
:default-openeds="defaultOpeneds"
|
||||
:unique-opened="true">
|
||||
<div v-for="L1 in docTreeData" :key="L1.i" class="menu-level-one">
|
||||
<el-menu-item v-if="isEmpty(L1.children)" :index="L1.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L1)">
|
||||
<DocTitle :trees="L1" :level="1" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L1.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper">
|
||||
<DocTitle :trees="L1" :level="1" style="font-size: 15px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================================================ L2 ================================================ -->
|
||||
<div v-for="L2 in L1.children" :key="L2.i">
|
||||
<el-menu-item v-if="isEmpty(L2.children)" :index="L2.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L2)">
|
||||
<DocTitle :trees="L2" :level="2" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L2.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper">
|
||||
<DocTitle :trees="L2" :level="2" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================================================ L3 ================================================ -->
|
||||
<div v-for="L3 in L2.children" :key="L3.i">
|
||||
<el-menu-item v-if="isEmpty(L3.children)" :index="L3.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L3)">
|
||||
<DocTitle :trees="L3" :level="3" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L3.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper">
|
||||
<DocTitle :trees="L3" :level="3" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ================================================ L4 ================================================ -->
|
||||
<div v-for="L4 in L3.children" :key="L4.i">
|
||||
<el-menu-item v-if="isEmpty(L4.children)" :index="L4.i">
|
||||
<template #title>
|
||||
<div class="menu-item-wrapper" @click="clickCurDoc(L4)">
|
||||
<DocTitle :trees="L4" :level="4" />
|
||||
</div>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</div>
|
||||
</el-sub-menu>
|
||||
<div class="doc-tree-container" :style="menuStyle">
|
||||
<el-tree
|
||||
ref="DocTreeRef"
|
||||
class="doc-tree"
|
||||
:data="docTreeData"
|
||||
:highlight-current="true"
|
||||
:indent="14"
|
||||
:icon="ArrowRightBold"
|
||||
node-key="i"
|
||||
@nodeClick="clickCurDoc">
|
||||
<template #default="{ _node, data }">
|
||||
<div
|
||||
class="menu-item-wrapper"
|
||||
:style="{
|
||||
marginTop: data.p === '0' ? '5px' : '1px',
|
||||
marginBottom: data.p === '0' ? '5px' : '1px'
|
||||
}">
|
||||
<div :class="[data.t.includes('subject') && userStore.userParams.WEB_BLOG_SUBJECT_TITLE === '1' ? 'subject-title' : 'doc-title']">
|
||||
<div class="doc-name">
|
||||
<img class="menu-icon-img" v-if="isShowImg(data)" :src="data.icon" />
|
||||
<svg v-else-if="isShowSvg(data)" class="icon menu-icon" aria-hidden="true">
|
||||
<use :xlink:href="'#' + data.icon"></use>
|
||||
</svg>
|
||||
<div class="name-wrapper" :style="{ maxWidth: isNotBlank(data.icon) ? 'calc(100% - 25px)' : '100%' }">
|
||||
{{ data.n }}
|
||||
</div>
|
||||
</el-sub-menu>
|
||||
<bl-tag v-for="tag in tags(data)" style="margin-top: 3px" :bg-color="tag.bgColor" :icon="tag.icon">
|
||||
{{ tag.content }}
|
||||
</bl-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
|
||||
<div class="article" ref="PreviewRef">
|
||||
<div class="doc-content-container" ref="PreviewRef" :style="{ fontSize: getFontSize() }">
|
||||
<div class="article-name">{{ article.name }}</div>
|
||||
<div class="bl-preview" :style="{ fontSize: getFontSize() }" v-html="article.html"></div>
|
||||
</div>
|
||||
|
||||
@ -140,15 +101,21 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, onUnmounted, nextTick } from 'vue'
|
||||
import { ArrowDownBold, ArrowRightBold, Setting } from '@element-plus/icons-vue'
|
||||
import { articleInfoOpenApi, articleInfoApi, docTreeOpenApi, docTreeApi } from '@/api/blossom'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { isNull, isEmpty, isNotNull } from '@/assets/utils/obj'
|
||||
import { useLifecycle } from '@/scripts/lifecycle'
|
||||
import DocTitle from './DocTitle.vue'
|
||||
import ArticleSetting from './ArticleSetting.vue'
|
||||
// element plus
|
||||
import { ArrowRightBold, Setting } from '@element-plus/icons-vue'
|
||||
// ts
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { getFontSize } from './article-setting'
|
||||
import { getFontSize } from './scripts/article-setting'
|
||||
import { parseToc, toScroll, type Toc } from './scripts/doc-toc'
|
||||
import { isShowImg, isShowSvg, tags } from './scripts/doc-tree-detail'
|
||||
import { onHtmlEventDispatch } from './scripts/doc-content-event-dispatch'
|
||||
import { articleInfoOpenApi, articleInfoApi, docTreeOpenApi, docTreeApi } from '@/api/blossom'
|
||||
// utils
|
||||
import { isNull, isNotNull, isNotBlank } from '@/assets/utils/obj'
|
||||
// components
|
||||
import ArticleSetting from './ArticleSetting.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
useLifecycle(
|
||||
@ -169,14 +136,20 @@ onUnmounted(() => {
|
||||
window.removeEventListener('resize', onresize)
|
||||
})
|
||||
|
||||
const DocTreeRef = ref()
|
||||
|
||||
/**
|
||||
* 路由中获取ID参数
|
||||
*/
|
||||
const getRouteQueryParams = () => {
|
||||
let articleId = route.query.articleId
|
||||
getDocTree()
|
||||
getDocTree(() => {
|
||||
nextTick(() => {
|
||||
DocTreeRef.value.setCurrentKey(docTreeCurrentId.value)
|
||||
})
|
||||
})
|
||||
if (isNotNull(articleId)) {
|
||||
docTreeDefaultActive.value = articleId as string
|
||||
docTreeCurrentId.value = articleId as string
|
||||
let treeParam: any = { ty: 3, i: articleId }
|
||||
clickCurDoc(treeParam)
|
||||
}
|
||||
@ -186,7 +159,7 @@ const route = useRoute()
|
||||
// 文档菜单的加载动画
|
||||
const docTreeLoading = ref(true)
|
||||
// 文档菜单
|
||||
const docTreeDefaultActive = ref('')
|
||||
const docTreeCurrentId = ref('')
|
||||
const docTreeData = ref<DocTree[]>([])
|
||||
// 当前选中的文章, 用于在编辑器中展示
|
||||
const article = ref<DocInfo>({
|
||||
@ -210,7 +183,7 @@ const PreviewRef = ref()
|
||||
* 1. 初始化是全否调用
|
||||
* 2. 在 workbench 中点击按钮调用, 每个按钮是单选的
|
||||
*/
|
||||
const getDocTree = () => {
|
||||
const getDocTree = (callback?: () => void) => {
|
||||
docTreeLoading.value = true
|
||||
docTreeData.value = []
|
||||
defaultOpeneds.value = []
|
||||
@ -220,6 +193,7 @@ const getDocTree = () => {
|
||||
docTreeData.value.forEach((l1: DocTree) => {
|
||||
defaultOpeneds.value.push(l1.i.toString())
|
||||
})
|
||||
if (callback) callback()
|
||||
}
|
||||
|
||||
if (userStore.isLogin) {
|
||||
@ -252,8 +226,8 @@ const clickCurDoc = async (tree: DocTree) => {
|
||||
/**
|
||||
* 如果点击的是文章, 则查询文章信息和正文, 并在编辑器中显示.
|
||||
*/
|
||||
const getCurEditArticle = async (id: number) => {
|
||||
if (id == -999) {
|
||||
const getCurEditArticle = async (id: string) => {
|
||||
if (id === '-999') {
|
||||
article.value.html = `<div style="color:#C6C6C6;font-weight: 300;width:100%;height:300px;padding:150px 20px;font-size:25px;text-align:center">
|
||||
该博客所配置的 USER_ID 为<br/><span style="color:#e3a300; border-bottom: 5px solid #e3a300;border-radius:5px">${window.blconfig.DOMAIN.USER_ID}</span>
|
||||
</div>`
|
||||
@ -277,80 +251,10 @@ const getCurEditArticle = async (id: number) => {
|
||||
* 解析目录
|
||||
*/
|
||||
const parseTocAsync = async (ele: HTMLElement) => {
|
||||
let heads = ele.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||
let tocs: Toc[] = []
|
||||
for (let i = 0; i < heads.length; i++) {
|
||||
let head: Element = heads[i]
|
||||
let level = 1
|
||||
let content = (head as HTMLElement).innerText
|
||||
let id = head.id
|
||||
switch (head.localName) {
|
||||
case 'h1':
|
||||
level = 1
|
||||
break
|
||||
case 'h2':
|
||||
level = 2
|
||||
break
|
||||
case 'h3':
|
||||
level = 3
|
||||
break
|
||||
case 'h4':
|
||||
level = 4
|
||||
break
|
||||
case 'h5':
|
||||
level = 5
|
||||
break
|
||||
case 'h6':
|
||||
level = 6
|
||||
break
|
||||
}
|
||||
let toc: Toc = { content: content, clazz: 'toc-' + level, id: id }
|
||||
tocs.push(toc)
|
||||
}
|
||||
tocList.value = tocs
|
||||
tocList.value = parseToc(ele)
|
||||
}
|
||||
|
||||
const toScroll = (id: string) => {
|
||||
let elm = document.getElementById(id)
|
||||
elm?.scrollIntoView(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 html 的内联事件
|
||||
*/
|
||||
type ArticleHtmlEvent = 'copyPreCode' | 'showArticleReferenceView'
|
||||
const onHtmlEventDispatch = (_t: any, _ty: any, _event: any, type: ArticleHtmlEvent, data: any) => {
|
||||
if (type === 'copyPreCode') {
|
||||
let code = document.getElementById(data)
|
||||
if (code) {
|
||||
// navigator.clipboard.writeText(code.innerText)
|
||||
// navigator clipboard 需要https等安全上下文
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard 向剪贴板写文本
|
||||
return navigator.clipboard.writeText(code.innerText)
|
||||
} else {
|
||||
// 创建text area
|
||||
let textArea = document.createElement('textarea')
|
||||
textArea.value = code.innerText
|
||||
// 使text area不在viewport,同时设置不可见
|
||||
textArea.style.position = 'absolute'
|
||||
textArea.style.opacity = '0'
|
||||
textArea.style.left = '-999999px'
|
||||
textArea.style.top = '-999999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
return new Promise<void>((res, rej) => {
|
||||
// 执行复制命令并移除文本框
|
||||
document.execCommand('copy') ? res() : rej()
|
||||
textArea.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#region setting
|
||||
//#region ----------------------------------------< setting >----------------------------------------
|
||||
const isShowSetting = ref(false)
|
||||
|
||||
const showSetting = () => {
|
||||
@ -444,6 +348,11 @@ const onresize = () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import './styles/doc-content.scss';
|
||||
@import './styles/doc-toc.scss';
|
||||
@import './styles/doc-tree.scss';
|
||||
@import './styles/doc-tree-detail.scss';
|
||||
|
||||
.articles-root {
|
||||
@include box(100vw, 100%);
|
||||
@include flex(column, flex-start, center);
|
||||
@ -476,622 +385,6 @@ const onresize = () => {
|
||||
@include flex(row, center, center);
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.menu {
|
||||
@include box(240px, 100%, 240px, 240px);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
font-weight: 200;
|
||||
transition: 0.1s;
|
||||
&:hover {
|
||||
:deep(.folder-level-line) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-level-one {
|
||||
margin-top: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 8px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.doc-trees {
|
||||
@include box(100%, 100%);
|
||||
font-weight: 200;
|
||||
padding-right: 0;
|
||||
border: 0;
|
||||
overflow-y: scroll;
|
||||
// padding-right: 6px;
|
||||
// 基础的 padding
|
||||
--el-menu-base-level-padding: 25px;
|
||||
// 每级别的的缩进
|
||||
--el-menu-level-padding: 10px;
|
||||
// ------------------- sub-item 的样式
|
||||
// sub-item 菜单的高度
|
||||
--el-menu-sub-item-height: 25px;
|
||||
|
||||
// ------------------- item 的样式
|
||||
// 菜单每个 item 的高度
|
||||
--el-menu-item-height: 25px;
|
||||
// item 的 字体大小
|
||||
--el-menu-item-font-size: 13px;
|
||||
--el-color-primary-light-9: #ffffff00;
|
||||
--el-menu-hover-bg-color: #ffffff00;
|
||||
|
||||
:deep(.el-menu) {
|
||||
transition: 0.1s !important;
|
||||
}
|
||||
|
||||
:deep(.el-sub-menu) {
|
||||
.el-sub-menu__title {
|
||||
height: auto;
|
||||
min-height: 25px;
|
||||
padding-right: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
border-radius: 5px;
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
right: calc(220px - var(--el-menu-level) * 14px);
|
||||
color: #b3b3b3;
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
--el-menu-hover-bg-color: #ffffff00 !important;
|
||||
height: auto;
|
||||
min-height: 25px;
|
||||
padding-right: 0;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item.is-active) {
|
||||
text-shadow: 0px 4px 5px rgba(107, 104, 104, 1);
|
||||
background: linear-gradient(90deg, #3d454d00 0%, #c3c3c3 40%, #c3c3c3 60%, #3d454d00 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
:deep(.el-badge__content) {
|
||||
top: 7px;
|
||||
transform: translateY(-50%) translateX(100%) scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toc-container {
|
||||
@include box(270px, 100%, 270px, 270px);
|
||||
border-left: 1px solid #eeeeee;
|
||||
overflow: auto;
|
||||
transition: 0.3s;
|
||||
|
||||
.viewer-toc {
|
||||
@include box(100%, 100%);
|
||||
color: #5e5e5e;
|
||||
padding: 10px;
|
||||
z-index: 1000;
|
||||
transition: 0.3s;
|
||||
|
||||
.toc-title {
|
||||
@include box(100%, 40px);
|
||||
@include font(30px, 700);
|
||||
line-height: 40px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 3px solid #bcbcbc;
|
||||
}
|
||||
|
||||
.toc-subtitle {
|
||||
width: 100%;
|
||||
@include flex(row, flex-start, center);
|
||||
@include font(12px);
|
||||
color: #ababab;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.toc-content {
|
||||
@include font(14px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
|
||||
.toc-1,
|
||||
.toc-2,
|
||||
.toc-3,
|
||||
.toc-4,
|
||||
.toc-5,
|
||||
.toc-6 {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.toc-1 {
|
||||
font-size: 1.1em;
|
||||
margin-top: 5px;
|
||||
padding-top: 5px;
|
||||
|
||||
&:first-child {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toc-2 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.toc-3 {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.toc-4 {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.toc-5 {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.toc-6 {
|
||||
padding-left: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.article {
|
||||
height: 100%;
|
||||
width: 1260px;
|
||||
max-width: 1260px;
|
||||
overflow-y: overlay;
|
||||
overflow-x: hidden;
|
||||
padding: 0 30px;
|
||||
|
||||
.bl-preview {
|
||||
$borderRadius: 4px;
|
||||
color: #4b4b4b;
|
||||
font-size: inherit;
|
||||
line-height: 1.6;
|
||||
|
||||
:deep(svg) {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.katex > *) {
|
||||
font-size: 1.2em !important;
|
||||
// font-family: 'KaTeX_Size1', sans-serif !important;
|
||||
// font-size: 1.3em !important;
|
||||
// font-family: 'KaTeX_Math', sans-serif !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
/* 滚动条里槽的背景色 */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 定义滑块 内阴影+圆角 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: #967777;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(a.inner-link) {
|
||||
border-bottom: 2px dashed var(--el-color-primary);
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
border-radius: $borderRadius;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 列表
|
||||
:deep(h1) {
|
||||
padding: 10px 0;
|
||||
margin-top: 70px;
|
||||
border-bottom: 2px solid #e5e5e5;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
:deep(h4) {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
:deep(h5) {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
:deep(h6) {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
:deep(h1:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
:deep(li::marker) {
|
||||
color: #989898;
|
||||
}
|
||||
|
||||
/* 有序列表 */
|
||||
:deep(ol) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* 无序列表 */
|
||||
:deep(ul) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* checkbox */
|
||||
:deep(li) {
|
||||
input {
|
||||
margin: 0 0 0 -1.4em;
|
||||
}
|
||||
|
||||
&:has(> input)::marker {
|
||||
content: none;
|
||||
}
|
||||
& :has(> p > input)::marker {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(hr) {
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
// 表格
|
||||
:deep(table) {
|
||||
border: 1px solid #939393;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border-spacing: 0;
|
||||
margin: 10px 0;
|
||||
max-width: 100%;
|
||||
// table-layout: fixed;
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
|
||||
thead {
|
||||
background-color: #2b2b2b;
|
||||
color: #d4d4d4;
|
||||
|
||||
code {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
tr {
|
||||
th {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
border-right: 1px solid #6e6e6e;
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
padding: 5px;
|
||||
border-right: 1px solid #939393;
|
||||
border-bottom: 1px solid #939393;
|
||||
word-wrap: break-word;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.bl-table-container) {
|
||||
border: 0;
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 引用
|
||||
:deep(blockquote) {
|
||||
padding: 1px 10px;
|
||||
margin: 10px 0;
|
||||
color: #7d7d7d;
|
||||
background-color: #f0f0f0;
|
||||
border-left: 3px solid #bebebe;
|
||||
border-radius: $borderRadius;
|
||||
|
||||
blockquote {
|
||||
border: 1px solid #dedede;
|
||||
border-left: 3px solid #bebebe;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-green) {
|
||||
background-color: #edf8db;
|
||||
border-left: 3px solid #bed609;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-yellow) {
|
||||
background-color: #faf0d5;
|
||||
border-left: 3px solid #efc75e;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-red) {
|
||||
background-color: #fbe6e9;
|
||||
border-left: 3px solid #ff9090;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-blue) {
|
||||
background-color: #dfeefd;
|
||||
border-left: 3px solid #81bbf8;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-purple) {
|
||||
background-color: #ece4fb;
|
||||
border-left: 3px solid #ba9bf2;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-black) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-left: 3px solid #000000;
|
||||
}
|
||||
|
||||
// 单行代码块
|
||||
:deep(code) {
|
||||
background-color: #dedede;
|
||||
padding: 0px 4px;
|
||||
border-radius: 3px;
|
||||
margin: 0 3px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 代码块
|
||||
:deep(pre) {
|
||||
padding: 0 0 0 30px;
|
||||
background-color: #2b2b2b;
|
||||
border-radius: $borderRadius;
|
||||
box-shadow: 2px 2px 5px rgb(76, 76, 76);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
.pre-copy {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
text-align: right;
|
||||
z-index: 10;
|
||||
color: #5c5c5c;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pre-copy:hover {
|
||||
background-color: #1a1a1a;
|
||||
color: #9d9d9d;
|
||||
}
|
||||
.pre-copy:active {
|
||||
color: #e2e2e2;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 3px;
|
||||
user-select: none;
|
||||
li {
|
||||
list-style: none;
|
||||
.line-num {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
color: #6a6a6a;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: inherit;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 15px 0 15px 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
color: #a9b7c6;
|
||||
background: #2b2b2b;
|
||||
}
|
||||
|
||||
.hljs ::selection,
|
||||
.hljs::selection {
|
||||
// background-color: #323232;
|
||||
// color: #a9b7c6;
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: #606366;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #a4a3a3;
|
||||
}
|
||||
|
||||
.hljs-operator,
|
||||
.hljs-punctuation,
|
||||
.hljs-subst {
|
||||
color: #a9b7c6;
|
||||
}
|
||||
|
||||
.hljs-operator {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.hljs-bullet,
|
||||
.hljs-deletion,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-variable {
|
||||
color: #4eade5;
|
||||
}
|
||||
|
||||
.hljs-attr {
|
||||
color: #cc7832;
|
||||
}
|
||||
|
||||
.hljs-link,
|
||||
.hljs-literal,
|
||||
.hljs-number,
|
||||
.hljs-symbol,
|
||||
.hljs-variable.constant_ {
|
||||
color: #689757;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-title,
|
||||
.hljs-title.class_ {
|
||||
color: #e4b568;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
color: #bbb529;
|
||||
}
|
||||
|
||||
.hljs-addition,
|
||||
.hljs-code,
|
||||
.hljs-string,
|
||||
.hljs-title.class_.inherited__ {
|
||||
color: #6a8759;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-doctag,
|
||||
.hljs-keyword.hljs-atrule,
|
||||
.hljs-quote,
|
||||
.hljs-regexp {
|
||||
color: #629755;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-function .hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-title.function_,
|
||||
.ruby .hljs-property {
|
||||
color: #9876aa;
|
||||
}
|
||||
|
||||
.diff .hljs-meta,
|
||||
.hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-type {
|
||||
color: #cc7832;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
color: #cc7832;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-meta,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #b4b428;
|
||||
}
|
||||
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-meta-keyword {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 屏幕宽度在 1100 以内时使用以下样式
|
||||
@ -1107,7 +400,7 @@ const onresize = () => {
|
||||
@include box(100%, calc(100% - 40px));
|
||||
padding: 0;
|
||||
|
||||
.menu {
|
||||
.doc-tree-container {
|
||||
height: calc(100% - 20px) !important;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
@ -1119,7 +412,7 @@ const onresize = () => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.article {
|
||||
.doc-content-container {
|
||||
padding: 0 10px;
|
||||
overflow-x: hidden;
|
||||
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<div :class="titleClass">
|
||||
<div class="doc-name">
|
||||
<img
|
||||
class="menu-icon-img"
|
||||
v-if="isNotBlank(props.trees.icon) && (props.trees.icon.startsWith('http') || props.trees.icon.startsWith('https'))"
|
||||
:src="props.trees.icon" />
|
||||
<svg v-else-if="isNotBlank(props.trees.icon)" class="icon menu-icon" aria-hidden="true">
|
||||
<use :xlink:href="'#' + props.trees.icon"></use>
|
||||
</svg>
|
||||
<div class="name-wrapper" :style="nameWrapperStyle">
|
||||
{{ props.trees.n }}
|
||||
</div>
|
||||
<bl-tag v-for="tag in tags" style="margin-top: 5px" :bg-color="tag.bgColor" :icon="tag.icon">{{ tag.content }}</bl-tag>
|
||||
</div>
|
||||
<div v-if="level === 2" class="folder-level-line" style="left: -26px"></div>
|
||||
<div v-if="level === 3" class="folder-level-line" style="left: -36px"></div>
|
||||
<div v-if="level === 3" class="folder-level-line" style="left: -22px"></div>
|
||||
<div v-if="level === 4" class="folder-level-line" style="left: -46px"></div>
|
||||
<div v-if="level === 4" class="folder-level-line" style="left: -32px"></div>
|
||||
<div v-if="level === 4" class="folder-level-line" style="left: -18px"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { isNotBlank } from '@/assets/utils/obj'
|
||||
|
||||
const userStore = useUserStore()
|
||||
//#region ----------------------------------------< 标题信息 >--------------------------------------
|
||||
|
||||
const props = defineProps({
|
||||
trees: { type: Object as PropType<DocTree>, default: {} },
|
||||
level: { type: Number, required: true }
|
||||
})
|
||||
|
||||
const nameWrapperStyle = computed(() => {
|
||||
return {
|
||||
maxWidth: isNotBlank(props.trees.icon) ? 'calc(100% - 25px)' : '100%'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 计算标签, 并返回便签对象集合
|
||||
*/
|
||||
const tags = computed(() => {
|
||||
let icons: any = []
|
||||
props.trees.t?.forEach((tag) => {
|
||||
if (tag === 'subject') {
|
||||
icons.unshift({ content: '专题', bgColor: '#C0C0C0', icon: 'bl-a-lowerrightpage-line' })
|
||||
} else if (tag === 'toc') {
|
||||
icons.push({ content: 'TOC', bgColor: '#c1992f' })
|
||||
} else {
|
||||
icons.push({ content: tag })
|
||||
}
|
||||
})
|
||||
return icons
|
||||
})
|
||||
|
||||
const titleClass = computed(() => {
|
||||
// if (!getThemeSubjecTitle()) {
|
||||
// return 'doc-title'
|
||||
// }
|
||||
if (props.trees.t.includes('subject') && userStore.userParams.WEB_BLOG_SUBJECT_TITLE === '1') {
|
||||
return 'subject-title'
|
||||
}
|
||||
return 'doc-title'
|
||||
})
|
||||
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$icon-size: 17px;
|
||||
|
||||
.doc-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
width: 100%;
|
||||
padding-bottom: 1px;
|
||||
position: relative;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include ellipsis();
|
||||
font-size: inherit;
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
@include ellipsis();
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
padding: 0 2px;
|
||||
right: 0px;
|
||||
top: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
.folder-level-line {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-level-line {
|
||||
width: 1.5px;
|
||||
background-color: var(--el-border-color);
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
// 专题样式, 包括边框和文字样式
|
||||
.subject-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
background: linear-gradient(135deg, #ffffff, #f0f0f0, #cacaca);
|
||||
box-shadow: 1px 1px 5px #a2a2a2;
|
||||
max-width: calc(100% - 15px);
|
||||
min-width: calc(100% - 15px);
|
||||
padding: 2px 5px;
|
||||
margin: 5px 0 10px 0;
|
||||
border-radius: 7px;
|
||||
position: relative;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
color: var(--el-color-primary);
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
@include ellipsis();
|
||||
max-width: calc(100% - 25px);
|
||||
min-width: calc(100% - 25px);
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
}
|
||||
|
||||
.folder-level-line {
|
||||
height: calc(100% + 25px);
|
||||
top: -5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
12
blossom-web/src/views/article/index.d.ts
vendored
12
blossom-web/src/views/article/index.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
declare interface DocTree {
|
||||
/** ID */
|
||||
i: number
|
||||
i: string
|
||||
p: string
|
||||
/** Name */
|
||||
n: string
|
||||
/** open: 0:否;1:是; */
|
||||
@ -53,12 +54,3 @@ declare type DocType = 1 | 2 | 3
|
||||
declare interface Window {
|
||||
onHtmlEventDispatch: any
|
||||
}
|
||||
|
||||
/**
|
||||
* 目录结构
|
||||
*/
|
||||
declare interface Toc {
|
||||
content: string
|
||||
clazz: string
|
||||
id: string
|
||||
}
|
||||
|
||||
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 监听 html 的内联事件
|
||||
*/
|
||||
export type ArticleHtmlEvent =
|
||||
| 'copyPreCode' // 代码复制
|
||||
| 'showArticleReferenceView' // 显示引用文章内容
|
||||
|
||||
/**
|
||||
* 处理内联事件
|
||||
*
|
||||
* blog 只有代码复制(copyPreCode)事件
|
||||
*
|
||||
* @param _t
|
||||
* @param _ty
|
||||
* @param _event
|
||||
* @param type
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
export const onHtmlEventDispatch = (_t: any, _ty: any, _event: any, type: ArticleHtmlEvent, data: any) => {
|
||||
if (type === 'copyPreCode') {
|
||||
let code = document.getElementById(data)
|
||||
if (code) {
|
||||
// navigator clipboard 需要https等安全上下文
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
// navigator clipboard 向剪贴板写文本
|
||||
return navigator.clipboard.writeText(code.innerText)
|
||||
} else {
|
||||
// 创建text area
|
||||
let textArea = document.createElement('textarea')
|
||||
textArea.value = code.innerText
|
||||
// 使text area不在viewport,同时设置不可见
|
||||
textArea.style.position = 'absolute'
|
||||
textArea.style.opacity = '0'
|
||||
textArea.style.left = '-999999px'
|
||||
textArea.style.top = '-999999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
return new Promise<void>((res, rej) => {
|
||||
// 执行复制命令并移除文本框
|
||||
document.execCommand('copy') ? res() : rej()
|
||||
textArea.remove()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
blossom-web/src/views/article/scripts/doc-toc.ts
Normal file
57
blossom-web/src/views/article/scripts/doc-toc.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 目录结构
|
||||
*/
|
||||
export interface Toc {
|
||||
content: string
|
||||
clazz: string
|
||||
id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析正文中的标题
|
||||
*
|
||||
* @param ele
|
||||
* @returns
|
||||
*/
|
||||
export const parseToc = (ele: HTMLElement): Toc[] => {
|
||||
let heads = ele.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
||||
let tocs: Toc[] = []
|
||||
for (let i = 0; i < heads.length; i++) {
|
||||
let head: Element = heads[i]
|
||||
let level = 1
|
||||
let content = (head as HTMLElement).innerText
|
||||
let id = head.id
|
||||
switch (head.localName) {
|
||||
case 'h1':
|
||||
level = 1
|
||||
break
|
||||
case 'h2':
|
||||
level = 2
|
||||
break
|
||||
case 'h3':
|
||||
level = 3
|
||||
break
|
||||
case 'h4':
|
||||
level = 4
|
||||
break
|
||||
case 'h5':
|
||||
level = 5
|
||||
break
|
||||
case 'h6':
|
||||
level = 6
|
||||
break
|
||||
}
|
||||
let toc: Toc = { content: content, clazz: 'toc-' + level, id: id }
|
||||
tocs.push(toc)
|
||||
}
|
||||
return tocs
|
||||
}
|
||||
/**
|
||||
* 前往指定标题
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
export const toScroll = (id: string) => {
|
||||
let elm = document.getElementById(id)
|
||||
elm?.scrollIntoView(true)
|
||||
}
|
||||
40
blossom-web/src/views/article/scripts/doc-tree-detail.ts
Normal file
40
blossom-web/src/views/article/scripts/doc-tree-detail.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { isNotBlank } from '@/assets/utils/obj'
|
||||
|
||||
/**
|
||||
* 文章图标是否是链接
|
||||
* @param doc
|
||||
* @param viewStyle
|
||||
* @returns
|
||||
*/
|
||||
export const isShowImg = (doc: DocTree) => {
|
||||
return isNotBlank(doc.icon) && (doc.icon.startsWith('http') || doc.icon.startsWith('https'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章图标是否是内置 svg
|
||||
* @param doc
|
||||
* @param viewStyle
|
||||
* @returns
|
||||
*/
|
||||
export const isShowSvg = (doc: DocTree) => {
|
||||
return isNotBlank(doc.icon)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算标签, 并返回便签对象集合
|
||||
* @param doc 文章内容
|
||||
* @param viewStyle 页面样式
|
||||
*/
|
||||
export const tags = (doc: DocTree) => {
|
||||
let icons: any = []
|
||||
doc.t.forEach((tag) => {
|
||||
if (tag.toLocaleLowerCase() === 'subject') {
|
||||
icons.unshift({ content: '专题', bgColor: '#939393', icon: 'bl-a-lowerrightpage-line' })
|
||||
} else if (tag.toLocaleLowerCase() === 'toc') {
|
||||
icons.push({ content: '目录', bgColor: '#565656' })
|
||||
} else {
|
||||
icons.push({ content: tag })
|
||||
}
|
||||
})
|
||||
return icons
|
||||
}
|
||||
447
blossom-web/src/views/article/styles/doc-content.scss
Normal file
447
blossom-web/src/views/article/styles/doc-content.scss
Normal file
@ -0,0 +1,447 @@
|
||||
.doc-content-container {
|
||||
height: 100%;
|
||||
width: 1160px;
|
||||
max-width: 1160px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
padding: 0 30px;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.article-name {
|
||||
color: #4b4b4b;
|
||||
font-weight: 700;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.bl-preview {
|
||||
$borderRadius: 4px;
|
||||
color: #4b4b4b;
|
||||
font-size: inherit;
|
||||
line-height: 1.6;
|
||||
|
||||
:deep(svg) {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.katex > *) {
|
||||
font-size: 1.2em !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
/* 滚动条里槽的背景色 */
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 定义滑块 内阴影+圆角 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: #967777;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(a.inner-link) {
|
||||
border-bottom: 2px dashed var(--el-color-primary);
|
||||
box-sizing: border-box;
|
||||
padding: 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
border-radius: $borderRadius;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// 列表
|
||||
:deep(h1) {
|
||||
padding: 10px 0;
|
||||
margin-top: 70px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
:deep(h2) {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
:deep(h4) {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
:deep(h5) {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
:deep(h6) {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
:deep(h1:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
:deep(li::marker) {
|
||||
color: #989898;
|
||||
}
|
||||
|
||||
/* 有序列表 */
|
||||
:deep(ol) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* 无序列表 */
|
||||
:deep(ul) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
/* checkbox */
|
||||
:deep(li) {
|
||||
input {
|
||||
margin: 0 0 0 -1.4em;
|
||||
}
|
||||
|
||||
&:has(> input)::marker {
|
||||
content: none;
|
||||
}
|
||||
& :has(> p > input)::marker {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(hr) {
|
||||
border-color: #ffffff75;
|
||||
}
|
||||
|
||||
// 表格
|
||||
:deep(table) {
|
||||
border: 1px solid #939393;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
border-spacing: 0;
|
||||
margin: 10px 0;
|
||||
max-width: 100%;
|
||||
// table-layout: fixed;
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
|
||||
thead {
|
||||
background-color: #2b2b2b;
|
||||
color: #d4d4d4;
|
||||
|
||||
code {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
tr {
|
||||
th {
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
border-right: 1px solid #6e6e6e;
|
||||
}
|
||||
|
||||
th:last-child {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
padding: 5px;
|
||||
border-right: 1px solid #939393;
|
||||
border-bottom: 1px solid #939393;
|
||||
word-wrap: break-word;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
td:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tr:last-child {
|
||||
td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.bl-table-container) {
|
||||
border: 0;
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 引用
|
||||
:deep(blockquote) {
|
||||
padding: 1px 10px;
|
||||
margin: 10px 0;
|
||||
color: #7d7d7d;
|
||||
background-color: #f0f0f0;
|
||||
border-left: 3px solid #bebebe;
|
||||
border-radius: $borderRadius;
|
||||
|
||||
blockquote {
|
||||
border: 1px solid #dedede;
|
||||
border-left: 3px solid #bebebe;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-green) {
|
||||
background-color: #edf8db;
|
||||
border-left: 3px solid #bed609;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-yellow) {
|
||||
background-color: #faf0d5;
|
||||
border-left: 3px solid #efc75e;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-red) {
|
||||
background-color: #fbe6e9;
|
||||
border-left: 3px solid #ff9090;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-blue) {
|
||||
background-color: #dfeefd;
|
||||
border-left: 3px solid #81bbf8;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-purple) {
|
||||
background-color: #ece4fb;
|
||||
border-left: 3px solid #ba9bf2;
|
||||
}
|
||||
|
||||
:deep(.bl-blockquote-black) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-left: 3px solid #000000;
|
||||
}
|
||||
|
||||
// 单行代码块
|
||||
:deep(code) {
|
||||
font-family: 'Jetbrains Mono';
|
||||
background-color: #dedede;
|
||||
padding: 0px 4px;
|
||||
border-radius: 3px;
|
||||
margin: 0 3px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
// 代码块
|
||||
:deep(pre) {
|
||||
padding: 0 0 0 30px;
|
||||
background-color: #2b2b2b;
|
||||
border-radius: $borderRadius;
|
||||
box-shadow: 2px 2px 5px rgb(76, 76, 76);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
* {
|
||||
font-family: 'Jetbrains Mono' !important;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: #5c5c5c;
|
||||
}
|
||||
|
||||
.pre-copy {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
text-align: right;
|
||||
z-index: 10;
|
||||
color: #5c5c5c;
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.pre-copy:hover {
|
||||
background-color: #1a1a1a;
|
||||
color: #9d9d9d;
|
||||
}
|
||||
.pre-copy:active {
|
||||
color: #e2e2e2;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 3px;
|
||||
user-select: none;
|
||||
li {
|
||||
list-style: none;
|
||||
.line-num {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
color: #6a6a6a;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: inherit;
|
||||
background-color: inherit;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 15px 0 15px 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
color: #a9b7c6;
|
||||
background: #2b2b2b;
|
||||
}
|
||||
|
||||
.hljs ::selection,
|
||||
.hljs::selection {
|
||||
// background-color: #323232;
|
||||
// color: #a9b7c6;
|
||||
}
|
||||
|
||||
.hljs-comment {
|
||||
color: #606366;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #a4a3a3;
|
||||
}
|
||||
|
||||
.hljs-operator,
|
||||
.hljs-punctuation,
|
||||
.hljs-subst {
|
||||
color: #a9b7c6;
|
||||
}
|
||||
|
||||
.hljs-operator {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.hljs-bullet,
|
||||
.hljs-deletion,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-template-variable,
|
||||
.hljs-variable {
|
||||
color: #4eade5;
|
||||
}
|
||||
|
||||
.hljs-attr {
|
||||
color: #cc7832;
|
||||
}
|
||||
|
||||
.hljs-link,
|
||||
.hljs-literal,
|
||||
.hljs-number,
|
||||
.hljs-symbol,
|
||||
.hljs-variable.constant_ {
|
||||
color: #689757;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-title,
|
||||
.hljs-title.class_ {
|
||||
color: #e4b568;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
color: #bbb529;
|
||||
}
|
||||
|
||||
.hljs-addition,
|
||||
.hljs-code,
|
||||
.hljs-string,
|
||||
.hljs-title.class_.inherited__ {
|
||||
color: #6a8759;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-doctag,
|
||||
.hljs-keyword.hljs-atrule,
|
||||
.hljs-quote,
|
||||
.hljs-regexp {
|
||||
color: #629755;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-function .hljs-title,
|
||||
.hljs-section,
|
||||
.hljs-title.function_,
|
||||
.ruby .hljs-property {
|
||||
color: #9876aa;
|
||||
}
|
||||
|
||||
.diff .hljs-meta,
|
||||
.hljs-keyword,
|
||||
.hljs-template-tag,
|
||||
.hljs-type {
|
||||
color: #cc7832;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
color: #cc7832;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-meta,
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #b4b428;
|
||||
}
|
||||
|
||||
.hljs-meta .hljs-keyword,
|
||||
.hljs-meta-keyword {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
blossom-web/src/views/article/styles/doc-toc.scss
Normal file
91
blossom-web/src/views/article/styles/doc-toc.scss
Normal file
@ -0,0 +1,91 @@
|
||||
.toc-container {
|
||||
@include box(270px, 100%, 270px, 270px);
|
||||
border-left: 1px solid #eeeeee;
|
||||
overflow: auto;
|
||||
transition: 0.3s;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
|
||||
.viewer-toc {
|
||||
@include box(100%, 100%);
|
||||
color: #5e5e5e;
|
||||
padding: 10px;
|
||||
z-index: 1000;
|
||||
transition: 0.3s;
|
||||
|
||||
.toc-title {
|
||||
@include box(100%, 40px);
|
||||
@include font(25px, 700);
|
||||
line-height: 40px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 3px solid #bcbcbc;
|
||||
}
|
||||
|
||||
.toc-subtitle {
|
||||
width: 100%;
|
||||
@include flex(row, flex-start, center);
|
||||
@include font(12px);
|
||||
color: #ababab;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.toc-content {
|
||||
@include font(14px);
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: 20px;
|
||||
|
||||
.toc-1,
|
||||
.toc-2,
|
||||
.toc-3,
|
||||
.toc-4,
|
||||
.toc-5,
|
||||
.toc-6 {
|
||||
min-height: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.toc-1 {
|
||||
font-size: 1.1em;
|
||||
margin-top: 5px;
|
||||
|
||||
&:first-child {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toc-2 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.toc-3 {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.toc-4 {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.toc-5 {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.toc-6 {
|
||||
padding-left: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
blossom-web/src/views/article/styles/doc-tree-detail.scss
Normal file
87
blossom-web/src/views/article/styles/doc-tree-detail.scss
Normal file
@ -0,0 +1,87 @@
|
||||
$icon-size: 17px;
|
||||
|
||||
.menu-item-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.doc-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 1px 0;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
@include ellipsis();
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-top: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
min-height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
padding: 0 2px;
|
||||
right: 2px;
|
||||
top: 2px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
// 专题样式, 包括边框和文字样式
|
||||
.subject-title {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
box-shadow: 2px 2px 8px 1px var(--el-color-primary-light-8);
|
||||
background: linear-gradient(135deg, var(--el-color-primary-light-7), var(--el-color-primary-light-8), var(--bl-html-color));
|
||||
min-height: 44px;
|
||||
max-width: 300px;
|
||||
width: calc(100% - 10px);
|
||||
padding: 4px 5px;
|
||||
margin: 5px 0 5px 0;
|
||||
border-radius: 7px;
|
||||
|
||||
.doc-name {
|
||||
@include flex(row, flex-start, flex-start);
|
||||
color: var(--el-color-primary);
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
.menu-icon,
|
||||
.menu-icon-img {
|
||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.menu-icon-img {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.name-wrapper {
|
||||
max-width: calc(100% - 25px);
|
||||
min-width: calc(100% - 25px);
|
||||
}
|
||||
}
|
||||
|
||||
.sort {
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
}
|
||||
}
|
||||
50
blossom-web/src/views/article/styles/doc-tree.scss
Normal file
50
blossom-web/src/views/article/styles/doc-tree.scss
Normal file
@ -0,0 +1,50 @@
|
||||
.doc-tree-container {
|
||||
@include box(300px, 100%, 300px, 300px);
|
||||
border-right: 1px solid var(--el-border-color);
|
||||
transition: 0.1s;
|
||||
|
||||
.doc-tree {
|
||||
@include box(100%, 100%);
|
||||
height: 100%;
|
||||
padding-right: 2px;
|
||||
overflow-y: scroll;
|
||||
--el-transition-duration: 0.2s; // 折叠展开的动画效果
|
||||
|
||||
:deep(.is-drop-inner) {
|
||||
box-shadow: inset 0 0 1px 2px var(--el-color-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-tree__drop-indicator) {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
color: var(--bl-text-doctree-color);
|
||||
}
|
||||
|
||||
:deep(.is-current) {
|
||||
& > .el-tree-node__content {
|
||||
border-radius: 5px;
|
||||
|
||||
&:has(.menu-divider) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content) {
|
||||
height: auto;
|
||||
|
||||
&:hover {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #ebebeb;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,9 +14,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-footer">
|
||||
<div class="about-us">
|
||||
<span>{{ blossom.SYS.VERSION + (isNotBlank(getEmail()) ? ' | 邮箱:' + getEmail() : '') }}</span>
|
||||
</div>
|
||||
<div class="custom-info">
|
||||
<div v-if="isNotBlank(gwab())" v-html="gwab()"></div>
|
||||
<div v-if="isNotBlank(ipc())" style="cursor: pointer" @click="openNew('https://beian.miit.gov.cn/')">
|
||||
|
||||
@ -54,6 +54,10 @@ const toToc = (articleId: number) => {
|
||||
padding: 20px;
|
||||
overflow: scroll;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #414141;
|
||||
}
|
||||
|
||||
.subject-item {
|
||||
@include box(260px, 150px);
|
||||
position: relative;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user