Merge branch 'blossom-editor:dev' into dev

This commit is contained in:
Tianjiu 2024-04-13 12:56:53 +08:00 committed by GitHub
commit c9c68cd0b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
186 changed files with 12353 additions and 6941 deletions

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>blossom-backend</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -39,10 +39,57 @@ public enum UserParamEnum {
*/
WEB_BLOG_LINKS(false, 0, ""),
/**
* 博客端专题特殊形式, 0:false;1:
* 博客端专题特殊形式
* 0:;1:
*
* @since 1.13.0
*/
WEB_BLOG_SUBJECT_TITLE(false, 0, "0"),
/**
* 是否在文章内容的顶部显示文章的标题
* 0:;1:
*
* @since 1.15.0
*/
WEB_BLOG_SHOW_ARTICLE_NAME(false, 0, "1"),
/**
* 博客主题色
*
* @since 1.15.0
*/
WEB_BLOG_COLOR(false, 0, "rgb(104, 104, 104)"),
// ----------< 博客水印 >----------
/**
* 启用博客水印
* 0:;1:
*
* @since 1.15.0
*/
WEB_BLOG_WATERMARK_ENABLED(false, 0, "0"),
/**
* 水印内容
*
* @since 1.15.0
*/
WEB_BLOG_WATERMARK_CONTENT(false, 0, ""),
/**
* 水印字体大小
*
* @since 1.15.0
*/
WEB_BLOG_WATERMARK_FONTSIZE(false, 0, "15"),
/**
* 水印颜色
*
* @since 1.15.0
*/
WEB_BLOG_WATERMARK_COLOR(false, 0, "rgba(157, 157, 157, 0.2)"),
/**
* 水印密集度
*
* @since 1.15.0
*/
WEB_BLOG_WATERMARK_GAP(false, 0, "100"),
;
/**

View File

@ -3,6 +3,7 @@ package com.blossom.backend.base.paramu.pojo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 修改参数
@ -21,6 +22,7 @@ public class UserParamUpdReq {
/**
* 参数值
*/
@NotNull(message = "参数值为必填项")
private String paramValue;
/**

View File

@ -1,6 +1,5 @@
package com.blossom.backend.base.search.message.consumer;
import cn.hutool.core.convert.Convert;
import com.blossom.backend.base.search.SearchProperties;
import com.blossom.backend.base.search.message.IndexMsg;
import com.blossom.backend.base.search.message.IndexMsgTypeEnum;
@ -51,7 +50,6 @@ public class IndexMsgConsumer {
final Long userId = indexMsg.getUserId();
final Long id = indexMsg.getId();
if (userId == null || id == null) {
log.error("消费异常. 获取用户id为空");
continue;
}
if (IndexMsgTypeEnum.ADD == indexMsg.getType()) {
@ -61,12 +59,15 @@ public class IndexMsgConsumer {
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
// 查询最新的消息
ArticleEntity article = this.articleService.selectById(id, false, true, false, userId);
if (null == article) {
continue;
}
Document document = new Document();
document.add(new StringField("id", Convert.toStr(id), Field.Store.YES));
document.add(new StringField("id", String.valueOf(id), Field.Store.YES));
document.add(new TextField("name", article.getName(), Field.Store.YES));
document.add(new TextField("tags", article.getTags(), Field.Store.YES));
document.add(new TextField("markdown", article.getMarkdown(), Field.Store.YES));
indexWriter.updateDocument(new Term("id", Convert.toStr(id)), document);
indexWriter.updateDocument(new Term("id", String.valueOf(id)), document);
indexWriter.flush();
indexWriter.commit();
}
@ -74,7 +75,7 @@ public class IndexMsgConsumer {
// 删除索引
try (Directory directory = FSDirectory.open(this.searchProperties.getUserIndexDirectory(userId));
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
indexWriter.deleteDocuments(new Term("id", Convert.toStr(id)));
indexWriter.deleteDocuments(new Term("id", String.valueOf(id)));
indexWriter.flush();
indexWriter.commit();
}

View File

@ -1,11 +1,33 @@
package com.blossom.backend.config;
import com.blossom.backend.server.folder.pojo.FolderEntity;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Slf4j
public class Test {
public static void main(String[] args) {
Set<FolderEntity> set = new HashSet<>();
FolderEntity f1 = new FolderEntity();
f1.setId(1L);
f1.setName("1");
FolderEntity f2 = new FolderEntity();
f2.setId(1L);
f2.setName("2");
set.add(f1);
set.add(f2);
System.out.println(set.size());
System.out.println(Arrays.toString(set.toArray()));
}
}

View File

@ -1,21 +1,19 @@
package com.blossom.backend.server.article.backup;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.param.pojo.ParamEntity;
import com.blossom.backend.server.article.backup.pojo.BackupFile;
import com.blossom.backend.server.article.backup.pojo.DownloadReq;
import com.blossom.backend.server.utils.DownloadUtil;
import com.blossom.common.base.enums.YesNo;
import com.blossom.common.base.exception.XzException404;
import com.blossom.common.base.exception.XzException500;
import com.blossom.common.base.pojo.R;
import com.blossom.common.base.util.SortUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.PathResource;
import org.springframework.core.io.support.ResourceRegion;
import org.springframework.http.*;
@ -23,10 +21,7 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
@ -36,6 +31,7 @@ import java.util.stream.Collectors;
* @author xzzz
* @order 8
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/article/backup")
@ -52,7 +48,7 @@ public class ArticleBackupController {
* @param articleId 备份指定的文章
*/
@GetMapping
public R<ArticleBackupService.BackupFile> backup(
public R<BackupFile> backup(
@RequestParam("type") String type,
@RequestParam("toLocal") String toLocal,
@RequestParam(value = "articleId", required = false) Long articleId) {
@ -77,7 +73,7 @@ public class ArticleBackupController {
* 备份记录
*/
@GetMapping("/list")
public R<List<ArticleBackupService.BackupFile>> list() {
public R<List<BackupFile>> list() {
return R.ok(backupService.listAll(AuthContext.getUserId())
.stream()
.sorted((b1, b2) -> SortUtil.dateSort.compare(b1.getDatetime(), b2.getDatetime()))
@ -89,9 +85,12 @@ public class ArticleBackupController {
* 下载压缩包
*
* @param filename 文件名称
* @deprecated 1.14.0
*/
@GetMapping("/download")
@Deprecated
public void download(@RequestParam("filename") String filename, HttpServletResponse response) {
/*
final ParamEntity param = paramService.getValue(ParamEnum.BACKUP_PATH);
XzException500.throwBy(ObjUtil.isNull(param), ArticleBackupService.ERROR_MSG);
final String rootPath = param.getParamValue();
@ -103,6 +102,7 @@ public class ArticleBackupController {
} catch (IOException e) {
e.printStackTrace();
}
*/
}
/**
@ -112,7 +112,6 @@ public class ArticleBackupController {
* @param request request
* @apiNote 返回类 ResponseEntity<ResourceRegion>
*/
@AuthIgnore
@GetMapping("/download/fragment")
public ResponseEntity<ResourceRegion> downloadFragment(@RequestParam("filename") String filename,
HttpServletRequest request) {
@ -129,15 +128,17 @@ public class ArticleBackupController {
* @apiNote 返回类 ResponseEntity<ResourceRegion>
* @apiNote 通过 Range 请求头获取分片请求, 返回头中会比说明本次分片大小 Content-Range
*/
@AuthIgnore
@PostMapping("/download/fragment")
public ResponseEntity<ResourceRegion> downloadFragment(@RequestBody DownloadReq req,
HttpServletRequest request) {
final ParamEntity param = paramService.getValue(ParamEnum.BACKUP_PATH);
XzException500.throwBy(ObjUtil.isNull(param), ArticleBackupService.ERROR_MSG);
final String rootPath = param.getParamValue();
XzException500.throwBy(StrUtil.isBlank(rootPath), ArticleBackupService.ERROR_MSG);
String filename = rootPath + "/" + req.getFilename();
// 检查文件名
if (!checkFilename(req.getFilename())) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ResourceRegion(new PathResource(""), 0, 0));
}
// 拼接文件名
String filename = getRootPath() + "/" + req.getFilename();
File file = new File(filename);
long contentLength = file.length();
@ -167,12 +168,43 @@ public class ArticleBackupController {
return ResponseEntity
.status(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE)
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(resourceRegion.getCount()))
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.header(HttpHeaders.CONTENT_RANGE, contentRange)
.body(resourceRegion);
}
/**
* 获取备份根目录
*/
private String getRootPath() {
final ParamEntity param = paramService.getValue(ParamEnum.BACKUP_PATH);
XzException500.throwBy(ObjUtil.isNull(param), ArticleBackupService.ERROR_MSG);
final String rootPath = param.getParamValue();
XzException500.throwBy(StrUtil.isBlank(rootPath), ArticleBackupService.ERROR_MSG);
return rootPath;
}
/**
* 标准化文件名, 不能包含 / 进行隐性的路径切换
*/
private boolean checkFilename(String filename) {
// 不能包含 /
if (filename.contains("/")) {
return false;
}
if (filename.contains("%2f")) {
return false;
}
if (filename.contains("%2F")) {
return false;
}
BackupFile backupFile = new BackupFile();
backupFile.build(filename);
if (!backupFile.checkPrefix()) {
return false;
}
return true;
}
}

View File

@ -2,7 +2,6 @@ package com.blossom.backend.server.article.backup;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
@ -12,6 +11,7 @@ import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.param.pojo.ParamEntity;
import com.blossom.backend.base.user.UserService;
import com.blossom.backend.base.user.pojo.UserEntity;
import com.blossom.backend.server.article.backup.pojo.BackupFile;
import com.blossom.backend.server.article.draft.ArticleService;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.backend.server.article.reference.ArticleReferenceService;
@ -29,8 +29,6 @@ import com.blossom.common.base.util.DateUtils;
import com.blossom.common.base.util.PrimaryKeyUtil;
import com.blossom.common.base.util.SortUtil;
import com.blossom.common.iaas.IaasProperties;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -72,7 +70,6 @@ public class ArticleBackupService {
private Executor executor;
public static final String ERROR_MSG = String.format("[文章备份] 备份失败, 未配置备份路径 [%s]", ParamEnum.BACKUP_PATH.name());
private static final String SEPARATOR = "_";
/**
* 查看记录
@ -212,6 +209,7 @@ public class ArticleBackupService {
if (toLocal == YesNo.YES) {
backLogs.add("");
if (articleId != null) {
// 查询文章引用的图片
List<ArticleReferenceEntity> refs = referenceService.listPics(articleId);
PictureEntity where = new PictureEntity();
where.setUrls(refs.stream().map(ArticleReferenceEntity::getTargetUrl).collect(Collectors.toList()));
@ -219,11 +217,16 @@ public class ArticleBackupService {
backLogs.add("[图片备份] 图片个数: " + pics.size());
backLogs.add("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓↓ 图片列表 ↓↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
for (PictureEntity pic : pics) {
backLogs.add("" + pic.getPathName());
FileUtil.copy(
pic.getPathName(),
backupFile.getRootPath() + "/" + pic.getPathName(),
true);
try {
FileUtil.copy(
pic.getPathName(),
backupFile.getRootPath() + "/" + pic.getPathName(),
true);
backLogs.add("" + pic.getPathName());
} catch (Exception e) {
backLogs.add("┃ [警告] " + pic.getPathName() + " 未在存储路径中找到");
log.warn("{} 未在存储路径中找到", pic.getPathName());
}
}
}
// 备份全部图片
@ -381,7 +384,7 @@ public class ArticleBackupService {
}
List<ArticleReferenceEntity> refs = referenceService.listPics(articleId);
final String domain = iaasProperties.getBlos().getDomain();
final String domain = paramService.getDomain();
// 计算字符出现的次数
int separatorCount = countChar(articleName, '/');
@ -400,12 +403,18 @@ public class ArticleBackupService {
}
String localPath = parent.substring(0, parent.length() > 0 ? parent.length() - 1 : 0) + ref.getTargetUrl().replace(domain, "");
content = content.replaceAll(ref.getTargetUrl(), localPath);
System.out.println(localPath);
}
return content;
}
/**
* 计算指定字符在字符串中出现的此处
*
* @param str 字符串
* @param target 查询字符
* @return 出现次数
*/
private static int countChar(String str, char target) {
int times = 0;
for (int i = 0; i < str.length(); i++) {
@ -443,111 +452,4 @@ public class ArticleBackupService {
.replaceAll("\\|", "")
;
}
/**
* 备份文件
*/
@Data
public static class BackupFile {
/**
* 用户ID
*/
private String userId;
/**
* 备份日期 YYYYMMDD
*
* @mock 20230101
*/
private String date;
/**
* 备份时间 HHMMSS
*
* @mock 123001
*/
private String time;
/**
* 备份的日期和时间, yyyy-MM-dd HH:mm:ss
*/
private Date datetime;
/**
* 备份包的名称
*/
private String filename;
/**
* 备份包路径
*/
private String path;
/**
* 本地文件
*/
@JsonIgnore
private File file;
/**
* 文件大小
*/
private Long fileLength;
/**
* 通过本地备份文件初始化
*
* @param file 本地备份文件
*/
public BackupFile(File file) {
build(FileUtil.getPrefix(file.getName()));
this.file = file;
this.fileLength = file.length();
}
/**
* 指定用户的开始备份
*
* @param userId 用户ID
*/
public BackupFile(Long userId, BackupTypeEnum type, YesNo toLocal) {
String filename = String.format("%s_%s_%s", buildFilePrefix(type, toLocal), userId, DateUtils.toYMDHMS_SSS(System.currentTimeMillis()));
filename = filename.replaceAll(" ", SEPARATOR)
.replaceAll("-", "")
.replaceAll(":", "")
.replaceAll("\\.", SEPARATOR);
build(filename);
}
private static String buildFilePrefix(BackupTypeEnum type, YesNo toLocal) {
String prefix = "B";
if (type == BackupTypeEnum.MARKDOWN) {
prefix += "M";
} else if (type == BackupTypeEnum.HTML) {
prefix += "H";
}
if (toLocal == YesNo.YES) {
prefix += "L";
} else if (toLocal == YesNo.NO) {
prefix += "N";
}
return prefix;
}
private void build(String filename) {
this.filename = filename;
String[] tags = filename.split(SEPARATOR);
if (tags.length < 5) {
return;
}
this.userId = tags[1];
this.date = tags[2];
this.time = tags[3];
this.datetime = DateUtil.parse(this.date + this.time);
}
/**
* 获取备份文件的路径, 由备份路径 + 本次备份名称构成
*/
public String getRootPath() {
return this.path + "/" + this.filename;
}
}
}

View File

@ -0,0 +1,145 @@
package com.blossom.backend.server.article.backup.pojo;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import com.blossom.backend.server.article.backup.BackupTypeEnum;
import com.blossom.common.base.enums.YesNo;
import com.blossom.common.base.util.DateUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import java.io.File;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Data
public class BackupFile {
private static final String SEPARATOR = "_";
private static Set<String> prefixs = new HashSet<String>() {{
this.add("BML");
this.add("BMN");
this.add("BHL");
this.add("BHN");
}};
/**
* 用户ID
*/
private String userId;
/**
* 备份日期 YYYYMMDD
*
* @mock 20230101
*/
private String date;
/**
* 备份时间 HHMMSS
*
* @mock 123001
*/
private String time;
/**
* 备份的日期和时间, yyyy-MM-dd HH:mm:ss
*/
private Date datetime;
/**
* 备份包的名称
*/
private String filename;
/**
* 备份包路径
*/
private String path;
/**
* 本地文件
*/
@JsonIgnore
private File file;
/**
* 文件大小
*/
private Long fileLength;
/**
* 通过本地备份文件初始化
*
* @param file 本地备份文件
*/
public BackupFile(File file) {
build(FileUtil.getPrefix(file.getName()));
this.file = file;
this.fileLength = file.length();
}
public BackupFile() {
}
/**
* 指定用户的开始备份
*
* @param userId 用户ID
*/
public BackupFile(Long userId, BackupTypeEnum type, YesNo toLocal) {
String filename = String.format("%s_%s_%s", buildFilePrefix(type, toLocal), userId, DateUtils.toYMDHMS_SSS(System.currentTimeMillis()));
filename = filename.replaceAll(" ", SEPARATOR)
.replaceAll("-", "")
.replaceAll(":", "")
.replaceAll("\\.", SEPARATOR);
build(filename);
}
private static String buildFilePrefix(BackupTypeEnum type, YesNo toLocal) {
String prefix = "B";
if (type == BackupTypeEnum.MARKDOWN) {
prefix += "M";
} else if (type == BackupTypeEnum.HTML) {
prefix += "H";
}
if (toLocal == YesNo.YES) {
prefix += "L";
} else if (toLocal == YesNo.NO) {
prefix += "N";
}
return prefix;
}
public void build(String filename) {
this.filename = filename;
String[] tags = filename.split(SEPARATOR);
if (tags.length < 5) {
return;
}
this.userId = tags[1];
this.date = tags[2];
this.time = tags[3];
this.datetime = DateUtil.parse(this.date + this.time);
}
/**
* 检查文件格式
*/
public boolean checkPrefix() {
String[] tags = filename.split(SEPARATOR);
if (tags.length != 5) {
return false;
}
String prefix = tags[0];
// 不是固定文件前缀则失败
if (!prefixs.contains(prefix)) {
return false;
}
return true;
}
/**
* 获取备份文件的路径, 由备份路径 + 本次备份名称构成
*/
public String getRootPath() {
return this.path + "/" + this.filename;
}
}

View File

@ -12,8 +12,10 @@ import com.blossom.backend.server.article.draft.pojo.*;
import com.blossom.backend.server.article.open.ArticleOpenService;
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
import com.blossom.backend.server.doc.DocService;
import com.blossom.backend.server.doc.DocSortChecker;
import com.blossom.backend.server.doc.DocTypeEnum;
import com.blossom.backend.server.folder.FolderService;
import com.blossom.backend.server.folder.FolderTypeEnum;
import com.blossom.backend.server.folder.pojo.FolderEntity;
import com.blossom.backend.server.utils.ArticleUtil;
import com.blossom.backend.server.utils.DocUtil;
@ -50,13 +52,14 @@ import java.util.List;
@AllArgsConstructor
@RequestMapping("/article")
public class ArticleController {
private final ArticleService baseService;
private final ArticleOpenService openService;
private final FolderService folderService;
private final UserService userService;
private final ArticleTempVisitService tempVisitService;
private final DocService docService;
private final DocSortChecker docSortChecker;
private final ImportManager importManager;
/**
* 查询列表
@ -122,9 +125,9 @@ public class ArticleController {
ArticleEntity article = req.to(ArticleEntity.class);
article.setTags(DocUtil.toTagStr(req.getTags()));
article.setUserId(AuthContext.getUserId());
// 如果新增到顶部, 获取最小的
// 如果新增到顶部, 获取最小的排序
if (BooleanUtil.isTrue(req.getAddToLast())) {
article.setSort(docService.selectMinSortByPid(req.getPid()) + 1);
article.setSort(docService.selectMaxSortByPid(req.getPid(), AuthContext.getUserId(), FolderTypeEnum.ARTICLE) + 1);
}
return R.ok(baseService.insert(article));
}
@ -136,10 +139,19 @@ public class ArticleController {
* @apiNote 该接口只能修改文章的基本信息, 正文及版本修改请使用 "/upd/content" 接口或者 {@link ArticleService#updateContentById(ArticleEntity)}
*/
@PostMapping("/upd")
public R<Long> insert(@Validated @RequestBody ArticleUpdReq req) {
public R<Long> update(@Validated @RequestBody ArticleUpdReq req) {
ArticleEntity article = req.to(ArticleEntity.class);
article.setTags(DocUtil.toTagStr(req.getTags()));
article.setUserId(AuthContext.getUserId());
// 检查排序是否重复
// if (req.getSort() != null && req.getPid() != null) {
// final long newPid = req.getPid();
// docSortChecker.checkUnique(CollUtil.newArrayList(newPid),
// null,
// CollUtil.newArrayList(article),
// FolderTypeEnum.ARTICLE,
// AuthContext.getUserId());
// }
return R.ok(baseService.update(article));
}
@ -173,7 +185,7 @@ public class ArticleController {
}
/**
* 为文章快速增加/删除标签
* 快速增加/删除标签
*
* @since 1.10.0
*/
@ -263,11 +275,11 @@ public class ArticleController {
* @param pid 上级菜单
*/
@PostMapping("import")
public R<?> upload(@RequestParam("file") MultipartFile file, @RequestParam(value = "pid") Long pid) {
public R<?> upload(@RequestParam("file") MultipartFile file, @RequestParam(value = "pid") Long pid, @RequestParam(value = "batchId") String batchId) {
try {
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
if (!"txt".equals(suffix) && !"md".equals(suffix)) {
throw new XzException404("不支持的文件类型: [" + suffix + "]");
throw new XzException400("不支持的文件类型: [" + suffix + "]");
}
FolderEntity folder = folderService.selectById(pid);
XzException404.throwBy(ObjUtil.isNull(folder), "上级文件夹不存在");
@ -278,10 +290,12 @@ public class ArticleController {
article.setPid(pid);
article.setUserId(AuthContext.getUserId());
article.setName(FileUtil.getPrefix(file.getOriginalFilename()));
article.setWords(ArticleUtil.statWords(content));
// article.setWords(ArticleUtil.statWords(content));
article.setSort(importManager.getSort(batchId, pid, AuthContext.getUserId()));
baseService.insert(article);
} catch (Exception e) {
e.printStackTrace();
throw new XzException400("上传失败");
}
return R.ok();
}

View File

@ -50,6 +50,16 @@ public interface ArticleMapper extends BaseMapper<ArticleEntity> {
*/
void updContentById(ArticleEntity entity);
/**
* 查询某段时间内编辑过内容的文章数
*
* @param beginUpdTime 开始修改日期
* @param endUpdTime 结束修改日期
*/
ArticleStatRes statUpdArticleCount(@Param("beginUpdTime") String beginUpdTime,
@Param("endUpdTime") String endUpdTime,
@Param("userId") Long userId);
/**
* 修改某段日期内修改的文章数据
*

View File

@ -2,6 +2,7 @@ package com.blossom.backend.server.article.draft;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.base.search.EnableIndex;
@ -18,6 +19,7 @@ import com.blossom.backend.server.doc.pojo.DocTreeRes;
import com.blossom.backend.server.utils.ArticleUtil;
import com.blossom.backend.server.utils.DocUtil;
import com.blossom.common.base.exception.XzException404;
import com.blossom.common.base.util.DateUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
@ -166,7 +168,9 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
public Long update(ArticleEntity req) {
XzException404.throwBy(req.getId() == null, "ID不得为空");
baseMapper.updById(req);
referenceService.updateInnerName(req.getUserId(), req.getId(), req.getName());
if(StrUtil.isNotBlank(req.getName())) {
referenceService.updateInnerName(req.getUserId(), req.getId(), req.getName());
}
return req.getId();
}
@ -185,6 +189,7 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
if (req.getHtml() != null) {
req.setHtml(req.getHtml().replaceAll("<p><br></p>", ""));
}
req.setUpdMarkdownTime(DateUtils.date());
baseMapper.updContentById(req);
referenceService.bind(req.getUserId(), req.getId(), req.getName(), req.getReferences());
logService.insert(req.getId(), 0, req.getMarkdown());

View File

@ -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;
}
}

View File

@ -118,6 +118,10 @@ public class ArticleEntity extends AbstractPOJO implements Serializable {
* 用户ID
*/
private Long userId;
/**
* 文章内容的修改时间
*/
private Date updMarkdownTime;
//region ============================== 非数据库字段 ==============================
/**

View File

@ -13,10 +13,7 @@ import com.blossom.backend.config.BlConstants;
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.ArticleInfoRes;
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
import com.blossom.backend.server.article.open.pojo.ArticleOpenReq;
import com.blossom.backend.server.article.open.pojo.ArticleOpenRes;
import com.blossom.backend.server.article.open.pojo.ArticleOpenSyncReq;
import com.blossom.backend.server.article.open.pojo.*;
import com.blossom.backend.server.doc.DocTypeEnum;
import com.blossom.backend.server.utils.DocUtil;
import com.blossom.common.base.exception.XzException404;
@ -90,7 +87,19 @@ public class ArticleOpenController {
@PostMapping
public R<Long> open(@Validated @RequestBody ArticleOpenReq req) {
req.setUserId(AuthContext.getUserId());
return R.ok(openService.open(req));
return R.ok(openService.openSingle(req));
}
/**
* 批量公开文章
*
* @param req 文章对象
*/
@PostMapping("/batch")
public R<?> open(@Validated @RequestBody ArticleBatchOpenReq req) {
req.setUserId(AuthContext.getUserId());
openService.openBatch(req);
return R.ok();
}
/**

View File

@ -7,6 +7,8 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.server.article.TagEnum;
import com.blossom.backend.server.article.draft.ArticleService;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
import com.blossom.backend.server.article.open.pojo.ArticleBatchOpenReq;
import com.blossom.backend.server.article.open.pojo.ArticleOpenEntity;
import com.blossom.backend.server.article.open.pojo.ArticleOpenReq;
import com.blossom.common.base.enums.YesNo;
@ -58,28 +60,92 @@ public class ArticleOpenService extends ServiceImpl<ArticleOpenMapper, ArticleOp
* 公开或关闭公开访问
*/
@Transactional(rollbackFor = Exception.class)
public Long open(ArticleOpenReq req) {
public Long openSingle(ArticleOpenReq req) {
ArticleEntity article = articleService.getById(req.getId());
ArticleEntity entity = req.to(ArticleEntity.class);
open(req, article);
// ArticleEntity upd = new ArticleEntity();
// upd.setId(req.getId());
// upd.setUserId(req.getUserId());
// upd.setOpenStatus(req.getOpenStatus());
// /*
// * 公开文章 article 表插入到 article_open
// */
// if (YesNo.YES.getValue().equals(req.getOpenStatus())) {
// XzException400.throwBy(article.getOpenStatus().equals(YesNo.YES.getValue()), "文章已[" + req.getId() + "]已允许公开访问, 若要同步最新文章内容, 请使用同步");
// upd.setOpenVersion(article.getVersion());
// baseMapper.open(req.getId());
// }
// /*
// * 取消公开 删除 article_open 表数据
// */
// else if (YesNo.NO.getValue().equals(req.getOpenStatus())) {
// upd.setOpenVersion(0);
// XzException400.throwBy(article.getOpenStatus().equals(YesNo.NO.getValue()), "文章[" + req.getId() + "]未公开, 无法取消公开访问");
// baseMapper.delById(req.getId());
// }
//
// // 修改文章的公开状态
// articleService.update(upd);
return req.getId();
}
/**
* 批量公开
*
* @param req
* @since 1.14.0
*/
@Transactional(rollbackFor = Exception.class)
public void openBatch(ArticleBatchOpenReq req) {
ArticleQueryReq where = new ArticleQueryReq();
where.setPids(CollUtil.newArrayList(req.getPid()));
List<ArticleEntity> articles = articleService.listAll(where);
for (ArticleEntity article : articles) {
ArticleOpenReq open = new ArticleOpenReq();
open.setId(article.getId());
open.setOpenStatus(req.getOpenStatus());
open.setUserId(req.getUserId());
open(open, article);
}
}
/**
* 公开状态
*
* @param req 本次公开请求
* @param article 文章
*/
@Transactional(rollbackFor = Exception.class)
public void open(ArticleOpenReq req, ArticleEntity article) {
ArticleEntity upd = new ArticleEntity();
upd.setId(req.getId());
upd.setUserId(req.getUserId());
upd.setOpenStatus(req.getOpenStatus());
/*
* 公开文章 article 表插入到 article_open
*/
if (YesNo.YES.getValue().equals(req.getOpenStatus())) {
XzException400.throwBy(article.getOpenStatus().equals(YesNo.YES.getValue()), "文章已[" + req.getId() + "]已允许公开访问, 若要同步最新文章内容, 请使用同步");
entity.setOpenVersion(article.getVersion());
if (YesNo.YES.getValue().equals(article.getOpenStatus())) {
return;
}
// XzException400.throwBy(article.getOpenStatus().equals(YesNo.YES.getValue()), "文章已[" + req.getId() + "]已允许公开访问, 若要同步最新文章内容, 请使用同步");
upd.setOpenVersion(article.getVersion());
baseMapper.open(req.getId());
}
/*
* 取消公开 删除 article_open 表数据
*/
else if (YesNo.NO.getValue().equals(req.getOpenStatus())) {
entity.setOpenVersion(0);
XzException400.throwBy(article.getOpenStatus().equals(YesNo.NO.getValue()), "文章[" + req.getId() + "]未公开, 无法取消公开访问");
if (YesNo.NO.getValue().equals(article.getOpenStatus())) {
return;
}
// XzException400.throwBy(article.getOpenStatus().equals(YesNo.NO.getValue()), "文章[" + req.getId() + "]未公开, 无法取消公开访问");
upd.setOpenVersion(0);
baseMapper.delById(req.getId());
}
articleService.update(entity);
return req.getId();
// 修改文章的公开状态
articleService.update(upd);
}
/**

View File

@ -0,0 +1,44 @@
package com.blossom.backend.server.article.open.pojo;
import com.blossom.common.base.enums.YesNo;
import com.blossom.common.base.pojo.AbstractPOJO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 文件夹下的文章全部公开
*
* @author xzzz
* @since 1.14.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ArticleBatchOpenReq extends AbstractPOJO {
/**
* 文章ID
*/
@Min(value = 0, message = "[文件夹ID] 不能小于0")
@NotNull(message = "[文件夹ID] 为必填项")
private Long pid;
/**
* 公开状态 {@link YesNo}
*
* @see YesNo
*/
@Min(value = 0, message = "[open 状态] 不能小于0")
@Max(value = 1, message = "[open 状态] 不能大于1")
@NotNull(message = "[open 状态] 为必填项")
private Integer openStatus;
/**
* 用户ID
*/
private Long userId;
}

View File

@ -42,12 +42,12 @@ public class ArticleStatJob {
}
for (UserEntity user : users) {
ArticleStatRes statCount = statService.statCount(toDayBegin, toDayEnd, user.getId());
statService.updByDate(1, toDay, statCount.getArticleCount(), user.getId());
ArticleStatRes statCount = statService.statUpdArticleCount(toDayBegin, toDayEnd, user.getId());
statService.updByDate(ArticleStatTypeEnum.ARTICLE_HEATMAP, toDay, statCount.getArticleCount(), user.getId());
String toMouth = DateUtils.format(DateUtils.beginOfMonth(DateUtils.date()), DateUtils.PATTERN_YYYYMMDD);
ArticleStatRes statWord = statService.statCount(null, null, user.getId());
statService.updByDate(2, toMouth, statWord.getArticleWords(), user.getId());
statService.updByDate(ArticleStatTypeEnum.ARTICLE_WORDS, toMouth, statWord.getArticleWords(), user.getId());
}
}
}

View File

@ -140,6 +140,16 @@ public class ArticleStatService extends ServiceImpl<ArticleStatMapper, ArticleSt
return res;
}
/**
* 文章统计, 文章数, 总字数
*
* @param beginUpdTime 修改日期的开始范围
* @param endUpdTime 修改日期的结束范围
*/
public ArticleStatRes statUpdArticleCount(String beginUpdTime, String endUpdTime, Long userId) {
return articleMapper.statUpdArticleCount(beginUpdTime, endUpdTime, userId);
}
/**
* 文章统计, 文章数, 总字数
*
@ -161,7 +171,7 @@ public class ArticleStatService extends ServiceImpl<ArticleStatMapper, ArticleSt
return;
}
for (ArticleWordsRes words : req.getWordsList()) {
this.updByDate(ArticleStatTypeEnum.ARTICLE_WORDS.getType(), words.getDate() + "-01", words.getValue(), userId);
this.updByDate(ArticleStatTypeEnum.ARTICLE_WORDS, words.getDate() + "-01", words.getValue(), userId);
}
}
@ -174,26 +184,26 @@ public class ArticleStatService extends ServiceImpl<ArticleStatMapper, ArticleSt
* @param userId 用户ID
*/
@Transactional(rollbackFor = Exception.class)
public void updByDate(Integer type, String date, Integer value, Long userId) {
public void updByDate(ArticleStatTypeEnum type, String date, Integer value, Long userId) {
if (value == null) {
value = 0;
}
LambdaQueryWrapper<ArticleStatEntity> existWhere = new LambdaQueryWrapper<>();
existWhere
.eq(ArticleStatEntity::getUserId, userId)
.eq(ArticleStatEntity::getType, type)
.eq(ArticleStatEntity::getType, type.getType())
.eq(ArticleStatEntity::getStatDate, date);
if (baseMapper.exists(existWhere)) {
LambdaQueryWrapper<ArticleStatEntity> updWhere = new LambdaQueryWrapper<>();
updWhere.eq(ArticleStatEntity::getUserId, userId)
.eq(ArticleStatEntity::getType, type)
.eq(ArticleStatEntity::getType, type.getType())
.eq(ArticleStatEntity::getStatDate, date);
ArticleStatEntity upd = new ArticleStatEntity();
upd.setStatValue(value);
baseMapper.update(upd, updWhere);
} else {
ArticleStatEntity ist = new ArticleStatEntity();
ist.setType(type);
ist.setType(type.getType());
ist.setStatDate(DateUtils.parse(date, DateUtils.PATTERN_YYYYMMDD));
ist.setStatValue(value);
ist.setUserId(userId);

View File

@ -5,6 +5,8 @@ import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.config.BlConstants;
import com.blossom.backend.server.doc.pojo.DocTreeReq;
import com.blossom.backend.server.doc.pojo.DocTreeRes;
import com.blossom.backend.server.doc.pojo.DocTreeUpdSortReq;
import com.blossom.backend.server.folder.FolderTypeEnum;
import com.blossom.common.base.pojo.R;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
@ -29,7 +31,7 @@ public class DocController {
/**
* 文档列表
*
* @return 件夹列表
* @return 列表
* @apiNote 文档包含文章和文件夹, 文件夹分为图片文件夹和文章文件夹 {@link DocTypeEnum}
*/
@GetMapping("/trees")
@ -56,4 +58,20 @@ public class DocController {
open.setUserId(userId);
return R.ok(docService.listTree(open));
}
/**
* 修改排序
*
* @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));
}
}

View File

@ -8,9 +8,10 @@ public interface DocMapper {
/**
* 查询 PID 下的最小排序
*
* @param pid PID
* @return 最小排序
*/
Integer selectMaxSortByPid(@Param("pid") Long pid);
Integer selectMaxSortByPid(@Param("pid") Long pid, @Param("userId") Long userId, @Param("type") Integer type);
}

View File

@ -1,14 +1,18 @@
package com.blossom.backend.server.doc;
import cn.hutool.core.collection.CollUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.server.article.TagEnum;
import com.blossom.backend.server.article.draft.ArticleMapper;
import com.blossom.backend.server.article.draft.ArticleService;
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.backend.server.doc.pojo.DocTreeReq;
import com.blossom.backend.server.doc.pojo.DocTreeRes;
import com.blossom.backend.server.doc.pojo.DocTreeUpdSortReq;
import com.blossom.backend.server.folder.FolderMapper;
import com.blossom.backend.server.folder.FolderService;
import com.blossom.backend.server.folder.FolderTypeEnum;
import com.blossom.backend.server.folder.pojo.FolderQueryReq;
import com.blossom.backend.server.folder.pojo.FolderEntity;
import com.blossom.backend.server.picture.PictureService;
import com.blossom.backend.server.utils.DocUtil;
import com.blossom.backend.server.utils.PictureUtil;
@ -16,6 +20,7 @@ import com.blossom.common.base.enums.YesNo;
import com.blossom.common.base.util.SortUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@ -29,23 +34,28 @@ import java.util.stream.Collectors;
*/
@Service
public class DocService {
private FolderService folderService;
private ArticleService articleService;
private PictureService pictureService;
private FolderService folderService;
@Autowired
private DocMapper baseMapper;
@Autowired
public void setFolderService(FolderService folderService) {
this.folderService = folderService;
}
private FolderMapper folderMapper;
@Autowired
private ArticleMapper articleMapper;
@Autowired
private DocSortChecker docSortChecker;
@Autowired
public void setArticleService(ArticleService articleService) {
this.articleService = articleService;
}
@Autowired
public void setFolderService(FolderService folderService) {
this.folderService = folderService;
}
@Autowired
public void setPictureService(PictureService pictureService) {
this.pictureService = pictureService;
@ -64,10 +74,10 @@ public class DocService {
* 只查询文件夹
* =============================================================================================== */
if (req.getOnlyFolder()) {
FolderQueryReq where = req.to(FolderQueryReq.class);
List<DocTreeRes> folder = folderService.listTree(where);
FolderEntity where = req.to(FolderEntity.class);
List<FolderEntity> folder = folderMapper.listAll(where);
all.addAll(CollUtil.newArrayList(PictureUtil.getDefaultFolder(req.getUserId())));
all.addAll(folder);
all.addAll(DocUtil.toDocTreesByFolders(folder));
priorityType = true;
}
/* ===============================================================================================
@ -75,16 +85,21 @@ public class DocService {
* =============================================================================================== */
else if (req.getOnlyPicture()) {
// 1. 所有图片文件夹
FolderQueryReq folder = req.to(FolderQueryReq.class);
folder.setType(FolderTypeEnum.PICTURE.getType());
List<DocTreeRes> picFolder = folderService.listTree(folder);
all.addAll(picFolder);
FolderEntity where = req.to(FolderEntity.class);
where.setType(FolderTypeEnum.PICTURE.getType());
List<FolderEntity> picFolder = folderMapper.listAll(where);
all.addAll(DocUtil.toDocTreesByFolders(picFolder));
// 2. 有图片的图片或文章文件夹
List<Long> pids = pictureService.listDistinctPid(req.getUserId());
if (CollUtil.isNotEmpty(pids)) {
List<DocTreeRes> articleTopFolder = folderService.recursiveToParentTree(pids);
all.addAll(articleTopFolder);
List<Long> picFolderIds = picFolder.stream().map(FolderEntity::getId).collect(Collectors.toList());
// 剔除掉图片文件夹
List<Long> articleFolderIds = pids.stream().filter(i -> !picFolderIds.contains(i)).collect(Collectors.toList());
if(CollUtil.isNotEmpty(articleFolderIds)) {
List<FolderEntity> articleFolder = folderMapper.recursiveToParent(articleFolderIds);
all.addAll(DocUtil.toDocTreesByFolders(articleFolder));
}
}
Optional<DocTreeRes> min = all.stream().min((f1, f2) -> SortUtil.intSort.compare(f1.getS(), f2.getS()));
@ -99,72 +114,83 @@ public class DocService {
* 只查询公开的的文章和文章文件夹
* =============================================================================================== */
else if (req.getOnlyOpen()) {
ArticleQueryReq articleWhere = req.to(ArticleQueryReq.class);
articleWhere.setOpenStatus(YesNo.YES.getValue());
List<DocTreeRes> articles = articleService.listTree(articleWhere);
all.addAll(articles);
ArticleEntity where = req.to(ArticleEntity.class);
where.setOpenStatus(YesNo.YES.getValue());
List<ArticleEntity> articles = articleMapper.listAll(where);
all.addAll(DocUtil.toDocTreesByArticles(articles));
if (CollUtil.isNotEmpty(articles)) {
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
all.addAll(folders);
List<Long> pidList = articles.stream().map(ArticleEntity::getPid).collect(Collectors.toList());
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
all.addAll(DocUtil.toDocTreesByFolders(folders));
}
}
/* ===============================================================================================
* 只查询专题的文章和文件夹
* =============================================================================================== */
else if (req.getOnlySubject()) {
FolderQueryReq folderWhere = req.to(FolderQueryReq.class);
folderWhere.setTags(TagEnum.subject.name());
folderWhere.setType(FolderTypeEnum.ARTICLE.getType());
List<DocTreeRes> subjects = folderService.listTree(folderWhere);
FolderEntity where = req.to(FolderEntity.class);
where.setTags(TagEnum.subject.name());
where.setType(FolderTypeEnum.ARTICLE.getType());
List<FolderEntity> subjects = folderMapper.listAll(where);
if (CollUtil.isNotEmpty(subjects)) {
List<Long> subjectIds = subjects.stream().map(DocTreeRes::getI).collect(Collectors.toList());
List<DocTreeRes> foldersTop = folderService.recursiveToParentTree(subjectIds);
List<DocTreeRes> foldersBottom = folderService.recursiveToChildrenTree(subjectIds);
all.addAll(foldersTop);
all.addAll(foldersBottom);
List<Long> subjectIds = subjects.stream().map(FolderEntity::getId).collect(Collectors.toList());
List<FolderEntity> foldersTop = folderMapper.recursiveToParent(subjectIds);
List<FolderEntity> foldersBottom = folderMapper.recursiveToChildren(subjectIds);
all.addAll(DocUtil.toDocTreesByFolders(foldersTop));
all.addAll(DocUtil.toDocTreesByFolders(foldersBottom));
}
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
all.addAll(articles);
List<ArticleEntity> articles = articleMapper.listAll(req.to(ArticleEntity.class));
all.addAll(DocUtil.toDocTreesByArticles(articles));
}
/* ===============================================================================================
* 只查询关注的
* =============================================================================================== */
else if (req.getOnlyStar()) {
ArticleQueryReq articleWhere = req.to(ArticleQueryReq.class);
articleWhere.setStarStatus(YesNo.YES.getValue());
List<DocTreeRes> articles = articleService.listTree(articleWhere);
all.addAll(articles);
ArticleEntity where = req.to(ArticleEntity.class);
where.setStarStatus(YesNo.YES.getValue());
List<ArticleEntity> articles = articleMapper.listAll(where);
all.addAll(DocUtil.toDocTreesByArticles(articles));
if (CollUtil.isNotEmpty(articles)) {
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
all.addAll(folders);
List<Long> pidList = articles.stream().map(ArticleEntity::getPid).collect(Collectors.toList());
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
all.addAll(DocUtil.toDocTreesByFolders(folders));
}
}
/* ===============================================================================================
* 只查询指定文章
* =============================================================================================== */
else if (req.getArticleId() != null && req.getArticleId() > 0) {
ArticleQueryReq articleWhere = req.to(ArticleQueryReq.class);
articleWhere.setId(req.getArticleId());
List<DocTreeRes> articles = articleService.listTree(articleWhere);
all.addAll(articles);
ArticleEntity where = req.to(ArticleEntity.class);
where.setId(req.getArticleId());
List<ArticleEntity> articles = articleMapper.listAll(where);
all.addAll(DocUtil.toDocTreesByArticles(articles));
if (CollUtil.isNotEmpty(articles)) {
List<Long> pidList = articles.stream().map(DocTreeRes::getP).collect(Collectors.toList());
List<DocTreeRes> folders = folderService.recursiveToParentTree(pidList);
all.addAll(folders);
List<Long> pidList = articles.stream().map(ArticleEntity::getPid).collect(Collectors.toList());
List<FolderEntity> folders = folderMapper.recursiveToParent(pidList);
all.addAll(DocUtil.toDocTreesByFolders(folders));
}
}
/* ===============================================================================================
* 默认查询文章文件夹
* =============================================================================================== */
else {
FolderQueryReq folder = req.to(FolderQueryReq.class);
FolderEntity folder = req.to(FolderEntity.class);
folder.setType(FolderTypeEnum.ARTICLE.getType());
List<DocTreeRes> folders = folderService.listTree(folder);
List<DocTreeRes> articles = articleService.listTree(req.to(ArticleQueryReq.class));
all.addAll(folders);
all.addAll(articles);
List<FolderEntity> folders = folderMapper.listAll(folder);
all.addAll(DocUtil.toDocTreesByFolders(folders));
List<ArticleEntity> articles = articleMapper.listAll(req.to(ArticleEntity.class));
all.addAll(DocUtil.toDocTreesByArticles(articles));
}
return DocUtil.treeWrap(all.stream().distinct().collect(Collectors.toList()), priorityType);
@ -177,8 +203,64 @@ public class DocService {
* @return 最大排序
* @since 1.10.0
*/
public int selectMinSortByPid(Long pid) {
return baseMapper.selectMaxSortByPid(pid);
public int selectMaxSortByPid(Long pid, Long userId, FolderTypeEnum type) {
return baseMapper.selectMaxSortByPid(pid, userId, type.getType());
}
/**
* 修改排序
*
* @param docs 需要修改的文档
* @since 1.14.0
*/
@Transactional(rollbackFor = Exception.class)
public void updSort(List<DocTreeUpdSortReq.Doc> docs, Long userId, FolderTypeEnum folderType) {
// 提取所有需要修改的文档的父文档
List<Long> pids = docs.stream().map(DocTreeUpdSortReq.Doc::getP).collect(Collectors.toList());
List<ArticleEntity> articles = new ArrayList<>();
List<FolderEntity> folders = new ArrayList<>();
// 一次拖拽所修改的文档可能包含文章和文件夹, 需要归类和转为 Entity
for (DocTreeUpdSortReq.Doc doc : docs) {
if (DocTypeEnum.A.getType().equals(doc.getTy())) {
ArticleEntity a = new ArticleEntity();
a.setId(doc.getI());
a.setPid(doc.getP());
a.setName(doc.getN());
a.setSort(doc.getS());
articles.add(a);
}
if (DocTypeEnum.FA.getType().equals(doc.getTy())) {
FolderEntity f = new FolderEntity();
f.setId(doc.getI());
f.setPid(doc.getP());
f.setName(doc.getN());
f.setSort(doc.getS());
folders.add(f);
}
}
docSortChecker.checkUnique(pids, folders, articles, folderType, userId);
for (DocTreeUpdSortReq.Doc tree : docs) {
if (DocTypeEnum.FA.getType().equals(tree.getTy()) || DocTypeEnum.FP.getType().equals(tree.getTy())) {
FolderEntity f = new FolderEntity();
f.setId(tree.getI());
f.setPid(tree.getP());
f.setSort(tree.getS());
f.setUserId(AuthContext.getUserId());
f.setStorePath(tree.getSp());
folderService.update(f);
} else if (DocTypeEnum.A.getType().equals(tree.getTy())) {
ArticleEntity a = new ArticleEntity();
a.setId(tree.getI());
a.setPid(tree.getP());
a.setSort(tree.getS());
a.setUserId(AuthContext.getUserId());
articleService.update(a);
}
}
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,81 @@
package com.blossom.backend.server.doc.pojo;
import com.blossom.backend.server.doc.DocTypeEnum;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
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
*/
@NotNull(message = "文件夹类型为必填项")
@Min(value = 1, message = "文件夹类型错误")
@Max(value = 2, message = "文件夹类型错误")
private Integer folderType;
/**
* [Picture + Article] 只查询图片文件夹, 以及含有图片的文章文件夹
*/
@NotNull(message = "文件夹类型为必填项")
private Boolean onlyPicture;
/**
* 获取查询的文件夹类型
*
* @return 返回结果不会为 null
*/
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;
}
}

View File

@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
@ -44,15 +45,30 @@ public class FolderController {
if (userId == null) {
return R.ok(new ArrayList<>());
}
return R.ok(baseService.subjects(userId));
return R.ok(baseService.subjects(userId, true, false));
}
/**
* 查询专题列表
*
* @param starStatus 公开状态
*/
@GetMapping("/subjects")
public R<List<FolderSubjectRes>> listSubject() {
return R.ok(baseService.subjects(AuthContext.getUserId()));
return R.ok(baseService.subjects(AuthContext.getUserId(), false, true));
}
/**
* 星标文件夹
*
* @param req 目录文件夹
* @since 1.14.0
*/
@PostMapping("/star")
public R<Long> star(@Validated @RequestBody FolderStarReq req) {
FolderEntity folder = req.to(FolderEntity.class);
folder.setUserId(AuthContext.getUserId());
return R.ok(baseService.update(folder));
}
/**
@ -68,6 +84,7 @@ public class FolderController {
FolderRes res = entity.to(FolderRes.class);
res.setTags(DocUtil.toTagList(entity.getTags()));
res.setType(entity.getType());
res.setStarStatus(entity.getStarStatus());
return R.ok(res);
}
@ -81,9 +98,12 @@ public class FolderController {
FolderEntity folder = req.to(FolderEntity.class);
folder.setTags(DocUtil.toTagStr(req.getTags()));
folder.setUserId(AuthContext.getUserId());
// 如果新增到顶部, 获取最小的
// 如果新增到底部, 获取最大的排序
if (BooleanUtil.isTrue(req.getAddToLast())) {
folder.setSort(docService.selectMinSortByPid(req.getPid()) + 1);
folder.setSort(docService.selectMaxSortByPid(
req.getPid(),
AuthContext.getUserId(),
Objects.requireNonNull(FolderTypeEnum.getType(req.getType()))) + 1);
}
return R.ok(baseService.insert(folder));
}
@ -97,6 +117,15 @@ public class FolderController {
public R<Long> update(@Validated @RequestBody FolderUpdReq req) {
FolderEntity folder = req.to(FolderEntity.class);
folder.setTags(DocUtil.toTagStr(req.getTags()));
// 检查排序是否重复
// if (folder.getSort() != null && folder.getPid() != null) {
// final long newPid = folder.getPid();
// docSortChecker.checkUnique(CollUtil.newArrayList(newPid),
// CollUtil.newArrayList(folder),
// null,
// FolderTypeEnum.ARTICLE,
// AuthContext.getUserId());
// }
return R.ok(baseService.update(folder));
}
@ -113,7 +142,7 @@ public class FolderController {
}
/**
* 为文件夹快速增加/删除标签
* 快速增加/删除标签
*
* @since 1.10.0
*/

View File

@ -5,14 +5,11 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.server.article.TagEnum;
import com.blossom.backend.server.article.draft.ArticleService;
import com.blossom.backend.server.article.draft.ArticleMapper;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.backend.server.article.draft.pojo.ArticleQueryReq;
import com.blossom.backend.server.doc.pojo.DocTreeRes;
import com.blossom.backend.server.folder.pojo.FolderEntity;
import com.blossom.backend.server.folder.pojo.FolderQueryReq;
import com.blossom.backend.server.folder.pojo.FolderSubjectRes;
import com.blossom.backend.server.picture.PictureService;
import com.blossom.backend.server.picture.PictureMapper;
import com.blossom.backend.server.picture.pojo.PictureEntity;
import com.blossom.backend.server.utils.DocUtil;
import com.blossom.common.base.enums.YesNo;
@ -38,18 +35,13 @@ import java.util.stream.Collectors;
@Slf4j
@Service
public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
private ArticleService articleService;
private PictureService pictureService;
@Autowired
public void setArticleService(ArticleService articleService) {
this.articleService = articleService;
}
private PictureMapper picMapper;
@Autowired
public void setPictureService(PictureService pictureService) {
this.pictureService = pictureService;
}
private ArticleMapper articleMapper;
/**
* 专题列表
@ -59,36 +51,42 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
* <p>4. 相同专题的所有文件夹ID归为一组.
* <p>5. 通过文件夹ID获取到专题下的所有文章, 从而统计文章的总字数, 修改时间, 创建时间等.
* <p>6. 如果文章包含 TOC 标签, 则该文章为专题的目录, 专题的默认跳转会跳转至该目录
*
* @param userId 用户ID
* @param starStatus 公开状态
*/
public List<FolderSubjectRes> subjects(Long userId) {
// 1. 查询所有公开的专题
public List<FolderSubjectRes> subjects(Long userId, boolean openStatus, boolean starStatus) {
// 1. 查询所有专题
FolderEntity where = new FolderEntity();
where.setTags(TagEnum.subject.name());
where.setOpenStatus(YesNo.YES.getValue());
where.setUserId(userId);
List<FolderEntity> allOpenSubject = baseMapper.listAll(where);
if (CollUtil.isEmpty(allOpenSubject)) {
if (openStatus) {
where.setOpenStatus(YesNo.YES.getValue());
}
if (starStatus) {
where.setStarStatus(YesNo.YES.getValue());
}
List<FolderEntity> allSubjects = baseMapper.listAll(where);
if (CollUtil.isEmpty(allSubjects)) {
return new ArrayList<>();
}
// 专题的ID
List<Long> allOpenSubjectIds = allOpenSubject.stream().map(FolderEntity::getId).collect(Collectors.toList());
List<Long> allSubjectIds = allSubjects.stream().map(FolderEntity::getId).collect(Collectors.toList());
// 2. 查询全部专题的子文件夹
List<FolderEntity> allOpenSubjectChildFolders = baseMapper.recursiveToChildren(CollUtil.newArrayList(allOpenSubjectIds));
allOpenSubjectIds.addAll(allOpenSubjectChildFolders.stream().map(FolderEntity::getId).collect(Collectors.toList()));
List<FolderEntity> allSubjectChildFolders = baseMapper.recursiveToChildren(CollUtil.newArrayList(allSubjectIds));
allSubjectIds.addAll(allSubjectChildFolders.stream().map(FolderEntity::getId).collect(Collectors.toList()));
// 3. 查询这些文件夹下的所有文章
ArticleQueryReq articleWhere = new ArticleQueryReq();
articleWhere.setPids(allOpenSubjectIds);
ArticleEntity articleWhere = new ArticleEntity();
articleWhere.setPids(allSubjectIds);
articleWhere.setUserId(userId);
// 统计专题信息时, 会包含非公开文章
// articleWhere.setOpenStatus(YesNo.YES.getValue());
List<ArticleEntity> articles = articleService.listAll(articleWhere);
List<ArticleEntity> articles = articleMapper.listAll(articleWhere);
List<FolderSubjectRes> results = new ArrayList<>();
for (FolderEntity subject : allOpenSubject) {
for (FolderEntity subject : allSubjects) {
// 专题对象, 包含字数, 更新日期等信息
FolderSubjectRes result = subject.to(FolderSubjectRes.class);
// 默认专题字数
@ -96,7 +94,7 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
// 默认专题修改时间
result.setSubjectUpdTime(subject.getCreTime());
// 4. 这个专题下的所有文件夹ID
List<Long> subjectAllId = DocUtil.getChildrenIds(subject.getId(), allOpenSubjectChildFolders);
List<Long> subjectAllId = DocUtil.getChildrenIds(subject.getId(), allSubjectChildFolders);
// 5. 遍历文章, 将文章归属到某个专题下, 并统计相关字数, 日期等信息
for (ArticleEntity article : articles) {
if (subjectAllId.contains(article.getPid())) {
@ -117,34 +115,6 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
return results;
}
/**
* 查询全部文件夹, 并转换成 {@link DocTreeRes}
*/
public List<DocTreeRes> listTree(FolderQueryReq req) {
List<FolderEntity> folders = baseMapper.listAll(req.to(FolderEntity.class));
return DocUtil.toTreeRes(folders);
}
/**
* 递归获取传入ID的所有的父文件夹, 并转换成 {@link DocTreeRes}, 结果会包含自己
*
* @param ids ID 集合
*/
public List<DocTreeRes> recursiveToParentTree(List<Long> ids) {
List<FolderEntity> folders = baseMapper.recursiveToParent(ids);
return DocUtil.toTreeRes(folders);
}
/**
* 递归获取传入ID的所有的子文件夹, 并转换成 {@link DocTreeRes}, 结果会包含自己
*
* @param ids ID 集合
*/
public List<DocTreeRes> recursiveToChildrenTree(List<Long> ids) {
List<FolderEntity> folders = baseMapper.recursiveToChildren(ids);
return DocUtil.toTreeRes(folders);
}
/**
* 根据ID查询
*
@ -194,9 +164,50 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
*/
@Transactional(rollbackFor = Exception.class)
public Long update(FolderEntity folder) {
XzException404.throwBy(folder.getId() == null, "ID不得为空");
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
// 如果
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) {
// 处理文件夹的存储地址
if (StrUtil.isNotBlank(folder.getStorePath())) {
final FolderEntity oldFolder = selectById(folder.getId());
// 获取所有子文件夹
@ -214,8 +225,6 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
}
}
folder.setStorePath(formatStorePath(folder.getStorePath()));
baseMapper.updById(folder);
return folder.getId();
}
/**
@ -245,34 +254,12 @@ public class FolderService extends ServiceImpl<FolderMapper, FolderEntity> {
}
/**
* 删除文件夹
* <p>1. 文件夹下有子文件夹时, 无法删除</p>
* <p>2. 文件夹下有文章时, 无法删除</p>
* 检查修改是否有效
*
* @param folderId 文件夹ID
* @param folder 文件夹
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Long folderId) {
// 文件夹下有文件夹, 无法删除
if (recursiveToChildrenTree(CollUtil.newArrayList(folderId)).stream().anyMatch(d -> !d.getI().equals(folderId))) {
throw new XzException500("文件夹下有子文件夹, 无法删除, 请先删除子文件夹");
}
// 文件夹下有文章, 无法删除
ArticleQueryReq articleReq = new ArticleQueryReq();
articleReq.setPids(CollUtil.newArrayList(folderId));
if (CollUtil.isNotEmpty(articleService.listTree(articleReq))) {
throw new XzException500("文件夹下有文章, 无法删除, 请先删除下属文章");
}
// 文件夹下有图片, 无法删除
PictureEntity picReq = new PictureEntity();
picReq.setPid(folderId);
if (CollUtil.isNotEmpty(pictureService.listAll(picReq))) {
throw new XzException500("文件夹下有图片, 无法删除, 请先删除下属图片");
}
baseMapper.deleteById(folderId);
private void updateParamValid(FolderEntity folder) {
XzException404.throwBy(folder.getId() == null, "ID不得为空");
XzException400.throwBy(folder.getId().equals(folder.getPid()), "上级文件夹不能是自己");
}
}

View File

@ -28,4 +28,14 @@ public enum FolderTypeEnum {
this.type = type;
this.desc = desc;
}
public static FolderTypeEnum getType(Integer type) {
for (FolderTypeEnum value : FolderTypeEnum.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return null;
}
}

View File

@ -1,13 +1,10 @@
package com.blossom.backend.server.folder.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.blossom.backend.server.folder.FolderTypeEnum;
import com.blossom.common.base.pojo.AbstractPOJO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
@ -18,7 +15,6 @@ import java.util.List;
*
* @author xzzz
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("blossom_folder")
public class FolderEntity extends AbstractPOJO implements Serializable {
@ -44,6 +40,10 @@ public class FolderEntity extends AbstractPOJO implements Serializable {
* 标签
*/
private String tags;
/**
* star状态
*/
private Integer starStatus;
/**
* 开放状态
*/
@ -102,6 +102,11 @@ public class FolderEntity extends AbstractPOJO implements Serializable {
@TableField(exist = false)
private List<Long> ids;
//endregion
/**
* 父ID集合
*/
@TableField(exist = false)
private List<Long> pids;
//endregion
}

View File

@ -16,6 +16,10 @@ import java.io.Serializable;
public class FolderQueryReq extends PageReq implements Serializable {
private static final long serialVersionUID = 1L;
/**
* star状态
*/
private Integer starStatus;
private String tags;

View File

@ -39,6 +39,10 @@ public class FolderRes extends AbstractPOJO implements Serializable {
* 标签
*/
private List<String> tags;
/**
* star状态
*/
private Integer starStatus;
/**
* 是否公开文件夹 [0:未公开1:公开]
*/

View File

@ -0,0 +1,37 @@
package com.blossom.backend.server.folder.pojo;
import com.blossom.common.base.enums.YesNo;
import com.blossom.common.base.pojo.AbstractPOJO;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* 文章 star
*
* @author xzzz
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class FolderStarReq extends AbstractPOJO {
/**
* 目录ID
*/
@Min(value = 0, message = "[目录ID] 不能小于0")
@NotNull(message = "[目录ID] 为必填项")
private Long id;
/**
* star 状态 {@link YesNo}
* @see YesNo
*/
@Min(value = 0, message = "[star 状态] 不能小于0")
@Max(value = 1, message = "[star 状态] 不能大于1")
@NotNull(message = "[star 状态] 为必填项")
private Integer starStatus;
}

View File

@ -33,6 +33,10 @@ public class FolderUpdReq extends AbstractPOJO implements Serializable {
private String name;
/** 图标 */
private String icon;
/**
* star状态
*/
private Integer starStatus;
/** 标签 */
private List<String> tags;
/** 排序 */

View File

@ -43,8 +43,11 @@ public class TodoService extends ServiceImpl<TodoMapper, TodoEntity> {
TodoGroupRes res = TodoGroupRes.build();
Map<String, List<TodoEntity>> map = todos.stream().collect(Collectors.groupingBy(TodoEntity::getTodoId));
// 遍历每个待办事项的所有任务
map.forEach((todoId, data) -> {
TodoGroupRes.TodoGroup group = data.get(0).to(TodoGroupRes.TodoGroup.class);
// 根据状态分组
Map<String, List<TodoEntity>> taskStatusMap = data.stream().collect(Collectors.groupingBy(TodoEntity::getTaskStatus));
int w = 0, p = 0, c = 0;
if (CollUtil.isNotEmpty(taskStatusMap.get(TaskStatusEnum.WAITING.name()))) {
@ -56,7 +59,8 @@ public class TodoService extends ServiceImpl<TodoMapper, TodoEntity> {
if (CollUtil.isNotEmpty(taskStatusMap.get(TaskStatusEnum.COMPLETED.name()))) {
c = taskStatusMap.get(TaskStatusEnum.COMPLETED.name()).get(0).getTaskCount();
}
group.setTaskCountStat(String.format("%d|%d|%d", w, p, c));
group.setTaskCountStat(String.format("%d/%d/%d", w, p, c));
group.setTaskCount(w + p + c);
if (TodoTypeEnum.DAY.getType().equals(data.get(0).getTodoType())) {
res.getTodoDays().put(group.getTodoId(), group);

View File

@ -60,7 +60,6 @@ public class DocUtil {
return SortUtil.intSort.compare(d1.getS(), d2.getS());
})
.collect(Collectors.toList());
return rootLevel;
}
@ -122,7 +121,7 @@ public class DocUtil {
tree.setTy(DocTypeEnum.A.getType());
// 判断文章的版本与公开版本是否有差异
if (article.getOpenStatus().equals(YesNo.YES.getValue()) && article.getVersion() > article.getOpenVersion()) {
if (YesNo.YES.getValue().equals(article.getOpenStatus()) && article.getVersion() > article.getOpenVersion()) {
tree.setVd(YesNo.YES.getValue());
}
@ -134,6 +133,20 @@ public class DocUtil {
return tree;
}
/**
* 文章集合转 docTree集合
*
* @param articles 文章集合
* @return docTree
*/
public static List<DocTreeRes> toDocTreesByArticles(List<ArticleEntity> articles) {
List<DocTreeRes> articleTrees = new ArrayList<>(articles.size());
for (ArticleEntity folder : articles) {
articleTrees.add(toDocTree(folder));
}
return articleTrees;
}
/**
* 文件夹转 docTree
*
@ -148,7 +161,7 @@ public class DocUtil {
tree.setS(folder.getSort());
tree.setN(folder.getName());
tree.setSp(folder.getStorePath());
tree.setStar(0);
tree.setStar(folder.getStarStatus());
tree.setTy(folder.getType());
tree.setIcon(folder.getIcon());
if (StrUtil.isBlank(folder.getTags())) {
@ -165,8 +178,11 @@ public class DocUtil {
* @param folders 文件夹集合
* @return docTree
*/
public static List<DocTreeRes> toTreeRes(List<FolderEntity> folders) {
List<DocTreeRes> folderTrees = new ArrayList(folders.size());
public static List<DocTreeRes> toDocTreesByFolders(List<FolderEntity> folders) {
if (CollUtil.isEmpty(folders)) {
return new ArrayList<>();
}
List<DocTreeRes> folderTrees = new ArrayList<>(folders.size());
for (FolderEntity folder : folders) {
folderTrees.add(toDocTree(folder));
}

View File

@ -58,9 +58,16 @@ public class TodoUtil {
if (CollUtil.isEmpty(todos)) {
return "无内容";
}
Map<String, List<TodoEntity>> maps = todos.stream().collect(Collectors.groupingBy(TodoEntity::getTodoName));
List<Map.Entry<String, List<TodoEntity>>> entryList = todos.stream()
.collect(Collectors.groupingBy(TodoEntity::getTodoName))
// 增加根据Todo name的排序
.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toList());
StringBuilder sb = new StringBuilder();
maps.forEach((todoName, tasks) -> {
entryList.forEach(entry -> {
String todoName = entry.getKey();
List<TodoEntity> tasks = entry.getValue();
sb.append(String.format("# %s \n\n", todoName));
for (TodoEntity task : tasks) {

View File

@ -37,6 +37,7 @@ spring:
init:
mode: always
platform: mysql
continue-on-error: true
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ mybatis ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -1,7 +1,7 @@
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/blossom?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8
url: jdbc:mysql://192.168.31.99:3306/xzzz-blossom?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&allowMultiQueries=true&useSSL=false&&serverTimezone=GMT%2B8
username: root
password: jasmine888
hikari:

View File

@ -92,12 +92,24 @@
html = #{html},
words = #{words},
toc = #{toc},
version = version + 1
version = version + 1,
upd_markdown_time = #{updMarkdownTime}
where id = #{id}
and user_id = #{userId}
</update>
<!-- ======================================== 统计 ======================================== -->
<select id="statUpdArticleCount" resultType="com.blossom.backend.server.article.draft.pojo.ArticleStatRes">
select count(*) as articleCount
from blossom_article
where user_id = #{userId}
<if test="beginUpdTime != null and beginUpdTime != ''">
and upd_markdown_time >= #{beginUpdTime}
</if>
<if test="endUpdTime != null and endUpdTime != ''">
and #{endUpdTime} >= upd_markdown_time
</if>
</select>
<select id="statCount" resultType="com.blossom.backend.server.article.draft.pojo.ArticleStatRes">
select count(*) as articleCount, sum(words) as articleWords

View File

@ -5,9 +5,9 @@
<select id="selectMaxSortByPid" resultType="java.lang.Integer">
select IFNULL(max(h.sort), 0)
from (
select sort from blossom_article where pid = #{pid}
select sort from blossom_article where pid = #{pid} and user_id = #{userId}
union
select sort from blossom_folder where pid = #{pid}
select sort from blossom_folder where pid = #{pid} and user_id = #{userId} and type = #{type}
) h;
</select>
</mapper>

View File

@ -9,6 +9,7 @@
`name`,
icon,
tags,
star_status,
open_status,
sort,
cover,
@ -21,9 +22,14 @@
cre_time
from blossom_folder
<where>
<if test="starStatus != null ">and star_status = #{starStatus}</if>
<if test="openStatus != null ">and open_status = #{openStatus}</if>
<if test="type != null ">and type = #{type}</if>
<if test="tags != null and tags != ''">and tags like concat('%',#{tags},'%')</if>
<if test="pid != null">and pid = #{pid}</if>
<if test="pids != null and pids.size() != 0">and pid in
<foreach collection="pids" item="item" open="(" close=")" separator=",">#{item}</foreach>
</if>
<if test="userId != null">and user_id = #{userId}</if>
</where>
</select>
@ -62,6 +68,7 @@
<if test="name != null">`name` = #{name},</if>
<if test="icon != null">icon = #{icon},</if>
<if test="tags != null">tags = #{tags},</if>
<if test="starStatus != null">star_status = #{starStatus},</if>
<if test="sort != null">sort = #{sort},</if>
<if test="cover != null">cover = #{cover},</if>
<if test="color != null">color = #{color},</if>

View File

@ -638,4 +638,10 @@ CREATE TABLE IF NOT EXISTS `base_user_param`
UNIQUE KEY `unq_bup_userid_paramname` (`user_id`, `param_name`) COMMENT '用户参数唯一'
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT = '用户参数'
COLLATE = utf8mb4_bin;
COLLATE = utf8mb4_bin;
-- Code that might be wrong goes last
-- since 1.14.0
alter table blossom_folder add column star_status tinyint(1) NOT NULL DEFAULT 0 COMMENT '收藏 0:否,1:是';
-- since 1.15.0
alter table blossom_article add column upd_markdown_time datetime COMMENT '文章内容的修改时间';

View File

@ -1 +1,7 @@
客户端静态文件放置位置。
客户端静态文件放置位置。
在 blossom-editor 工程下执行如下语句, 打包成网页后复制到后台 static/editor 资源目录下
```
npm run build
```

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>

View File

@ -42,4 +42,13 @@ public enum YesNo {
}
return NO.aBoolean;
}
public static YesNo getValue(Integer value) {
for (YesNo yesNo : YesNo.values()) {
if (yesNo.getValue().equals(value)) {
return yesNo;
}
}
return YesNo.NO;
}
}

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -13,7 +13,7 @@
<properties>
<!-- 数据库连接 -->
<mysql.version>8.0.21</mysql.version>
<mysql.version>8.0.28</mysql.version>
<!-- 分页插件 -->
<pagehelper.version>5.3.2</pagehelper.version>
<pagehelper-springboot.version>1.4.6</pagehelper-springboot.version>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>common</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -173,7 +173,11 @@ public class IaasProperties {
public static class BLOS {
/**
* BLOS 查看图片的接口的地址, 默认在 PictureController#getFile() 方法中, 末尾带有 "/" 会自动清除
*
* @deprecated 该配置项已转移至系统配置 base_sys_param {@link ParamEnum#BLOSSOM_OBJECT_STORAGE_DOMAIN},
* 但在 base_sys_param 为配置时仍然生效
*/
@Deprecated
private String domain;
/**
* BLOS 默认上传地址, 不能为空, 注意不同系统的区分, 末尾带有 "/" 会自动清除
@ -202,7 +206,7 @@ public class IaasProperties {
}
if (blos != null) {
String domain = formatDomain(blos.getDomain());
if (!StrUtil.endWith(domain,"/pic")) {
if (!StrUtil.endWith(domain, "/pic")) {
domain = domain + "/pic";
}
blos.setDomain(domain);

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.blossom</groupId>
<artifactId>blossom-backend</artifactId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.blossom</groupId>
<artifactId>expand-sentinel</artifactId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>com.blossom</groupId>
<artifactId>expand-sentinel</artifactId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>blossom-backend</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>expand-tracker</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>blossom-backend</artifactId>
<groupId>com.blossom</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -8,17 +8,17 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.blossom</groupId>
<artifactId>blossom-backend</artifactId>
<version>1.13.0-SNAPSHOT</version>
<version>1.15.0-SNAPSHOT</version>
<!-- 版本控制 -->
<properties>
<revision>1.13.0-SNAPSHOT</revision>
<revision>1.15.0-SNAPSHOT</revision>
<!-- 编译设置 -->
<java.version>1.8</java.version>
@ -30,7 +30,7 @@
SpringBoot 版本
https://spring.io/projects/spring-boot#support
-->
<spring-boot.version>2.7.11</spring-boot.version>
<spring-boot.version>2.7.18</spring-boot.version>
<!-- 其他三方包版本 -->
<lombok.version>1.18.8</lombok.version>

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "blossom",
"productName": "Blossom",
"version": "1.13.0",
"version": "1.15.0",
"description": "A markdown editor",
"license": "MIT",
"main": "./out/main/index.js",
@ -29,50 +29,50 @@
"build:linux": "npm run build && electron-builder --linux --config"
},
"dependencies": {
"@codemirror/lang-markdown": "^6.2.0",
"@codemirror/language-data": "^6.3.1",
"@electron-toolkit/preload": "^2.0.0",
"@electron-toolkit/utils": "^1.0.2",
"@codemirror/lang-markdown": "^6.2.4",
"@codemirror/language-data": "^6.4.1",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@types/marked": "^5.0.1",
"axios": "^1.4.0",
"axios": "^1.6.8",
"codemirror": "^6.0.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"electron-updater": "^5.3.0",
"element-plus": "^2.4.4",
"echarts": "^5.5.0",
"electron-updater": "^6.1.8",
"element-plus": "^2.6.3",
"highlight.js": "^11.8.0",
"hotkeys-js": "^3.12.0",
"katex": "^0.16.8",
"hotkeys-js": "^3.13.7",
"katex": "^0.16.10",
"marked": "^7.0.5",
"marked-highlight": "^2.0.4",
"marked-katex-extension": "^4.0.5",
"markmap-lib": "^0.15.4",
"markmap-view": "^0.15.4",
"mermaid": "^10.4.0",
"markmap-lib": "^0.16.1",
"markmap-view": "^0.16.0",
"mermaid": "^10.9.0",
"pinia": "^2.1.6",
"prettier": "^3.0.2",
"sass": "^1.66.1",
"vue-router": "^4.2.4"
"prettier": "^3.2.5",
"sass": "^1.74.1",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron/notarize": "^1.2.4",
"@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^18.17.8",
"@vitejs/plugin-vue": "^4.6.2",
"@electron/notarize": "^2.3.0",
"@rushstack/eslint-patch": "^1.10.1",
"@types/node": "^18.19.29",
"@vitejs/plugin-vue": "^5.0.4",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"electron": "^24.8.0",
"electron-builder": "^24.6.3",
"electron-vite": "^1.0.27",
"eslint": "^8.47.0",
"eslint-plugin-vue": "^9.17.0",
"rollup-plugin-visualizer": "^5.9.2",
"typescript": "^5.1.6",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.5.1",
"@vue/eslint-config-typescript": "^13.0.0",
"electron": "^28.2.10",
"electron-builder": "^24.13.3",
"electron-vite": "^2.1.0",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.24.0",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.4.4",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.8",
"vue": "^3.4.5",
"vue-tsc": "^1.8.27"
"vue-tsc": "^2.0.10"
}
}

View File

@ -168,6 +168,8 @@ const initTray = () => {
console.log('1. 创建托盘 Tray')
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: 'Blossom 官网 ', click: () => shell.openExternal('https://www.wangyunf.com/blossom-doc/index') },
{ type: 'separator' },
{ label: '显示', click: () => mainWindow!.show() },
{ label: '退出', click: () => app.quit() }
])
@ -432,9 +434,9 @@ export const initOnWindow = (window: BrowserWindow) => {
/**
*
*/
window.webContents.on('will-navigate', (event: Event, url: string) => {
interceptorATag(event, url)
})
// window.webContents.on('will-navigate', (event: Event, url: string) => {
// interceptorATag(event, url)
// })
/**
* , ,
@ -451,19 +453,19 @@ export const initOnWindow = (window: BrowserWindow) => {
})
}
/**
* a
* @param e
* @param url
*/
const interceptorATag = (e: Event, url: string): boolean => {
e.preventDefault()
let innerUrl = url
if (blossomUserinfo && innerUrl.startsWith(blossomUserinfo.userParams.WEB_ARTICLE_URL)) {
let articleId: string = innerUrl.replaceAll(blossomUserinfo.userParams.WEB_ARTICLE_URL, '')
createNewWindow('article', articleId, Number(articleId))
} else if (!is.dev) {
shell.openExternal(url)
}
return true
}
// /**
// * 拦截 a 标签
// * @param e
// * @param url
// */
// const interceptorATag = (e: Event, url: string): boolean => {
// e.preventDefault()
// let innerUrl = url
// if (blossomUserinfo && innerUrl.startsWith(blossomUserinfo.userParams.WEB_ARTICLE_URL)) {
// let articleId: string = innerUrl.replaceAll(blossomUserinfo.userParams.WEB_ARTICLE_URL, '')
// createNewWindow('article', articleId, Number(articleId))
// } else if (!is.dev) {
// shell.openExternal(url)
// }
// return true
// }

View File

@ -14,8 +14,8 @@
img-src * blob: data:;
connect-src *;
frame-src *;
font-src * data:;
" />
<!-- 加载动画 -->
<style>
:root {
--index-loading-color: #859e6052;
@ -336,7 +336,6 @@
}
@keyframes skeleton {
/* 骨架屏的动画 */
100% {
transform: translateX(100%);
}
@ -346,7 +345,6 @@
<body>
<div id="app">
<!-- 首页加载正常运行在这里 -->
<div class="html-loading">
<div class="header">
<div class="logo"></div>
@ -387,7 +385,7 @@
<br />
<span style="font-size: 13px">加载中...</span>
</div>
<div class="html-loading-version">Blossom | v1.13.0</div>
<div class="html-loading-version">Blossom | v1.15.0</div>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
<style lang="scss">

View File

@ -94,6 +94,15 @@ export const docTreeApi = (params?: object): Promise<R<any>> => {
return rq.get<R<any>>('/doc/trees', { params })
}
/**
*
* @param data
* @returns
*/
export const docUpdSortApi = (data: object): Promise<R<any>> => {
return rq.post<R<any>>('/doc/upd/sort', data)
}
//#endregion
//#region ====================================================< folder >====================================================
@ -274,6 +283,15 @@ export const articleStarApi = (data?: object): Promise<R<any>> => {
return rq.post<R<any>>('/article/star', data)
}
/**
* star star
* @param data
* @returns
*/
export const folderStarApi = (data?: object): Promise<R<any>> => {
return rq.post<R<any>>('/folder/star', data)
}
/**
* markdown
* @param params
@ -364,6 +382,15 @@ export const articleOpenApi = (data?: object): Promise<R<any>> => {
return rq.post<R<any>>('/article/open', data)
}
/**
*
* @param data
* @returns
*/
export const articleOpenBatchApi = (data?: object): Promise<R<any>> => {
return rq.post<R<any>>('/article/open/batch', data)
}
/**
*
* @param data
@ -431,16 +458,6 @@ export const articleBackupListApi = (): Promise<R<BackupFile[]>> => {
return rq.get<BackupFile[]>('/article/backup/list')
}
/**
*
* @param params
* @returns
*/
export const articleBackupDownloadApi = (params?: object): Promise<any> => {
let config = { params: params, responseType: 'blob' }
return rq.get('/article/backup/download', config)
}
/**
*
* @param data

View File

@ -90,7 +90,7 @@ export class Request {
/* 其他接口报错, 直接拒绝并提示错误信息 */
let errorResponse = data
errorResponse['url'] = res.config.url
Notify.error(data.msg, '请求失败')
Notify.error(data.msg, '处理失败')
return Promise.reject(res)
}
},
@ -106,22 +106,23 @@ export class Request {
}
let code = err.code
let resp = err.response
if (resp && resp.data) {
Notify.error(resp.data.msg, '请求失败')
return Promise.reject(err)
}
console.log("🚀 ~ Request ~ constructor ~ resp:123123123", resp)
if (code === 'ERR_NETWORK') {
Notify.error('网络错误,请检查您的网络是否通畅', '请求失败')
Notify.error('网络错误, 请检查您的网络是否通畅', '请求失败')
return Promise.reject(err)
}
if (err.request && err.request.status === 404) {
Notify.error('未找到您的请求, 请您检查服务器地址!', '请求失败(404)')
Notify.error('未找到您的请求', '请求失败(404)')
return Promise.reject(err)
}
if (err.request && err.request.status === 405) {
Notify.error(`您的请求地址可能有误, 请检查请求地址${url}`, '请求失败(405)')
return Promise.reject(err)
}
if (resp && resp.data) {
Notify.error(resp.data.msg, '请求失败')
return Promise.reject(err)
}
return Promise.reject(err)
}
)

View File

@ -5,7 +5,7 @@ const blossom = {
SYS: {
NAME: 'Blossom',
FULL_NAME: 'BLOSSOM-EDITOR',
VERSION: 'v1.13.0',
VERSION: 'v1.15.0',
//
DOC: 'https://www.wangyunf.com/blossom-doc/index',

View File

@ -31,14 +31,14 @@ img {
/* 定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸 */
::-webkit-scrollbar {
/* 滚动条宽度 */
width: 7px;
width: 5px;
/* 滚动条高度 */
height: 7px;
height: 5px;
}
::-webkit-scrollbar:hover {
width: 7px;
height: 7px;
width: 5px;
height: 5px;
}
/* 定义滑块 内阴影+圆角 */

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconbl"; /* Project id 4118609 */
src: url('iconfont.woff2?t=1706597865404') format('woff2'),
url('iconfont.woff?t=1706597865404') format('woff'),
url('iconfont.ttf?t=1706597865404') format('truetype');
src: url('iconfont.woff2?t=1712427980711') format('woff2'),
url('iconfont.woff?t=1712427980711') format('woff'),
url('iconfont.ttf?t=1712427980711') format('truetype');
}
.iconbl {
@ -13,6 +13,26 @@
-moz-osx-font-smoothing: grayscale;
}
.bl-collimation:before {
content: "\e62c";
}
.bl-search-item:before {
content: "\e753";
}
.bl-fileadd-line:before {
content: "\e659";
}
.bl-folderadd-line:before {
content: "\ea51";
}
.bl-collapse:before {
content: "\e60e";
}
.bl-brush-line:before {
content: "\ea4f";
}
@ -237,10 +257,6 @@
content: "\ea17";
}
.bl-a-leftdirection-fill:before {
content: "\ea18";
}
.bl-record-line:before {
content: "\ea19";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,41 @@
"css_prefix_text": "bl-",
"description": "",
"glyphs": [
{
"icon_id": "5203274",
"name": "瞄准",
"font_class": "collimation",
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "577366",
"name": "搜索类目-fill",
"font_class": "search-item",
"unicode": "e753",
"unicode_decimal": 59219
},
{
"icon_id": "7440601",
"name": "新增文件",
"font_class": "fileadd-line",
"unicode": "e659",
"unicode_decimal": 58969
},
{
"icon_id": "24341715",
"name": "folder add-line",
"font_class": "folderadd-line",
"unicode": "ea51",
"unicode_decimal": 59985
},
{
"icon_id": "11855598",
"name": "收起",
"font_class": "collapse",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "24341231",
"name": "brush-line",
@ -397,13 +432,6 @@
"unicode": "ea17",
"unicode_decimal": 59927
},
{
"icon_id": "24341919",
"name": "left direction-fill",
"font_class": "a-leftdirection-fill",
"unicode": "ea18",
"unicode_decimal": 59928
},
{
"icon_id": "24342162",
"name": "record-line",

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

View File

@ -1,10 +1,17 @@
.el-dialog {
--el-dialog-border-radius: 8px !important;
--el-dialog-padding-primary: 0 !important;
--el-dialog-padding-primary: 10px 0 0 0 !important;
--el-dialog-bg-color: var(--bl-dialog-bg-color) !important;
--el-dialog-box-shadow: var(--bl-dialog-box-shadow) !important;
}
.bl-dialog-draggable-header {
--el-dialog-padding-primary: 0 !important;
.el-dialog__header {
height: 10px;
}
}
// 更大的 header close 按钮
.bl-dialog-bigger-headerbtn {
.el-dialog__headerbtn {

View File

@ -61,24 +61,28 @@
td {
.cell {
border-radius: 0;
width: 44px;
border-radius: 4px;
}
}
.el-year-table {
td {
width: auto;
padding: 7px 0;
}
}
.el-month-table {
td {
width: auto;
padding: 1px 0;
}
}
.el-date-table {
td {
width: auto;
padding: 2px 0;
}
}
@ -121,4 +125,9 @@
.el-popper.is-small {
padding: 0 8px;
box-shadow: 1px 1px 3px #f4f4f4;
transition: none;
.el-popper__arrow {
display: none;
}
}

View File

@ -0,0 +1,27 @@
.resize-divider-vertical {
width: 1px;
height: 100%;
background-color: var(--el-border-color);
position: relative;
z-index: 1001;
cursor: ew-resize;
&::before {
content: '';
width: 3px;
height: 100%;
background-color: transparent;
display: block;
transition: 0.3s;
position: absolute;
left: -1px;
z-index: 1002;
cursor: ew-resize;
}
&:hover {
&::before {
background-color: var(--el-color-primary);
}
}
}

View File

@ -9,7 +9,7 @@
@import './bl-image.scss';
@import './bl-message.scss';
@import './bl-notification.scss';
@import './bl-popper.scss';
@import './bl-tip.scss';
@import './bl-tooltip.scss';
@import './bl-tree.scss';
@import '../../views/doc/tree-docs-right-menu.scss';
@import '../../views/doc/doc-tree-right-menu.scss';

View 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())

View File

@ -433,3 +433,11 @@ export const isBase64Img = (image: string) => {
let prefix = image.substring(0, Math.max(image.indexOf(','), 0))
return prefix.startsWith('data:image') && prefix.endsWith('base64')
}
export const uuid = (): string => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}

View File

@ -8,7 +8,7 @@
fontWeight: props.weight
}">
<!-- {{ !!slots.default }}| -->
<span v-if="props.icon" :class="['tag-iconbl iconbl', props.icon, !!slots.default ? 'tag-icon-margin' : '']" />
<span v-if="props.icon" :class="['tag-iconbl iconbl', props.icon]" />
<span class="tag-content">
<slot />
</span>
@ -16,10 +16,6 @@
</template>
<script setup lang="ts">
import { useSlots } from 'vue'
const slots = useSlots()
const props = defineProps({
/**
* background-color
@ -54,7 +50,7 @@ const props = defineProps({
<style scoped lang="scss">
.tag-root {
@include flex(row, center, center);
@include themeShadow(2px 2px 3px 0 #bbbbbb, 1px 2px 3px #0F0F0F);
@include themeShadow(2px 1px 3px 0 #bbbbbb, 1px 1px 3px #0f0f0f);
border-radius: 4px;
padding: 1px 4px;
margin: 3px;
@ -67,10 +63,6 @@ const props = defineProps({
font-size: 12px;
}
.tag-icon-margin {
margin-right: 2px;
}
.tag-content {
line-height: 12px;
}

View File

@ -48,6 +48,7 @@ import blossomIcons from '@renderer/assets/iconfont/blossom/iconfont.json'
import weblogIcons from '@renderer/assets/iconfont/weblogo/iconfont.json'
onMounted(() => {
document.title = 'Blossom 图标库'
blossom.value = blossomIcons.glyphs
weblogo.value = weblogIcons.glyphs.sort((w1, w2) => {
return w1.font_class.localeCompare(w2.font_class)
@ -125,8 +126,7 @@ const copyIcon = (icon: string) => {
align-content: flex-start;
flex-wrap: wrap;
background-color: var(--bl-html-color);
overflow: scroll;
overflow-y: overlay;
overflow-y: scroll;
.icon-item {
@include flex(column, space-between, center);

View File

@ -242,7 +242,7 @@ const showWebCollectCard = (card: boolean) => {
.web-item-container {
@include box(100%, calc(100% - 31px));
overflow-y: overlay;
overflow-y: scroll;
.placeholder {
@include font(15px, 300);

View File

@ -36,19 +36,19 @@ router.addRoute({
component: Index,
meta: { keepAlive: true },
children: [
{ path: '/home', name: 'Home', component: Home, meta: { keepAlive: true } },
{ path: '/settingIndex', name: 'SettingIndex', component: SettingIndex, meta: { keepAlive: false } },
{ path: '/home', name: 'Home', component: Home, meta: { keepAlive: true, title: 'Blossom 首页' } },
{ path: '/settingIndex', name: 'SettingIndex', component: SettingIndex, meta: { keepAlive: false, title: 'Blossom 设置' } },
// 功能页面
{ path: '/articleIndex', name: 'ArticleIndex', component: ArticleIndex, meta: { keepAlive: true } },
{ path: '/pictureIndex', name: 'PictureIndex', component: PictureIndex, meta: { keepAlive: true } },
{ path: '/todoIndex', name: 'TodoIndex', component: TodoIndex, meta: { keepAlive: true } },
{ path: '/noteIndex', name: 'NoteIndex', component: NoteIndex, meta: { keepAlive: false } },
{ path: '/planIndex', name: 'PlanIndex', component: PlanIndex, meta: { keepAlive: false } },
{ path: '/articleIndex', name: 'ArticleIndex', component: ArticleIndex, meta: { keepAlive: true, title: 'Blossom 文章编辑' } },
{ path: '/pictureIndex', name: 'PictureIndex', component: PictureIndex, meta: { keepAlive: true, title: 'Blossom 资源库' } },
{ path: '/todoIndex', name: 'TodoIndex', component: TodoIndex, meta: { keepAlive: true, title: 'Blossom 待办事项' } },
{ path: '/noteIndex', name: 'NoteIndex', component: NoteIndex, meta: { keepAlive: false, title: 'Blossom 便签' } },
{ path: '/planIndex', name: 'PlanIndex', component: PlanIndex, meta: { keepAlive: false, title: 'Blossom 日历计划' } },
{
path: '/iconListIndex',
name: 'IconListIndex',
component: IconListIndex,
meta: { keepAlive: false },
meta: { keepAlive: false, title: 'Blossom 图标库' },
props: {
window: false
}
@ -58,5 +58,10 @@ router.addRoute({
router.addRoute({ path: '/articleViewWindow', name: 'ArticleViewWindow', component: ArticleViewWindow, meta: { keepAlive: true } })
router.addRoute({ path: '/iconListIndexWindow', name: 'IconListIndexWindow', component: IconListIndex, meta: { keepAlive: true } })
router.addRoute({ path: '/articleReferenceWindow', name: 'ArticleReferenceWindow', component: ArticleReference, meta: { keepAlive: true } })
router.addRoute({
path: '/articleReferenceWindow',
name: 'ArticleReferenceWindow',
component: ArticleReference,
meta: { keepAlive: true }
})
router.addRoute({ path: '/articleHistory', name: 'ArticleHistory', component: ArticleHistory, meta: { keepAlive: true } })

View File

@ -1,14 +1,21 @@
import { onBeforeUnmount, onMounted, watchEffect } from 'vue'
import type { Ref } from 'vue'
/**
*
* @param targetRef
* @param dragRef
* @param regionRef
*/
export const useDraggable = (
targetRef: Ref<HTMLElement | undefined>,
dragRef: Ref<HTMLElement | undefined>,
regionRef?: Ref<HTMLElement | undefined>
// draggable: ComputedRef<boolean>
) => {
let transform = {
offsetX: 0,
offsetY: 0,
offsetY: 0
}
const onMousedown = (e: MouseEvent) => {
@ -22,27 +29,33 @@ export const useDraggable = (
const targetWidth = targetRect.width
const targetHeight = targetRect.height
const clientWidth = document.documentElement.clientWidth
const clientHeight = document.documentElement.clientHeight
let clientWidth = document.documentElement.clientWidth
let clientHeight = document.documentElement.clientHeight
let minLeft = -targetLeft + offsetX
let minTop = -targetTop + offsetY
if (regionRef) {
console.log(regionRef.value!.getBoundingClientRect())
const rect = regionRef.value!.getBoundingClientRect()
clientWidth = rect.width + rect.x
clientHeight = rect.height + rect.y
minLeft += rect.x
minTop += rect.y
} else {
clientWidth = document.documentElement.clientWidth
clientHeight = document.documentElement.clientHeight
}
const minLeft = -targetLeft + offsetX
const minTop = -targetTop + offsetY
const maxLeft = clientWidth - targetLeft - targetWidth + offsetX
const maxTop = clientHeight - targetTop - targetHeight + offsetY
const onMousemove = (e: MouseEvent) => {
const moveX = Math.min(
Math.max(offsetX + e.clientX - downX, minLeft),
maxLeft
)
const moveY = Math.min(
Math.max(offsetY + e.clientY - downY, minTop),
maxTop
)
const moveX = Math.min(Math.max(offsetX + e.clientX - downX, minLeft), maxLeft)
const moveY = Math.min(Math.max(offsetY + e.clientY - downY, minTop), maxTop)
transform = {
offsetX: moveX,
offsetY: moveY,
offsetY: moveY
}
targetRef.value!.style.transform = `translate(${moveX}px, ${moveY}px)`
}
@ -71,7 +84,7 @@ export const useDraggable = (
onMounted(() => {
watchEffect(() => {
// if (draggable.value) {
onDraggable()
onDraggable()
// } else {
// offDraggable()
// }
@ -81,4 +94,4 @@ export const useDraggable = (
onBeforeUnmount(() => {
offDraggable()
})
}
}

View File

@ -45,8 +45,8 @@ const DEFAULT_DARK = {
'--el-color-primary-light-7': 'rgba(165, 184, 20, 0.3)',
'--el-color-primary-light-8': 'rgba(165, 184, 20, 0.2)',
'--el-color-primary-light-9': 'rgba(165, 184, 20, 0.1)',
'--bl-tag-color-open': '#476716',
'--bl-tag-color-subject': '#D9B049',
'--bl-tag-color-open': '#B3ADA0',
'--bl-tag-color-subject': '#B3ADA0',
'--bl-tag-color-toc': '#8E8C8E',
'--bl-todo-wait-color': '#505050A5',
'--bl-todo-proc-color': '#fba85f6b',

View File

@ -2,6 +2,15 @@ import { onBeforeUnmount, onMounted, watchEffect } from 'vue'
import type { Ref } from 'vue'
import { Local } from '@renderer/assets/utils/storage'
/**
* persistent: 是否持久化
* keyOne: 组件1持久化 key,
* keyTwo: 组件2持久化 key,
* defaultOne: 组件1默认宽度,
* defaultTwo: 组件2默认宽度,
* maxOne: 组件1最大宽度,
* minOne: 组件1最小宽度
*/
type Option = {
persistent: boolean
keyOne: string
@ -13,18 +22,15 @@ type Option = {
}
/**
*
*
*
* @param oneRef
* @param twoRef
* @param resizeDividerRef
* @param oneRef 1
* @param twoRef 2
* @param resizeDividerRef
* @param twoPendantRef
* @param options {
* persistent: 是否持久化
*
* }
* @param options
*/
export const useResize = (
export const useResizeVertical = (
oneRef: Ref<HTMLElement | undefined>,
twoRef: Ref<HTMLElement | undefined>,
resizeDividerRef: Ref<HTMLElement | undefined>,

View File

@ -16,6 +16,8 @@ export interface ViewStyle {
todoStatExpand: boolean
// 展开收起首页网页收藏
webCollectExpand: boolean
// 是否显示文件夹收藏图标
isShowFolderStarTag: boolean
// 是否显示专题样式
isShowSubjectStyle: boolean
// 是否以卡片方式显示文章收藏
@ -121,9 +123,10 @@ export const useConfigStore = defineStore('configStore', {
treeDocsFontSize: '13px',
todoStatExpand: true,
webCollectExpand: true,
isShowSubjectStyle: true,
isShowFolderStarTag: true,
isShowSubjectStyle: false,
isHomeStarCard: true,
isHomeSubjectCard: true,
isHomeSubjectCard: false,
isWebCollectCard: true,
isGlobalShadow: false,
isShowTryuseBtn: true,

View File

@ -97,7 +97,14 @@ export const DEFAULT_USER_INFO = {
WEB_GONG_WANG_AN_BEI: '',
WEB_BLOG_URL_ERROR_TIP_SHOW: 1,
WEB_BLOG_LINKS: '',
WEB_BLOG_SUBJECT_TITLE: false
WEB_BLOG_SUBJECT_TITLE: false,
WEB_BLOG_COLOR: '',
WEB_BLOG_SHOW_ARTICLE_NAME: true,
WEB_BLOG_WATERMARK_ENABLED: false,
WEB_BLOG_WATERMARK_CONTENT: '',
WEB_BLOG_WATERMARK_FONTSIZE: 15,
WEB_BLOG_WATERMARK_COLOR: '',
WEB_BLOG_WATERMARK_GAP: 100
}
}
@ -127,6 +134,9 @@ export const useUserStore = defineStore('userStore', {
userinfo: (Local.get(userinfoKey) as Userinfo) || initUserinfo()
}),
getters: {
currentUserId(state) {
return state.userinfo.id
},
/**
*
*/

View File

@ -38,6 +38,7 @@ const includeRouter = ref<any>(['settingIndex'])
watch(
() => router.currentRoute.value,
(newRoute) => {
document.title = newRoute.meta.title as string
if (newRoute.meta.keepAlive && includeRouter.value.indexOf(newRoute.name) === -1) {
includeRouter.value.push(newRoute.name)
}

View File

@ -191,7 +191,7 @@ const cancelDownload = async () => {
@include flex(column, flex-start, flex-start);
align-content: flex-start;
flex-wrap: wrap;
overflow-x: overlay;
overflow-x: scroll;
padding: 10px;
.bak-item {

View File

@ -7,7 +7,7 @@
</div>
<div class="content">
<div class="duration">有效时长(分钟) <el-input-number v-model="duration" min="1" controls-position="right"></el-input-number></div>
<div class="duration">有效时长(分钟) <el-input-number v-model="duration" :min="1" controls-position="right"></el-input-number></div>
<div class="expire">将在 {{ expire }} 后失效</div>
<div class="btns">
<el-button size="default" type="primary" @click="create">创建访问链接</el-button>

View File

@ -64,6 +64,7 @@ const initEditor = (_doc?: string) => {
const getLogs = (articleId: string | number) => {
articleInfoApi({ id: articleId, showToc: false, showMarkdown: true, showHtml: false }).then((resp) => {
article.value = resp.data
document.title = `${resp.data.name}》编辑历史`
})
articleLogsApi({ articleId: articleId }).then((resp) => {
historyList.value = resp.data
@ -106,8 +107,8 @@ onMounted(() => {
.history-list {
@include box(100%, calc(100% - 80px - 21px));
overflow-y: overlay;
padding: 10px 15px 10px 10px;
overflow-y: scroll;
.history-item {
@include box(100%, 55px);

View File

@ -8,14 +8,14 @@
<div class="content">
<el-upload
multiple
class="article-upload"
ref="uploadRef"
name="file"
multiple
:action="serverStore.serverUrl + articleImportApiUrl"
:data="{ pid: porps.doc.i }"
:data="{ pid: porps.doc.i, batchId: importBatch }"
:headers="{ Authorization: 'Bearer ' + userStore.auth.token }"
:on-change="onChange"
:show-file-list="true"
:before-upload="beforeUpload"
:on-success="onUploadSeccess"
:on-error="onError"
@ -38,6 +38,7 @@ import type { UploadInstance } from 'element-plus'
import { articleImportApiUrl } from '@renderer/api/blossom'
import { useUserStore } from '@renderer/stores/user'
import { useServerStore } from '@renderer/stores/server'
import { uuid } from '@renderer/assets/utils/util'
import { onChange, beforeUpload, onUploadSeccess, onError } from './scripts/article-import'
const userStore = useUserStore()
@ -50,6 +51,8 @@ const porps = defineProps({
}
})
const importBatch = ref(uuid())
const uploadRef = ref<UploadInstance>()
const submitUpload = () => {
@ -66,21 +69,21 @@ const submitUpload = () => {
.content {
padding: 20px;
.upload-tip {
border: 1px solid var(--el-border-color);
padding: 5px 10px;
border-radius: 5px;
color: rgb(188, 55, 55);
}
// .upload-tip {
// border: 1px solid var(--el-border-color);
// padding: 5px 10px;
// border-radius: 5px;
// color: rgb(188, 55, 55);
// }
.article-upload {
:deep(.el-upload-list) {
li {
transition: none;
margin-bottom: 0;
}
}
}
// .article-upload {
// :deep(.el-upload-list) {
// li {
// transition: none;
// margin-bottom: 0;
// }
// }
// }
}
}
</style>

View File

@ -16,7 +16,7 @@
</bl-row>
</div>
</div>
<div class="resize-docs-divider" ref="ResizeDocsDividerRef"></div>
<div class="resize-divider-vertical" ref="ResizeDocsDividerRef"></div>
<!-- editor -->
<div class="editor-container" ref="EditorContainerRef" v-loading="editorLoading" element-loading-text="正在读取文章内容...">
<div class="editor-tools">
@ -93,7 +93,7 @@
</div>
<div class="gutter-holder" ref="GutterHolderRef"></div>
<div class="editor-codemirror" ref="EditorRef" @click.right="handleEditorClickRight"></div>
<div class="resize-divider" ref="ResizeEditorDividerRef"></div>
<div class="resize-divider-vertical editor-resize-divider" ref="ResizeEditorDividerRef"></div>
<div class="preview-marked bl-preview" ref="PreviewRef" v-html="articleHtml"></div>
</div>
@ -207,7 +207,7 @@ import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo, isArticle } fr
import { TempTextareaKey, ArticleReference, parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture'
import { useResize } from './scripts/editor-preview-resize'
import { useResizeVertical } from '@renderer/scripts/resize-devider-vertical'
// codemirror
import { CmWrapper } from './scripts/codemirror'
// marked
@ -354,7 +354,7 @@ const exitView = () => {
autoSave()
}
const { hideOne, resotreOne } = useResize(DocsRef, EditorContainerRef, ResizeDocsDividerRef, undefined, {
const { hideOne, resotreOne } = useResizeVertical(DocsRef, EditorContainerRef, ResizeDocsDividerRef, undefined, {
persistent: true,
keyOne: 'article_docs_width',
keyTwo: 'article_editor_preview_width',
@ -363,7 +363,7 @@ const { hideOne, resotreOne } = useResize(DocsRef, EditorContainerRef, ResizeDoc
maxOne: 700,
minOne: 250
})
useResize(EditorRef, PreviewRef, ResizeEditorDividerRef, EditorOperatorRef)
useResizeVertical(EditorRef, PreviewRef, ResizeEditorDividerRef, EditorOperatorRef)
//#endregion
//#region ----------------------------------------< >--------------------------------------
@ -939,6 +939,7 @@ const unbindKeys = () => {
</script>
<style scoped lang="scss">
@import '@renderer/assets/styles/bl-resize-divider.scss';
@import '@renderer/assets/styles/bl-loading-spinner.scss';
@import './styles/article-index.scss';
@import './styles/article-view-absolute.scss';

View File

@ -249,7 +249,6 @@ import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo, isArticle } fr
import { TempTextareaKey, ArticleReference, DocsEditorStyle, parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture'
import { useResize } from './scripts/editor-preview-resize'
// codemirror
import { CmWrapper } from './scripts/codemirror'
// marked
@ -401,8 +400,6 @@ const enterView = () => {
const exitView = () => {
autoSave()
}
useResize(EditorRef, PreviewRef, ResizeDividerRef, EditorOperatorRef)
//#endregion
//#region ----------------------------------------< >--------------------------------------

View File

@ -218,7 +218,6 @@ import { treeToInfo, provideKeyDocInfo, provideKeyCurArticleInfo, isArticle } fr
import { TempTextareaKey, ArticleReference, DocsEditorStyle, parseTocAsync } from './scripts/article'
import type { Toc } from './scripts/article'
import { beforeUpload, onError, picCacheWrapper, picCacheRefresh, uploadForm, uploadDate } from '@renderer/views/picture/scripts/picture'
import { useResize } from './scripts/editor-preview-resize'
// codemirror
import { CmWrapper } from './scripts/codemirror'
// marked
@ -367,8 +366,6 @@ const enterView = () => {
const exitView = () => {
autoSave()
}
useResize(EditorRef, PreviewRef, ResizeDividerRef, EditorOperatorRef)
//#endregion
//#region ----------------------------------------< >--------------------------------------

View File

@ -69,7 +69,7 @@
<!-- 星标 -->
<div class="stat-star">
<div v-if="!curIsStar" :class="['iconbl bl-star-line', curDocDialogType == 'add' || curIsFolder ? 'disabled' : '']" @click="star(1)"></div>
<div v-if="!curIsStar" :class="['iconbl bl-star-line', curDocDialogType == 'add']" @click="star(1)"></div>
<div v-else class="iconbl bl-star-fill" @click="star(0)"></div>
</div>
</div>
@ -141,9 +141,9 @@
<!-- -->
<el-form-item label="类型">
<el-radio-group v-model="docForm.type" style="width: 106px" :disabled="curDocDialogType == 'upd'">
<el-radio-button :label="1">文件夹</el-radio-button>
<el-radio-button :label="3">文章</el-radio-button>
<el-radio-group v-model="docForm.type" style="width: 110px" :disabled="curDocDialogType == 'upd'">
<el-radio-button :value="1">文件夹</el-radio-button>
<el-radio-button :value="3">文章</el-radio-button>
</el-radio-group>
</el-form-item>
@ -151,13 +151,13 @@
<el-form-item label="主色调">
<el-input
v-model="docForm.color"
:style="{ width: '245px', '--el-input-text-color': '#000000', '--el-input-bg-color': docForm.color }"
:style="{ width: '242px', '--el-input-text-color': '#000000', '--el-input-bg-color': docForm.color }"
placeholder="主色调 #FFFFFF">
</el-input>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="docForm.sort" style="width: 106px" />
<el-input-number v-model="docForm.sort" style="width: 110px" />
</el-form-item>
<!-- -->
<!-- <el-form-item label="封面">
@ -165,7 +165,7 @@
</el-form-item> -->
<el-form-item label="图标">
<el-input v-model="docForm.icon" style="width: 245px" placeholder="图标/图片http地址">
<el-input v-model="docForm.icon" style="width: 242px" placeholder="图标/图片http地址">
<template #append>
<el-tooltip content="查看所有图标" effect="light" placement="top" :hide-after="0">
<div style="cursor: pointer; font-size: 20px" @click="openNewIconWindow()">
@ -207,11 +207,9 @@
</div>
<div class="info-footer">
<div>
<el-button size="default" type="primary" :disabled="saveLoading" @click="saveDoc(DocFormRef)">
<span class="iconbl bl-a-filechoose-line" /> 保存
</el-button>
</div>
<el-button size="default" type="primary" :disabled="saveLoading" @click="saveDoc(DocFormRef)">
<span class="iconbl bl-a-filechoose-line" /> 保存
</el-button>
</div>
</div>
</div>
@ -222,9 +220,10 @@ import { ref, nextTick, inject, computed, watch, Ref } from 'vue'
import { ElInput, ElMessageBox, FormInstance } from 'element-plus'
import type { FormRules } from 'element-plus'
import { Document } from '@element-plus/icons-vue'
import { provideKeyDocTree, getCDocsByPid, getDocById, checkLevel } from '@renderer/views/doc/doc'
import { provideKeyDocTree, getCDocsByPid, getDocById } from '@renderer/views/doc/doc'
import { useUserStore } from '@renderer/stores/user'
import {
folderStarApi,
folderInfoApi,
folderAddApi,
folderUpdApi,
@ -387,6 +386,10 @@ const formatStorePath = () => {
}
const showStorePathWarning = ref(false)
/**
* 填充文件夹路径
*/
const fillStorePath = (id: string, path: string = ''): void => {
let doc = getDocById(id, docTreeData!.value)
if (!doc) {
@ -484,6 +487,12 @@ const star = (changeStarStatus: number) => {
Notify.success(docForm.value.starStatus === 0 ? '取消 Star 成功' : 'Star 成功')
})
}
if (curIsFolder.value) {
docForm.value.starStatus = changeStarStatus
folderStarApi({ id: docForm.value.id, starStatus: docForm.value.starStatus }).then(() => {
Notify.success(docForm.value.starStatus === 0 ? '取消 Star 成功' : 'Star 成功')
})
}
}
//#endregion
@ -535,13 +544,9 @@ const saveDoc = async (formEl: FormInstance | undefined) => {
await formEl.validate((valid, _fields) => {
if (valid) {
saveLoading.value = true
if (!checkLevel(docForm.value.pid, docTreeData!.value)) {
saveLoading.value = false
return
}
const handleResp = (_: any) => {
Notify.success(curDocDialogType.value === 'upd' ? `修改《${docForm.value.name}》成功` : `新增《${docForm.value.name}》成功`)
emits('saved', curDocDialogType.value)
emits('saved', curDocDialogType.value, docForm.value)
}
const handleFinally = () => setTimeout(() => (saveLoading.value = false), 300)
//
@ -816,7 +821,7 @@ $height-form: calc(100% - #{$height-title} - #{$height-img} - #{$height-stat} -
.info-footer {
@include box(100%, $height-footer);
@include flex(row, space-between, center);
@include flex(row, flex-end, center);
border-top: 1px solid var(--el-border-color);
padding: 10px;
text-align: right;

View File

@ -111,7 +111,7 @@ const download = (id: string) => {
@include flex(column, flex-start, flex-start);
align-content: flex-start;
flex-wrap: wrap;
overflow-x: overlay;
overflow-x: scroll;
padding: 10px;
.recycle-item {

View File

@ -321,6 +321,7 @@ const windowResize = () => {
}
onMounted(() => {
document.title = 'Blossom 双链图表'
init()
windowResize()
articleId = route.query.articleId as string

View File

@ -1,257 +0,0 @@
<template>
<div :class="[viewStyle.isShowSubjectStyle ? (isSubject ? 'subject-title' : 'doc-title') : 'doc-title']">
<bl-tag class="sort" v-show="props.trees.showSort" :bgColor="levelColor">
{{ props.trees.s }}
</bl-tag>
<div 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" 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: -20px"></div>
<div v-if="level >= 3" class="folder-level-line" style="left: -30px"></div>
<div v-if="level >= 4" class="folder-level-line" style="left: -40px"></div>
<div
v-if="viewStyle.isShowArticleType"
v-for="(line, index) in tagLins"
:key="line"
:class="[line]"
:style="{ left: -1 * (index + 1.5) * 4 + 'px' }"></div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PropType } from 'vue'
import { useConfigStore } from '@renderer/stores/config'
import { isNotBlank } from '@renderer/assets/utils/obj'
import { computedDocTitleColor } from '@renderer/views/doc/doc'
import { articleUpdNameApi, folderUpdNameApi } from '@renderer/api/blossom'
const { viewStyle } = useConfigStore()
const props = defineProps({
trees: { type: Object as PropType<DocTree>, default: {} },
size: { type: Number, default: 14 },
level: { type: Number, required: true }
})
const levelColor = computed(() => {
return computedDocTitleColor(props.level)
})
const nameWrapperStyle = computed(() => {
return {
maxWidth: isNotBlank(props.trees.icon) ? 'calc(100% - 25px)' : '100%'
}
})
/**
* 是否是专题
*/
const isSubject = computed(() => {
return props.trees.t?.includes('subject')
})
/**
* 计算标签, 并返回便签对象集合
*/
const tags = computed(() => {
let icons: any = []
props.trees.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 (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 tagLins = computed(() => {
let lines: string[] = []
if (props.trees.star === 1) {
lines.push('star-line')
}
if (props.trees.o === 1 && props.trees.ty === 3) {
lines.push('open-line')
}
if (props.trees.vd === 1) {
lines.push('sync-line')
}
return lines
})
/**
* 重命名文章
*/
const blurArticleNameInput = () => {
if (props.trees.ty === 3) {
articleUpdNameApi({ id: props.trees.i, name: props.trees.n }).then((_resp) => {
props.trees.updn = false
})
} else {
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;
padding: 0 2px;
right: 0px;
top: 2px;
z-index: 10;
}
.folder-level-line {
@include box(1px, 100%);
}
}
.folder-level-line {
background-color: var(--el-border-color);
position: absolute;
opacity: 0;
transition: opacity 0.5s;
}
// ,
.subject-title {
@include flex(row, flex-start, flex-start);
@include themeShadow(2px 2px 10px 1px var(--el-color-primary-light-8), 2px 2px 10px 1px #131313);
background: linear-gradient(135deg, var(--el-color-primary-light-7), var(--el-color-primary-light-8), var(--bl-html-color));
max-width: calc(100% - 15px);
min-width: calc(100% - 15px);
padding: 2px 5px;
margin: 5px 0 10px 0;
border-radius: 7px;
position: relative;
.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-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 {
@include box(1px, calc(100% + 15px));
top: -5px;
}
}
//
.open-line,
.star-line,
.sync-line {
position: absolute;
width: 2px;
height: 60%;
top: 20%;
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;
}
</style>

View File

@ -1,154 +1,26 @@
<template>
<div class="doc-workbench-root">
<bl-col class="workbench-name" just="flex-start" align="flex-end" height="46px" v-show="curArticle !== undefined">
<span>{{ curArticle?.name }}</span>
<span style="font-size: 9px; padding-right: 5px">{{ curArticle?.id }}</span>
</bl-col>
<bl-row class="wb-page-container">
<bl-row class="wb-page-item" just="flex-start" align="flex-end" width="calc(100% - 16px)" height="44px">
<el-tooltip
content="文章引用网络"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="-5"
:hide-after="0">
<bl-row class="wb-page-item" just="flex-start" align="flex-end" height="44px">
<el-tooltip content="文章引用网络" effect="light" popper-class="is-small" placement="top" :offset="-5" :hide-after="0">
<div class="iconbl bl-correlation-line" @click="openArticleReferenceWindow()"></div>
</el-tooltip>
<el-tooltip
content="新增文件夹或文章"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<div class="iconbl bl-a-fileadd-line" @click="handleShowAddDocInfoDialog()"></div>
</el-tooltip>
<el-tooltip
content="刷新"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<div class="iconbl bl-a-cloudrefresh-line" @click="refreshDocTree()"></div>
</el-tooltip>
<el-tooltip
content="全文搜索"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="9"
:hide-after="0">
<el-tooltip content="全文搜索" effect="light" popper-class="is-small" placement="top" :offset="9" :hide-after="0">
<div class="iconbl bl-search-line" @click="showSearch()"></div>
</el-tooltip>
<el-tooltip effect="light" popper-class="is-small" transition="none" placement="top" :show-arrow="false" :offset="5" :hide-after="0">
<div class="iconbl bl-a-leftdirection-line" @click="emits('show-sort')"></div>
<template #content>
显示排序<br />
<bl-row>
<bl-tag :bgColor="SortLevelColor.ONE">一级</bl-tag>
<bl-tag :bgColor="SortLevelColor.TWO">二级</bl-tag>
</bl-row>
<bl-row style="padding-bottom: 5px">
<bl-tag :bgColor="SortLevelColor.THREE">三级</bl-tag>
<bl-tag :bgColor="SortLevelColor.FOUR">四级</bl-tag>
</bl-row>
</template>
</el-tooltip>
<el-tooltip
content="备份记录"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<el-tooltip content="备份记录" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
<div class="iconbl bl-a-cloudstorage-line" @click="handleShowBackupDialog"></div>
</el-tooltip>
<el-tooltip
content="文章回收站"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<el-tooltip content="文章回收站" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
<div class="iconbl bl-delete-line" @click="handleShowRecycleDialog"></div>
</el-tooltip>
<el-tooltip
content="查看收藏"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<div v-if="props.showStar">
<div v-if="onlyStars" class="iconbl bl-star-fill" @click="changeOnlyStar()"></div>
<div v-else class="iconbl bl-star-line" @click="changeOnlyStar()"></div>
</div>
</el-tooltip>
<el-tooltip
content="查看专题"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<div v-if="props.showSubject">
<div v-if="onlySubject" class="iconbl bl-a-lowerrightpage-fill" @click="changeOnlySubject()"></div>
<div v-else class="iconbl bl-a-lowerrightpage-line" @click="changeOnlySubject()"></div>
</div>
</el-tooltip>
<el-tooltip
content="查看公开"
effect="light"
popper-class="is-small"
transition="none"
placement="top"
:show-arrow="false"
:offset="8"
:hide-after="0">
<div v-if="props.showOpen">
<div v-if="onlyOpen" class="iconbl bl-cloud-fill" @click="changeOnlyOpen()"></div>
<div v-else class="iconbl bl-cloud-line" @click="changeOnlyOpen()"></div>
</div>
</el-tooltip>
<!-- <el-tooltip content="文档快速编辑" effect="light" popper-class="is-small" placement="top" :offset="8" :hide-after="0">
<div class="iconbl bl-article-line" @click=""></div>
</el-tooltip> -->
</bl-row>
<bl-col width="12px" height="30px" just="end" class="workbench-more" style="">
<div class="iconbl bl-a-morevertical-line" @click="showMoreMenu"></div>
</bl-col>
</bl-row>
</div>
<el-dialog
v-model="isShowDocInfoDialog"
width="535"
top="100px"
style="margin-left: 320px"
:append-to-body="true"
:destroy-on-close="true"
:close-on-click-modal="false"
draggable>
<ArticleInfo ref="ArticleInfoRef" @saved="savedCallback"></ArticleInfo>
</el-dialog>
<el-dialog
class="bl-dialog-fixed-body"
v-model="isShowBackupDialog"
@ -174,25 +46,13 @@
draggable>
<ArticleRecycle ref="ArticleRecycleRef"></ArticleRecycle>
</el-dialog>
<Teleport to="body">
<div v-show="moreMenu.show" class="tree-menu" :style="{ left: moreMenu.clientX + 'px', top: moreMenu.clientY + 'px', width: '120px' }">
<div class="menu-content" style="border: none">
<div @click="changeOnlyOpen"><span class="iconbl bl-cloud-line"></span>查看公开</div>
<div @click="changeOnlySubject"><span class="iconbl bl-a-lowerrightpage-line"></span>查看专题</div>
<div @click="changeOnlyStar"><span class="iconbl bl-star-line"></span>查看收藏</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, nextTick, inject, onDeactivated } from 'vue'
import { provideKeyCurArticleInfo, SortLevelColor } from '@renderer/views/doc/doc'
import { ref, nextTick, onDeactivated } from 'vue'
import { openNewArticleReferenceWindow } from '@renderer/assets/utils/electron'
import { useLifecycle } from '@renderer/scripts/lifecycle'
import hotkeys from 'hotkeys-js'
import ArticleInfo from './ArticleInfo.vue'
import ArticleBackup from './ArticleBackup.vue'
import ArticleRecycle from './ArticleRecycle.vue'
@ -205,43 +65,9 @@ onDeactivated(() => {
unbindKeys()
})
const props = defineProps({
showOpen: {
type: Boolean,
default: true
},
showSubject: {
type: Boolean,
default: true
},
showStar: {
type: Boolean,
default: true
}
})
//#region --------------------------------------------------< >--------------------------------------------------
const moreMenu = ref<RightMenu>({ show: false, clientX: 0, clientY: 0 })
/**
* 显示右键菜单
* @param doc 文档
* @param event 事件
*/
const showMoreMenu = (event: MouseEvent) => {
moreMenu.value.show = true
nextTick(() => {
let y = event.clientY
if (document.body.clientHeight - event.clientY < 50) {
y = event.clientY - 50
}
moreMenu.value = { show: true, clientX: event.clientX, clientY: y }
setTimeout(() => {
document.body.addEventListener('click', closeMoreMenu)
}, 100)
})
}
const closeMoreMenu = (event: MouseEvent) => {
if (event.target) {
let isPrevent = (event.target as HTMLElement).getAttribute('data-bl-prevet')
@ -257,36 +83,6 @@ const closeMoreMenu = (event: MouseEvent) => {
//#endregion
//#region --------------------------------------------------< >--------------------------------------------------
const curArticle = inject(provideKeyCurArticleInfo)
const onlyOpen = ref<boolean>(false) //
const onlySubject = ref<boolean>(false) //
const onlyStars = ref<boolean>(false) // star
const changeOnlyOpen = () => {
onlyOpen.value = !onlyOpen.value
onlySubject.value = false
onlyStars.value = false
refreshDocTree()
}
const changeOnlySubject = () => {
onlyOpen.value = false
onlySubject.value = !onlySubject.value
onlyStars.value = false
refreshDocTree()
}
const changeOnlyStar = () => {
onlyOpen.value = false
onlySubject.value = false
onlyStars.value = !onlyStars.value
refreshDocTree()
}
//#endregion
//#region --------------------------------------------------< >--------------------------------------------------
const ArticleInfoRef = ref()
const isShowDocInfoDialog = ref<boolean>(false)
@ -302,23 +98,6 @@ const openArticleReferenceWindow = () => {
openNewArticleReferenceWindow()
}
/**
* 控制台刷新文档列表
*/
const refreshDocTree = () => {
emits('refreshDocTree', onlyOpen.value, onlySubject.value, onlyStars.value)
}
/**
* 保存后的回调
* 1. 刷新菜单列表
* 2. 关闭 dialog 页面
*/
const savedCallback = () => {
isShowDocInfoDialog.value = false
emits('refreshDocTree', onlyOpen.value, onlySubject.value, onlyStars.value)
}
const showSearch = () => {
emits('show-search')
}
@ -363,7 +142,7 @@ const unbindKeys = () => {
}
//#endregion
const emits = defineEmits(['refreshDocTree', 'show-sort', 'show-search'])
const emits = defineEmits(['show-sort', 'show-search'])
defineExpose({ handleShowBackupDialog })
</script>
@ -387,14 +166,6 @@ defineExpose({ handleShowBackupDialog })
height: 0px;
}
//
.bl-a-leftdirection-line {
font-size: 27px;
padding-bottom: 3px;
padding-right: 0;
padding-left: 0;
}
//
.bl-search-line {
font-size: 23px;
@ -408,14 +179,6 @@ defineExpose({ handleShowBackupDialog })
padding-bottom: 5px;
}
//
.bl-a-cloudrefresh-line,
.bl-a-fileadd-line {
&:active {
color: #ffffff;
}
}
.bl-correlation-line {
font-size: 40px;
padding-bottom: 0px;

View File

@ -57,6 +57,7 @@ const toScroll = (id: string) => {
const initPreview = (articleId: string) => {
articleInfoApi({ id: articleId, showToc: false, showMarkdown: false, showHtml: true }).then((resp) => {
article.value = resp.data
document.title = `${resp.data.name}`
nextTick(() => initToc())
})
}
@ -87,7 +88,7 @@ onMounted(() => {
@include box(100%, 100%);
font-size: 15px;
padding: 30px;
overflow-y: overlay;
overflow-y: scroll;
overflow-x: hidden;
line-height: 23px;

Some files were not shown because too many files have changed in this diff Show More