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;
|
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.SearchProperties;
|
||||||
import com.blossom.backend.base.search.message.IndexMsg;
|
import com.blossom.backend.base.search.message.IndexMsg;
|
||||||
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
|
||||||
@ -51,7 +50,6 @@ public class IndexMsgConsumer {
|
|||||||
final Long userId = indexMsg.getUserId();
|
final Long userId = indexMsg.getUserId();
|
||||||
final Long id = indexMsg.getId();
|
final Long id = indexMsg.getId();
|
||||||
if (userId == null || id == null) {
|
if (userId == null || id == null) {
|
||||||
log.error("消费异常. 获取用户id为空");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (IndexMsgTypeEnum.ADD == indexMsg.getType()) {
|
if (IndexMsgTypeEnum.ADD == indexMsg.getType()) {
|
||||||
@ -62,11 +60,11 @@ public class IndexMsgConsumer {
|
|||||||
// 查询最新的消息
|
// 查询最新的消息
|
||||||
ArticleEntity article = this.articleService.selectById(id, false, true, false, userId);
|
ArticleEntity article = this.articleService.selectById(id, false, true, false, userId);
|
||||||
Document document = new Document();
|
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("name", article.getName(), Field.Store.YES));
|
||||||
document.add(new TextField("tags", article.getTags(), Field.Store.YES));
|
document.add(new TextField("tags", article.getTags(), Field.Store.YES));
|
||||||
document.add(new TextField("markdown", article.getMarkdown(), 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.flush();
|
||||||
indexWriter.commit();
|
indexWriter.commit();
|
||||||
}
|
}
|
||||||
@ -74,7 +72,7 @@ public class IndexMsgConsumer {
|
|||||||
// 删除索引
|
// 删除索引
|
||||||
try (Directory directory = FSDirectory.open(this.searchProperties.getUserIndexDirectory(userId));
|
try (Directory directory = FSDirectory.open(this.searchProperties.getUserIndexDirectory(userId));
|
||||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
|
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.flush();
|
||||||
indexWriter.commit();
|
indexWriter.commit();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,33 @@
|
|||||||
package com.blossom.backend.config;
|
package com.blossom.backend.config;
|
||||||
|
|
||||||
|
import com.blossom.backend.server.folder.pojo.FolderEntity;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class Test {
|
public class Test {
|
||||||
public static void main(String[] args) {
|
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.ArticleOpenService;
|
||||||
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
|
||||||
import com.blossom.backend.server.doc.DocService;
|
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.doc.DocTypeEnum;
|
||||||
import com.blossom.backend.server.folder.FolderService;
|
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.folder.pojo.FolderEntity;
|
||||||
import com.blossom.backend.server.utils.ArticleUtil;
|
import com.blossom.backend.server.utils.ArticleUtil;
|
||||||
import com.blossom.backend.server.utils.DocUtil;
|
import com.blossom.backend.server.utils.DocUtil;
|
||||||
@ -50,13 +52,14 @@ import java.util.List;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@RequestMapping("/article")
|
@RequestMapping("/article")
|
||||||
public class ArticleController {
|
public class ArticleController {
|
||||||
|
|
||||||
private final ArticleService baseService;
|
private final ArticleService baseService;
|
||||||
private final ArticleOpenService openService;
|
private final ArticleOpenService openService;
|
||||||
private final FolderService folderService;
|
private final FolderService folderService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final ArticleTempVisitService tempVisitService;
|
private final ArticleTempVisitService tempVisitService;
|
||||||
private final DocService docService;
|
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);
|
ArticleEntity article = req.to(ArticleEntity.class);
|
||||||
article.setTags(DocUtil.toTagStr(req.getTags()));
|
article.setTags(DocUtil.toTagStr(req.getTags()));
|
||||||
article.setUserId(AuthContext.getUserId());
|
article.setUserId(AuthContext.getUserId());
|
||||||
// 如果新增到顶部, 获取最小的
|
// 如果新增到顶部, 获取最小的排序
|
||||||
if (BooleanUtil.isTrue(req.getAddToLast())) {
|
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));
|
return R.ok(baseService.insert(article));
|
||||||
}
|
}
|
||||||
@ -136,10 +139,19 @@ public class ArticleController {
|
|||||||
* @apiNote 该接口只能修改文章的基本信息, 正文及版本修改请使用 "/upd/content" 接口,或者 {@link ArticleService#updateContentById(ArticleEntity)}
|
* @apiNote 该接口只能修改文章的基本信息, 正文及版本修改请使用 "/upd/content" 接口,或者 {@link ArticleService#updateContentById(ArticleEntity)}
|
||||||
*/
|
*/
|
||||||
@PostMapping("/upd")
|
@PostMapping("/upd")
|
||||||
public R<Long> insert(@Validated @RequestBody ArticleUpdReq req) {
|
public R<Long> update(@Validated @RequestBody ArticleUpdReq req) {
|
||||||
ArticleEntity article = req.to(ArticleEntity.class);
|
ArticleEntity article = req.to(ArticleEntity.class);
|
||||||
article.setTags(DocUtil.toTagStr(req.getTags()));
|
article.setTags(DocUtil.toTagStr(req.getTags()));
|
||||||
article.setUserId(AuthContext.getUserId());
|
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));
|
return R.ok(baseService.update(article));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,11 +275,11 @@ public class ArticleController {
|
|||||||
* @param pid 上级菜单
|
* @param pid 上级菜单
|
||||||
*/
|
*/
|
||||||
@PostMapping("import")
|
@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 {
|
try {
|
||||||
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
|
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
|
||||||
if (!"txt".equals(suffix) && !"md".equals(suffix)) {
|
if (!"txt".equals(suffix) && !"md".equals(suffix)) {
|
||||||
throw new XzException404("不支持的文件类型: [" + suffix + "]");
|
throw new XzException400("不支持的文件类型: [" + suffix + "]");
|
||||||
}
|
}
|
||||||
FolderEntity folder = folderService.selectById(pid);
|
FolderEntity folder = folderService.selectById(pid);
|
||||||
XzException404.throwBy(ObjUtil.isNull(folder), "上级文件夹不存在");
|
XzException404.throwBy(ObjUtil.isNull(folder), "上级文件夹不存在");
|
||||||
@ -278,10 +290,12 @@ public class ArticleController {
|
|||||||
article.setPid(pid);
|
article.setPid(pid);
|
||||||
article.setUserId(AuthContext.getUserId());
|
article.setUserId(AuthContext.getUserId());
|
||||||
article.setName(FileUtil.getPrefix(file.getOriginalFilename()));
|
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);
|
baseService.insert(article);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
throw new XzException400("上传失败");
|
||||||
}
|
}
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.blossom.backend.server.article.draft;
|
|||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.ObjUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.blossom.backend.base.search.EnableIndex;
|
import com.blossom.backend.base.search.EnableIndex;
|
||||||
@ -166,7 +167,9 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
|||||||
public Long update(ArticleEntity req) {
|
public Long update(ArticleEntity req) {
|
||||||
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
XzException404.throwBy(req.getId() == null, "ID不得为空");
|
||||||
baseMapper.updById(req);
|
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();
|
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.config.BlConstants;
|
||||||
import com.blossom.backend.server.doc.pojo.DocTreeReq;
|
import com.blossom.backend.server.doc.pojo.DocTreeReq;
|
||||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
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 com.blossom.common.base.pojo.R;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -29,7 +31,7 @@ public class DocController {
|
|||||||
/**
|
/**
|
||||||
* 文档列表
|
* 文档列表
|
||||||
*
|
*
|
||||||
* @return 文件夹列表
|
* @return 文档列表
|
||||||
* @apiNote 文档包含文章和文件夹, 文件夹分为图片文件夹和文章文件夹 {@link DocTypeEnum}
|
* @apiNote 文档包含文章和文件夹, 文件夹分为图片文件夹和文章文件夹 {@link DocTypeEnum}
|
||||||
*/
|
*/
|
||||||
@GetMapping("/trees")
|
@GetMapping("/trees")
|
||||||
@ -56,4 +58,21 @@ public class DocController {
|
|||||||
open.setUserId(userId);
|
open.setUserId(userId);
|
||||||
return R.ok(docService.listTree(open));
|
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 下的最小排序
|
* 查询 PID 下的最小排序
|
||||||
|
*
|
||||||
* @param pid PID
|
* @param pid PID
|
||||||
* @return 最小排序
|
* @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;
|
package com.blossom.backend.server.doc;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
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.TagEnum;
|
||||||
|
import com.blossom.backend.server.article.draft.ArticleMapper;
|
||||||
import com.blossom.backend.server.article.draft.ArticleService;
|
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.article.draft.pojo.ArticleQueryReq;
|
||||||
import com.blossom.backend.server.doc.pojo.DocTreeReq;
|
import com.blossom.backend.server.doc.pojo.DocTreeReq;
|
||||||
import com.blossom.backend.server.doc.pojo.DocTreeRes;
|
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.FolderService;
|
||||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
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.picture.PictureService;
|
||||||
import com.blossom.backend.server.utils.DocUtil;
|
import com.blossom.backend.server.utils.DocUtil;
|
||||||
import com.blossom.backend.server.utils.PictureUtil;
|
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 com.blossom.common.base.util.SortUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -29,23 +35,28 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class DocService {
|
public class DocService {
|
||||||
private FolderService folderService;
|
|
||||||
private ArticleService articleService;
|
private ArticleService articleService;
|
||||||
private PictureService pictureService;
|
private PictureService pictureService;
|
||||||
|
private FolderService folderService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private DocMapper baseMapper;
|
private DocMapper baseMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setFolderService(FolderService folderService) {
|
private FolderMapper folderMapper;
|
||||||
this.folderService = folderService;
|
@Autowired
|
||||||
}
|
private ArticleMapper articleMapper;
|
||||||
|
@Autowired
|
||||||
|
private DocSortChecker docSortChecker;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setArticleService(ArticleService articleService) {
|
public void setArticleService(ArticleService articleService) {
|
||||||
this.articleService = articleService;
|
this.articleService = articleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public void setFolderService(FolderService folderService) {
|
||||||
|
this.folderService = folderService;
|
||||||
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setPictureService(PictureService pictureService) {
|
public void setPictureService(PictureService pictureService) {
|
||||||
this.pictureService = pictureService;
|
this.pictureService = pictureService;
|
||||||
@ -64,10 +75,10 @@ public class DocService {
|
|||||||
* 只查询文件夹
|
* 只查询文件夹
|
||||||
* =============================================================================================== */
|
* =============================================================================================== */
|
||||||
if (req.getOnlyFolder()) {
|
if (req.getOnlyFolder()) {
|
||||||
FolderQueryReq where = req.to(FolderQueryReq.class);
|
FolderEntity where = req.to(FolderEntity.class);
|
||||||
List<DocTreeRes> folder = folderService.listTree(where);
|
List<FolderEntity> folder = folderMapper.listAll(where);
|
||||||
all.addAll(CollUtil.newArrayList(PictureUtil.getDefaultFolder(req.getUserId())));
|
all.addAll(CollUtil.newArrayList(PictureUtil.getDefaultFolder(req.getUserId())));
|
||||||
all.addAll(folder);
|
all.addAll(DocUtil.toTreeRes(folder));
|
||||||
priorityType = true;
|
priorityType = true;
|
||||||
}
|
}
|
||||||
/* ===============================================================================================
|
/* ===============================================================================================
|
||||||
@ -75,16 +86,16 @@ public class DocService {
|
|||||||
* =============================================================================================== */
|
* =============================================================================================== */
|
||||||
else if (req.getOnlyPicture()) {
|
else if (req.getOnlyPicture()) {
|
||||||
// 1. 所有图片文件夹
|
// 1. 所有图片文件夹
|
||||||
FolderQueryReq folder = req.to(FolderQueryReq.class);
|
FolderEntity where = req.to(FolderEntity.class);
|
||||||
folder.setType(FolderTypeEnum.PICTURE.getType());
|
where.setType(FolderTypeEnum.PICTURE.getType());
|
||||||
List<DocTreeRes> picFolder = folderService.listTree(folder);
|
List<FolderEntity> picFolder = folderMapper.listAll(where);
|
||||||
all.addAll(picFolder);
|
all.addAll(DocUtil.toTreeRes(picFolder));
|
||||||
|
|
||||||
// 2. 有图片的图片或文章文件夹
|
// 2. 有图片的图片或文章文件夹
|
||||||
List<Long> pids = pictureService.listDistinctPid(req.getUserId());
|
List<Long> pids = pictureService.listDistinctPid(req.getUserId());
|
||||||
if (CollUtil.isNotEmpty(pids)) {
|
if (CollUtil.isNotEmpty(pids)) {
|
||||||
List<DocTreeRes> articleTopFolder = folderService.recursiveToParentTree(pids);
|
List<FolderEntity> articleTopFolder = folderMapper.recursiveToParent(pids);
|
||||||
all.addAll(articleTopFolder);
|
all.addAll(DocUtil.toTreeRes(articleTopFolder));
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<DocTreeRes> min = all.stream().min((f1, f2) -> SortUtil.intSort.compare(f1.getS(), f2.getS()));
|
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);
|
all.addAll(articles);
|
||||||
if (CollUtil.isNotEmpty(articles)) {
|
if (CollUtil.isNotEmpty(articles)) {
|
||||||
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
||||||
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
|
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
|
||||||
all.addAll(folders);
|
all.addAll(DocUtil.toTreeRes(folders));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ===============================================================================================
|
/* ===============================================================================================
|
||||||
* 只查询专题的文章和文件夹
|
* 只查询专题的文章和文件夹
|
||||||
* =============================================================================================== */
|
* =============================================================================================== */
|
||||||
else if (req.getOnlySubject()) {
|
else if (req.getOnlySubject()) {
|
||||||
FolderQueryReq folderWhere = req.to(FolderQueryReq.class);
|
FolderEntity folderWhere = req.to(FolderEntity.class);
|
||||||
folderWhere.setTags(TagEnum.subject.name());
|
folderWhere.setTags(TagEnum.subject.name());
|
||||||
folderWhere.setType(FolderTypeEnum.ARTICLE.getType());
|
folderWhere.setType(FolderTypeEnum.ARTICLE.getType());
|
||||||
List<DocTreeRes> subjects = folderService.listTree(folderWhere);
|
List<FolderEntity> subjects = folderMapper.listAll(folderWhere);
|
||||||
if (CollUtil.isNotEmpty(subjects)) {
|
if (CollUtil.isNotEmpty(subjects)) {
|
||||||
List<Long> subjectIds = subjects.stream().map(DocTreeRes::getI).collect(Collectors.toList());
|
List<Long> subjectIds = subjects.stream().map(FolderEntity::getId).collect(Collectors.toList());
|
||||||
List<DocTreeRes> foldersTop = folderService.recursiveToParentTree(subjectIds);
|
List<FolderEntity> foldersTop = folderMapper.recursiveToParent(subjectIds);
|
||||||
List<DocTreeRes> foldersBottom = folderService.recursiveToChildrenTree(subjectIds);
|
List<FolderEntity> foldersBottom = folderMapper.recursiveToChildren(subjectIds);
|
||||||
all.addAll(foldersTop);
|
all.addAll(DocUtil.toTreeRes(foldersTop));
|
||||||
all.addAll(foldersBottom);
|
all.addAll(DocUtil.toTreeRes(foldersBottom));
|
||||||
}
|
}
|
||||||
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
|
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
|
||||||
all.addAll(articles);
|
all.addAll(articles);
|
||||||
@ -137,8 +148,8 @@ public class DocService {
|
|||||||
all.addAll(articles);
|
all.addAll(articles);
|
||||||
if (CollUtil.isNotEmpty(articles)) {
|
if (CollUtil.isNotEmpty(articles)) {
|
||||||
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
||||||
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
|
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
|
||||||
all.addAll(folders);
|
all.addAll(DocUtil.toTreeRes(folders));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ===============================================================================================
|
/* ===============================================================================================
|
||||||
@ -151,19 +162,19 @@ public class DocService {
|
|||||||
all.addAll(articles);
|
all.addAll(articles);
|
||||||
if (CollUtil.isNotEmpty(articles)) {
|
if (CollUtil.isNotEmpty(articles)) {
|
||||||
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
|
||||||
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
|
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
|
||||||
all.addAll(folders);
|
all.addAll(DocUtil.toTreeRes(folders));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ===============================================================================================
|
/* ===============================================================================================
|
||||||
* 默认查询文章文件夹
|
* 默认查询文章文件夹
|
||||||
* =============================================================================================== */
|
* =============================================================================================== */
|
||||||
else {
|
else {
|
||||||
FolderQueryReq folder = req.to(FolderQueryReq.class);
|
FolderEntity folder = req.to(FolderEntity.class);
|
||||||
folder.setType(FolderTypeEnum.ARTICLE.getType());
|
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));
|
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
|
||||||
all.addAll(folders);
|
all.addAll(DocUtil.toTreeRes(folders));
|
||||||
all.addAll(articles);
|
all.addAll(articles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,8 +188,64 @@ public class DocService {
|
|||||||
* @return 最大排序
|
* @return 最大排序
|
||||||
* @since 1.10.0
|
* @since 1.10.0
|
||||||
*/
|
*/
|
||||||
public int selectMinSortByPid(Long pid) {
|
public int selectMaxSortByPid(Long pid, Long userId, FolderTypeEnum type) {
|
||||||
return baseMapper.selectMaxSortByPid(pid);
|
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.AuthContext;
|
||||||
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
import com.blossom.backend.base.auth.annotation.AuthIgnore;
|
||||||
import com.blossom.backend.config.BlConstants;
|
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.article.draft.pojo.ArticleUpdTagReq;
|
||||||
import com.blossom.backend.server.doc.DocService;
|
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.folder.pojo.*;
|
||||||
import com.blossom.backend.server.utils.DocUtil;
|
import com.blossom.backend.server.utils.DocUtil;
|
||||||
import com.blossom.common.base.exception.XzException404;
|
import com.blossom.common.base.exception.XzException404;
|
||||||
@ -34,6 +33,7 @@ import java.util.List;
|
|||||||
public class FolderController {
|
public class FolderController {
|
||||||
private final FolderService baseService;
|
private final FolderService baseService;
|
||||||
private final DocService docService;
|
private final DocService docService;
|
||||||
|
private final DocSortChecker docSortChecker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询专题列表 [OP]
|
* 查询专题列表 [OP]
|
||||||
@ -96,9 +96,9 @@ public class FolderController {
|
|||||||
FolderEntity folder = req.to(FolderEntity.class);
|
FolderEntity folder = req.to(FolderEntity.class);
|
||||||
folder.setTags(DocUtil.toTagStr(req.getTags()));
|
folder.setTags(DocUtil.toTagStr(req.getTags()));
|
||||||
folder.setUserId(AuthContext.getUserId());
|
folder.setUserId(AuthContext.getUserId());
|
||||||
// 如果新增到顶部, 获取最小的
|
// 如果新增到底部, 获取最大的排序
|
||||||
if (BooleanUtil.isTrue(req.getAddToLast())) {
|
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));
|
return R.ok(baseService.insert(folder));
|
||||||
}
|
}
|
||||||
@ -112,6 +112,15 @@ public class FolderController {
|
|||||||
public R<Long> update(@Validated @RequestBody FolderUpdReq req) {
|
public R<Long> update(@Validated @RequestBody FolderUpdReq req) {
|
||||||
FolderEntity folder = req.to(FolderEntity.class);
|
FolderEntity folder = req.to(FolderEntity.class);
|
||||||
folder.setTags(DocUtil.toTagStr(req.getTags()));
|
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));
|
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.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.blossom.backend.server.article.TagEnum;
|
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.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.FolderEntity;
|
||||||
import com.blossom.backend.server.folder.pojo.FolderQueryReq;
|
|
||||||
import com.blossom.backend.server.folder.pojo.FolderSubjectRes;
|
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.picture.pojo.PictureEntity;
|
||||||
import com.blossom.backend.server.utils.DocUtil;
|
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.XzException400;
|
||||||
import com.blossom.common.base.exception.XzException404;
|
import com.blossom.common.base.exception.XzException404;
|
||||||
import com.blossom.common.base.exception.XzException500;
|
import com.blossom.common.base.exception.XzException500;
|
||||||
@ -29,7 +25,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,19 +35,12 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
||||||
private ArticleService articleService;
|
|
||||||
private PictureService pictureService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setArticleService(ArticleService articleService) {
|
private PictureMapper picMapper;
|
||||||
this.articleService = articleService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public void setPictureService(PictureService pictureService) {
|
private ArticleMapper articleMapper;
|
||||||
this.pictureService = pictureService;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,9 +57,11 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
|||||||
FolderEntity where = new FolderEntity();
|
FolderEntity where = new FolderEntity();
|
||||||
where.setTags(TagEnum.subject.name());
|
where.setTags(TagEnum.subject.name());
|
||||||
where.setUserId(userId);
|
where.setUserId(userId);
|
||||||
if (Objects.nonNull(starStatus) &&
|
if (null != starStatus && (starStatus.equals(1) || starStatus.equals(0))) {
|
||||||
(starStatus.equals(1) || starStatus.equals(0))) { where.setStarStatus(starStatus); }
|
where.setStarStatus(starStatus);
|
||||||
else {where.setStarStatus(0);}
|
} else {
|
||||||
|
where.setStarStatus(0);
|
||||||
|
}
|
||||||
List<FolderEntity> allOpenSubject = baseMapper.listAll(where);
|
List<FolderEntity> allOpenSubject = baseMapper.listAll(where);
|
||||||
if (CollUtil.isEmpty(allOpenSubject)) {
|
if (CollUtil.isEmpty(allOpenSubject)) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
@ -85,12 +75,10 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
|||||||
allOpenSubjectIds.addAll(allOpenSubjectChildFolders.stream().map(FolderEntity::getId).collect(Collectors.toList()));
|
allOpenSubjectIds.addAll(allOpenSubjectChildFolders.stream().map(FolderEntity::getId).collect(Collectors.toList()));
|
||||||
|
|
||||||
// 3. 查询这些文件夹下的所有文章
|
// 3. 查询这些文件夹下的所有文章
|
||||||
ArticleQueryReq articleWhere = new ArticleQueryReq();
|
ArticleEntity articleWhere = new ArticleEntity();
|
||||||
articleWhere.setPids(allOpenSubjectIds);
|
articleWhere.setPids(allOpenSubjectIds);
|
||||||
articleWhere.setUserId(userId);
|
articleWhere.setUserId(userId);
|
||||||
// 统计专题信息时, 会包含非公开文章
|
List<ArticleEntity> articles = articleMapper.listAll(articleWhere);
|
||||||
// articleWhere.setOpenStatus(YesNo.YES.getValue());
|
|
||||||
List<ArticleEntity> articles = articleService.listAll(articleWhere);
|
|
||||||
|
|
||||||
List<FolderSubjectRes> results = new ArrayList<>();
|
List<FolderSubjectRes> results = new ArrayList<>();
|
||||||
|
|
||||||
@ -123,34 +111,6 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
|||||||
return results;
|
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查询
|
* 根据ID查询
|
||||||
*
|
*
|
||||||
@ -200,9 +160,52 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
|||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Long update(FolderEntity folder) {
|
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不得为空");
|
XzException404.throwBy(folder.getId() == null, "ID不得为空");
|
||||||
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
|
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
|
||||||
if (Objects.isNull(folder.getStarStatus())) {folder.setStarStatus(0);}
|
|
||||||
// 如果
|
// 如果
|
||||||
if (StrUtil.isNotBlank(folder.getStorePath())) {
|
if (StrUtil.isNotBlank(folder.getStorePath())) {
|
||||||
final FolderEntity oldFolder = selectById(folder.getId());
|
final FolderEntity oldFolder = selectById(folder.getId());
|
||||||
@ -221,9 +224,6 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
folder.setStorePath(formatStorePath(folder.getStorePath()));
|
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)
|
private void updateParamValid(FolderEntity folder) {
|
||||||
public void delete(Long folderId) {
|
XzException404.throwBy(folder.getId() == null, "ID不得为空");
|
||||||
// 文件夹下有文件夹, 无法删除
|
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -28,4 +28,14 @@ public enum FolderTypeEnum {
|
|||||||
this.type = type;
|
this.type = type;
|
||||||
this.desc = desc;
|
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;
|
package com.blossom.backend.server.folder.pojo;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.blossom.backend.server.folder.FolderTypeEnum;
|
import com.blossom.backend.server.folder.FolderTypeEnum;
|
||||||
import com.blossom.common.base.pojo.AbstractPOJO;
|
import com.blossom.common.base.pojo.AbstractPOJO;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -18,7 +15,6 @@ import java.util.List;
|
|||||||
*
|
*
|
||||||
* @author xzzz
|
* @author xzzz
|
||||||
*/
|
*/
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("blossom_folder")
|
@TableName("blossom_folder")
|
||||||
public class FolderEntity extends AbstractPOJO implements Serializable {
|
public class FolderEntity extends AbstractPOJO implements Serializable {
|
||||||
@ -106,6 +102,26 @@ public class FolderEntity extends AbstractPOJO implements Serializable {
|
|||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private List<Long> ids;
|
private List<Long> ids;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 父ID集合
|
||||||
|
*/
|
||||||
|
@TableField(exist = false)
|
||||||
|
private List<Long> pids;
|
||||||
|
|
||||||
//endregion
|
//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());
|
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());
|
tree.setVd(YesNo.YES.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
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
|
username: root
|
||||||
password: jasmine888
|
password: jasmine888
|
||||||
hikari:
|
hikari:
|
||||||
@ -9,7 +9,7 @@ spring:
|
|||||||
|
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
com.blossom: debug
|
com.blossom: info
|
||||||
com.blossom.expand.tracker: info
|
com.blossom.expand.tracker: info
|
||||||
com.blossom.backend.base.auth: info
|
com.blossom.backend.base.auth: info
|
||||||
org.springframework.boot.web.embedded.tomcat.TomcatWebServer: warn
|
org.springframework.boot.web.embedded.tomcat.TomcatWebServer: warn
|
||||||
|
|||||||
@ -5,9 +5,9 @@
|
|||||||
<select id="selectMaxSortByPid" resultType="java.lang.Integer">
|
<select id="selectMaxSortByPid" resultType="java.lang.Integer">
|
||||||
select IFNULL(max(h.sort), 0)
|
select IFNULL(max(h.sort), 0)
|
||||||
from (
|
from (
|
||||||
select sort from blossom_article where pid = #{pid}
|
select sort from blossom_article where pid = #{pid} and user_id = #{userId}
|
||||||
union
|
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;
|
) h;
|
||||||
</select>
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
@ -26,6 +26,10 @@
|
|||||||
<if test="openStatus != null ">and open_status = #{openStatus}</if>
|
<if test="openStatus != null ">and open_status = #{openStatus}</if>
|
||||||
<if test="type != null ">and type = #{type}</if>
|
<if test="type != null ">and type = #{type}</if>
|
||||||
<if test="tags != null and tags != ''">and tags like concat('%',#{tags},'%')</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>
|
<if test="userId != null">and user_id = #{userId}</if>
|
||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElConfigProvider } from 'element-plus'
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@ -94,6 +94,15 @@ export const docTreeApi = (params?: object): Promise<R<any>> => {
|
|||||||
return rq.get<R<any>>('/doc/trees', { params })
|
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
|
//#endregion
|
||||||
|
|
||||||
//#region ====================================================< folder >====================================================
|
//#region ====================================================< folder >====================================================
|
||||||
@ -440,16 +449,6 @@ export const articleBackupListApi = (): Promise<R<BackupFile[]>> => {
|
|||||||
return rq.get<BackupFile[]>('/article/backup/list')
|
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
|
* @param data
|
||||||
|
|||||||
@ -90,7 +90,7 @@ export class Request {
|
|||||||
/* 其他接口报错, 直接拒绝并提示错误信息 */
|
/* 其他接口报错, 直接拒绝并提示错误信息 */
|
||||||
let errorResponse = data
|
let errorResponse = data
|
||||||
errorResponse['url'] = res.config.url
|
errorResponse['url'] = res.config.url
|
||||||
Notify.error(data.msg, '请求失败')
|
Notify.error(data.msg, '处理失败')
|
||||||
return Promise.reject(res)
|
return Promise.reject(res)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -106,22 +106,23 @@ export class Request {
|
|||||||
}
|
}
|
||||||
let code = err.code
|
let code = err.code
|
||||||
let resp = err.response
|
let resp = err.response
|
||||||
if (resp && resp.data) {
|
console.log("🚀 ~ Request ~ constructor ~ resp:123123123", resp)
|
||||||
Notify.error(resp.data.msg, '请求失败')
|
|
||||||
return Promise.reject(err)
|
|
||||||
}
|
|
||||||
if (code === 'ERR_NETWORK') {
|
if (code === 'ERR_NETWORK') {
|
||||||
Notify.error('网络错误,请检查您的网络是否通畅', '请求失败')
|
Notify.error('网络错误, 请检查您的网络是否通畅', '请求失败')
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
if (err.request && err.request.status === 404) {
|
if (err.request && err.request.status === 404) {
|
||||||
Notify.error('未找到您的请求, 请您检查服务器地址!', '请求失败(404)')
|
Notify.error('未找到您的请求', '请求失败(404)')
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
if (err.request && err.request.status === 405) {
|
if (err.request && err.request.status === 405) {
|
||||||
Notify.error(`您的请求地址可能有误, 请检查请求地址${url}`, '请求失败(405)')
|
Notify.error(`您的请求地址可能有误, 请检查请求地址${url}`, '请求失败(405)')
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
|
if (resp && resp.data) {
|
||||||
|
Notify.error(resp.data.msg, '请求失败')
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@ -12,4 +12,4 @@
|
|||||||
@import './bl-tip.scss';
|
@import './bl-tip.scss';
|
||||||
@import './bl-tooltip.scss';
|
@import './bl-tooltip.scss';
|
||||||
@import './bl-tree.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))
|
let prefix = image.substring(0, Math.max(image.indexOf(','), 0))
|
||||||
return prefix.startsWith('data:image') && prefix.endsWith('base64')
|
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()
|
userinfo: (Local.get(userinfoKey) as Userinfo) || initUserinfo()
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
|
currentUserId(state) {
|
||||||
|
return state.userinfo.id
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 是否登录
|
* 是否登录
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -8,14 +8,14 @@
|
|||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<el-upload
|
<el-upload
|
||||||
multiple
|
|
||||||
class="article-upload"
|
|
||||||
ref="uploadRef"
|
ref="uploadRef"
|
||||||
name="file"
|
name="file"
|
||||||
|
multiple
|
||||||
:action="serverStore.serverUrl + articleImportApiUrl"
|
:action="serverStore.serverUrl + articleImportApiUrl"
|
||||||
:data="{ pid: porps.doc.i }"
|
:data="{ pid: porps.doc.i, batchId: importBatch }"
|
||||||
:headers="{ Authorization: 'Bearer ' + userStore.auth.token }"
|
:headers="{ Authorization: 'Bearer ' + userStore.auth.token }"
|
||||||
:on-change="onChange"
|
:on-change="onChange"
|
||||||
|
:show-file-list="true"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:on-success="onUploadSeccess"
|
:on-success="onUploadSeccess"
|
||||||
:on-error="onError"
|
:on-error="onError"
|
||||||
@ -38,6 +38,7 @@ import type { UploadInstance } from 'element-plus'
|
|||||||
import { articleImportApiUrl } from '@renderer/api/blossom'
|
import { articleImportApiUrl } from '@renderer/api/blossom'
|
||||||
import { useUserStore } from '@renderer/stores/user'
|
import { useUserStore } from '@renderer/stores/user'
|
||||||
import { useServerStore } from '@renderer/stores/server'
|
import { useServerStore } from '@renderer/stores/server'
|
||||||
|
import { uuid } from '@renderer/assets/utils/util'
|
||||||
import { onChange, beforeUpload, onUploadSeccess, onError } from './scripts/article-import'
|
import { onChange, beforeUpload, onUploadSeccess, onError } from './scripts/article-import'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -50,6 +51,8 @@ const porps = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const importBatch = ref(uuid())
|
||||||
|
|
||||||
const uploadRef = ref<UploadInstance>()
|
const uploadRef = ref<UploadInstance>()
|
||||||
|
|
||||||
const submitUpload = () => {
|
const submitUpload = () => {
|
||||||
@ -66,21 +69,21 @@ const submitUpload = () => {
|
|||||||
.content {
|
.content {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
||||||
.upload-tip {
|
// .upload-tip {
|
||||||
border: 1px solid var(--el-border-color);
|
// border: 1px solid var(--el-border-color);
|
||||||
padding: 5px 10px;
|
// padding: 5px 10px;
|
||||||
border-radius: 5px;
|
// border-radius: 5px;
|
||||||
color: rgb(188, 55, 55);
|
// color: rgb(188, 55, 55);
|
||||||
}
|
// }
|
||||||
|
|
||||||
.article-upload {
|
// .article-upload {
|
||||||
:deep(.el-upload-list) {
|
// :deep(.el-upload-list) {
|
||||||
li {
|
// li {
|
||||||
transition: none;
|
// transition: none;
|
||||||
margin-bottom: 0;
|
// margin-bottom: 0;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -222,7 +222,7 @@ import { ref, nextTick, inject, computed, watch, Ref } from 'vue'
|
|||||||
import { ElInput, ElMessageBox, FormInstance } from 'element-plus'
|
import { ElInput, ElMessageBox, FormInstance } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
import { Document } from '@element-plus/icons-vue'
|
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 { useUserStore } from '@renderer/stores/user'
|
||||||
import {
|
import {
|
||||||
folderInfoApi,
|
folderInfoApi,
|
||||||
@ -388,6 +388,10 @@ const formatStorePath = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showStorePathWarning = ref(false)
|
const showStorePathWarning = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 填充文件夹路径
|
||||||
|
*/
|
||||||
const fillStorePath = (id: string, path: string = ''): void => {
|
const fillStorePath = (id: string, path: string = ''): void => {
|
||||||
let doc = getDocById(id, docTreeData!.value)
|
let doc = getDocById(id, docTreeData!.value)
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
@ -542,13 +546,9 @@ const saveDoc = async (formEl: FormInstance | undefined) => {
|
|||||||
await formEl.validate((valid, _fields) => {
|
await formEl.validate((valid, _fields) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
saveLoading.value = true
|
saveLoading.value = true
|
||||||
if (!checkLevel(docForm.value.pid, docTreeData!.value)) {
|
|
||||||
saveLoading.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const handleResp = (_: any) => {
|
const handleResp = (_: any) => {
|
||||||
Notify.success(curDocDialogType.value === 'upd' ? `修改《${docForm.value.name}》成功` : `新增《${docForm.value.name}》成功`)
|
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)
|
const handleFinally = () => setTimeout(() => (saveLoading.value = false), 300)
|
||||||
// 新增文件夹
|
// 新增文件夹
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="[viewStyle.isShowSubjectStyle ? (isSubject ? 'subject-title' : 'doc-title') : 'doc-title']">
|
<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 }}
|
{{ props.trees.s }}
|
||||||
</bl-tag>
|
</bl-tag>
|
||||||
<div class="doc-name">
|
<div class="doc-name">
|
||||||
@ -30,9 +30,6 @@
|
|||||||
{{ tag.content }}
|
{{ tag.content }}
|
||||||
</bl-tag>
|
</bl-tag>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="viewStyle.isShowArticleType"
|
v-if="viewStyle.isShowArticleType"
|
||||||
v-for="(line, index) in tagLins"
|
v-for="(line, index) in tagLins"
|
||||||
@ -47,19 +44,13 @@ import { computed } from 'vue'
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { useConfigStore } from '@renderer/stores/config'
|
import { useConfigStore } from '@renderer/stores/config'
|
||||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||||
import { computedDocTitleColor } from '@renderer/views/doc/doc'
|
|
||||||
import { articleUpdNameApi, folderUpdNameApi } from '@renderer/api/blossom'
|
import { articleUpdNameApi, folderUpdNameApi } from '@renderer/api/blossom'
|
||||||
|
|
||||||
const { viewStyle } = useConfigStore()
|
const { viewStyle } = useConfigStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
trees: { type: Object as PropType<DocTree>, default: {} },
|
trees: { type: Object as PropType<DocTree>, default: {} },
|
||||||
size: { type: Number, default: 14 },
|
size: { type: Number, default: 14 }
|
||||||
level: { type: Number, required: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
const levelColor = computed(() => {
|
|
||||||
return computedDocTitleColor(props.level)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const nameWrapperStyle = computed(() => {
|
const nameWrapperStyle = computed(() => {
|
||||||
@ -137,8 +128,8 @@ $icon-size: 17px;
|
|||||||
.doc-title {
|
.doc-title {
|
||||||
@include flex(row, flex-start, flex-start);
|
@include flex(row, flex-start, flex-start);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 1px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 1px 0;
|
||||||
|
|
||||||
.doc-name {
|
.doc-name {
|
||||||
@include flex(row, flex-start, flex-start);
|
@include flex(row, flex-start, flex-start);
|
||||||
@ -147,12 +138,13 @@ $icon-size: 17px;
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.menu-icon,
|
.menu-icon,
|
||||||
.menu-icon-img {
|
.menu-icon-img {
|
||||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||||
margin-top: 5px;
|
margin-top: 4px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
.menu-icon-img {
|
.menu-icon-img {
|
||||||
@ -160,7 +152,8 @@ $icon-size: 17px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-wrapper {
|
.name-wrapper {
|
||||||
@include ellipsis();
|
min-height: 23px;
|
||||||
|
line-height: 23px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,27 +164,18 @@ $icon-size: 17px;
|
|||||||
top: 2px;
|
top: 2px;
|
||||||
z-index: 10;
|
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 {
|
.subject-title {
|
||||||
@include flex(row, flex-start, flex-start);
|
@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));
|
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-height: 44px;
|
||||||
min-width: calc(100% - 15px);
|
max-width: 300px;
|
||||||
padding: 2px 5px;
|
width: calc(100% - 10px);
|
||||||
margin: 5px 0 10px 0;
|
padding: 4px 5px;
|
||||||
|
margin: 5px 0 5px 0;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@ -206,7 +190,6 @@ $icon-size: 17px;
|
|||||||
.menu-icon,
|
.menu-icon,
|
||||||
.menu-icon-img {
|
.menu-icon-img {
|
||||||
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
@include box($icon-size, $icon-size, $icon-size, $icon-size, $icon-size, $icon-size);
|
||||||
margin-top: 5px;
|
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +198,6 @@ $icon-size: 17px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-wrapper {
|
.name-wrapper {
|
||||||
@include ellipsis();
|
|
||||||
max-width: calc(100% - 25px);
|
max-width: calc(100% - 25px);
|
||||||
min-width: calc(100% - 25px);
|
min-width: calc(100% - 25px);
|
||||||
}
|
}
|
||||||
@ -223,13 +205,9 @@ $icon-size: 17px;
|
|||||||
|
|
||||||
.sort {
|
.sort {
|
||||||
position: absolute;
|
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 {
|
.sync-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 2px;
|
width: 2px;
|
||||||
height: 60%;
|
height: 70%;
|
||||||
top: 20%;
|
top: 15%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,154 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="doc-workbench-root">
|
<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-container">
|
||||||
<bl-row class="wb-page-item" just="flex-start" align="flex-end" width="calc(100% - 16px)" height="44px">
|
<bl-row class="wb-page-item" just="flex-start" align="flex-end" height="44px">
|
||||||
<el-tooltip
|
<el-tooltip content="文章引用网络" effect="light" popper-class="is-small" placement="top" :offset="-5" :hide-after="0">
|
||||||
content="文章引用网络"
|
|
||||||
effect="light"
|
|
||||||
popper-class="is-small"
|
|
||||||
transition="none"
|
|
||||||
placement="top"
|
|
||||||
:show-arrow="false"
|
|
||||||
:offset="-5"
|
|
||||||
:hide-after="0">
|
|
||||||
<div class="iconbl bl-correlation-line" @click="openArticleReferenceWindow()"></div>
|
<div class="iconbl bl-correlation-line" @click="openArticleReferenceWindow()"></div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip
|
<el-tooltip content="全文搜索" effect="light" popper-class="is-small" placement="top" :offset="9" :hide-after="0">
|
||||||
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">
|
|
||||||
<div class="iconbl bl-search-line" @click="showSearch()"></div>
|
<div class="iconbl bl-search-line" @click="showSearch()"></div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip effect="light" popper-class="is-small" transition="none" placement="top" :show-arrow="false" :offset="5" :hide-after="0">
|
<el-tooltip content="备份记录" effect="light" popper-class="is-small" placement="top" :offset="8" :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">
|
|
||||||
<div class="iconbl bl-a-cloudstorage-line" @click="handleShowBackupDialog"></div>
|
<div class="iconbl bl-a-cloudstorage-line" @click="handleShowBackupDialog"></div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip
|
<el-tooltip content="文章回收站" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
|
||||||
content="文章回收站"
|
|
||||||
effect="light"
|
|
||||||
popper-class="is-small"
|
|
||||||
transition="none"
|
|
||||||
placement="top"
|
|
||||||
:show-arrow="false"
|
|
||||||
:offset="8"
|
|
||||||
:hide-after="0">
|
|
||||||
<div class="iconbl bl-delete-line" @click="handleShowRecycleDialog"></div>
|
<div class="iconbl bl-delete-line" @click="handleShowRecycleDialog"></div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip
|
<el-tooltip content="文档快速编辑" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
|
||||||
content="查看收藏"
|
<div class="iconbl bl-article-line" @click=""></div>
|
||||||
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>
|
</el-tooltip>
|
||||||
</bl-row>
|
</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>
|
</bl-row>
|
||||||
</div>
|
</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
|
<el-dialog
|
||||||
class="bl-dialog-fixed-body"
|
class="bl-dialog-fixed-body"
|
||||||
v-model="isShowBackupDialog"
|
v-model="isShowBackupDialog"
|
||||||
@ -174,25 +46,13 @@
|
|||||||
draggable>
|
draggable>
|
||||||
<ArticleRecycle ref="ArticleRecycleRef"></ArticleRecycle>
|
<ArticleRecycle ref="ArticleRecycleRef"></ArticleRecycle>
|
||||||
</el-dialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, inject, onDeactivated } from 'vue'
|
import { ref, nextTick, onDeactivated } from 'vue'
|
||||||
import { provideKeyCurArticleInfo, SortLevelColor } from '@renderer/views/doc/doc'
|
|
||||||
import { openNewArticleReferenceWindow } from '@renderer/assets/utils/electron'
|
import { openNewArticleReferenceWindow } from '@renderer/assets/utils/electron'
|
||||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||||
import hotkeys from 'hotkeys-js'
|
import hotkeys from 'hotkeys-js'
|
||||||
import ArticleInfo from './ArticleInfo.vue'
|
|
||||||
import ArticleBackup from './ArticleBackup.vue'
|
import ArticleBackup from './ArticleBackup.vue'
|
||||||
import ArticleRecycle from './ArticleRecycle.vue'
|
import ArticleRecycle from './ArticleRecycle.vue'
|
||||||
|
|
||||||
@ -205,43 +65,9 @@ onDeactivated(() => {
|
|||||||
unbindKeys()
|
unbindKeys()
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
showOpen: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showSubject: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showStar: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
//#region --------------------------------------------------< 控制台更多选项 >--------------------------------------------------
|
//#region --------------------------------------------------< 控制台更多选项 >--------------------------------------------------
|
||||||
const moreMenu = ref<RightMenu>({ show: false, clientX: 0, clientY: 0 })
|
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) => {
|
const closeMoreMenu = (event: MouseEvent) => {
|
||||||
if (event.target) {
|
if (event.target) {
|
||||||
let isPrevent = (event.target as HTMLElement).getAttribute('data-bl-prevet')
|
let isPrevent = (event.target as HTMLElement).getAttribute('data-bl-prevet')
|
||||||
@ -257,36 +83,6 @@ const closeMoreMenu = (event: MouseEvent) => {
|
|||||||
|
|
||||||
//#endregion
|
//#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 --------------------------------------------------< 新增窗口 >--------------------------------------------------
|
//#region --------------------------------------------------< 新增窗口 >--------------------------------------------------
|
||||||
const ArticleInfoRef = ref()
|
const ArticleInfoRef = ref()
|
||||||
const isShowDocInfoDialog = ref<boolean>(false)
|
const isShowDocInfoDialog = ref<boolean>(false)
|
||||||
@ -302,23 +98,6 @@ const openArticleReferenceWindow = () => {
|
|||||||
openNewArticleReferenceWindow()
|
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 = () => {
|
const showSearch = () => {
|
||||||
emits('show-search')
|
emits('show-search')
|
||||||
}
|
}
|
||||||
@ -363,7 +142,7 @@ const unbindKeys = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
const emits = defineEmits(['refreshDocTree', 'show-sort', 'show-search'])
|
const emits = defineEmits(['show-sort', 'show-search'])
|
||||||
defineExpose({ handleShowBackupDialog })
|
defineExpose({ handleShowBackupDialog })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -387,14 +166,6 @@ defineExpose({ handleShowBackupDialog })
|
|||||||
height: 0px;
|
height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排序
|
|
||||||
.bl-a-leftdirection-line {
|
|
||||||
font-size: 27px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
padding-right: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
.bl-search-line {
|
.bl-search-line {
|
||||||
font-size: 23px;
|
font-size: 23px;
|
||||||
@ -408,14 +179,6 @@ defineExpose({ handleShowBackupDialog })
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新图标
|
|
||||||
.bl-a-cloudrefresh-line,
|
|
||||||
.bl-a-fileadd-line {
|
|
||||||
&:active {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bl-correlation-line {
|
.bl-correlation-line {
|
||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0px;
|
||||||
|
|||||||
@ -1,21 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor-status-root">
|
<div class="editor-status-root">
|
||||||
<bl-row width="calc(100% - 240px)" height="100%" class="status-item-container">
|
<bl-row width="calc(100% - 240px)" height="100%" class="status-item-container">
|
||||||
<div>
|
<div>《{{ curDoc?.name }}》</div>
|
||||||
《{{ curDoc?.name }}》
|
<div>ID:{{ curDoc?.id }}</div>
|
||||||
</div>
|
<div>版本:{{ curDoc?.version }}</div>
|
||||||
<div>
|
<div>字数:{{ curDoc?.words }}</div>
|
||||||
版本:{{ curDoc?.version }}
|
<div>最近修改:{{ curDoc?.updTime }}</div>
|
||||||
</div>
|
<div v-if="curDoc?.openTime">发布:{{ curDoc?.openTime }}</div>
|
||||||
<div>
|
|
||||||
字数:{{ curDoc?.words }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
最近修改:{{ curDoc?.updTime }}
|
|
||||||
</div>
|
|
||||||
<div v-if="curDoc?.openTime">
|
|
||||||
发布:{{ curDoc?.openTime }}
|
|
||||||
</div>
|
|
||||||
</bl-row>
|
</bl-row>
|
||||||
<bl-row just="flex-end" width="240px" height="100%" class="status-item-container">
|
<bl-row just="flex-end" width="240px" height="100%" class="status-item-container">
|
||||||
<div @click="openArticleLogWindow">
|
<div @click="openArticleLogWindow">
|
||||||
@ -26,9 +17,7 @@
|
|||||||
<span class="iconbl bl-correlation-line"></span>
|
<span class="iconbl bl-correlation-line"></span>
|
||||||
引用网络
|
引用网络
|
||||||
</div>
|
</div>
|
||||||
<bl-col width="100px" just="center">
|
<bl-col width="100px" just="center"> 渲染用时: {{ props.renderInterval }}ms </bl-col>
|
||||||
渲染用时: {{ props.renderInterval }}ms
|
|
||||||
</bl-col>
|
|
||||||
</bl-row>
|
</bl-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -37,7 +26,7 @@
|
|||||||
import { inject, toRaw } from 'vue'
|
import { inject, toRaw } from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { provideKeyCurArticleInfo } from '@renderer/views/doc/doc'
|
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({
|
const props = defineProps({
|
||||||
renderInterval: {
|
renderInterval: {
|
||||||
@ -73,7 +62,7 @@ const openArticleLogWindow = () => {
|
|||||||
overflow-x: overlay;
|
overflow-x: overlay;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&>div {
|
& > div {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -96,4 +85,4 @@ const openArticleLogWindow = () => {
|
|||||||
@include flex(row, flex-start, center);
|
@include flex(row, flex-start, center);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -378,7 +378,6 @@ const handleShowHotKeyDialog = () => {
|
|||||||
.editor-tools-root {
|
.editor-tools-root {
|
||||||
@include box(calc(100% - 20px), 35px);
|
@include box(calc(100% - 20px), 35px);
|
||||||
@include themeShadow(0 1px 4px 1px #d3d3d3, 0 1px 4px 1px rgb(20, 20, 20));
|
@include themeShadow(0 1px 4px 1px #d3d3d3, 0 1px 4px 1px rgb(20, 20, 20));
|
||||||
margin: 5px 10px 10px 10px;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -426,7 +425,7 @@ const handleShowHotKeyDialog = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!--
|
<!--
|
||||||
快捷键说明为弹出框 需要设置全局的样式
|
快捷键说明为弹出框 需要设置全局的样式
|
||||||
-->
|
-->
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import Notify from '@renderer/scripts/notify'
|
|||||||
* 添加文件的检查, 当前支持如下格式:
|
* 添加文件的检查, 当前支持如下格式:
|
||||||
* .txt
|
* .txt
|
||||||
* .md
|
* .md
|
||||||
*
|
*
|
||||||
* @param _file
|
* @param _file
|
||||||
* @param files
|
* @param files
|
||||||
*/
|
*/
|
||||||
export const onChange: UploadProps['onChange'] = (_file: UploadUserFile, files: UploadUserFile[]) => {
|
export const onChange: UploadProps['onChange'] = (_file: UploadUserFile, files: UploadUserFile[]) => {
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
@ -19,11 +19,10 @@ export const onChange: UploadProps['onChange'] = (_file: UploadUserFile, files:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param rawFile
|
* @param rawFile
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
export const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||||
if (rawFile.size / 1024 / 1024 > 10) {
|
if (rawFile.size / 1024 / 1024 > 10) {
|
||||||
@ -34,14 +33,19 @@ export const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件成功
|
* 文件上传成功时的钩子, 成功代表 http 成功, 不代表后台处理成功, 后台处理成功的返回 20000 之外的状态的, 并在这里重置为其他状态
|
||||||
* @param resp
|
* 'ready' | 'uploading' | 'success' | 'fail'
|
||||||
* @param _file
|
*
|
||||||
|
* @param resp
|
||||||
|
* @param _file
|
||||||
*/
|
*/
|
||||||
export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file: UploadFile, _files: UploadFiles) => {
|
export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file: UploadFile, _files: UploadFiles) => {
|
||||||
const result = handleUploadSeccess(resp)
|
const result = handleUploadSeccess(resp)
|
||||||
|
console.log(_file)
|
||||||
if (result) {
|
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 => {
|
export const handleUploadSeccess = (resp: any): boolean => {
|
||||||
if (resp.code === '20000') {
|
if (resp.code === '20000') {
|
||||||
Notify.success('上传成功')
|
// Notify.success('上传成功')
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
Notify.error(resp.msg, '上传失败')
|
Notify.error(resp.msg, '上传失败')
|
||||||
@ -61,23 +65,24 @@ export const handleUploadSeccess = (resp: any): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param error
|
* @param error
|
||||||
* @param _file
|
* @param _file
|
||||||
* @param _files
|
* @param _files
|
||||||
*/
|
*/
|
||||||
export const onError: UploadProps['onError'] = (error, _file, _files) => {
|
export const onError: UploadProps['onError'] = (error, _file, _files) => {
|
||||||
handleUploadError(error)
|
handleUploadError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* 上传失败时的处理, 失败是根据 http 响应码决定, 通常是网络失败等原因
|
||||||
* @param error
|
* @param error
|
||||||
*/
|
*/
|
||||||
export const handleUploadError = (error: Error) => {
|
export const handleUploadError = (error: Error) => {
|
||||||
|
console.log('🚀 ~ handleUploadError ~ error:', error)
|
||||||
if (error.message != undefined) {
|
if (error.message != undefined) {
|
||||||
try {
|
try {
|
||||||
let resp = JSON.parse(error.message);
|
let resp = JSON.parse(error.message)
|
||||||
if (resp != undefined) {
|
if (resp != undefined) {
|
||||||
Notify.error(resp.msg, '上传失败')
|
Notify.error(resp.msg, '上传失败')
|
||||||
}
|
}
|
||||||
@ -85,4 +90,4 @@ export const handleUploadError = (error: Error) => {
|
|||||||
Notify.error(error.message, '上传失败')
|
Notify.error(error.message, '上传失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,4 +94,27 @@ export const parseTocAsync = async (ele: HTMLElement): Promise<Toc[]> => {
|
|||||||
tocs.push(toc)
|
tocs.push(toc)
|
||||||
}
|
}
|
||||||
return tocs
|
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;
|
position: relative;
|
||||||
background-color: #ffffff00;
|
background-color: #ffffff00;
|
||||||
|
|
||||||
$heightTools: 45px;
|
$heightTools: 50px;
|
||||||
$heightStatus: 28px;
|
$heightStatus: 28px;
|
||||||
$heightEP: calc(100% - 5px - #{$heightStatus} - #{$heightTools});
|
$heightEP: calc(100% - #{$heightStatus} - #{$heightTools});
|
||||||
|
|
||||||
.editor-tools {
|
.editor-tools {
|
||||||
@include box(100%, $heightTools);
|
@include box(100%, $heightTools);
|
||||||
|
padding: 5px 10px 10px 10px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-preview {
|
.editor-preview {
|
||||||
@include box(100%, $heightEP);
|
@include box(100%, $heightEP);
|
||||||
@include flex(row, flex-start, center);
|
@include flex(row, flex-start, center);
|
||||||
border-top: 1px solid var(--el-border-color);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
@ -137,6 +138,11 @@
|
|||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|
||||||
|
/* 定义滑块 内阴影+圆角 */
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(*) {
|
:deep(*) {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
@ -234,6 +240,11 @@
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
|
/* 定义滑块 内阴影+圆角 */
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.katex > *) {
|
:deep(.katex > *) {
|
||||||
font-size: 1.2em !important;
|
font-size: 1.2em !important;
|
||||||
font-family: 'KaTeX_Size1', sans-serif !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 pid 上级ID
|
||||||
* @param trees 树状列表
|
* @param trees 树状列表
|
||||||
* @returns
|
* @returns
|
||||||
|
* @deprecated 1.14.0 支持无限菜单后不需要控制层级
|
||||||
*/
|
*/
|
||||||
export const checkLevel = (pid: string, trees: DocTree[]): boolean => {
|
export const checkLevel = (pid: string, trees: DocTree[]): boolean => {
|
||||||
let parents = getPDocsByPid(pid, trees)
|
// let parents = getPDocsByPid(pid, trees)
|
||||||
if (parents.length >= 4) {
|
// if (parents.length >= 4) {
|
||||||
Notify.error('最多仅支持4级层级关系', '菜单层级错误')
|
// Notify.error('最多仅支持4级层级关系', '菜单层级错误')
|
||||||
return false
|
// return false
|
||||||
}
|
// }
|
||||||
return true
|
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 { ElInput } from 'element-plus'
|
||||||
import type { FormRules } from 'element-plus'
|
import type { FormRules } from 'element-plus'
|
||||||
import { Document } from '@element-plus/icons-vue'
|
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 { useUserStore } from '@renderer/stores/user'
|
||||||
import { folderInfoApi, folderAddApi, folderUpdApi } from '@renderer/api/blossom'
|
import { folderInfoApi, folderAddApi, folderUpdApi } from '@renderer/api/blossom'
|
||||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||||
@ -272,14 +272,10 @@ const fillStorePath = (id: string, path: string = ''): void => {
|
|||||||
const saveLoading = ref<boolean>(false)
|
const saveLoading = ref<boolean>(false)
|
||||||
const saveDoc = () => {
|
const saveDoc = () => {
|
||||||
saveLoading.value = true
|
saveLoading.value = true
|
||||||
if (!checkLevel(docForm.value.pid, docTreeData!.value)) {
|
|
||||||
saveLoading.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// then 回调
|
// then 回调
|
||||||
const handleResp = (_: any) => {
|
const handleResp = (_: any) => {
|
||||||
Notify.success(curDocDialogType === 'upd' ? `修改《${docForm.value.name}》成功` : `新增《${docForm.value.name}》成功`)
|
Notify.success(curDocDialogType === 'upd' ? `修改《${docForm.value.name}》成功` : `新增《${docForm.value.name}》成功`)
|
||||||
emits('saved')
|
emits('saved', curDocDialogType, docForm.value)
|
||||||
}
|
}
|
||||||
const handleFinally = () => setTimeout(() => (saveLoading.value = false), 300)
|
const handleFinally = () => setTimeout(() => (saveLoading.value = false), 300)
|
||||||
if (curDocDialogType == 'add')
|
if (curDocDialogType == 'add')
|
||||||
|
|||||||
@ -1,93 +1,92 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 文件夹操作 -->
|
<!-- 文件夹操作 -->
|
||||||
<div class="doc-workbench">
|
<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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
ref="DocTreeContainer"
|
||||||
class="doc-trees-container"
|
class="doc-trees-container"
|
||||||
v-loading="docTreeLoading"
|
v-loading="docTreeLoading"
|
||||||
element-loading-text="正在读取文档..."
|
element-loading-text="正在读取文档..."
|
||||||
:style="{ fontSize: configStore.viewStyle.treeDocsFontSize }">
|
:style="{ fontSize: viewStyle.treeDocsFontSize }">
|
||||||
<!-- 文件夹 -->
|
<el-tree
|
||||||
<el-menu v-if="!isEmpty(docTreeData)" ref="DocTreeRef" class="doc-trees" :unique-opened="configStore.viewStyle.isMenuUniqueOpened">
|
v-if="docTreeData.length > 0"
|
||||||
<!-- ================================================ L1 ================================================ -->
|
ref="DocTreeRef"
|
||||||
<div v-for="L1 in docTreeData" :key="L1.i">
|
class="doc-tree"
|
||||||
<div v-if="L1.ty == 11" class="menu-divider" />
|
:data="docTreeData"
|
||||||
|
:allow-drag="handleAllowDrag"
|
||||||
<!-- L1无下级 -->
|
:allow-drop="handleAllowDrop"
|
||||||
<el-menu-item v-else-if="isEmpty(L1.children)" :index="L1.i">
|
:highlight-current="true"
|
||||||
<template #title>
|
:indent="14"
|
||||||
<div class="menu-item-wrapper" @click="clickCurDoc(L1)" @click.right="handleClickRight(L1, $event)">
|
:icon="ArrowRightBold"
|
||||||
<PictureTitle :trees="L1" :level="1" />
|
:accordion="false"
|
||||||
</div>
|
:default-expanded-keys="Array.from(docTreeCurrentExpandId)"
|
||||||
</template>
|
:filter-node-method="filterNode"
|
||||||
</el-menu-item>
|
:draggable="isBlank(treeFilterText)"
|
||||||
|
node-key="i"
|
||||||
<!-- L1有下级 -->
|
@nodeClick="clickCurDoc"
|
||||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L1.i">
|
@nodeExpand="handleNodeExpand"
|
||||||
<template #title>
|
@nodeCollapse="handleNodeCollapse"
|
||||||
<div class="menu-item-wrapper" @click="clickCurDoc(L1)" @click.right="handleClickRight(L1, $event)">
|
@nodeDrop="handleDrop">
|
||||||
<PictureTitle :trees="L1" :level="1" />
|
<template #default="{ node, data }">
|
||||||
</div>
|
<div v-if="data.ty === 11" class="menu-divider"></div>
|
||||||
</template>
|
<div v-else class="menu-item-wrapper" @click.right="handleClickRightMenu($event, data)">
|
||||||
|
<div class="doc-title">
|
||||||
<!-- ================================================ L2 ================================================ -->
|
<bl-tag v-if="isShowSort && data.ty === 2" class="sort" :bgColor="getColor(node)">
|
||||||
<div v-for="L2 in L1.children" :key="L2.i">
|
{{ data.s }}
|
||||||
<!-- L2无下级 -->
|
</bl-tag>
|
||||||
<el-menu-item v-if="isEmpty(L2.children)" :index="L2.i">
|
<div class="doc-name">
|
||||||
<template #title>
|
<img class="menu-icon-img" v-if="isShowImg(data, viewStyle)" :src="data.icon" />
|
||||||
<div class="menu-item-wrapper" @click="clickCurDoc(L2)" @click.right="handleClickRight(L2, $event)">
|
<svg v-else-if="isShowSvg(data, viewStyle)" class="icon menu-icon" aria-hidden="true">
|
||||||
<PictureTitle :trees="L2" :level="2" />
|
<use :xlink:href="'#' + data.icon"></use>
|
||||||
</div>
|
</svg>
|
||||||
</template>
|
<el-input
|
||||||
</el-menu-item>
|
v-if="data?.updn"
|
||||||
|
v-model="data.n"
|
||||||
<!-- L2有下级 -->
|
:id="'article-doc-name-' + data.i"
|
||||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L2.i">
|
@blur="blurArticleNameInput(data)"
|
||||||
<template #title>
|
@keyup.enter="blurArticleNameInput(data)"
|
||||||
<div class="menu-item-wrapper" @click="clickCurDoc(L2)" @click.right="handleClickRight(L2, $event)">
|
style="width: 95%"></el-input>
|
||||||
<PictureTitle :trees="L2" :level="2" />
|
<div v-else class="name-wrapper" :style="{ maxWidth: isNotBlank(data.icon) ? 'calc(100% - 25px)' : '100%' }">
|
||||||
</div>
|
{{ data.n }}
|
||||||
</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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</el-sub-menu>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</el-menu>
|
</el-tree>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右键菜单, 添加到 body 下 -->
|
<!-- 右键菜单, 添加到 body 下 -->
|
||||||
@ -96,14 +95,14 @@
|
|||||||
<div class="doc-name">{{ curDoc.n }}</div>
|
<div class="doc-name">{{ curDoc.n }}</div>
|
||||||
<div class="menu-content">
|
<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 ? '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>编辑详情
|
<span class="iconbl bl-a-fileedit-line"></span>编辑详情
|
||||||
</div>
|
</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>新增文件夹
|
<span class="iconbl bl-a-fileadd-line"></span>新增文件夹
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-item-divider"></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>删除文件夹
|
<span class="iconbl bl-a-fileprohibit-line"></span>删除文件夹
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -122,116 +121,355 @@
|
|||||||
draggable>
|
draggable>
|
||||||
<PictureInfo ref="PictureInfoRef" @saved="savedCallback"></PictureInfo>
|
<PictureInfo ref="PictureInfoRef" @saved="savedCallback"></PictureInfo>
|
||||||
</el-dialog>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, provide, nextTick, watch } from 'vue'
|
||||||
import { useConfigStore } from '@renderer/stores/config'
|
import { useConfigStore } from '@renderer/stores/config'
|
||||||
import { ref, provide, nextTick } from 'vue'
|
import { useUserStore } from '@renderer/stores/user'
|
||||||
import { ArrowDownBold, ArrowRightBold } from '@element-plus/icons-vue'
|
// element plus
|
||||||
import Workbench from './PictureTreeWorkbench.vue'
|
import { ElMessageBox, TreeNode } from 'element-plus'
|
||||||
import { docTreeApi, folderAddApi, folderDelApi } from '@renderer/api/blossom'
|
import { NodeDropType } from 'element-plus/es/components/tree/src/tree.type'
|
||||||
import { checkLevel, provideKeyDocTree } from '@renderer/views/doc/doc'
|
import { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
|
||||||
import { isEmpty } from 'lodash'
|
import { ArrowRightBold, Rank, Close } from '@element-plus/icons-vue'
|
||||||
import PictureTitle from './PictureTreeTitle.vue'
|
import Node from 'element-plus/es/components/tree/src/model/node'
|
||||||
import PictureInfo from '@renderer/views/picture/PictureInfo.vue'
|
// ts
|
||||||
import Notify from '@renderer/scripts/notify'
|
import { docTreeApi, docUpdSortApi, folderAddApi, folderDelApi, folderUpdNameApi } from '@renderer/api/blossom'
|
||||||
import { ElMessageBox, MenuInstance } from 'element-plus'
|
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'
|
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(
|
useLifecycle(
|
||||||
() => getDocTree(),
|
() => getDocTree(),
|
||||||
() => getDocTree()
|
() => getDocTree()
|
||||||
)
|
)
|
||||||
|
watch(
|
||||||
|
() => user.currentUserId,
|
||||||
|
(_newVal, _oldVal) => getDocTree()
|
||||||
|
)
|
||||||
|
|
||||||
|
//#region ----------------------------------------< 树状列表 >--------------------------------------
|
||||||
let editorLoadingTimeout: NodeJS.Timeout
|
let editorLoadingTimeout: NodeJS.Timeout
|
||||||
const DocTreeRef = ref<MenuInstance>()
|
const DocTreeRef = ref()
|
||||||
const docTreeLoading = ref(true) // 文档菜单的加载动画
|
const docTreeLoading = ref(true) // 文档菜单的加载动画
|
||||||
const showSort = ref(false) // 是否显示文档排序
|
const isShowSort = ref(false) // 是否显示文档排序
|
||||||
const docTreeData = ref<DocTree[]>([]) // 文档菜单
|
const docTreeData = ref<DocTree[]>([]) // 文档菜单
|
||||||
provide(provideKeyDocTree, docTreeData)
|
provide(provideKeyDocTree, docTreeData)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文档树状列表
|
* 刷新文档, 并在渲染结束后选中最后一次选中项
|
||||||
* 1. 初始化是全否调用
|
|
||||||
* 2. 在 workbench 中点击按钮调用, 每个按钮是单选的
|
|
||||||
*/
|
*/
|
||||||
const getDocTree = () => {
|
const refreshDocTree = () => {
|
||||||
editorLoadingTimeout = setTimeout(() => (docTreeLoading.value = true), 100)
|
getDocTree(() => {
|
||||||
docTreeApi({ onlyPicture: true })
|
nextTick(() => {
|
||||||
.then((resp) => {
|
if (!isEmpty(docTreeData.value) && isNotBlank(docTreeCurrentId.value)) {
|
||||||
const docTree: DocTree[] = resp.data
|
DocTreeRef.value.setCurrentKey(docTreeCurrentId.value)
|
||||||
// 两种类型的交界位置
|
|
||||||
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
|
|
||||||
concatSort(docTreeData.value)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (editorLoadingTimeout) {
|
|
||||||
clearTimeout(editorLoadingTimeout)
|
|
||||||
}
|
|
||||||
docTreeLoading.value = false
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在名称中显式排序
|
* 获取文档树状列表
|
||||||
* @param trees
|
*
|
||||||
|
* @param callback 获取文档后的自定义回调
|
||||||
*/
|
*/
|
||||||
const concatSort = (trees: DocTree[]) => {
|
const getDocTree = (callback?: () => void) => {
|
||||||
for (let i = 0; i < trees.length; i++) {
|
startLoading()
|
||||||
if (!isEmpty(trees[i].children)) {
|
docTreeApi({ onlyPicture: true })
|
||||||
concatSort(trees[i].children as DocTree[])
|
.then((resp) => {
|
||||||
}
|
addTreeDivider(resp.data)
|
||||||
trees[i].showSort = showSort.value
|
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 = () => {
|
const handleShowSort = () => (isShowSort.value = !isShowSort.value)
|
||||||
showSort.value = !showSort.value
|
|
||||||
concatSort(docTreeData.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 ----------------------------------------< 右键菜单 >--------------------------------------
|
//#region ----------------------------------------< 右键菜单 >--------------------------------------
|
||||||
const curDoc = ref<DocTree>({ i: '0', p: '0', n: '选择菜单', o: 0, t: [], s: 0, icon: '', ty: 1, star: 0 })
|
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 rMenu = ref<RightMenu>({ show: false, clientX: 0, clientY: 0 })
|
||||||
const rMenuHeight = 151 // 固定的菜单高度, 每次增加右键菜单项时需要修改该值
|
const rMenuHeight = 151 // 固定的菜单高度, 每次增加右键菜单项时需要修改该值
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示有检查菜单
|
* 显示右键菜单
|
||||||
|
* 文章文件夹不显示右键菜单, 文章文件夹的管理一律在文章编辑功能中
|
||||||
|
*
|
||||||
* @param doc 文档
|
* @param doc 文档
|
||||||
* @param event 事件
|
* @param event 事件
|
||||||
*/
|
*/
|
||||||
const handleClickRight = (doc: DocTree, event: MouseEvent) => {
|
const handleClickRightMenu = (event: MouseEvent, doc: DocTree) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!doc) {
|
docTreeCurrentExpandId.value.add(doc.p)
|
||||||
return
|
if (!doc) return
|
||||||
}
|
if (doc.ty !== 2) return
|
||||||
|
|
||||||
curDoc.value = doc
|
curDoc.value = doc
|
||||||
rMenu.value = { show: false, clientX: 0, clientY: 0 }
|
rMenu.value = { show: false, clientX: 0, clientY: 0 }
|
||||||
let y = event.clientY
|
let y = event.clientY
|
||||||
@ -244,20 +482,25 @@ const handleClickRight = (doc: DocTree, event: MouseEvent) => {
|
|||||||
}, 100)
|
}, 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
|
rMenu.value.show = false
|
||||||
}
|
|
||||||
|
|
||||||
const removeListenerTreeDocsRightMenu = () => {
|
|
||||||
document.body.removeEventListener('click', closeTreeDocsMenuShow)
|
document.body.removeEventListener('click', closeTreeDocsMenuShow)
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region 右键菜单功能
|
|
||||||
/**
|
/**
|
||||||
* 删除文档
|
* 删除文档, 删除后将文档从树状节点中删除
|
||||||
*/
|
*/
|
||||||
const delDoc = () => {
|
const delDoc = () => {
|
||||||
ElMessageBox.confirm(`是否确定删除文件夹: <span style="color:#C02B2B;text-decoration: underline;">${curDoc.value.n}</span>?删除后将不可恢复!`, {
|
ElMessageBox.confirm(`是否确定删除文件夹: <span style="color:#C02B2B;text-decoration: underline;">${curDoc.value.n}</span>?删除后将不可恢复!`, {
|
||||||
@ -269,28 +512,53 @@ const delDoc = () => {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
folderDelApi({ id: curDoc.value.i }).then((_resp) => {
|
folderDelApi({ id: curDoc.value.i }).then((_resp) => {
|
||||||
Notify.success(`删除文件夹成功`)
|
Notify.success(`删除文件夹成功`)
|
||||||
getDocTree()
|
DocTreeRef.value.remove(curDoc.value.i)
|
||||||
|
closeParentIfNoChild(curDoc.value.p)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重命名文章 */
|
/**
|
||||||
|
* 重命名文章, 重命名时该节点无法拖拽
|
||||||
|
*/
|
||||||
const rename = () => {
|
const rename = () => {
|
||||||
curDoc.value.updn = true
|
curDoc.value.updn = true
|
||||||
|
notAllowDragKey = curDoc.value.i
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
let ele = document.getElementById('article-doc-name-' + curDoc.value.i)
|
let ele = document.getElementById('article-doc-name-' + curDoc.value.i)
|
||||||
if (ele) ele.focus()
|
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) => {
|
folderAddApi({ pid: pid, name: '新文件夹', storePath: '/', type: 2, icon: 'wl-folder', sort: 0, addToLast: true }).then((resp) => {
|
||||||
let doc: DocTree = {
|
let newFolder: DocTree = {
|
||||||
i: resp.data.id,
|
i: resp.data.id,
|
||||||
p: resp.data.pid,
|
p: resp.data.pid,
|
||||||
n: resp.data.name,
|
n: resp.data.name,
|
||||||
@ -303,21 +571,34 @@ const addFolder = () => {
|
|||||||
star: 0,
|
star: 0,
|
||||||
showSort: curDoc.value.showSort
|
showSort: curDoc.value.showSort
|
||||||
}
|
}
|
||||||
if (isEmpty(curDoc.value.children)) {
|
addDocToTail(newFolder)
|
||||||
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()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将文档添加至末尾并选中
|
||||||
|
*/
|
||||||
|
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 ----------------------------------------< 新增修改详情弹框 >--------------------------------------
|
//#region ----------------------------------------< 新增修改详情弹框 >--------------------------------------
|
||||||
const PictureInfoRef = ref()
|
const PictureInfoRef = ref()
|
||||||
const isShowDocInfoDialog = ref<boolean>(false)
|
const isShowDocInfoDialog = ref<boolean>(false)
|
||||||
@ -332,39 +613,41 @@ const handleShowDocInfoDialog = (dialogType: DocDialogType, pid?: number) => {
|
|||||||
Notify.info('当前文档为系统默认文档, 无法操作', '操作无效')
|
Notify.info('当前文档为系统默认文档, 无法操作', '操作无效')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dialogType === 'upd' && (curDoc.value == undefined || curDoc.value?.i == undefined)) {
|
if (dialogType === 'upd' && (curDoc.value == undefined || curDoc.value?.i == undefined)) {
|
||||||
Notify.info('请先选则要修改的文件夹或文档')
|
Notify.info('请先选则要修改的文件夹或文档')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isShowDocInfoDialog.value = true
|
isShowDocInfoDialog.value = true
|
||||||
if (dialogType === 'add') {
|
if (dialogType === 'add') nextTick(() => PictureInfoRef.value.reload(dialogType, undefined, pid))
|
||||||
nextTick(() => {
|
if (dialogType === 'upd') nextTick(() => PictureInfoRef.value.reload(dialogType, curDoc.value.i))
|
||||||
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
|
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 右键菜单
|
//#endregion
|
||||||
|
|
||||||
const clickCurDoc = (tree: DocTree) => {
|
|
||||||
emits('clickDoc', tree)
|
|
||||||
}
|
|
||||||
|
|
||||||
const emits = defineEmits(['clickDoc'])
|
const emits = defineEmits(['clickDoc'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '../doc/tree-docs.scss';
|
@import '../doc/doc-tree.scss';
|
||||||
@import '../doc/tree-docs-right-menu.scss';
|
@import '../doc/doc-tree-detail.scss';
|
||||||
|
@import '../doc/doc-tree-right-menu.scss';
|
||||||
</style>
|
</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>
|
<template>
|
||||||
<div class="doc-workbench-root">
|
<div class="doc-workbench-root">
|
||||||
<bl-col class="workbench-name" just="flex-start" align="flex-end" height="46px" v-show="curFolder !== undefined">
|
<bl-row just="flex-end" align="flex-end"> </bl-row>
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, inject } from 'vue'
|
import { ref, nextTick } from 'vue'
|
||||||
import { provideKeyDocInfo, SortLevelColor } from '@renderer/views/doc/doc'
|
|
||||||
import PictureInfo from '@renderer/views/picture/PictureInfo.vue'
|
|
||||||
|
|
||||||
// 当前菜单中选择的文档
|
|
||||||
const curFolder = inject(provideKeyDocInfo)
|
|
||||||
|
|
||||||
// ---------- 新增修改按钮 ----------
|
// ---------- 新增修改按钮 ----------
|
||||||
const PictureInfoRef = ref()
|
|
||||||
// 显示编辑 dialog
|
|
||||||
const isShowDocInfoDialog = ref<boolean>(false)
|
|
||||||
const handleShowAddDocInfoDialog = () => {
|
|
||||||
isShowDocInfoDialog.value = true
|
|
||||||
nextTick(() => {
|
|
||||||
PictureInfoRef.value.reload('add')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshDocTree = () => {
|
const refreshDocTree = () => {
|
||||||
emits('refreshDocTree')
|
emits('refreshDocTree')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存后的回调
|
|
||||||
* 1. [暂无] 刷新菜单列表
|
|
||||||
* 2. 关闭 dialog 页面
|
|
||||||
*/
|
|
||||||
const savedCallback = () => {
|
|
||||||
isShowDocInfoDialog.value = false
|
|
||||||
refreshDocTree()
|
|
||||||
}
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const emits = defineEmits(['refreshDocTree', 'show-sort'])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -126,7 +126,7 @@ export const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file?) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传文件结果处理
|
* 上传文件结果处理, 失败是根据
|
||||||
* @param resp 接口响应
|
* @param resp 接口响应
|
||||||
* @returns 是否成功
|
* @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/dialog';
|
||||||
@import './element/drawer';
|
@import './element/drawer';
|
||||||
@import './element/popper';
|
@import './element/popper';
|
||||||
|
|||||||
@ -39,11 +39,10 @@ body {
|
|||||||
|
|
||||||
/* 定义滑块 内阴影+圆角 */
|
/* 定义滑块 内阴影+圆角 */
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
border-radius: 4px;
|
|
||||||
background-color: #c7c7c7;
|
background-color: #c7c7c7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 解决滚动条右下角出现白色方块 */
|
/* 滚动条右下角出现白色方块 */
|
||||||
::-webkit-scrollbar-corner {
|
::-webkit-scrollbar-corner {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
/* 数字越大, 颜色越淡 */
|
/* 数字越大, 颜色越淡 */
|
||||||
--el-color-primary: #c1992f;
|
/* --el-color-primary: #c1992f;
|
||||||
--el-color-primary-dark-2: rgba(250, 195, 56, 0.8);
|
--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-1: rgba(193, 153, 47, 0.9);
|
||||||
--el-color-primary-light-2: rgba(193, 153, 47, 0.8);
|
--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-6: rgba(193, 153, 47, 0.4);
|
||||||
--el-color-primary-light-7: rgba(193, 153, 47, 0.3);
|
--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-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;
|
--el-text-color-primary: #606266;
|
||||||
|
|||||||
@ -53,8 +53,8 @@ const props = defineProps({
|
|||||||
@include flex(row, center, center);
|
@include flex(row, center, center);
|
||||||
box-shadow: 2px 2px 3px 0 #999999;
|
box-shadow: 2px 2px 3px 0 #999999;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0px 4px;
|
padding: 0 4px;
|
||||||
margin: 3px;
|
margin: 4px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
min-height: 15px;
|
min-height: 15px;
|
||||||
max-height: 15px;
|
max-height: 15px;
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Plus, Minus } from '@element-plus/icons-vue'
|
import { Plus, Minus } from '@element-plus/icons-vue'
|
||||||
import { increase, decrease, getFontSizeValue } from './article-setting'
|
import { increase, decrease, getFontSizeValue } from './scripts/article-setting'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -13,83 +13,44 @@
|
|||||||
</bl-row>
|
</bl-row>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="menu" :style="menuStyle">
|
<div class="doc-tree-container" :style="menuStyle">
|
||||||
<el-menu
|
<el-tree
|
||||||
v-if="docTreeData != undefined && docTreeData.length > 0"
|
ref="DocTreeRef"
|
||||||
class="doc-trees"
|
class="doc-tree"
|
||||||
:default-active="docTreeDefaultActive"
|
:data="docTreeData"
|
||||||
:default-openeds="defaultOpeneds"
|
:highlight-current="true"
|
||||||
:unique-opened="true">
|
:indent="14"
|
||||||
<div v-for="L1 in docTreeData" :key="L1.i" class="menu-level-one">
|
:icon="ArrowRightBold"
|
||||||
<el-menu-item v-if="isEmpty(L1.children)" :index="L1.i">
|
node-key="i"
|
||||||
<template #title>
|
@nodeClick="clickCurDoc">
|
||||||
<div class="menu-item-wrapper" @click="clickCurDoc(L1)">
|
<template #default="{ _node, data }">
|
||||||
<DocTitle :trees="L1" :level="1" />
|
<div
|
||||||
</div>
|
class="menu-item-wrapper"
|
||||||
</template>
|
:style="{
|
||||||
</el-menu-item>
|
marginTop: data.p === '0' ? '5px' : '1px',
|
||||||
|
marginBottom: data.p === '0' ? '5px' : '1px'
|
||||||
<el-sub-menu v-else :expand-open-icon="ArrowDownBold" :expand-close-icon="ArrowRightBold" :index="L1.i">
|
}">
|
||||||
<template #title>
|
<div :class="[data.t.includes('subject') && userStore.userParams.WEB_BLOG_SUBJECT_TITLE === '1' ? 'subject-title' : 'doc-title']">
|
||||||
<div class="menu-item-wrapper">
|
<div class="doc-name">
|
||||||
<DocTitle :trees="L1" :level="1" style="font-size: 15px" />
|
<img class="menu-icon-img" v-if="isShowImg(data)" :src="data.icon" />
|
||||||
</div>
|
<svg v-else-if="isShowSvg(data)" class="icon menu-icon" aria-hidden="true">
|
||||||
</template>
|
<use :xlink:href="'#' + data.icon"></use>
|
||||||
|
</svg>
|
||||||
<!-- ================================================ L2 ================================================ -->
|
<div class="name-wrapper" :style="{ maxWidth: isNotBlank(data.icon) ? 'calc(100% - 25px)' : '100%' }">
|
||||||
<div v-for="L2 in L1.children" :key="L2.i">
|
{{ data.n }}
|
||||||
<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>
|
</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>
|
</div>
|
||||||
</el-sub-menu>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</el-menu>
|
</el-tree>
|
||||||
</div>
|
</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 class="bl-preview" :style="{ fontSize: getFontSize() }" v-html="article.html"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -140,15 +101,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ref, onUnmounted, nextTick } from 'vue'
|
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 { useUserStore } from '@/stores/user'
|
||||||
import { isNull, isEmpty, isNotNull } from '@/assets/utils/obj'
|
|
||||||
import { useLifecycle } from '@/scripts/lifecycle'
|
import { useLifecycle } from '@/scripts/lifecycle'
|
||||||
import DocTitle from './DocTitle.vue'
|
// element plus
|
||||||
import ArticleSetting from './ArticleSetting.vue'
|
import { ArrowRightBold, Setting } from '@element-plus/icons-vue'
|
||||||
|
// ts
|
||||||
import 'katex/dist/katex.min.css'
|
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()
|
const userStore = useUserStore()
|
||||||
useLifecycle(
|
useLifecycle(
|
||||||
@ -169,14 +136,20 @@ onUnmounted(() => {
|
|||||||
window.removeEventListener('resize', onresize)
|
window.removeEventListener('resize', onresize)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const DocTreeRef = ref()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由中获取ID参数
|
* 路由中获取ID参数
|
||||||
*/
|
*/
|
||||||
const getRouteQueryParams = () => {
|
const getRouteQueryParams = () => {
|
||||||
let articleId = route.query.articleId
|
let articleId = route.query.articleId
|
||||||
getDocTree()
|
getDocTree(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
DocTreeRef.value.setCurrentKey(docTreeCurrentId.value)
|
||||||
|
})
|
||||||
|
})
|
||||||
if (isNotNull(articleId)) {
|
if (isNotNull(articleId)) {
|
||||||
docTreeDefaultActive.value = articleId as string
|
docTreeCurrentId.value = articleId as string
|
||||||
let treeParam: any = { ty: 3, i: articleId }
|
let treeParam: any = { ty: 3, i: articleId }
|
||||||
clickCurDoc(treeParam)
|
clickCurDoc(treeParam)
|
||||||
}
|
}
|
||||||
@ -186,7 +159,7 @@ const route = useRoute()
|
|||||||
// 文档菜单的加载动画
|
// 文档菜单的加载动画
|
||||||
const docTreeLoading = ref(true)
|
const docTreeLoading = ref(true)
|
||||||
// 文档菜单
|
// 文档菜单
|
||||||
const docTreeDefaultActive = ref('')
|
const docTreeCurrentId = ref('')
|
||||||
const docTreeData = ref<DocTree[]>([])
|
const docTreeData = ref<DocTree[]>([])
|
||||||
// 当前选中的文章, 用于在编辑器中展示
|
// 当前选中的文章, 用于在编辑器中展示
|
||||||
const article = ref<DocInfo>({
|
const article = ref<DocInfo>({
|
||||||
@ -210,7 +183,7 @@ const PreviewRef = ref()
|
|||||||
* 1. 初始化是全否调用
|
* 1. 初始化是全否调用
|
||||||
* 2. 在 workbench 中点击按钮调用, 每个按钮是单选的
|
* 2. 在 workbench 中点击按钮调用, 每个按钮是单选的
|
||||||
*/
|
*/
|
||||||
const getDocTree = () => {
|
const getDocTree = (callback?: () => void) => {
|
||||||
docTreeLoading.value = true
|
docTreeLoading.value = true
|
||||||
docTreeData.value = []
|
docTreeData.value = []
|
||||||
defaultOpeneds.value = []
|
defaultOpeneds.value = []
|
||||||
@ -220,6 +193,7 @@ const getDocTree = () => {
|
|||||||
docTreeData.value.forEach((l1: DocTree) => {
|
docTreeData.value.forEach((l1: DocTree) => {
|
||||||
defaultOpeneds.value.push(l1.i.toString())
|
defaultOpeneds.value.push(l1.i.toString())
|
||||||
})
|
})
|
||||||
|
if (callback) callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userStore.isLogin) {
|
if (userStore.isLogin) {
|
||||||
@ -252,8 +226,8 @@ const clickCurDoc = async (tree: DocTree) => {
|
|||||||
/**
|
/**
|
||||||
* 如果点击的是文章, 则查询文章信息和正文, 并在编辑器中显示.
|
* 如果点击的是文章, 则查询文章信息和正文, 并在编辑器中显示.
|
||||||
*/
|
*/
|
||||||
const getCurEditArticle = async (id: number) => {
|
const getCurEditArticle = async (id: string) => {
|
||||||
if (id == -999) {
|
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">
|
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>
|
该博客所配置的 USER_ID 为<br/><span style="color:#e3a300; border-bottom: 5px solid #e3a300;border-radius:5px">${window.blconfig.DOMAIN.USER_ID}</span>
|
||||||
</div>`
|
</div>`
|
||||||
@ -277,80 +251,10 @@ const getCurEditArticle = async (id: number) => {
|
|||||||
* 解析目录
|
* 解析目录
|
||||||
*/
|
*/
|
||||||
const parseTocAsync = async (ele: HTMLElement) => {
|
const parseTocAsync = async (ele: HTMLElement) => {
|
||||||
let heads = ele.querySelectorAll('h1, h2, h3, h4, h5, h6')
|
tocList.value = parseToc(ele)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toScroll = (id: string) => {
|
//#region ----------------------------------------< setting >----------------------------------------
|
||||||
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
|
|
||||||
const isShowSetting = ref(false)
|
const isShowSetting = ref(false)
|
||||||
|
|
||||||
const showSetting = () => {
|
const showSetting = () => {
|
||||||
@ -444,6 +348,11 @@ const onresize = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<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 {
|
.articles-root {
|
||||||
@include box(100vw, 100%);
|
@include box(100vw, 100%);
|
||||||
@include flex(column, flex-start, center);
|
@include flex(column, flex-start, center);
|
||||||
@ -476,622 +385,6 @@ const onresize = () => {
|
|||||||
@include flex(row, center, center);
|
@include flex(row, center, center);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow: hidden;
|
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 以内时使用以下样式
|
// 屏幕宽度在 1100 以内时使用以下样式
|
||||||
@ -1107,7 +400,7 @@ const onresize = () => {
|
|||||||
@include box(100%, calc(100% - 40px));
|
@include box(100%, calc(100% - 40px));
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.menu {
|
.doc-tree-container {
|
||||||
height: calc(100% - 20px) !important;
|
height: calc(100% - 20px) !important;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
@ -1119,7 +412,7 @@ const onresize = () => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article {
|
.doc-content-container {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
overflow-x: hidden;
|
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 {
|
declare interface DocTree {
|
||||||
/** ID */
|
/** ID */
|
||||||
i: number
|
i: string
|
||||||
|
p: string
|
||||||
/** Name */
|
/** Name */
|
||||||
n: string
|
n: string
|
||||||
/** open: 0:否;1:是; */
|
/** open: 0:否;1:是; */
|
||||||
@ -53,12 +54,3 @@ declare type DocType = 1 | 2 | 3
|
|||||||
declare interface Window {
|
declare interface Window {
|
||||||
onHtmlEventDispatch: any
|
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>
|
</div>
|
||||||
<div class="home-footer">
|
<div class="home-footer">
|
||||||
<div class="about-us">
|
|
||||||
<span>{{ blossom.SYS.VERSION + (isNotBlank(getEmail()) ? ' | 邮箱:' + getEmail() : '') }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="custom-info">
|
<div class="custom-info">
|
||||||
<div v-if="isNotBlank(gwab())" v-html="gwab()"></div>
|
<div v-if="isNotBlank(gwab())" v-html="gwab()"></div>
|
||||||
<div v-if="isNotBlank(ipc())" style="cursor: pointer" @click="openNew('https://beian.miit.gov.cn/')">
|
<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;
|
padding: 20px;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #414141;
|
||||||
|
}
|
||||||
|
|
||||||
.subject-item {
|
.subject-item {
|
||||||
@include box(260px, 150px);
|
@include box(260px, 150px);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user