mirror of
https://gitee.com/blossom-editor/blossom.git
synced 2025-12-07 01:08:26 +08:00
Merge branch 'blossom-editor:dev' into dev
This commit is contained in:
commit
96bbbb1788
28
.github/workflows/stale.yml
vendored
Normal file
28
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: "Close inactive issues"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "10 15 * * *"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
debug-only: false
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue has been open 30 days with no activity. This will be closed in 7 days."
|
||||
close-issue-message: "This issue has been automatically marked as stale because it hasn't had any recent activity."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
exempt-issue-labels: bug,enhancement
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,6 +28,7 @@ stats.html
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
/build/
|
||||
/lucene/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
@ -41,7 +41,7 @@ public class ParamService extends ServiceImpl<ParamMapper, ParamEntity> {
|
||||
*/
|
||||
@EventListener(ApplicationStartedEvent.class)
|
||||
public void refresh() {
|
||||
log.info("[ BASE] 初始化系统参数缓存");
|
||||
log.info("[ PARAM] 初始化系统参数缓存");
|
||||
CACHE.clear();
|
||||
List<ParamEntity> params = baseMapper.selectList(new QueryWrapper<>());
|
||||
if (CollUtil.isEmpty(params)) {
|
||||
|
||||
@ -15,6 +15,7 @@ import com.blossom.backend.base.user.pojo.UserEntity;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
import com.blossom.common.base.util.BeanUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -28,6 +29,7 @@ import java.util.stream.Collectors;
|
||||
/**
|
||||
* 用户参数
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class UserParamService extends ServiceImpl<UserParamMapper, UserParamEntity> {
|
||||
@ -42,6 +44,7 @@ public class UserParamService extends ServiceImpl<UserParamMapper, UserParamEnti
|
||||
*/
|
||||
@EventListener(ApplicationStartedEvent.class)
|
||||
public void refresh() {
|
||||
log.info("[ U_PARAM] 初始化用户参数缓存");
|
||||
CACHE.clear();
|
||||
List<UserEntity> users = userMapper.selectList(new QueryWrapper<>());
|
||||
// 初始化所有用户的配置参数
|
||||
|
||||
@ -16,13 +16,11 @@ public @interface EnableIndex {
|
||||
|
||||
/**
|
||||
* 索引操作类型, 默认值为追加
|
||||
* @return
|
||||
*/
|
||||
IndexMsgTypeEnum type();
|
||||
|
||||
/**
|
||||
* id字段表达式
|
||||
* @return
|
||||
* ID 字段表达式
|
||||
*/
|
||||
String id();
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@ public class IndexAspect {
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回后调用该方法,维护索引。 使用after防止事务未提交导致的数据滞后
|
||||
* 成功返回后调用该方法, 维护索引. 使用after防止事务未提交导致的数据滞后
|
||||
*/
|
||||
@AfterReturning("cutMethod()")
|
||||
public void afterReturning(JoinPoint joinPoint) {
|
||||
@ -52,10 +52,6 @@ public class IndexAspect {
|
||||
}
|
||||
|
||||
IndexMsgTypeEnum indexMsgTypeEnum = annotation.type();
|
||||
if (indexMsgTypeEnum == null){
|
||||
log.error("获取索引消息操作类型失败");
|
||||
return;
|
||||
}
|
||||
String idSpEL = annotation.id();
|
||||
if (!StringUtils.hasText(idSpEL)) {
|
||||
log.error("获取id表达式失败");
|
||||
@ -76,6 +72,7 @@ public class IndexAspect {
|
||||
|
||||
/**
|
||||
* 获取method
|
||||
*
|
||||
* @return method
|
||||
*/
|
||||
private Method getTargetMethod(JoinPoint joinPoint) {
|
||||
@ -93,8 +90,6 @@ public class IndexAspect {
|
||||
|
||||
/**
|
||||
* 获取注解声明对象
|
||||
* @param joinPoint
|
||||
* @return
|
||||
*/
|
||||
private EnableIndex getAnnotation(JoinPoint joinPoint) {
|
||||
// 获取方法上的注解
|
||||
@ -105,11 +100,6 @@ public class IndexAspect {
|
||||
|
||||
/**
|
||||
* 解析SpEL表达式, 提供后续拓展的灵活性
|
||||
* @param spel
|
||||
* @param joinPoint
|
||||
* @param clazz
|
||||
* @return
|
||||
* @param <T>
|
||||
*/
|
||||
private <T> T parse(String spel, JoinPoint joinPoint, Class<T> clazz) {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
@ -135,5 +125,4 @@ public class IndexAspect {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -7,9 +7,10 @@ import com.blossom.backend.base.search.message.consumer.BatchIndexMsgConsumer;
|
||||
import com.blossom.backend.server.article.draft.ArticleService;
|
||||
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationStartedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@ -18,15 +19,13 @@ import java.util.List;
|
||||
/**
|
||||
* 对既有索引进行监控与维护
|
||||
*/
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IndexObserver {
|
||||
|
||||
private SearchProperties searchProperties;
|
||||
private ArticleService articleService;
|
||||
private BatchIndexMsgConsumer batchIndexMsgConsumer;
|
||||
|
||||
private final SearchProperties searchProperties;
|
||||
private final ArticleService articleService;
|
||||
private final BatchIndexMsgConsumer batchIndexMsgConsumer;
|
||||
|
||||
IndexObserver(SearchProperties searchProperties, ArticleService articleService, BatchIndexMsgConsumer batchIndexMsgConsumer) {
|
||||
this.searchProperties = searchProperties;
|
||||
@ -34,22 +33,34 @@ public class IndexObserver {
|
||||
this.batchIndexMsgConsumer = batchIndexMsgConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动时维护索引
|
||||
*/
|
||||
@EventListener(ApplicationStartedEvent.class)
|
||||
public void refresh() {
|
||||
try {
|
||||
log.info("[ SEARCH] 重建全部用户索引开始");
|
||||
long start = System.currentTimeMillis();
|
||||
this.reloadIndex();
|
||||
log.info("[ SEARCH] 重建全部用户索引完成, 用时: {} ms", (System.currentTimeMillis() - start));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行索引的维护
|
||||
*/
|
||||
@Scheduled(cron = "0 0 04 * * ?")
|
||||
public void reloadIndex() throws IOException {
|
||||
if (StringUtils.hasText(searchProperties.getPath())){
|
||||
List<ArticleEntity> allArticleWithContent = articleService.listAllArticleWithContent();
|
||||
List<ArticleEntity> allArticleWithContent = articleService.listAllIndexField();
|
||||
List<IndexMsg> batchReloadMsgs = new ArrayList<>();
|
||||
allArticleWithContent.forEach(article -> {
|
||||
ArticleIndexMsg articleIndexMsg = new ArticleIndexMsg(IndexMsgTypeEnum.ADD, article.getId(), article.getName(), article.getTags(), article.getMarkdown(), article.getUserId());
|
||||
batchReloadMsgs.add(articleIndexMsg);
|
||||
});
|
||||
|
||||
batchIndexMsgConsumer.batchReload(batchReloadMsgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,37 @@
|
||||
package com.blossom.backend.base.search;
|
||||
|
||||
import com.blossom.backend.base.auth.AuthContext;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 搜索接口
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class SearchController {
|
||||
|
||||
@Autowired
|
||||
private Searcher searcher;
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
*
|
||||
* @param keyword 搜索关键字
|
||||
* @param hlColor 高亮颜色
|
||||
* @param operator 是否全部匹配
|
||||
* @param debug 是否DEBUG, 为 true 时高亮前后缀为【】
|
||||
* @since 1.12.0
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
public R<SearchRes> search(@RequestParam("keyword") String keyword,
|
||||
@RequestParam("hlColor") String hlColor,
|
||||
@RequestParam("operator") boolean operator,
|
||||
@RequestParam("debug") boolean debug) {
|
||||
return R.ok(searcher.search(keyword, AuthContext.getUserId(), hlColor, operator, debug));
|
||||
}
|
||||
}
|
||||
@ -1,26 +1,32 @@
|
||||
package com.blossom.backend.base.search;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "project.search")
|
||||
public class SearchProperties {
|
||||
|
||||
private String path = "";
|
||||
|
||||
/**
|
||||
* 根据用户id, 获取对应索引库path
|
||||
* @param userId 用户id
|
||||
* @return 索引库path
|
||||
* 全文搜索配置项
|
||||
*/
|
||||
@Component
|
||||
public class SearchProperties {
|
||||
|
||||
private static final String USER_HOME = "user.dir";
|
||||
|
||||
/**
|
||||
* 根据用户ID, 获取对应索引库 Path
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public Path getUserIndexDirectory(Long userId) {
|
||||
return Paths.get(this.path, Convert.toStr(userId));
|
||||
File file = new File(addSeparator("/lucene/" + userId));
|
||||
return file.toPath();
|
||||
}
|
||||
|
||||
public static String addSeparator(String dir) {
|
||||
if (!dir.endsWith(File.separator)) {
|
||||
dir += File.separator;
|
||||
}
|
||||
return System.getProperty(USER_HOME) + dir;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
package com.blossom.backend.base.search;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 全文搜索结果
|
||||
*/
|
||||
@Data
|
||||
public class SearchRes {
|
||||
|
||||
/**
|
||||
* 命中总数
|
||||
*/
|
||||
private Long totalHit;
|
||||
|
||||
/**
|
||||
* 命中数据
|
||||
*/
|
||||
private List<Hit> hits;
|
||||
|
||||
/**
|
||||
* 命中数据
|
||||
*/
|
||||
@Data
|
||||
public static class Hit {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 文章名称
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 源文章名称
|
||||
*/
|
||||
private String originName;
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private List<String> tags;
|
||||
/**
|
||||
* 正文
|
||||
*/
|
||||
private String markdown;
|
||||
/**
|
||||
* 命中分数
|
||||
*/
|
||||
private Float score;
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
package com.blossom.backend.base.search;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 全文搜索返回对象
|
||||
*/
|
||||
@Data
|
||||
public class SearchResult {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private String tags;
|
||||
/**
|
||||
* 正文
|
||||
*/
|
||||
private String content;
|
||||
|
||||
}
|
||||
@ -2,12 +2,15 @@ package com.blossom.backend.base.search;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.blossom.common.base.exception.XzException500;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.blossom.backend.server.utils.DocUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.lucene.analysis.standard.StandardAnalyzer;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.index.DirectoryReader;
|
||||
import org.apache.lucene.index.IndexReader;
|
||||
import org.apache.lucene.queryparser.classic.MultiFieldQueryParser;
|
||||
import org.apache.lucene.queryparser.classic.QueryParser;
|
||||
import org.apache.lucene.search.IndexSearcher;
|
||||
import org.apache.lucene.search.Query;
|
||||
import org.apache.lucene.search.ScoreDoc;
|
||||
@ -20,52 +23,58 @@ import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 搜索类
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class Searcher {
|
||||
|
||||
private SimpleHTMLFormatter simpleHTMLFormatter;
|
||||
private final SimpleHTMLFormatter debugFmt = new SimpleHTMLFormatter("【", "】");
|
||||
|
||||
private String[] queryField;
|
||||
private final String[] queryField;
|
||||
|
||||
private Map<String, Float> boostsMap;
|
||||
private final Map<String, Float> boostsMap;
|
||||
|
||||
@Autowired
|
||||
private SearchProperties searchProperties;
|
||||
|
||||
|
||||
Searcher() {
|
||||
// 构造f高亮显示formatter
|
||||
this.simpleHTMLFormatter = new SimpleHTMLFormatter("<B>", "<B>");
|
||||
// 构造默认查询域
|
||||
this.queryField = new String[3];
|
||||
this.queryField[0] = "content";
|
||||
this.queryField[1] = "title";
|
||||
this.queryField[0] = "markdown";
|
||||
this.queryField[1] = "name";
|
||||
this.queryField[2] = "tags";
|
||||
// 构造权重配置
|
||||
this.boostsMap = new HashMap<>();
|
||||
boostsMap.put("title", 2F);
|
||||
boostsMap.put("tags", 2F);
|
||||
boostsMap.put("content", 1F);
|
||||
this.boostsMap.put("name", 3F);
|
||||
this.boostsMap.put("tags", 3F);
|
||||
this.boostsMap.put("markdown", 1F);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 进行索引查询, 传入关键词以及用户id
|
||||
* 搜索
|
||||
*
|
||||
* @param keyword 关键词
|
||||
* @param userId 用户id
|
||||
* @param hlColor 高亮颜色
|
||||
* @param operator 是否全部匹配
|
||||
* @param debug 是否DEBUG, 为 true 时高亮前后缀为【】
|
||||
* @return 查询结果
|
||||
*/
|
||||
public List<SearchResult> search(String keyword, Long userId) {
|
||||
List<SearchResult> result = new ArrayList<>();
|
||||
if (!StringUtils.hasText(searchProperties.getPath())) {
|
||||
throw new XzException500("未配置索引库地址,无法进行全文检索");
|
||||
public SearchRes search(String keyword, Long userId, String hlColor, boolean operator, boolean debug) {
|
||||
SearchRes result = new SearchRes();
|
||||
result.setHits(new ArrayList<>());
|
||||
|
||||
if (StrUtil.isBlank(keyword)) {
|
||||
result.setTotalHit(0L);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (userId == null) {
|
||||
@ -73,64 +82,89 @@ public class Searcher {
|
||||
}
|
||||
|
||||
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||
IndexReader indexReader = DirectoryReader.open(directory);
|
||||
) {
|
||||
IndexReader indexReader = DirectoryReader.open(directory)) {
|
||||
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
|
||||
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(queryField, new StandardAnalyzer(), boostsMap);
|
||||
if (operator) {
|
||||
multiFieldQueryParser.setDefaultOperator(QueryParser.Operator.AND);
|
||||
}
|
||||
|
||||
Query query = multiFieldQueryParser.parse(keyword);
|
||||
TopDocs topDocs = indexSearcher.search(query, 10);
|
||||
|
||||
TopDocs topDocs = indexSearcher.search(query, 20);
|
||||
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
|
||||
result.setTotalHit(topDocs.totalHits.value);
|
||||
if (!ArrayUtil.isEmpty(scoreDocs)) {
|
||||
Highlighter highlighter = new Highlighter(simpleHTMLFormatter, new QueryScorer(query));
|
||||
highlighter.setTextFragmenter(new SimpleFragmenter(20));
|
||||
Highlighter highlighter;
|
||||
if (debug) {
|
||||
highlighter = new Highlighter(debugFmt, new QueryScorer(query));
|
||||
} else {
|
||||
highlighter = new Highlighter(new SimpleHTMLFormatter("<span style=\"background-color:" + hlColor + ";color:#ffffff\">", "</span>"), new QueryScorer(query));
|
||||
}
|
||||
highlighter.setTextFragmenter(new SimpleFragmenter(100));
|
||||
|
||||
for (ScoreDoc doc : scoreDocs) {
|
||||
Document document = indexSearcher.doc(doc.doc);
|
||||
String id = document.get("id");
|
||||
String title = document.get("title");
|
||||
String content = document.get("content");
|
||||
String name = document.get("name");
|
||||
String markdown = document.get("markdown");
|
||||
String tags = document.get("tags");
|
||||
SearchResult searchResult = new SearchResult();
|
||||
searchResult.setId(Convert.toLong(id));
|
||||
if (StringUtils.hasText(title)){
|
||||
String matchTitle = highlighter.getBestFragment(new StandardAnalyzer(), "title", title);
|
||||
if (StringUtils.hasText(matchTitle)){
|
||||
searchResult.setTitle(matchTitle);
|
||||
|
||||
SearchRes.Hit hit = new SearchRes.Hit();
|
||||
hit.setScore(doc.score);
|
||||
hit.setId(Convert.toLong(id));
|
||||
if (StrUtil.isNotBlank(name)) {
|
||||
hit.setOriginName(name);
|
||||
String hlName = highlighter.getBestFragment(new StandardAnalyzer(), "name", name);
|
||||
if (StrUtil.isNotBlank(hlName)) {
|
||||
hit.setName(hlName);
|
||||
} else {
|
||||
searchResult.setTitle(title);
|
||||
hit.setName(name);
|
||||
}
|
||||
} else {
|
||||
searchResult.setContent(title);
|
||||
}
|
||||
if (StringUtils.hasText(content)){
|
||||
String matchContent = highlighter.getBestFragment(new StandardAnalyzer(), "content", content);
|
||||
if (StringUtils.hasText(matchContent)){
|
||||
searchResult.setContent(matchContent);
|
||||
}else {
|
||||
searchResult.setContent(content);
|
||||
}
|
||||
}else {
|
||||
searchResult.setContent(content);
|
||||
hit.setName("");
|
||||
hit.setOriginName("");
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(tags)){
|
||||
String matchTags= highlighter.getBestFragment(new StandardAnalyzer(), "tags", tags);
|
||||
if (StringUtils.hasText(matchTags)){
|
||||
searchResult.setTags(matchTags);
|
||||
// 无内容或无高亮匹配时, 返回 ""
|
||||
if (StrUtil.isNotBlank(markdown)) {
|
||||
String hlMarkdown = highlighter.getBestFragment(new StandardAnalyzer(), "markdown", markdown);
|
||||
if (StrUtil.isNotBlank(hlMarkdown)) {
|
||||
hit.setMarkdown(fmtMarkdown(hlMarkdown));
|
||||
} else {
|
||||
searchResult.setTags(tags);
|
||||
hit.setMarkdown("");
|
||||
}
|
||||
} else {
|
||||
searchResult.setTags(tags);
|
||||
}
|
||||
result.add(searchResult);
|
||||
}
|
||||
hit.setMarkdown("");
|
||||
}
|
||||
|
||||
if (StrUtil.isNotBlank(tags)) {
|
||||
String hlTags = highlighter.getBestFragment(new StandardAnalyzer(), "tags", tags);
|
||||
if (StrUtil.isNotBlank(hlTags)) {
|
||||
hit.setTags(DocUtil.toTagList(hlTags));
|
||||
} else {
|
||||
hit.setTags(DocUtil.toTagList(tags));
|
||||
}
|
||||
} else {
|
||||
hit.setTags(DocUtil.toTagList(tags));
|
||||
}
|
||||
|
||||
result.getHits().add(hit);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new XzException500("索引查询异常");
|
||||
log.error("搜索异常: {}", e.getMessage());
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将正文中的换行转换成 html 内容
|
||||
*
|
||||
* @param markdown markdown 正文内容
|
||||
*/
|
||||
public String fmtMarkdown(String markdown) {
|
||||
return markdown.replaceAll("(\r\n|\n\r|\r|\n)", "<br />");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,70 +1,59 @@
|
||||
package com.blossom.backend.base.search.message;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Getter;
|
||||
import org.apache.lucene.document.Document;
|
||||
import org.apache.lucene.document.Field;
|
||||
import org.apache.lucene.document.StringField;
|
||||
import org.apache.lucene.document.TextField;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 文章索引消息的实现
|
||||
*/
|
||||
|
||||
@Getter
|
||||
public class ArticleIndexMsg implements IndexMsg {
|
||||
|
||||
private IndexMsgTypeEnum type;
|
||||
private final IndexMsgTypeEnum type;
|
||||
|
||||
private Long data;
|
||||
|
||||
private Document document;
|
||||
|
||||
private Long userId;
|
||||
private final Long id;
|
||||
|
||||
private Document doc;
|
||||
|
||||
private final Long userId;
|
||||
|
||||
public ArticleIndexMsg(IndexMsgTypeEnum indexMsgType, Long id, Long userId) {
|
||||
this.type = indexMsgType;
|
||||
this.data = id;
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public ArticleIndexMsg(IndexMsgTypeEnum indexMsgType, Long id, String title, String tags, String content, Long userId){
|
||||
/**
|
||||
* 创建文章索引消息
|
||||
*
|
||||
* @param indexMsgType 操作类型
|
||||
* @param id 唯一ID
|
||||
* @param name 标题
|
||||
* @param tags 标签
|
||||
* @param markdown 正文内容
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public ArticleIndexMsg(IndexMsgTypeEnum indexMsgType, Long id, String name, String tags, String markdown, Long userId) {
|
||||
this.type = indexMsgType;
|
||||
this.data = id;
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
Document document = new Document();
|
||||
// 存储文章的id, content
|
||||
// 存储文章的id, markdown
|
||||
document.add(new StringField("id", Convert.toStr(id), Field.Store.YES));
|
||||
if (StringUtils.hasText(title)){
|
||||
document.add(new TextField("title", title, Field.Store.YES));
|
||||
if (StrUtil.isNotBlank(name)) {
|
||||
document.add(new TextField("name", name, Field.Store.YES));
|
||||
}
|
||||
if (StringUtils.hasText(content)){
|
||||
document.add(new TextField("content", content, Field.Store.YES));
|
||||
if (StrUtil.isNotBlank(markdown)) {
|
||||
document.add(new TextField("markdown", markdown, Field.Store.YES));
|
||||
}
|
||||
if (StringUtils.hasText(tags)){
|
||||
if (StrUtil.isNotBlank(tags)) {
|
||||
document.add(new TextField("tags", tags, Field.Store.YES));
|
||||
}
|
||||
this.document = document;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IndexMsgTypeEnum getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Document getDoc() {
|
||||
return this.document;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getCurrentUserId() {
|
||||
return this.userId;
|
||||
this.doc = document;
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,27 +8,23 @@ import org.apache.lucene.document.Document;
|
||||
public interface IndexMsg {
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
* @return
|
||||
* 消息操作类型
|
||||
*/
|
||||
IndexMsgTypeEnum getType();
|
||||
|
||||
/**
|
||||
* 主键id
|
||||
* @return
|
||||
* 主键 ID
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 为批量reload提供的提前构造数据的接口,避免多次查询数据库
|
||||
* @return
|
||||
* 为批量 reload 提供的提前构造数据的接口, 避免多次查询数据库
|
||||
*/
|
||||
Document getDoc();
|
||||
|
||||
/**
|
||||
* 消息对应用户id
|
||||
* @return
|
||||
* 消息对应用户 ID
|
||||
*/
|
||||
Long getCurrentUserId();
|
||||
Long getUserId();
|
||||
|
||||
}
|
||||
|
||||
@ -5,6 +5,13 @@ package com.blossom.backend.base.search.message;
|
||||
*/
|
||||
public enum IndexMsgTypeEnum {
|
||||
|
||||
ADD,DELETE
|
||||
/**
|
||||
* 新增文档
|
||||
*/
|
||||
ADD,
|
||||
/**
|
||||
* 删除文档
|
||||
*/
|
||||
DELETE
|
||||
|
||||
}
|
||||
|
||||
@ -13,30 +13,34 @@ import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
/**
|
||||
* 批量构建索引
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class BatchIndexMsgConsumer {
|
||||
|
||||
private SearchProperties searchProperties;
|
||||
private final SearchProperties searchProperties;
|
||||
|
||||
BatchIndexMsgConsumer(SearchProperties searchProperties) {
|
||||
this.searchProperties = searchProperties;
|
||||
if (!StringUtils.hasText(searchProperties.getPath())) {
|
||||
log.info("未配置索引库地址, 关闭全文搜索功能支持");
|
||||
}
|
||||
}
|
||||
|
||||
public void batchReload(List<IndexMsg> list) throws IOException {
|
||||
// 需要对所有用户的索引进行维护,减少文件打开次数, 优先进行分组
|
||||
Map<Long, List<IndexMsg>> userGroupMsgMap = list.stream().collect(Collectors.groupingBy(IndexMsg::getCurrentUserId));
|
||||
// 遍历map, 逐个用户进行索引重建
|
||||
/**
|
||||
* 批量构建所有用户的索引
|
||||
*
|
||||
* @param articles 全部文章
|
||||
*/
|
||||
public void batchReload(List<IndexMsg> articles) throws IOException {
|
||||
// 需要对所有用户的索引进行维护, 减少文件打开次数, 优先进行分组
|
||||
Map<Long, List<IndexMsg>> userGroupMsgMap = articles.stream().collect(Collectors.groupingBy(IndexMsg::getUserId));
|
||||
// 遍历 Map, 逐个用户进行索引重建
|
||||
for (Map.Entry<Long, List<IndexMsg>> entity : userGroupMsgMap.entrySet()) {
|
||||
Long userId = entity.getKey();
|
||||
List<IndexMsg> msgList = entity.getValue();
|
||||
@ -44,28 +48,28 @@ public class BatchIndexMsgConsumer {
|
||||
continue;
|
||||
}
|
||||
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()));
|
||||
){
|
||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
|
||||
for (IndexMsg indexMsg : msgList) {
|
||||
if (IndexMsgTypeEnum.ADD.equals(indexMsg.getType())) {
|
||||
// 插入 or 更新索引
|
||||
// 打开索引库 --->通过getDoc方法获取索引文档
|
||||
/*
|
||||
* 插入或更新索引, 通过 getDoc 方法获取索引文档
|
||||
*/
|
||||
if (IndexMsgTypeEnum.ADD == indexMsg.getType()) {
|
||||
Document document = indexMsg.getDoc();
|
||||
String id = document.get("id");
|
||||
indexWriter.updateDocument(new Term("id", id), document);
|
||||
} else if (IndexMsgTypeEnum.DELETE.equals(indexMsg.getType())) {
|
||||
// 删除索引
|
||||
}
|
||||
/*
|
||||
* 删除索引内容, 批量处理时不会进行删除, 所有的删除都由用户主动操作
|
||||
*/
|
||||
else if (IndexMsgTypeEnum.DELETE == indexMsg.getType()) {
|
||||
Long id = indexMsg.getId();
|
||||
indexWriter.deleteDocuments(new Term("id", Convert.toStr(id)));
|
||||
}
|
||||
}
|
||||
// 完成
|
||||
indexWriter.flush();
|
||||
indexWriter.commit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ import org.apache.lucene.index.Term;
|
||||
import org.apache.lucene.store.Directory;
|
||||
import org.apache.lucene.store.FSDirectory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@ -28,70 +27,53 @@ import java.util.concurrent.Executors;
|
||||
*
|
||||
* @author Andecheal
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Component
|
||||
public class IndexMsgConsumer {
|
||||
|
||||
private SearchProperties searchProperties;
|
||||
private final SearchProperties searchProperties;
|
||||
|
||||
private ArticleService articleService;
|
||||
private final ArticleService articleService;
|
||||
|
||||
/**
|
||||
* 单线程处理索引文档消息
|
||||
*
|
||||
* @param searchProperties 索引配置
|
||||
* @param articleService 文章服务
|
||||
*/
|
||||
IndexMsgConsumer(SearchProperties searchProperties, ArticleService articleService) {
|
||||
this.searchProperties = searchProperties;
|
||||
this.articleService = articleService;
|
||||
if (!StringUtils.hasText(searchProperties.getPath())) {
|
||||
log.info("未配置索引库地址, 关闭全文搜索功能支持");
|
||||
return;
|
||||
}
|
||||
Executors.newSingleThreadExecutor().submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Executors.newSingleThreadExecutor().submit(() -> {
|
||||
while (true) {
|
||||
try {
|
||||
IndexMsg indexMsg = IndexMsgQueue.take();
|
||||
// 首先获取消息中的userId, 根据userId 打开对应的索引库
|
||||
Long userId = indexMsg.getCurrentUserId();
|
||||
if (userId == null){
|
||||
// 记录异常并继续消费
|
||||
final Long userId = indexMsg.getUserId();
|
||||
final Long id = indexMsg.getId();
|
||||
if (userId == null || id == null) {
|
||||
log.error("消费异常. 获取用户id为空");
|
||||
continue;
|
||||
}
|
||||
if (IndexMsgTypeEnum.ADD.equals(indexMsg.getType())) {
|
||||
if (IndexMsgTypeEnum.ADD == indexMsg.getType()) {
|
||||
// 插入 or 更新索引
|
||||
// 打开索引库
|
||||
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()));
|
||||
) {
|
||||
// 查询doc数据 ---> 避免service部分功能未查询全部字段数据导致的索引field丢失
|
||||
Long id = indexMsg.getId();
|
||||
ArticleEntity article = articleService.getById(id);
|
||||
try (Directory directory = FSDirectory.open(this.searchProperties.getUserIndexDirectory(userId));
|
||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()))) {
|
||||
// 查询最新的消息
|
||||
ArticleEntity article = this.articleService.selectById(id, false, true, false);
|
||||
Document document = new Document();
|
||||
String title = article.getName();
|
||||
String markdownContent = article.getMarkdown();
|
||||
String tags = article.getTags();
|
||||
// 存储文章的id, content
|
||||
document.add(new StringField("id", Convert.toStr(id), Field.Store.YES));
|
||||
if (StringUtils.hasText(title)){
|
||||
document.add(new TextField("title", article.getName(), Field.Store.YES));
|
||||
}
|
||||
if (StringUtils.hasText(markdownContent)){
|
||||
document.add(new TextField("content", markdownContent, Field.Store.YES));
|
||||
}
|
||||
if (StringUtils.hasText(tags)){
|
||||
document.add(new TextField("tags", tags, 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.flush();
|
||||
indexWriter.commit();
|
||||
}
|
||||
} else if (IndexMsgTypeEnum.DELETE.equals(indexMsg.getType())) {
|
||||
} else if (IndexMsgTypeEnum.DELETE == indexMsg.getType()) {
|
||||
// 删除索引
|
||||
try (Directory directory = FSDirectory.open(searchProperties.getUserIndexDirectory(userId));
|
||||
IndexWriter indexWriter = new IndexWriter(directory, new IndexWriterConfig(new StandardAnalyzer()));
|
||||
|
||||
) {
|
||||
Long id = indexMsg.getId();
|
||||
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.flush();
|
||||
indexWriter.commit();
|
||||
@ -102,7 +84,6 @@ public class IndexMsgConsumer {
|
||||
log.error("消费失败" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* 消息处理使用的阻塞队列
|
||||
*
|
||||
* @author Andecheal
|
||||
*/
|
||||
public class IndexMsgQueue {
|
||||
@ -17,9 +18,7 @@ public class IndexMsgQueue {
|
||||
private static final BlockingQueue<IndexMsg> indexMsgQueue = new ArrayBlockingQueue<>(2048);
|
||||
|
||||
/**
|
||||
* 应用提交消息
|
||||
* @param msg
|
||||
* @throws InterruptedException
|
||||
* 提交消息
|
||||
*/
|
||||
public static void add(IndexMsg msg) throws InterruptedException {
|
||||
indexMsgQueue.add(msg);
|
||||
@ -27,8 +26,6 @@ public class IndexMsgQueue {
|
||||
|
||||
/**
|
||||
* 提供一个阻塞式消息入口
|
||||
* @param msg
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public static void put(IndexMsg msg) throws InterruptedException {
|
||||
indexMsgQueue.put(msg);
|
||||
@ -36,8 +33,6 @@ public class IndexMsgQueue {
|
||||
|
||||
/**
|
||||
* 获取消息
|
||||
* @return
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public static IndexMsg take() throws InterruptedException {
|
||||
return indexMsgQueue.take();
|
||||
|
||||
@ -15,6 +15,7 @@ import com.blossom.backend.server.article.stat.ArticleStatService;
|
||||
import com.blossom.common.base.exception.XzException400;
|
||||
import com.blossom.common.base.exception.XzException404;
|
||||
import com.blossom.common.base.pojo.R;
|
||||
import com.blossom.common.base.util.spring.SpringUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@ -45,6 +46,7 @@ public class UserController {
|
||||
user.setOsRes(sysService.getOsConfig());
|
||||
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
|
||||
user.setParams(paramMap);
|
||||
paramMap.put("SERVER_VERSION", SpringUtil.get("project.base.version"));
|
||||
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
|
||||
user.setUserParams(userParamMap);
|
||||
return R.ok(user);
|
||||
|
||||
@ -18,6 +18,7 @@ public interface ArticleMapper extends BaseMapper<ArticleEntity> {
|
||||
|
||||
/**
|
||||
* 批量查询文章正文
|
||||
*
|
||||
* @param ids 文章ID
|
||||
* @param contentType 正文类型 MARKDOWN/HTML
|
||||
*/
|
||||
@ -28,7 +29,10 @@ public interface ArticleMapper extends BaseMapper<ArticleEntity> {
|
||||
*/
|
||||
List<ArticleEntity> listAll(ArticleEntity entity);
|
||||
|
||||
List<ArticleEntity> listAllArticleWithContent();
|
||||
/**
|
||||
* 查询全部需要索引的字段
|
||||
*/
|
||||
List<ArticleEntity> listAllIndexField();
|
||||
|
||||
/**
|
||||
* 根据ID修改
|
||||
|
||||
@ -83,10 +83,11 @@ public class ArticleService extends ServiceImpl<ArticleMapper, ArticleEntity> {
|
||||
|
||||
/**
|
||||
* 获取所有文章,包含markdown字段,用于索引的批量维护
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public List<ArticleEntity> listAllArticleWithContent() {
|
||||
List<ArticleEntity> articles = baseMapper.listAllArticleWithContent();
|
||||
public List<ArticleEntity> listAllIndexField() {
|
||||
List<ArticleEntity> articles = baseMapper.listAllIndexField();
|
||||
if (CollUtil.isEmpty(articles)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ project:
|
||||
duration: 600000 # 动态日志级别 6 分钟后失效
|
||||
restore-duration: 30000 # 30 秒判断一次是否失效
|
||||
db:
|
||||
slow-interval: 10 # 慢SQL
|
||||
slow-interval: 100 # 慢SQL
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ Auth ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
auth: # 授权
|
||||
enabled: true # 开启授权
|
||||
@ -87,6 +87,3 @@ project:
|
||||
domain: "http://www.xxx.com/"
|
||||
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
||||
default-path: "/home/bl/img/"
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
search:
|
||||
path: "/home/index/"
|
||||
@ -79,6 +79,3 @@ project:
|
||||
domain: "http://www.xxx.com/"
|
||||
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
||||
default-path: "/home/bl/img/"
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
search:
|
||||
path: "/home/index/"
|
||||
|
||||
@ -81,6 +81,3 @@ project:
|
||||
domain: "https://www.wangyunf.com/blall/pic/"
|
||||
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
|
||||
default-path: "/home/blall/img/"
|
||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
search:
|
||||
path: "/home/index/"
|
||||
@ -49,7 +49,7 @@
|
||||
</select>
|
||||
|
||||
<!-- 查询全部文章,包含id, name , markdown, tags , userId字段,用于批量索引的建立 -->
|
||||
<select id="listAllArticleWithContent" resultType="com.blossom.backend.server.article.draft.pojo.ArticleEntity">
|
||||
<select id="listAllIndexField" resultType="com.blossom.backend.server.article.draft.pojo.ArticleEntity">
|
||||
select
|
||||
id,
|
||||
`name`,
|
||||
|
||||
@ -391,8 +391,10 @@ CREATE TABLE IF NOT EXISTS `blossom_article_reference`
|
||||
ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of blossom_article_reference
|
||||
-- since: 1.12.0
|
||||
-- ----------------------------
|
||||
alter table blossom_article_reference
|
||||
modify target_url varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL comment '链接地址';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for blossom_article_view
|
||||
|
||||
@ -472,6 +472,13 @@ export const articleRecycleListApi = (): Promise<any> => {
|
||||
export const articleRecycleRestoreApi = (data?: object): Promise<any> => {
|
||||
return rq.post('/article/recycle/restore', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章全文搜索
|
||||
*/
|
||||
export const articleSearchApi = (params?: object): Promise<any> => {
|
||||
return rq.get('/search', { params })
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region ====================================================< picture >===================================================
|
||||
|
||||
@ -9,7 +9,8 @@ const blossom = {
|
||||
|
||||
//
|
||||
DOC: 'https://www.wangyunf.com/blossom-doc/index',
|
||||
CONTACT: 'https://www.wangyunf.com/blossom-doc/guide/about/all.html',
|
||||
CONTACT: 'https://www.wangyunf.com/blossom-doc/guide/about/contact.html',
|
||||
SPONSOR: 'https://www.wangyunf.com/blossom-doc/guide/about/sponsor.html',
|
||||
GITHUB_REPO: 'https://github.com/blossom-editor/blossom',
|
||||
GITHUB_RELEASE: 'https://github.com/blossom-editor/blossom/releases',
|
||||
GITEE_REPO: 'https://gitee.com/blossom-editor/blossom'
|
||||
|
||||
@ -45,7 +45,6 @@ img {
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 2px;
|
||||
background-color: var(--bl-scrollbar-color);
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconbl"; /* Project id 4118609 */
|
||||
src: url('iconfont.woff2?t=1703588816023') format('woff2'),
|
||||
url('iconfont.woff?t=1703588816023') format('woff'),
|
||||
url('iconfont.ttf?t=1703588816023') format('truetype');
|
||||
src: url('iconfont.woff2?t=1704477350520') format('woff2'),
|
||||
url('iconfont.woff?t=1704477350520') format('woff'),
|
||||
url('iconfont.ttf?t=1704477350520') format('truetype');
|
||||
}
|
||||
|
||||
.iconbl {
|
||||
@ -13,6 +13,18 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.bl-and:before {
|
||||
content: "\e71b";
|
||||
}
|
||||
|
||||
.bl-wuneirong:before {
|
||||
content: "\e6a7";
|
||||
}
|
||||
|
||||
.bl-enter:before {
|
||||
content: "\e65a";
|
||||
}
|
||||
|
||||
.bl-blog:before {
|
||||
content: "\e7c6";
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -5,6 +5,27 @@
|
||||
"css_prefix_text": "bl-",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "8990451",
|
||||
"name": "相交",
|
||||
"font_class": "and",
|
||||
"unicode": "e71b",
|
||||
"unicode_decimal": 59163
|
||||
},
|
||||
{
|
||||
"icon_id": "17525623",
|
||||
"name": "无内容",
|
||||
"font_class": "wuneirong",
|
||||
"unicode": "e6a7",
|
||||
"unicode_decimal": 59047
|
||||
},
|
||||
{
|
||||
"icon_id": "12687054",
|
||||
"name": "回车",
|
||||
"font_class": "enter",
|
||||
"unicode": "e65a",
|
||||
"unicode_decimal": 58970
|
||||
},
|
||||
{
|
||||
"icon_id": "12579639",
|
||||
"name": "blog",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -10,16 +10,35 @@
|
||||
text-align: center;
|
||||
box-shadow:
|
||||
inset 0 0 20px white,
|
||||
0 5px 0 #b1b1b1,
|
||||
0 6px 0 1px #7e7e7e,
|
||||
0 8px 5px #a5a5a5;
|
||||
0 3px 0 #b1b1b1,
|
||||
0 4px 0 1px #7e7e7e,
|
||||
0 7px 5px #a5a5a5;
|
||||
|
||||
[class='dark'] & {
|
||||
box-shadow:
|
||||
inset 0 0 20px #000000,
|
||||
0 5px 0 #272727,
|
||||
0 6px 0 2px #121212,
|
||||
0 8px 5px #252525;
|
||||
0 3px 0 #272727,
|
||||
0 4px 0 1px #121212,
|
||||
0 7px 5px #252525;
|
||||
}
|
||||
}
|
||||
|
||||
.keyboard.small {
|
||||
padding: 0 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 300;
|
||||
box-shadow:
|
||||
inset 0 0 20px white,
|
||||
0 2px 0 #b1b1b1,
|
||||
0 2px 0 1px #7e7e7e,
|
||||
0 5px 4px #a5a5a5;
|
||||
|
||||
[class='dark'] & {
|
||||
box-shadow:
|
||||
inset 0 0 20px #000000,
|
||||
0 2px 0 #272727,
|
||||
0 3px 0 1px #121212,
|
||||
0 5px 4px #252525;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ $height-form: calc(100% - #{$height-title} - #{$height-footer});
|
||||
padding-left: 10px;
|
||||
color: var(--bl-text-title-color);
|
||||
text-shadow: var(--bl-text-shadow);
|
||||
padding-bottom: 8px;
|
||||
|
||||
.iconbl {
|
||||
font-size: 25px;
|
||||
|
||||
@ -3,5 +3,43 @@
|
||||
--el-dialog-padding-primary: 0 !important;
|
||||
--el-dialog-bg-color: var(--bl-dialog-bg-color) !important;
|
||||
--el-dialog-box-shadow: var(--bl-dialog-box-shadow) !important;
|
||||
|
||||
}
|
||||
|
||||
// 更大的 header close 按钮
|
||||
.bl-dialog-bigger-headerbtn {
|
||||
.el-dialog__headerbtn {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// 无 header
|
||||
.bl-dialog-hidden-header {
|
||||
.el-dialog__header {
|
||||
display: none !important;
|
||||
}
|
||||
.el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 固定 body 长度
|
||||
.bl-dialog-fixed-body {
|
||||
.el-dialog__body {
|
||||
height: calc(100% - 10px);
|
||||
}
|
||||
}
|
||||
|
||||
// 无 header 且固定 body 长度
|
||||
.bl-dialog-hidden-header-fixed-body {
|
||||
.el-dialog__header {
|
||||
display: none !important;
|
||||
}
|
||||
.el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
.el-dialog__body {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
.el-notification {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
border-radius: 2px !important;
|
||||
border: 2px solid var(--el-color-primary-light-5) !important;
|
||||
}
|
||||
@ -37,10 +37,6 @@
|
||||
.el-popper.is-light {
|
||||
@include themeShadow(1px 3px 10px #dedede, 1px 3px 10px #000000);
|
||||
color: var(--bl-text-doctree-color);
|
||||
|
||||
.keyboard {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper.el-picker__popper {
|
||||
|
||||
144
blossom-editor/src/renderer/src/assets/utils/color.ts
Normal file
144
blossom-editor/src/renderer/src/assets/utils/color.ts
Normal file
@ -0,0 +1,144 @@
|
||||
export interface IColorObj {
|
||||
r: number
|
||||
g: number
|
||||
b: number
|
||||
a?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 255颜色值转16进制颜色值
|
||||
* @param n 255颜色值
|
||||
* @returns hex 16进制颜色值
|
||||
*/
|
||||
export const toHex = (n: number) => `${n > 15 ? '' : 0}${n.toString(16)}`
|
||||
|
||||
/**
|
||||
* 颜色对象转化为16进制颜色字符串
|
||||
* @param colorObj 颜色对象
|
||||
*/
|
||||
export const toHexString = (colorObj: IColorObj) => {
|
||||
const { r, g, b, a = 1 } = colorObj
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}${a === 1 ? '' : toHex(Math.floor(a * 255))}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色对象转化为rgb颜色字符串
|
||||
* @param colorObj 颜色对象
|
||||
*/
|
||||
export const toRgbString = (colorObj: IColorObj) => {
|
||||
const { r, g, b } = colorObj
|
||||
return `rgb(${r},${g},${b})`
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色对象转化为rgba颜色字符串
|
||||
* @param colorObj 颜色对象
|
||||
*/
|
||||
export const toRgbaString = (colorObj: IColorObj, n = 10000) => {
|
||||
const { r, g, b, a = 1 } = colorObj
|
||||
return `rgba(${r},${g},${b},${Math.floor(a * n) / n})`
|
||||
}
|
||||
|
||||
/**
|
||||
* 16进制颜色字符串解析为颜色对象
|
||||
* @param color 颜色字符串
|
||||
* @returns IColorObj
|
||||
*/
|
||||
export const parseHexColor = (color: string) => {
|
||||
let hex = color.slice(1)
|
||||
let a = 1
|
||||
if (hex.length === 3) {
|
||||
hex = `${hex[0]}${hex[0]}${hex[1]}${hex[1]}${hex[2]}${hex[2]}`
|
||||
}
|
||||
if (hex.length === 8) {
|
||||
a = parseInt(hex.slice(6), 16) / 255
|
||||
hex = hex.slice(0, 6)
|
||||
}
|
||||
const bigint = parseInt(hex, 16)
|
||||
return {
|
||||
r: (bigint >> 16) & 255,
|
||||
g: (bigint >> 8) & 255,
|
||||
b: bigint & 255,
|
||||
a
|
||||
} as IColorObj
|
||||
}
|
||||
|
||||
/**
|
||||
* rgba颜色字符串解析为颜色对象
|
||||
* @param color 颜色字符串
|
||||
* @returns IColorObj
|
||||
*/
|
||||
export const parseRgbaColor = (color: string) => {
|
||||
const arr = color.match(/(\d(\.\d+)?)+/g) || []
|
||||
const res = arr.map((s: string) => parseInt(s, 10))
|
||||
return {
|
||||
r: res[0],
|
||||
g: res[1],
|
||||
b: res[2],
|
||||
a: parseFloat(arr[3]) || 1
|
||||
} as IColorObj
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色字符串解析为颜色对象
|
||||
* @param color 颜色字符串
|
||||
* @returns IColorObj
|
||||
*/
|
||||
export const parseColorString = (color: string) => {
|
||||
if (color.startsWith('#')) {
|
||||
return parseHexColor(color)
|
||||
} else if (color.startsWith('rgb')) {
|
||||
return parseRgbaColor(color)
|
||||
} else if (color === 'transparent') {
|
||||
return parseHexColor('#00000000')
|
||||
}
|
||||
throw new Error(`color string error: ${color}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色字符串解析为各种颜色表达方式
|
||||
* @param color 颜色字符串
|
||||
* @returns IColorObj
|
||||
*/
|
||||
export const getColorInfo = (color: string) => {
|
||||
const colorObj = parseColorString(color)
|
||||
const hex = toHexString(colorObj)
|
||||
const rgba = toRgbaString(colorObj)
|
||||
const rgb = toRgbString(colorObj)
|
||||
return {
|
||||
hex,
|
||||
rgba,
|
||||
rgb,
|
||||
rgbaObj: colorObj
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 16进制颜色字符串转化为rgba颜色字符串
|
||||
* @param hex 16进制颜色字符串
|
||||
* @returns rgba颜色字符串
|
||||
*/
|
||||
export const hexToRgba = (hex: string) => {
|
||||
const colorObj = parseColorString(hex)
|
||||
return toRgbaString(colorObj)
|
||||
}
|
||||
|
||||
/**
|
||||
* rgb颜色字符串转化为16进制颜色字符串
|
||||
* @param rgb rgb颜色字符串
|
||||
* @returns 16进制颜色字符串
|
||||
*/
|
||||
export const rgbToHex = (rgb: string) => {
|
||||
const colorObj = parseColorString(rgb)
|
||||
return toHexString(colorObj)
|
||||
}
|
||||
|
||||
/**
|
||||
* rgba颜色字符串转化为16进制颜色字符串
|
||||
* @param rgba rgba颜色字符串
|
||||
* @returns 16进制颜色字符串
|
||||
*/
|
||||
export const rgbaToHex = (rgba: string) => {
|
||||
const colorObj = parseColorString(rgba)
|
||||
return toHexString(colorObj)
|
||||
}
|
||||
@ -152,7 +152,7 @@ export const timestampToDatetime = (timestamp: number | string | Date): string =
|
||||
}
|
||||
|
||||
/**
|
||||
* 两个日期相差的条数
|
||||
* 两个日期相差的天数
|
||||
*
|
||||
* @param date1 yyyy-MM-dd
|
||||
* @param date2 yyyy-MM-dd
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
|
||||
<el-dialog
|
||||
v-model="isShowQuickSetting"
|
||||
class="dialog-quick-setting"
|
||||
class="bl-dialog-bigger-headerbtn"
|
||||
width="750px"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
@ -280,13 +280,6 @@ const quickSettingComplete = () => {
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.dialog-quick-setting {
|
||||
.el-dialog__headerbtn {
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
.web-collect-drawer {
|
||||
--el-drawer-bg-color: var(--bl-html-color);
|
||||
.el-drawer__header {
|
||||
|
||||
@ -30,14 +30,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="系统图片" name="sysimg">
|
||||
<!-- <el-tab-pane label="系统图片" name="sysimg">
|
||||
<div class="icon-container">
|
||||
<div v-for="img in imgs" class="icon-item" :key="img.icon_id">
|
||||
<img style="width: 40px; height: 40px; object-fit: contain" :src="img" />
|
||||
<div class="icon-name">{{ img.substring(img.lastIndexOf('/') + 1) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tab-pane> -->
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
@ -57,57 +57,57 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const getImg = (img: string) => {
|
||||
return new URL(`../assets/imgs/${img}`, import.meta.url).href
|
||||
}
|
||||
// const getImg = (img: string) => {
|
||||
// return new URL(`../assets/imgs/${img}`, import.meta.url).href
|
||||
// }
|
||||
|
||||
const activeTab = ref('weblogo')
|
||||
|
||||
const blossom = shallowRef<any[]>([])
|
||||
const weblogo = shallowRef<any[]>([])
|
||||
const imgs = shallowRef<any[]>([
|
||||
getImg('plan/base-awesome.png'),
|
||||
getImg('plan/base-cool.png'),
|
||||
getImg('plan/base-learning.png'),
|
||||
getImg('plan/cat-kiss.png'),
|
||||
getImg('plan/cat-nice.png'),
|
||||
getImg('plan/cat-smile.png'),
|
||||
getImg('plan/cat.png'),
|
||||
getImg('plan/coffee.png'),
|
||||
getImg('plan/juice.png'),
|
||||
getImg('plan/beer.png'),
|
||||
// note
|
||||
getImg('note/cd.png'),
|
||||
getImg('note/dustbin.png'),
|
||||
getImg('note/note.png'),
|
||||
getImg('note/pin.png'),
|
||||
getImg('note/plane.png'),
|
||||
// pe
|
||||
getImg('pe/headset.png'),
|
||||
getImg('pe/phone.png'),
|
||||
getImg('pe/sound.png'),
|
||||
getImg('pe/watch.png'),
|
||||
// plant
|
||||
getImg('plant/02.png'),
|
||||
getImg('plant/08.png'),
|
||||
getImg('plant/cactus.png'),
|
||||
// weather
|
||||
getImg('weather/feng-s.png'),
|
||||
getImg('weather/feng.png'),
|
||||
getImg('weather/qing-s.png'),
|
||||
getImg('weather/qing.png'),
|
||||
getImg('weather/qing-moon.png'),
|
||||
getImg('weather/wu-s.png'),
|
||||
getImg('weather/wu.png'),
|
||||
getImg('weather/xue-s.png'),
|
||||
getImg('weather/xue.png'),
|
||||
getImg('weather/yin-s.png'),
|
||||
getImg('weather/yin.png'),
|
||||
getImg('weather/yu-s.png'),
|
||||
getImg('weather/yu.png'),
|
||||
getImg('weather/zhongyu-s.png'),
|
||||
getImg('weather/zhongyu.png')
|
||||
])
|
||||
// const imgs = shallowRef<any[]>([
|
||||
// getImg('plan/base-awesome.png'),
|
||||
// getImg('plan/base-cool.png'),
|
||||
// getImg('plan/base-learning.png'),
|
||||
// getImg('plan/cat-kiss.png'),
|
||||
// getImg('plan/cat-nice.png'),
|
||||
// getImg('plan/cat-smile.png'),
|
||||
// getImg('plan/cat.png'),
|
||||
// getImg('plan/coffee.png'),
|
||||
// getImg('plan/juice.png'),
|
||||
// getImg('plan/beer.png'),
|
||||
// // note
|
||||
// getImg('note/cd.png'),
|
||||
// getImg('note/dustbin.png'),
|
||||
// getImg('note/note.png'),
|
||||
// getImg('note/pin.png'),
|
||||
// getImg('note/plane.png'),
|
||||
// // pe
|
||||
// getImg('pe/headset.png'),
|
||||
// getImg('pe/phone.png'),
|
||||
// getImg('pe/sound.png'),
|
||||
// getImg('pe/watch.png'),
|
||||
// // plant
|
||||
// getImg('plant/02.png'),
|
||||
// getImg('plant/08.png'),
|
||||
// getImg('plant/cactus.png'),
|
||||
// // weather
|
||||
// getImg('weather/feng-s.png'),
|
||||
// getImg('weather/feng.png'),
|
||||
// getImg('weather/qing-s.png'),
|
||||
// getImg('weather/qing.png'),
|
||||
// getImg('weather/qing-moon.png'),
|
||||
// getImg('weather/wu-s.png'),
|
||||
// getImg('weather/wu.png'),
|
||||
// getImg('weather/xue-s.png'),
|
||||
// getImg('weather/xue.png'),
|
||||
// getImg('weather/yin-s.png'),
|
||||
// getImg('weather/yin.png'),
|
||||
// getImg('weather/yu-s.png'),
|
||||
// getImg('weather/yu.png'),
|
||||
// getImg('weather/zhongyu-s.png'),
|
||||
// getImg('weather/zhongyu.png')
|
||||
// ])
|
||||
|
||||
const iconSearch = ref('')
|
||||
|
||||
|
||||
@ -38,7 +38,7 @@
|
||||
<el-input-number v-model="webForm.sort" :min="1" style="width: 80px; margin-left: 10px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="web-item hover">
|
||||
<div class="web-item-card hover">
|
||||
<img v-if="isNotBlank(webForm.img)" :src="webForm.img" style="width: 40px; height: 40px; object-fit: contain" />
|
||||
<svg v-else style="width: 40px; height: 40px" aria-hidden="true">
|
||||
<use :xlink:href="'#' + webForm.icon"></use>
|
||||
@ -54,10 +54,13 @@
|
||||
|
||||
<bl-row just="flex-end" class="web-collect-title">
|
||||
<span class="title-remind" style="">右键点击卡片修改</span>
|
||||
<span class="iconbl bl-add-line" style="font-size: 20px; margin-right: 10px; cursor: pointer" @click="showForm($event)"></span>
|
||||
<span class="iconbl bl-refresh-smile" style="font-size: 20px; margin-right: 10px; cursor: pointer" @click="getWebAll"></span>
|
||||
<span v-if="viewStyle.isWebCollectCard" class="iconbl bl-array-line" @click="showWebCollectCard(false)"></span>
|
||||
<span v-else class="iconbl bl-article-line container-operator" @click="showWebCollectCard(true)" />
|
||||
<span class="iconbl bl-add-line" @click="showForm($event)"></span>
|
||||
<span class="iconbl bl-refresh-smile" @click="getWebAll"></span>
|
||||
Quick Access
|
||||
</bl-row>
|
||||
|
||||
<div class="web-item-container">
|
||||
<div v-for="(collect, index) in data" @click="closeForm">
|
||||
<bl-row just="flex-end" class="web-collect-group" :style="index == 0 ? 'marginTop:0' : ''">
|
||||
@ -65,13 +68,17 @@
|
||||
</bl-row>
|
||||
<div class="web-collect-content">
|
||||
<div
|
||||
:class="['web-item', viewStyle.isGlobalShadow ? 'web-item-heavy' : 'web-item-light']"
|
||||
v-for="web in collect.webs"
|
||||
:key="web.name"
|
||||
:class="[
|
||||
'web-item',
|
||||
viewStyle.isWebCollectCard ? 'web-item-card' : 'web-item-list',
|
||||
viewStyle.isGlobalShadow ? 'web-item-heavy' : 'web-item-light'
|
||||
]"
|
||||
@click="openExtenal(web.url)"
|
||||
@contextmenu="showForm($event, web)">
|
||||
<img v-if="isNotBlank(web.img)" :src="web.img" style="width: 40px; height: 40px; object-fit: contain" />
|
||||
<svg v-else style="width: 40px; height: 40px" aria-hidden="true">
|
||||
<img v-if="isNotBlank(web.img)" :src="web.img" />
|
||||
<svg v-else aria-hidden="true">
|
||||
<use :xlink:href="'#' + web.icon"></use>
|
||||
</svg>
|
||||
<div class="web-name">{{ web.name }}</div>
|
||||
@ -89,10 +96,10 @@ import { webAllApi, webSaveApi, webDelApi } from '@renderer/api/web'
|
||||
import { isNotBlank, isNotNull } from '@renderer/assets/utils/obj'
|
||||
import { openExtenal, openNewIconWindow } from '@renderer/assets/utils/electron'
|
||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||
import { useConfigStore } from '@renderer/stores/config'
|
||||
import { ViewStyle, useConfigStore } from '@renderer/stores/config'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const { viewStyle } = configStore
|
||||
const viewStyle = ref<ViewStyle>(configStore.viewStyle)
|
||||
|
||||
useLifecycle(
|
||||
() => getWebAll(),
|
||||
@ -145,6 +152,11 @@ const delWeb = () => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const showWebCollectCard = (card: boolean) => {
|
||||
viewStyle.value.isWebCollectCard = card
|
||||
configStore.setViewStyle(viewStyle.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -168,6 +180,12 @@ const delWeb = () => {
|
||||
text-shadow: var(--bl-text-shadow);
|
||||
padding: 5px 10px;
|
||||
|
||||
.iconbl {
|
||||
font-size: 17px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title-remind {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 11px;
|
||||
@ -234,19 +252,28 @@ const delWeb = () => {
|
||||
}
|
||||
|
||||
.web-item {
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
img,
|
||||
svg {
|
||||
object-fit: contain;
|
||||
filter: var(--bl-drop-shadow-star);
|
||||
}
|
||||
}
|
||||
|
||||
.web-item-card {
|
||||
@include flex(column, space-between, center);
|
||||
@include box(80px, 110px);
|
||||
@include themeBrightness(100%, 80%);
|
||||
padding: 15px 10px 10px 10px;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
svg {
|
||||
filter: var(--bl-drop-shadow-star);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.web-name {
|
||||
@ -260,6 +287,28 @@ const delWeb = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.web-item-list {
|
||||
@include flex(row, flex-start, center);
|
||||
@include box(200px, auto);
|
||||
border-radius: 4px;
|
||||
padding: 3px 3px 3px 5px;
|
||||
margin-bottom: 5px;
|
||||
color: var(--bl-text-color-light);
|
||||
|
||||
img,
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.web-name {
|
||||
@include ellipsis();
|
||||
width: 175px;
|
||||
font-size: 13px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.web-item-heavy {
|
||||
&:hover {
|
||||
@include themeShadow(0 3px 5px 0 rgb(190, 190, 190), 0 3px 5px 0 rgba(0, 0, 0, 1));
|
||||
@ -274,9 +323,9 @@ const delWeb = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.web-item.hover {
|
||||
transform: translateY(-5px);
|
||||
.web-item-card.hover {
|
||||
@include themeShadow(0 3px 5px 0 rgb(190, 190, 190), 0 3px 5px 0 rgba(0, 0, 0, 1));
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.save-form {
|
||||
|
||||
@ -18,10 +18,12 @@ export interface ViewStyle {
|
||||
webCollectExpand: boolean
|
||||
// 是否显示专题样式
|
||||
isShowSubjectStyle: boolean
|
||||
// 是否在首页显示收藏卡片
|
||||
// 是否以卡片方式显示文章收藏
|
||||
isHomeStarCard: boolean
|
||||
// 是否在首页显示专题卡片
|
||||
// 是否以卡片方式显示专题
|
||||
isHomeSubjectCard: boolean
|
||||
// 是否以卡片方式显示网页收藏
|
||||
isWebCollectCard: boolean
|
||||
// 是否开启全局阴影, 在 ThemeSetting.vue#changeGlobalShadow 中设置
|
||||
isGlobalShadow: boolean
|
||||
// 是否显示试用按钮
|
||||
@ -102,7 +104,8 @@ export const useConfigStore = defineStore('configStore', {
|
||||
isShowSubjectStyle: true,
|
||||
isHomeStarCard: true,
|
||||
isHomeSubjectCard: true,
|
||||
isGlobalShadow: true,
|
||||
isWebCollectCard: true,
|
||||
isGlobalShadow: false,
|
||||
isShowTryuseBtn: true
|
||||
},
|
||||
...Local.get(VIEW_STYLE_KEY)
|
||||
|
||||
@ -81,7 +81,8 @@ const DEFAULT_USER_INFO = {
|
||||
SERVER_MACHINE_EXPIRE: '',
|
||||
SERVER_DATABASE_EXPIRE: '',
|
||||
SERVER_HTTPS_EXPIRE: '',
|
||||
SERVER_DOMAIN_EXPIRE: ''
|
||||
SERVER_DOMAIN_EXPIRE: '',
|
||||
SERVER_VERSION: ''
|
||||
},
|
||||
/**
|
||||
* 用户参数配置
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
</el-button>
|
||||
<div class="tips">服务器将于每日早上 7 点备份 Markdown 数据。</div>
|
||||
<div class="download-process">
|
||||
<!-- 当前仅支持下载最大 10MB 的文件, 过大时请您自行从服务器中下载。若您的服务器带宽较小,也建议您自行从服务器下载。 -->
|
||||
<el-progress :text-inside="true" :stroke-width="20" :percentage="downloadProgress" striped striped-flow :duration="200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -102,8 +102,6 @@
|
||||
<div v-if="isElectron()" class="menu-item" @click="rightMenuCopy"><span class="iconbl bl-copy-line"></span>复制</div>
|
||||
<div v-if="isElectron()" class="menu-item" @click="rightMenuPaste"><span class="iconbl bl-a-texteditorpastetext-line"></span>黏贴</div>
|
||||
<div class="menu-item">
|
||||
<!--
|
||||
:data="{ pid: curArticle?.pid }" -->
|
||||
<el-upload
|
||||
name="file"
|
||||
:action="serverStore.serverUrl + uploadFileApiUrl"
|
||||
@ -468,7 +466,8 @@ const saveCurArticleContent = async (auto: boolean = false) => {
|
||||
toc: JSON.stringify(articleToc.value),
|
||||
references: articleImg.value.concat(articleLink.value)
|
||||
}
|
||||
await articleUpdContentApi(data).then((resp) => {
|
||||
await articleUpdContentApi(data)
|
||||
.then((resp) => {
|
||||
lastSaveTime = new Date().getTime()
|
||||
curArticle.value!.words = resp.data.words as number
|
||||
curArticle.value!.updTime = resp.data.updTime as string
|
||||
@ -479,6 +478,9 @@ const saveCurArticleContent = async (auto: boolean = false) => {
|
||||
}
|
||||
saveCallback()
|
||||
})
|
||||
.catch(() => {
|
||||
articleChanged = true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 初始化自动保存定时器
|
||||
|
||||
@ -6,9 +6,11 @@
|
||||
<div class="desc-text">显示排序</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iconbl bl-cloud-line"></div>
|
||||
<div class="iconbl bl-a-lowerrightpage-line"></div>
|
||||
<div class="iconbl bl-star-line"></div>
|
||||
<div class="iconbl bl-search-line">
|
||||
<div class="desc-line">
|
||||
<div class="desc-text">全文搜索</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iconbl bl-a-cloudrefresh-line"></div>
|
||||
<div class="iconbl bl-a-fileadd-line">
|
||||
<div class="desc-line">
|
||||
@ -35,6 +37,10 @@
|
||||
<div class="label">格式化</div>
|
||||
<div class="key">{{ keymaps.formatAll }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="label">全文搜索</div>
|
||||
<div class="key">{{ keymaps.fullSearch }}</div>
|
||||
</bl-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -70,7 +76,10 @@ import { keymaps } from './scripts/editor-tools'
|
||||
}
|
||||
}
|
||||
|
||||
// 排序
|
||||
.bl-a-leftdirection-line {
|
||||
padding-bottom: 5px;
|
||||
padding-right: 0px;
|
||||
.desc-line {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
@ -86,6 +95,24 @@ import { keymaps } from './scripts/editor-tools'
|
||||
}
|
||||
}
|
||||
|
||||
.bl-search-line {
|
||||
font-size: 22px;
|
||||
padding-bottom: 5px;
|
||||
.desc-line {
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
height: 20px;
|
||||
width: 30px;
|
||||
left: -20px;
|
||||
top: -20px;
|
||||
|
||||
.desc-text {
|
||||
left: -55px;
|
||||
top: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bl-a-fileadd-line {
|
||||
.desc-line {
|
||||
border-top: none;
|
||||
|
||||
523
blossom-editor/src/renderer/src/views/article/ArticleSearch.vue
Normal file
523
blossom-editor/src/renderer/src/views/article/ArticleSearch.vue
Normal file
@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<div class="article-search-root">
|
||||
<!-- 标题 -->
|
||||
<div class="info-title">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
ref="SearchInputRef"
|
||||
size="large"
|
||||
placeholder="输入关键字"
|
||||
class="search-input"
|
||||
@input="handleInput"
|
||||
@keyup.enter="search">
|
||||
<template #prefix>
|
||||
<el-icon size="25" @click="search"><Search /></el-icon>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<el-tooltip effect="light" placement="top" :show-after="0" :hide-after="0" :auto-close="2000" :offset="3">
|
||||
<template #content>
|
||||
<div>匹配全部关键字</div>
|
||||
<div class="keyboard">{{ keymaps.fullSearchOperatorAnd }}</div>
|
||||
</template>
|
||||
<div :class="['iconbl bl-and', isOperator ? 'and' : 'or']" @click="handleOperator"></div>
|
||||
</el-tooltip>
|
||||
<el-icon class="clear-btn" size="25" @click="clear"><CircleClose /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="results" ref="ResultsRef">
|
||||
<div v-if="noResult" class="placeholder">
|
||||
<div>未找到相关结果 "{{ searchKeyword }}"</div>
|
||||
<div class="iconbl bl-wuneirong"></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-for="(article, index) in result"
|
||||
:id="'result-item' + article.id"
|
||||
:key="article.id"
|
||||
:class="['result-item', article.id === currentId ? 'current' : '']"
|
||||
@click="openArticle"
|
||||
@mouseenter="hover(article)">
|
||||
<div class="index">{{ index + 1 }}</div>
|
||||
<bl-row just="space-between" class="infos">
|
||||
<div class="name" v-html="article.name"></div>
|
||||
<bl-row class="tags" height="100%">
|
||||
<bl-tag v-for="tag in article.tags" :key="tag"><span v-html="tag"></span></bl-tag>
|
||||
</bl-row>
|
||||
</bl-row>
|
||||
<div v-if="isNotBlank(article.markdown)" class="markdown" v-html="article.markdown"></div>
|
||||
<div class="workbench">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-correlation-line" @click.stop="copyUrlLink(article)"></div>
|
||||
<div class="iconbl bl-a-computerend-line" @click.stop="openArticleWindow(article)"></div>
|
||||
</bl-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-footer">
|
||||
<bl-row class="keys" just="space-between">
|
||||
<bl-row width="250px">
|
||||
<kbd class="keyboard small iconbl bl-enter"></kbd>
|
||||
<div>打开</div>
|
||||
<kbd class="keyboard small">↑</kbd>
|
||||
<kbd class="keyboard small">↓</kbd>
|
||||
<div>切换</div>
|
||||
<kbd class="keyboard small">ESC</kbd>
|
||||
<div>关闭</div>
|
||||
</bl-row>
|
||||
<div class="totalhit">搜索到 {{ totalHit }} 项结果</div>
|
||||
</bl-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { Search, CircleClose } from '@element-plus/icons-vue'
|
||||
import { articleSearchApi } from '@renderer/api/blossom'
|
||||
import { isBlank, isNotBlank, isNull } from '@renderer/assets/utils/obj'
|
||||
import { getPrimaryColor } from '@renderer/scripts/global-theme'
|
||||
import { rgbaToHex, rgbToHex } from '@renderer/assets/utils/color'
|
||||
import { openNewArticleWindow } from '@renderer/assets/utils/electron'
|
||||
import { keymaps } from './scripts/editor-tools'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
document.addEventListener('mousemove', enabledMouse)
|
||||
bindKeys()
|
||||
nextTick(() => {
|
||||
SearchInputRef.value.focus()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('mousemove', enabledMouse)
|
||||
unbindKeys()
|
||||
})
|
||||
|
||||
type Article = { id: string; name: string; originName: string; markdown: string; tags: string[]; currnet: boolean }
|
||||
const SearchInputRef = ref()
|
||||
const searchKeyword = ref('')
|
||||
const ResultsRef = ref<HTMLElement>()
|
||||
const result = ref<Article[]>([])
|
||||
const totalHit = ref(0)
|
||||
const isOperator = ref(false)
|
||||
const noResult = ref(false)
|
||||
const currentId = ref('')
|
||||
|
||||
/**
|
||||
* 输入后清空
|
||||
*/
|
||||
const handleInput = () => {
|
||||
noResult.value = false
|
||||
currentId.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换搜索 operator
|
||||
*/
|
||||
const handleOperator = () => {
|
||||
isOperator.value = !isOperator.value
|
||||
search()
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索文章
|
||||
*/
|
||||
const search = () => {
|
||||
articleSearchApi({ keyword: searchKeyword.value, hlColor: getColor(), debug: false, operator: isOperator.value }).then((resp) => {
|
||||
result.value = resp.data.hits
|
||||
totalHit.value = resp.data.totalHit
|
||||
if (isNull(resp.data.hits)) {
|
||||
noResult.value = true
|
||||
} else {
|
||||
noResult.value = false
|
||||
}
|
||||
SearchInputRef.value.blur()
|
||||
ResultsRef.value!.focus()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 将颜色转换为 16 进制, 用于后台分隔 tag
|
||||
*/
|
||||
const getColor = () => {
|
||||
let color = getPrimaryColor().color.trim()
|
||||
if (isBlank(color)) {
|
||||
return '#E3A300'
|
||||
}
|
||||
if (color.startsWith('rgba(')) {
|
||||
return rgbaToHex(getPrimaryColor().color)
|
||||
}
|
||||
if (color.startsWith('rgb(')) {
|
||||
return rgbToHex(getPrimaryColor().color)
|
||||
}
|
||||
if (color.startsWith('#')) {
|
||||
return color
|
||||
}
|
||||
return '#E3A300'
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
searchKeyword.value = ''
|
||||
result.value = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开搜索的文章
|
||||
*/
|
||||
const openArticle = () => {
|
||||
let curArticle = result.value.find((item) => item.id === currentId.value)
|
||||
if (isNull(curArticle)) {
|
||||
return
|
||||
}
|
||||
let tree: DocTree = {
|
||||
i: curArticle!.id,
|
||||
p: '0',
|
||||
n: curArticle!.originName,
|
||||
o: 0,
|
||||
t: [],
|
||||
s: 0,
|
||||
icon: '',
|
||||
ty: 3,
|
||||
star: 0
|
||||
}
|
||||
emits('openArticle', tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制搜索文章的双链链接
|
||||
* @param article 文章信息
|
||||
*/
|
||||
const copyUrlLink = (article: Article) => {
|
||||
emits('createLink', article.originName, article.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 新窗口中打开搜索的文章
|
||||
* @param article
|
||||
*/
|
||||
const openArticleWindow = (article: Article) => {
|
||||
openNewArticleWindow(article.name, article.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录当前选中的文章
|
||||
* @param id
|
||||
*/
|
||||
const setCurrentId = (id: string) => {
|
||||
currentId.value = id
|
||||
}
|
||||
|
||||
const emits = defineEmits(['openArticle', 'createLink'])
|
||||
|
||||
//#region
|
||||
|
||||
//#region ----------------------------------------< 键盘选择 >--------------------------------------
|
||||
// 禁用鼠标
|
||||
let disabledMouse = false
|
||||
|
||||
/**
|
||||
* 鼠标移入的事件
|
||||
* @param article
|
||||
*/
|
||||
const hover = (article: Article) => {
|
||||
if (disabledMouse) {
|
||||
return
|
||||
}
|
||||
article.currnet = true
|
||||
setCurrentId(article.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听键盘, 监听 ↑|↓|Enter 键
|
||||
* @param event
|
||||
*/
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
let keyCode = event.code
|
||||
if (keyCode === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
down()
|
||||
} else if (keyCode === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
up()
|
||||
} else if (keyCode === 'Enter') {
|
||||
let target = event.target
|
||||
if (target && (target as HTMLElement).nodeName !== 'INPUT' && isNotBlank(currentId.value)) {
|
||||
event.preventDefault()
|
||||
openArticle()
|
||||
}
|
||||
} else {
|
||||
SearchInputRef.value.focus()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择上一个
|
||||
*/
|
||||
const up = () => {
|
||||
if (isNull(result.value)) {
|
||||
return
|
||||
}
|
||||
disabledMouse = true
|
||||
let lenght = result.value.length
|
||||
for (let i = 0; i < lenght; i++) {
|
||||
const article = result.value[i]
|
||||
if (article.id === currentId.value && i > 0) {
|
||||
setCurrentId(result.value[i - 1].id)
|
||||
scrollIntoView(currentId.value, 'up')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择下一个
|
||||
*/
|
||||
const down = () => {
|
||||
if (isNull(result.value)) {
|
||||
return
|
||||
}
|
||||
disabledMouse = true
|
||||
if (isBlank(currentId.value)) {
|
||||
currentId.value = result.value[0].id
|
||||
return
|
||||
}
|
||||
let lenght = result.value.length
|
||||
for (let i = 0; i < lenght; i++) {
|
||||
const article = result.value[i]
|
||||
if (article.id === currentId.value && i < lenght - 1) {
|
||||
setCurrentId(result.value[i + 1].id)
|
||||
scrollIntoView(currentId.value, 'down')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示选中元素
|
||||
* @param id 元素ID
|
||||
* @param type 选中的是上一个, 或是下一个
|
||||
*/
|
||||
const scrollIntoView = (id: string, type: 'up' | 'down') => {
|
||||
let item = document.getElementById('result-item' + id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
if (type === 'up') {
|
||||
// console.log(ResultsRef.value?.scrollTop)
|
||||
// console.log(item.offsetTop)
|
||||
let top = item.offsetTop - ResultsRef.value!.scrollTop
|
||||
if (top <= 90) {
|
||||
item.scrollIntoView({ block: 'start' })
|
||||
ResultsRef.value?.scrollTo({ top: ResultsRef.value?.scrollTop - 10 })
|
||||
}
|
||||
} else {
|
||||
// console.log(ResultsRef.value?.scrollTop)
|
||||
// console.log(item.offsetTop)
|
||||
let top = item.offsetTop - ResultsRef.value!.scrollTop
|
||||
if (top >= 500) {
|
||||
item.scrollIntoView({ block: 'end' })
|
||||
ResultsRef.value?.scrollTo({ top: ResultsRef.value?.scrollTop + 10 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const enabledMouse = () => {
|
||||
disabledMouse = false
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region
|
||||
const bindKeys = () => {
|
||||
hotkeys('alt+g, command+g', () => {
|
||||
handleOperator()
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const unbindKeys = () => {
|
||||
hotkeys.unbind('alt+g, command+g')
|
||||
}
|
||||
//#endregion
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@renderer/assets/styles/bl-dialog-info';
|
||||
|
||||
.article-search-root {
|
||||
border-radius: 10px;
|
||||
@include box(100%, 100%);
|
||||
|
||||
.info-title {
|
||||
height: 60px;
|
||||
padding: 0;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.content {
|
||||
@include box(100%, calc(100% - 110px));
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
--el-input-bg-color: #00000000;
|
||||
.bl-and {
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.bl-and.and {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.clear-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper.is-focus) {
|
||||
box-shadow: none;
|
||||
}
|
||||
:deep(.el-input__inner) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: 0 16px 0 20px;
|
||||
padding-bottom: 0;
|
||||
padding-top: 10px;
|
||||
.placeholder {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: var(--bl-text-color-light);
|
||||
|
||||
.iconbl {
|
||||
@include themeColor(#f1f1f1, #272727);
|
||||
margin-top: 80px;
|
||||
font-size: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-item {
|
||||
@include themeShadow(0 0 2px #d4d4d4, 0 0 3px #000000);
|
||||
max-height: 100px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bl-html-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
.index {
|
||||
@include font(11px, 100);
|
||||
width: 13px;
|
||||
color: var(--bl-text-color-light);
|
||||
font-style: italic;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: -20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.infos {
|
||||
height: 26px;
|
||||
padding: 0 4px;
|
||||
.name {
|
||||
@include ellipsis();
|
||||
color: var(--el-color-primary);
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
.tags {
|
||||
@include ellipsis();
|
||||
width: auto !important;
|
||||
max-width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
height: 69px;
|
||||
font-size: 12px;
|
||||
padding: 0 3px 2px 3px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.workbench {
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
background-color: var(--bl-bg-color);
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
|
||||
.iconbl {
|
||||
padding: 0 5px;
|
||||
line-height: 20px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-item.current {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
|
||||
.workbench {
|
||||
border-top: 1px solid var(--el-color-primary);
|
||||
border-left: 1px solid var(--el-color-primary);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.keys {
|
||||
@include font(13px, 100);
|
||||
|
||||
kbd {
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.totalhit {
|
||||
@include font(13px, 100);
|
||||
color: var(--bl-text-color-light);
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
height: 21px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<!-- 文件夹操作 -->
|
||||
<div class="doc-workbench">
|
||||
<ArticleTreeWorkbench @refresh-doc-tree="getDocTree" @show-sort="handleShowSort" ref="ArticleTreeWorkbenchRef"> </ArticleTreeWorkbench>
|
||||
<ArticleTreeWorkbench
|
||||
@refresh-doc-tree="getDocTree"
|
||||
@show-sort="handleShowSort"
|
||||
@show-search="handleShowArticleSearchDialog"
|
||||
ref="ArticleTreeWorkbenchRef">
|
||||
</ArticleTreeWorkbench>
|
||||
</div>
|
||||
<!-- -->
|
||||
<div
|
||||
@ -184,7 +189,14 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 二维码 -->
|
||||
<el-dialog v-model="isShowQrCodeDialog" width="335" :append-to-body="true" :destroy-on-close="true" :close-on-click-modal="false" draggable>
|
||||
<el-dialog
|
||||
v-model="isShowQrCodeDialog"
|
||||
width="335"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
draggable>
|
||||
<ArticleQrCode ref="ArticleQrCodeRef"></ArticleQrCode>
|
||||
</el-dialog>
|
||||
|
||||
@ -199,6 +211,19 @@
|
||||
draggable>
|
||||
<ArticleImport ref="ArticleImportRef" :doc="curDoc"></ArticleImport>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<el-dialog
|
||||
v-model="isShowArticleSearchDialog"
|
||||
class="bl-dialog-hidden-header-fixed-body"
|
||||
width="705"
|
||||
style="height: 80%"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true">
|
||||
<ArticleSearch @open-article="openArticle" @create-link="createUrlLink"></ArticleSearch>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -224,6 +249,7 @@ import {
|
||||
import { isNotNull } from '@renderer/assets/utils/obj'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { checkLevel, provideKeyDocTree } from '@renderer/views/doc/doc'
|
||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||
import { grammar } from './scripts/markedjs'
|
||||
import {
|
||||
folderDelApi,
|
||||
@ -243,7 +269,7 @@ import ArticleTreeWorkbench from './ArticleTreeWorkbench.vue'
|
||||
import ArticleQrCode from './ArticleQrCode.vue'
|
||||
import ArticleInfo from './ArticleInfo.vue'
|
||||
import ArticleImport from './ArticleImport.vue'
|
||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||
import ArticleSearch from './ArticleSearch.vue'
|
||||
|
||||
const server = useServerStore()
|
||||
const user = useUserStore()
|
||||
@ -319,6 +345,10 @@ const getDocTreeData = (): DocTree[] => {
|
||||
return docTreeData.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单中选中文章
|
||||
* @param index 文章ID
|
||||
*/
|
||||
const openMenu = (index: string) => {
|
||||
docTreeActiveArticleId.value = index
|
||||
}
|
||||
@ -442,8 +472,7 @@ const createUrl = (type: 'open' | 'copy' | 'link' | 'tempVisit', open: boolean =
|
||||
} else if (type === 'copy') {
|
||||
writeText(url)
|
||||
} else if (type === 'link') {
|
||||
url = `[${curDoc.value.n}](${user.userParams.WEB_ARTICLE_URL + curDoc.value.i} "${grammar}${curDoc.value.i}${grammar}")`
|
||||
writeText(url)
|
||||
createUrlLink(curDoc.value.n, curDoc.value.i)
|
||||
} else if (type === 'tempVisit') {
|
||||
articleTempKey({ id: curDoc.value.i }).then((resp) => {
|
||||
url = server.serverUrl + articleTempH + resp.data
|
||||
@ -455,6 +484,16 @@ const createUrl = (type: 'open' | 'copy' | 'link' | 'tempVisit', open: boolean =
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制双链引用
|
||||
* @param name : 文章名称
|
||||
* @param id : 文章ID
|
||||
*/
|
||||
const createUrlLink = (name: string, id: string) => {
|
||||
let url = `[${name}](${user.userParams.WEB_ARTICLE_URL + id} "${grammar}${id}${grammar}")`
|
||||
writeText(url)
|
||||
}
|
||||
|
||||
/** 下载文章 */
|
||||
const articleDownload = () => {
|
||||
articleDownloadApi({ id: curDoc.value.i }).then((resp) => {
|
||||
@ -723,7 +762,6 @@ const savedCallback = (_dialogType: DocDialogType) => {
|
||||
//#endregion
|
||||
|
||||
//#region ----------------------------------------< 导入文章 >--------------------------------------
|
||||
|
||||
const ArticleImportRef = ref()
|
||||
const isShowArticleImportDialog = ref<boolean>(false)
|
||||
|
||||
@ -736,6 +774,23 @@ const handleShowArticleImportDialog = () => {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region ----------------------------------------< 搜索文章 >--------------------------------------
|
||||
const isShowArticleSearchDialog = ref<boolean>(false)
|
||||
|
||||
const handleShowArticleSearchDialog = () => {
|
||||
isShowArticleSearchDialog.value = true
|
||||
}
|
||||
|
||||
const openArticle = (article: DocTree) => {
|
||||
openMenu(article.i)
|
||||
emits('clickDoc', article)
|
||||
isShowArticleSearchDialog.value = false
|
||||
nextTick(() => {
|
||||
docTreeActiveArticleId.value = article.i
|
||||
})
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const clickCurDoc = (tree: DocTree) => {
|
||||
emits('clickDoc', tree)
|
||||
}
|
||||
|
||||
@ -12,15 +12,40 @@
|
||||
<template #content>
|
||||
显示排序<br />
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="TitleColor.ONE">一级</bl-tag>
|
||||
<bl-tag :bgColor="TitleColor.TWO">二级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.ONE">一级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.TWO">二级</bl-tag>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="TitleColor.THREE">三级</bl-tag>
|
||||
<bl-tag :bgColor="TitleColor.FOUR">四级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.THREE">三级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.FOUR">四级</bl-tag>
|
||||
</bl-row>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<template #content>
|
||||
<div>全文搜索</div>
|
||||
<div class="keyboard small">Ctrl+Shift+F</div>
|
||||
</template>
|
||||
<div class="iconbl bl-search-line" @click="showSearch()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-a-cloudrefresh-line" @click="refreshDocTree()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<template #content>
|
||||
<div>新增文件夹或文章</div>
|
||||
<div class="keyboard small">Ctrl + N</div>
|
||||
</template>
|
||||
<div class="iconbl bl-a-fileadd-line" @click="handleShowAddDocInfoDialog()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="文章引用网络" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-correlation-line" @click="openArticleReferenceWindow()"></div>
|
||||
</el-tooltip>
|
||||
</bl-row>
|
||||
</Transition>
|
||||
|
||||
<Transition name="wbpage-two">
|
||||
<bl-row class="wb-page-item" just="flex-end" align="flex-end" v-if="workbenchPage == 2">
|
||||
<el-tooltip content="只显示公开" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div v-if="props.showOpen">
|
||||
<div v-if="onlyOpen" class="iconbl bl-cloud-fill" @click="changeOnlyOpen()"></div>
|
||||
@ -39,20 +64,6 @@
|
||||
<div v-else class="iconbl bl-star-line" @click="changeOnlyStar()"></div>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新列表" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-a-cloudrefresh-line" @click="refreshDocTree()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新增文件夹或文档" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-a-fileadd-line" @click="handleShowAddDocInfoDialog()"></div>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="文章引用网络" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-correlation-line" @click="openArticleReferenceWindow()"></div>
|
||||
</el-tooltip>
|
||||
</bl-row>
|
||||
</Transition>
|
||||
|
||||
<Transition name="wbpage-two">
|
||||
<bl-row class="wb-page-item" just="flex-end" align="flex-end" v-if="workbenchPage == 2">
|
||||
<el-tooltip content="查看回收站" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
|
||||
<div class="iconbl bl-delete-line" @click="handleShowRecycleDialog"></div>
|
||||
</el-tooltip>
|
||||
@ -86,7 +97,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
class="backup-dialog"
|
||||
class="bl-dialog-fixed-body"
|
||||
v-model="isShowBackupDialog"
|
||||
width="80%"
|
||||
style="height: 80%"
|
||||
@ -99,7 +110,7 @@
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
class="backup-dialog"
|
||||
class="bl-dialog-fixed-body"
|
||||
v-model="isShowRecycleDialog"
|
||||
width="80%"
|
||||
style="height: 80%"
|
||||
@ -113,18 +124,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, inject } from 'vue'
|
||||
import { ref, nextTick, inject, onDeactivated } from 'vue'
|
||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { provideKeyDocInfo, TitleColor } from '@renderer/views/doc/doc'
|
||||
import { provideKeyDocInfo, SortLevelColor } from '@renderer/views/doc/doc'
|
||||
import { openNewArticleReferenceWindow } from '@renderer/assets/utils/electron'
|
||||
import { useConfigStore } from '@renderer/stores/config'
|
||||
import ArticleInfo from './ArticleInfo.vue'
|
||||
import ArticleBackup from './ArticleBackup.vue'
|
||||
import ArticleRecycle from './ArticleRecycle.vue'
|
||||
import { useLifecycle } from '@renderer/scripts/lifecycle'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const { viewStyle } = configStore
|
||||
|
||||
useLifecycle(
|
||||
() => bindKeys(),
|
||||
() => bindKeys()
|
||||
)
|
||||
|
||||
onDeactivated(() => {
|
||||
unbindKeys()
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
showOpen: {
|
||||
type: Boolean,
|
||||
@ -140,7 +162,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
//#region 控制台翻页
|
||||
//#region --------------------------------------------------< 控制台翻页 >--------------------------------------------------
|
||||
|
||||
const workbenchPage = ref(1)
|
||||
|
||||
@ -150,7 +172,7 @@ const toWorkbenchPage = (page: number) => {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 查询
|
||||
//#region --------------------------------------------------< 查询 >--------------------------------------------------
|
||||
const curDoc = inject(provideKeyDocInfo)
|
||||
|
||||
const onlyOpen = ref<boolean>(false) // 只显示公开
|
||||
@ -180,7 +202,7 @@ const changeOnlyStar = () => {
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 新增窗口
|
||||
//#region --------------------------------------------------< 新增窗口 >--------------------------------------------------
|
||||
const ArticleInfoRef = ref()
|
||||
const isShowDocInfoDialog = ref<boolean>(false)
|
||||
|
||||
@ -212,9 +234,13 @@ const savedCallback = () => {
|
||||
emits('refreshDocTree', onlyOpen.value, onlySubject.value, onlyStars.value)
|
||||
}
|
||||
|
||||
const showSearch = () => {
|
||||
emits('show-search')
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 备份记录
|
||||
//#region --------------------------------------------------< 备份记录 >--------------------------------------------------
|
||||
const ArticleBackupRef = ref()
|
||||
const isShowBackupDialog = ref<boolean>(false)
|
||||
|
||||
@ -223,7 +249,7 @@ const handleShowBackupDialog = () => {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region 备份记录
|
||||
//#region --------------------------------------------------< 备份记录 >--------------------------------------------------
|
||||
const ArticleRecycleRef = ref()
|
||||
const isShowRecycleDialog = ref<boolean>(false)
|
||||
|
||||
@ -232,7 +258,25 @@ const handleShowRecycleDialog = () => {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const emits = defineEmits(['refreshDocTree', 'show-sort'])
|
||||
//#region --------------------------------------------------< 绑定快捷键 >--------------------------------------------------
|
||||
const bindKeys = () => {
|
||||
hotkeys('ctrl+shift+f, ctrl+shift+f', () => {
|
||||
showSearch()
|
||||
return false
|
||||
})
|
||||
hotkeys('ctrl+n, ctrl+n', () => {
|
||||
handleShowAddDocInfoDialog()
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const unbindKeys = () => {
|
||||
hotkeys.unbind('ctrl+shift+f, ctrl+shift+f')
|
||||
hotkeys.unbind('ctrl+n, ctrl+n')
|
||||
}
|
||||
|
||||
//#endregion
|
||||
const emits = defineEmits(['refreshDocTree', 'show-sort', 'show-search'])
|
||||
defineExpose({ handleShowBackupDialog })
|
||||
</script>
|
||||
|
||||
@ -270,11 +314,3 @@ defineExpose({ handleShowBackupDialog })
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.backup-dialog {
|
||||
.el-dialog__body {
|
||||
height: calc(100% - 10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -47,235 +47,7 @@
|
||||
|
||||
<!-- 其他工具 -->
|
||||
<div class="divider"></div>
|
||||
<el-tooltip placement="top" effect="light" :hide-after="0" trigger="click">
|
||||
<template #content>
|
||||
<div class="editor-tools-content">
|
||||
<bl-row>
|
||||
<div class="info-title">编辑器工具栏</div>
|
||||
</bl-row>
|
||||
<bl-row align="flex-start">
|
||||
<bl-col width="230px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-texteditorsave-line"></div>
|
||||
<div class="label">保存内容</div>
|
||||
<div class="keyboard">{{ keymaps.save }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-eyeclose-line"></div>
|
||||
<div class="label">隐藏菜单</div>
|
||||
<div class="keyboard">{{ keymaps.hideDocs }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-eyeclose-line"></div>
|
||||
<div class="label">隐藏目录</div>
|
||||
<div class="keyboard">{{ keymaps.hideToc }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-eye-line"></div>
|
||||
<div class="label">全屏预览</div>
|
||||
<div class="keyboard">{{ keymaps.fullViewer }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-expansion-line"></div>
|
||||
<div class="label">全屏编辑</div>
|
||||
<div class="keyboard">{{ keymaps.fullEditor }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-transcript-fill"></div>
|
||||
<div class="label">格式化</div>
|
||||
<div class="keyboard">{{ keymaps.formatAll }}</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
<bl-col width="230px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-bold"></div>
|
||||
<div class="label">加粗</div>
|
||||
<div class="keyboard">{{ keymaps.blod }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-italic"></div>
|
||||
<div class="label">斜体</div>
|
||||
<div class="keyboard">{{ keymaps.italic }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-strikethrough"></div>
|
||||
<div class="label">删除</div>
|
||||
<div class="keyboard">Alt + S</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-texteditorsuperscript-line"></div>
|
||||
<div class="label">上标</div>
|
||||
<div class="keyboard">{{ keymaps.sup }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-texteditorsubscript-line"></div>
|
||||
<div class="label">下标</div>
|
||||
<div class="keyboard">{{ keymaps.sub }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-separator"></div>
|
||||
<div class="label">分割线</div>
|
||||
<div class="keyboard">{{ keymaps.separator }}</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
<!-- -->
|
||||
<bl-col width="230px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-rightsmallline-line"></div>
|
||||
<div class="label">引用</div>
|
||||
<div class="keyboard">></div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-single-quotes-r"></div>
|
||||
<div class="label">行内代码</div>
|
||||
<div class="keyboard">{{ keymaps.code }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-double-quotes-r"></div>
|
||||
<div class="label">多行代码</div>
|
||||
<div class="keyboard">{{ keymaps.pre }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-underbox-line"></div>
|
||||
<div class="label">单选框</div>
|
||||
<div class="keyboard">- []</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-list-unordered"></div>
|
||||
<div class="label">无序列表</div>
|
||||
<div class="keyboard">-</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-list-ordered"></div>
|
||||
<div class="label">有序列表</div>
|
||||
<div class="keyboard">1.</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
|
||||
<bl-col>
|
||||
<!-- -->
|
||||
<el-divider style="margin: 5px 0; border: 0"></el-divider>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-table-"></div>
|
||||
<div class="label">插入表格</div>
|
||||
<div class="keyboard">{{ keymaps.table }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-image--line"></div>
|
||||
<div class="label">添加图片</div>
|
||||
<div class="keyboard">{{ keymaps.image }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-link-m"></div>
|
||||
<div class="label">添加链接</div>
|
||||
<div class="keyboard">{{ keymaps.link }}</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-jianpan-xianxing"></div>
|
||||
<div class="label">按键说明</div>
|
||||
<div class="keyboard">暂无</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-fanqiezhong"></div>
|
||||
<div class="label">番茄时钟</div>
|
||||
<div class="keyboard">暂无</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
</bl-row>
|
||||
|
||||
<el-divider style="margin: 5px 0; border: 0"></el-divider>
|
||||
<bl-row>
|
||||
<div class="info-title">编辑器快捷键</div>
|
||||
</bl-row>
|
||||
<bl-row align="flex-start">
|
||||
<bl-col width="190px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-icon_jiandaojianqie"></div>
|
||||
<div class="label">剪切整行</div>
|
||||
<div class="keyboard">Ctrl + X</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-texteditorback-line"></div>
|
||||
<div class="label">撤销</div>
|
||||
<div class="keyboard">Ctrl + Z</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-texteditorforward-line"></div>
|
||||
<div class="label">恢复</div>
|
||||
<div class="keyboard">Ctrl + Y</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-search-line"></div>
|
||||
<div class="label">查找</div>
|
||||
<div class="keyboard">Ctrl + F</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-switch-line"></div>
|
||||
<div class="label">替换</div>
|
||||
<div class="keyboard">Ctrl + G</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
<bl-col width="190px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-problem-line"></div>
|
||||
<div class="label">注释</div>
|
||||
<div class="keyboard">Ctrl + /</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-indent-decrease"></div>
|
||||
<div class="label">向前缩进</div>
|
||||
<div class="keyboard">Ctrl + [</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-indent-increase"></div>
|
||||
<div class="label">向后缩进</div>
|
||||
<div class="keyboard">Ctrl + ]</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
<bl-col width="190px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-listview-line"></div>
|
||||
<div class="label">列模式</div>
|
||||
<div class="keyboard">Alt</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-radiochoose-line"></div>
|
||||
<div class="label">选中该行</div>
|
||||
<div class="keyboard">Alt + L</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-rightto-line"></div>
|
||||
<div class="label">前往行数</div>
|
||||
<div class="keyboard">Alt + G</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-online-line"></div>
|
||||
<div class="label">行上移</div>
|
||||
<div class="keyboard">Alt + ↑</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-underline-line"></div>
|
||||
<div class="label">行下移</div>
|
||||
<div class="keyboard">Alt + ↓</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
<bl-col width="230px">
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-doubleonline-line"></div>
|
||||
<div class="label">上方复制</div>
|
||||
<div class="keyboard">Shift + Alt + ↑</div>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<div class="iconbl bl-a-doubleunderline-line"></div>
|
||||
<div class="label">下方复制</div>
|
||||
<div class="keyboard">Shift + Alt + ↓</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
</bl-row>
|
||||
</div>
|
||||
</template>
|
||||
<div class="iconbl bl-jianpan-xianxing"></div>
|
||||
</el-tooltip>
|
||||
<div class="iconbl bl-jianpan-xianxing" @click="handleShowHotKeyDialog"></div>
|
||||
|
||||
<!-- 番茄 -->
|
||||
<el-popover placement="bottom" :width="220" trigger="click" popper-style="padding:0;">
|
||||
@ -303,13 +75,25 @@
|
||||
<div style="font-size: 12px; padding: 4px 5px">{{ remainStr }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入 -->
|
||||
<el-dialog
|
||||
v-model="isShowHotKeyDialog"
|
||||
class="bl-dialog-fixed-body"
|
||||
width="500"
|
||||
style="height: 80%"
|
||||
:align-center="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true">
|
||||
<ShortcutKeyDesc></ShortcutKeyDesc>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { secondsToDatetime, formateMs } from '@renderer/assets/utils/util'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { keymaps } from './scripts/editor-tools'
|
||||
import ShortcutKeyDesc from './ShortcutkeyDesc.vue'
|
||||
|
||||
const emits = defineEmits([
|
||||
'save',
|
||||
@ -393,6 +177,12 @@ const stop = () => {
|
||||
TomatoBellRef.value.style.transform = `translateX(100%)`
|
||||
}
|
||||
}
|
||||
|
||||
const isShowHotKeyDialog = ref<boolean>(false)
|
||||
|
||||
const handleShowHotKeyDialog = () => {
|
||||
isShowHotKeyDialog.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -450,7 +240,7 @@ const stop = () => {
|
||||
<!--
|
||||
快捷键说明为弹出框 需要设置全局的样式
|
||||
-->
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.editor-tools-content {
|
||||
@include flex(column, flex-start, flex-start);
|
||||
color: var(--bl-text-color);
|
||||
|
||||
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="key-desc-root">
|
||||
<div class="info-title">
|
||||
<div class="iconbl bl-jianpan-xianxing"></div>
|
||||
文章编辑快捷键
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="kind">页面快捷键</div>
|
||||
<bl-row v-for="key in keymapsView" :key="key.name">
|
||||
<div>
|
||||
<span :class="['iconbl', key.icon]"></span>
|
||||
<span class="label">{{ key.name }}</span>
|
||||
</div>
|
||||
<div class="keyboard">{{ key.key }}</div>
|
||||
</bl-row>
|
||||
|
||||
<div class="kind">工具栏快捷键</div>
|
||||
<bl-row v-for="key in keymapsTool" :key="key.name">
|
||||
<div>
|
||||
<span :class="['iconbl', key.icon]"></span>
|
||||
<span class="label">{{ key.name }}</span>
|
||||
</div>
|
||||
<div class="keyboard">{{ key.key }}</div>
|
||||
</bl-row>
|
||||
|
||||
<div class="kind">编辑器快捷键</div>
|
||||
<bl-row v-for="key in keymapsEditor" :key="key.name">
|
||||
<div>
|
||||
<span :class="['iconbl', key.icon]"></span>
|
||||
<span class="label">{{ key.name }}</span>
|
||||
</div>
|
||||
<div class="keyboard">{{ key.key }}</div>
|
||||
</bl-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { keymaps } from './scripts/editor-tools'
|
||||
|
||||
const keymapsView = [
|
||||
{ icon: 'bl-a-eyeclose-line', name: '隐藏左侧菜单', key: keymaps.hideDocs },
|
||||
{ icon: 'bl-a-filetext-line', name: '折叠浮动目录', key: keymaps.hideToc },
|
||||
{ icon: 'bl-search-line', name: '全文搜索', key: keymaps.fullSearch },
|
||||
{ icon: 'bl-and', name: '全文搜索时匹配所有关键字', key: keymaps.fullSearchOperatorAnd },
|
||||
{ icon: 'bl-transcript-line', name: '格式化 Markdown 文章', key: keymaps.formatAll },
|
||||
{ icon: 'bl-a-fileadd-line', name: '打开新增文件夹或文章窗口', key: 'Ctrl + N' }
|
||||
]
|
||||
|
||||
const keymapsTool = [
|
||||
{ icon: 'bl-a-texteditorsave-line', name: '保存内容', key: keymaps.save },
|
||||
{ icon: 'bl-eye-line', name: '全屏预览', key: keymaps.fullViewer },
|
||||
{ icon: 'bl-expansion-line', name: '全屏编辑', key: keymaps.fullEditor },
|
||||
|
||||
{ icon: 'bl-bold', name: '加粗', key: keymaps.blod },
|
||||
{ icon: 'bl-italic', name: '斜体', key: keymaps.italic },
|
||||
{ icon: 'bl-strikethrough', name: '删除线', key: 'Alt + S' },
|
||||
{ icon: 'bl-a-texteditorsuperscript-line', name: '上标(sup)', key: keymaps.sup },
|
||||
{ icon: 'bl-a-texteditorsubscript-line', name: '下标(sub)', key: keymaps.sub },
|
||||
{ icon: 'bl-separator', name: '分割线', key: keymaps.separator },
|
||||
|
||||
{ icon: 'bl-a-rightsmallline-line', name: '引用', key: '>' },
|
||||
{ icon: 'bl-single-quotes-r', name: '行内代码', key: keymaps.code },
|
||||
{ icon: 'bl-double-quotes-r', name: '多行代码', key: keymaps.pre },
|
||||
{ icon: 'bl-a-underbox-line', name: '单选框', key: '- []' },
|
||||
{ icon: 'bl-list-unordered', name: '无序列表', key: '-' },
|
||||
{ icon: 'bl-list-ordered', name: '有序列表', key: '1.' },
|
||||
|
||||
{ icon: 'bl-table-', name: '插入表格', key: keymaps.table },
|
||||
{ icon: 'bl-image--line', name: '添加图片,也可拖入或黏贴', key: keymaps.image },
|
||||
{ icon: 'bl-link-m', name: '添加链接', key: keymaps.link }
|
||||
]
|
||||
|
||||
const keymapsEditor = [
|
||||
{ icon: 'bl-a-listview-line', name: '列模式', key: 'Alt' },
|
||||
{ icon: 'bl-search-line', name: '光标聚焦多行', key: 'Ctrl' },
|
||||
{ icon: 'bl-search-line', name: '查找', key: 'Ctrl + F' },
|
||||
{ icon: 'bl-switch-line', name: '替换', key: 'Ctrl + G' },
|
||||
{ icon: 'bl-a-icon_jiandaojianqie', name: '剪切整行', key: 'Ctrl + X' },
|
||||
{ icon: 'bl-a-texteditorforward-line', name: '恢复', key: 'Ctrl + Y' },
|
||||
{ icon: 'bl-a-texteditorback-line', name: '撤销', key: 'Ctrl + Z' },
|
||||
|
||||
{ icon: 'bl-transcript-line', name: '注释', key: 'Ctrl + /' },
|
||||
{ icon: 'bl-indent-decrease', name: '向前缩进', key: 'Ctrl + [' },
|
||||
{ icon: 'bl-indent-increase', name: '向后缩进', key: 'Ctrl + ]' },
|
||||
|
||||
{ icon: 'bl-a-radiochoose-line', name: '选中该行', key: 'Alt + L' },
|
||||
{ icon: 'bl-a-rightto-line', name: '前往行数', key: 'Alt + G' },
|
||||
{ icon: 'bl-a-online-line', name: '行上移', key: 'Alt + ↑' },
|
||||
{ icon: 'bl-a-underline-line', name: '行下移', key: 'Alt + ↓' },
|
||||
{ icon: 'bl-a-doubleonline-line', name: '复制到上方', key: 'Shift + Alt + ↑' },
|
||||
{ icon: 'bl-a-doubleunderline-line', name: '复制到下方', key: 'Shift + Alt + ↓' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@renderer/assets/styles/bl-dialog-info';
|
||||
|
||||
.key-desc-root {
|
||||
@include box(100%, 100%);
|
||||
border-radius: 10px;
|
||||
|
||||
.content {
|
||||
height: calc(100% - 60px);
|
||||
padding: 0 10px;
|
||||
overflow: scroll;
|
||||
font-weight: 300;
|
||||
|
||||
.kind {
|
||||
font-size: 16px;
|
||||
margin: 10px 0;
|
||||
color: var(--el-color-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bl-row-root {
|
||||
justify-content: space-between !important;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
|
||||
&:hover {
|
||||
@include themeBg(#ececec, #171717);
|
||||
}
|
||||
}
|
||||
|
||||
.iconbl {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -9,6 +9,8 @@ export const keymaps = {
|
||||
fullViewer: isMac ? 'Cmd + 3' : 'Alt + 3',
|
||||
fullEditor: isMac ? 'Cmd + 4' : 'Alt + 4',
|
||||
formatAll: isMac ? 'Slift + Cmd + F' : 'Slift + Alt + F',
|
||||
fullSearch: isMac ? 'Ctrl + Shift + F' : 'Ctrl + Shift + F',
|
||||
fullSearchOperatorAnd: isMac ? 'Cmd + G' : 'Alt + G',
|
||||
|
||||
blod: isMac ? 'Cmd + B' : 'Alt + B',
|
||||
italic: isMac ? 'Cmd + I' : 'Alt + I',
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
|
||||
.ep-placeholder {
|
||||
position: absolute;
|
||||
z-index: 2001;
|
||||
z-index: 1999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--bl-html-color);
|
||||
|
||||
@ -29,7 +29,7 @@ export const treeToInfo = (tree: DocTree): DocInfo => {
|
||||
}
|
||||
}
|
||||
|
||||
export enum TitleColor {
|
||||
export enum SortLevelColor {
|
||||
ONE = '#C9515193',
|
||||
TWO = '#E6981293',
|
||||
THREE = '#127EA993',
|
||||
@ -43,15 +43,15 @@ export enum TitleColor {
|
||||
*/
|
||||
export const computedDocTitleColor = (level: number) => {
|
||||
if (level === 1) {
|
||||
return TitleColor.ONE
|
||||
return SortLevelColor.ONE
|
||||
}
|
||||
if (level === 2) {
|
||||
return TitleColor.TWO
|
||||
return SortLevelColor.TWO
|
||||
}
|
||||
if (level === 3) {
|
||||
return TitleColor.THREE
|
||||
return SortLevelColor.THREE
|
||||
}
|
||||
return TitleColor.FOUR
|
||||
return SortLevelColor.FOUR
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -45,7 +45,9 @@
|
||||
text-shadow: var(--bl-text-shadow);
|
||||
font-size: 25px;
|
||||
padding: 3px 2px;
|
||||
transition: 0.3s;
|
||||
transition:
|
||||
color 0.3s,
|
||||
text-shadow 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
@ -58,50 +60,55 @@
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.bl-search-line {
|
||||
font-size: 22px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
// 公开图标
|
||||
.bl-cloud-fill {
|
||||
color: #7ac20c;
|
||||
@include themeText(0px 5px 5px #79c20ca4, 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
}
|
||||
// .bl-cloud-fill {
|
||||
// color: #7ac20c;
|
||||
// @include themeText(0px 5px 5px #79c20ca4, 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
// }
|
||||
|
||||
.bl-cloud-fill,
|
||||
.bl-cloud-line {
|
||||
&:hover {
|
||||
color: #7ac20c;
|
||||
@include themeText(5px 5px 5px #79c20ca4, 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
}
|
||||
}
|
||||
// .bl-cloud-fill,
|
||||
// .bl-cloud-line {
|
||||
// &:hover {
|
||||
// color: #7ac20c;
|
||||
// @include themeText(5px 5px 5px #79c20ca4, 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
// }
|
||||
// }
|
||||
|
||||
// 专题图标
|
||||
.bl-a-lowerrightpage-fill {
|
||||
color: salmon;
|
||||
@include themeText(0px 5px 5px rgba(250, 128, 114, 0.546), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
}
|
||||
// .bl-a-lowerrightpage-fill {
|
||||
// color: salmon;
|
||||
// @include themeText(0px 5px 5px rgba(250, 128, 114, 0.546), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
// }
|
||||
|
||||
// 专题图标
|
||||
.bl-a-lowerrightpage-fill,
|
||||
.bl-a-lowerrightpage-line {
|
||||
&:hover {
|
||||
color: salmon;
|
||||
@include themeText(5px 5px 5px rgba(250, 128, 114, 0.546), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
}
|
||||
}
|
||||
// .bl-a-lowerrightpage-fill,
|
||||
// .bl-a-lowerrightpage-line {
|
||||
// &:hover {
|
||||
// color: salmon;
|
||||
// @include themeText(5px 5px 5px rgba(250, 128, 114, 0.546), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
// }
|
||||
// }
|
||||
|
||||
// star 图标
|
||||
.bl-star-fill {
|
||||
color: rgb(237, 204, 11);
|
||||
@include themeText(0px 5px 5px rgba(237, 204, 11, 0.546), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
}
|
||||
// .bl-star-fill {
|
||||
// color: rgb(237, 204, 11);
|
||||
// @include themeText(0px 5px 5px rgba(237, 204, 11, 0.546), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
// }
|
||||
|
||||
.bl-star-line,
|
||||
.bl-star-fill {
|
||||
font-size: 23px;
|
||||
padding-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
color: rgb(237, 204, 11);
|
||||
@include themeText(5px 5px 5px rgba(237, 203, 11, 0.756), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
}
|
||||
// &:hover {
|
||||
// color: rgb(237, 204, 11);
|
||||
// @include themeText(5px 5px 5px rgba(237, 203, 11, 0.756), 2px 3px 5px rgba(183, 183, 183, 0.8));
|
||||
// }
|
||||
}
|
||||
|
||||
// 刷新图标
|
||||
|
||||
@ -14,10 +14,10 @@
|
||||
<Weather></Weather>
|
||||
<UserAvatar style="margin-left: 20px"></UserAvatar>
|
||||
</bl-row>
|
||||
|
||||
<div style="height: 10px"></div>
|
||||
<!-- 统计 -->
|
||||
<div class="chart-container">
|
||||
<bl-row class="container-name">字数统计</bl-row>
|
||||
<bl-row class="container-name" style="height: 50px; min-height: 50px">字数统计</bl-row>
|
||||
<bl-row height="270px">
|
||||
<!-- 字数图表 -->
|
||||
<bl-col width="670px">
|
||||
|
||||
@ -1,83 +1,70 @@
|
||||
<template>
|
||||
<div class="about-root">
|
||||
<div class="project-name">Blossom</div>
|
||||
<bl-row class="repository" just="center">
|
||||
<bl-col @click="toView(CONFIG.SYS.GITHUB_REPO)">
|
||||
<div class="project-name">
|
||||
<div>Blossom</div>
|
||||
<div class="version">Client {{ CONFIG.SYS.VERSION + getServerVersion() }}</div>
|
||||
</div>
|
||||
<!-- <bl-row class="repository" just="center">
|
||||
<bl-col @click="openExtenal(CONFIG.SYS.GITHUB_REPO)">
|
||||
<svg class="github" aria-hidden="true"><use xlink:href="#wl-github2"></use></svg>
|
||||
</bl-col>
|
||||
|
||||
<bl-col @click="toView(CONFIG.SYS.GITEE_REPO)" style="margin-left: 20px">
|
||||
<bl-col @click="openExtenal(CONFIG.SYS.GITEE_REPO)" style="margin-left: 20px">
|
||||
<svg class="gitee" aria-hidden="true"><use xlink:href="#wl-gitee"></use></svg>
|
||||
</bl-col>
|
||||
</bl-row> -->
|
||||
|
||||
<bl-col height="fit-content">
|
||||
<bl-row just="center" class="about-item" height="50px" width="700px">
|
||||
<div class="iconbl bl-books-line"></div>
|
||||
<bl-col just="center" align="flex-start" height="100%" width="400px">
|
||||
<div class="title">官方帮助文档</div>
|
||||
<div class="desc">可查看 Blossom 更新日志,功能介绍。</div>
|
||||
</bl-col>
|
||||
<div class="btns">
|
||||
<el-button size="default" style="width: 55px" @click="openExtenal(CONFIG.SYS.DOC)"> 浏览 </el-button>
|
||||
</div>
|
||||
</bl-row>
|
||||
|
||||
<div class="bl-preview">
|
||||
<p class="paragraph">
|
||||
Blossom 是一个支持<span class="blod">私有部署</span
|
||||
>的云端存储双链笔记软件,你可以将你所有的笔记,图片,个人计划安排保存在自己的服务器中,并在任意设备之间实时同步,且基于MIT协议完全开源。
|
||||
</p>
|
||||
<p class="paragraph" style="margin-bottom: 0"><span class="blod">Blossom 具有以下优势:</span></p>
|
||||
<ol>
|
||||
<li><strong>不再为同步设备数量付费。</strong></li>
|
||||
<li><strong>不再为公网访问付费。</strong></li>
|
||||
<li>
|
||||
<strong>不再为软件付费</strong>,基于<a
|
||||
class="bl-tip bl-tip-bottom"
|
||||
data-tip="什么是 MIT 协议?"
|
||||
target="_blank"
|
||||
href="https://choosealicense.com/licenses/mit/"
|
||||
>MIT 协议</a
|
||||
>完全开源。
|
||||
</li>
|
||||
<li><strong>自带截图功能</strong><span class="blod">(仅windows)</span>,你不再需要使用其他截图工具截图后保存在本地再上传到文章了。</li>
|
||||
<li><strong>文章与图片都在你的服务器存储</strong>,不需要再使用任何三方图床,不需要购买任何对象存储。</li>
|
||||
<li><strong>没有任何私有协议</strong>,基于 Markdown 语法,采用约定格式拓展样式。迁移到其他软件可以正常显示。</li>
|
||||
<li><strong>多账号权限</strong>,可以和你的朋友一起使用。</li>
|
||||
<li><strong>一键导出</strong>,可以一键导出所有文章和图片,并将图片链接转换为图片路径,轻松转为本地笔记。</li>
|
||||
<li><strong>网页转换</strong>,可以一键将指定或全部文章转换为网页,方便分享。</li>
|
||||
<li><strong>丰富的功能拓展</strong>,包含日历计划,待办事项清单,一个完善的个人管理工具。</li>
|
||||
<li>你甚至可以只把 Blossom <strong>当做个人图床</strong>,并使用你自己的域名。</li>
|
||||
</ol>
|
||||
<p class="paragraph" style="margin-bottom: 0">
|
||||
<span class="blod">Blossom 具有以下功能:</span>
|
||||
</p>
|
||||
<ol>
|
||||
<li><strong>文章编辑:</strong>Markdown 文章编写,文章公网访问权限,文章分类管理。</li>
|
||||
<li><strong>双链笔记:</strong>内部文章与外部链接引用形成的双链笔记知识网络。</li>
|
||||
<li><strong>全量导出:</strong>每日全量备份,备份一键下载。</li>
|
||||
<li><strong>网页转换:</strong>将 Markdown 文章转换成网页,一键打包分享。</li>
|
||||
<li><strong>番茄时钟:</strong>编辑器包含一个番茄钟功能。</li>
|
||||
<li><strong>图片存储:</strong>按文章目录分类你的图片,或自定义图片目录,图片与文章双向查询。</li>
|
||||
<li><strong>自带截图:</strong>(仅windows),你不再需要使用其他截图工具截图后保存在本地再上传到云端了。</li>
|
||||
<li><strong>待办清单:</strong>以 Todo List 的方式管理你的每日事项或阶段性计划。</li>
|
||||
<li><strong>计划安排:</strong>阶段性的长期计划,或者日历中的短期计划安排。</li>
|
||||
<li><strong>快捷便签:</strong>方便你记录随意的日常信息。</li>
|
||||
<li><strong>网站收藏:</strong>常用网站快捷访问,或者说是一个书签功能。</li>
|
||||
<li>笔记总字数折线图,笔记编辑数量热力图,服务器请求量折线图展示。</li>
|
||||
</ol>
|
||||
|
||||
<p class="paragraph">
|
||||
更多内容,可前往<a :href="CONFIG.SYS.GITHUB_REPO" target="_blank">源码仓库</a>、<a :href="CONFIG.SYS.DOC" target="_blank">查看文档</a>、或<a
|
||||
:href="CONFIG.SYS.CONTACT"
|
||||
target="_blank"
|
||||
>联系作者</a
|
||||
>。
|
||||
</p>
|
||||
<bl-row just="center" class="about-item" height="50px" width="700px">
|
||||
<div class="iconbl bl-visit"></div>
|
||||
<bl-col just="center" align="flex-start" height="100%" width="400px">
|
||||
<div class="title">加入群聊</div>
|
||||
<div class="desc">和更多 Blossom 用户一起沟通交流,了解最新更新内容,说出你想要的功能!</div>
|
||||
</bl-col>
|
||||
<div class="btns">
|
||||
<el-button size="default" style="width: 55px" @click="openExtenal(CONFIG.SYS.CONTACT)"> 前往 </el-button>
|
||||
</div>
|
||||
|
||||
<div class="reference">
|
||||
<div class="blod">三方引用:</div>
|
||||
<ol>
|
||||
<li v-for="ref in references">
|
||||
<bl-row>
|
||||
<span style="width: 180px">{{ ref.name }}</span
|
||||
>: <a :href="ref.url">{{ ref.url }}</a>
|
||||
</bl-row>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="blod" style="margin: 30px 0 10px 0">开发者列表</div>
|
||||
<bl-row just="center" class="about-item" height="50px" width="700px">
|
||||
<div class="iconbl bl-github"></div>
|
||||
<bl-col just="center" align="flex-start" height="100%" width="400px">
|
||||
<div class="title">查看源码</div>
|
||||
<div class="desc">Blossom 是一个开源项目,公开所有源代码。你可以加入社区反馈问题,参与开发。</div>
|
||||
</bl-col>
|
||||
<div class="btns">
|
||||
<el-button style="width: 55px" @click="openExtenal(CONFIG.SYS.GITHUB_REPO)">Github</el-button>
|
||||
<el-button style="width: 55px; margin: 3px 0 0 0" @click="openExtenal(CONFIG.SYS.GITEE_REPO)">Gitee</el-button>
|
||||
</div>
|
||||
</bl-row>
|
||||
|
||||
<bl-row just="center" class="about-item" height="50px" width="700px">
|
||||
<div class="iconbl">❤️</div>
|
||||
<bl-col just="center" align="flex-start" height="100%" width="400px">
|
||||
<div class="title">赞助项目</div>
|
||||
<div class="desc">
|
||||
Blossom 的设计、开发、测试等工作需要耗费大量的精力。开源项目难以维持生计,如果你觉得项目还不错,可以赞助开发者来支持该项目。
|
||||
</div>
|
||||
</bl-col>
|
||||
<div class="btns">
|
||||
<el-button size="default" style="width: 55px" @click="openExtenal(CONFIG.SYS.SPONSOR)"> 前往 </el-button>
|
||||
</div>
|
||||
</bl-row>
|
||||
</bl-col>
|
||||
|
||||
<bl-col just="center" height="fit-content">
|
||||
<div class="blod" style="margin: 80px 0 10px 0">开发者列表</div>
|
||||
<div class="developer">
|
||||
<bl-row class="item" v-for="dever in developer" width="250px" height="70px">
|
||||
<div>
|
||||
@ -93,12 +80,30 @@
|
||||
</bl-col>
|
||||
</bl-row>
|
||||
</div>
|
||||
</bl-col>
|
||||
|
||||
<bl-row just="center">
|
||||
<div class="reference">
|
||||
<div class="blod">三方引用:</div>
|
||||
<ol>
|
||||
<li v-for="ref in references">
|
||||
<bl-row>
|
||||
<span style="width: 180px">{{ ref.name }}</span
|
||||
>: <a :href="ref.url">{{ ref.url }}</a>
|
||||
</bl-row>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</bl-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CONFIG from '@renderer/assets/constants/system'
|
||||
import { toView } from '@renderer/assets/utils/util'
|
||||
import { openExtenal } from '@renderer/assets/utils/electron'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const references = [
|
||||
{ name: '基础icon(作者:宗伟)', url: 'https://www.iconfont.cn/collections/detail?cid=35578' },
|
||||
@ -113,6 +118,13 @@ const developer = [
|
||||
avatar: 'https://www.wangyunf.com/bl/pic/home/bl/img/U1/head/luban.png'
|
||||
}
|
||||
]
|
||||
|
||||
const getServerVersion = () => {
|
||||
if (userStore.sysParams && userStore.sysParams.SERVER_VERSION) {
|
||||
return ' | Server v' + userStore.sysParams.SERVER_VERSION.replaceAll('-SNAPSHOT', '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@ -120,6 +132,7 @@ const developer = [
|
||||
@include box(100%, 100%);
|
||||
padding: 0 30px 130px 30px;
|
||||
overflow: scroll;
|
||||
color: var(--bl-text-color);
|
||||
|
||||
.project-name {
|
||||
@include font(50px, 500);
|
||||
@ -127,6 +140,12 @@ const developer = [
|
||||
color: var(--el-color-primary);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
.version {
|
||||
color: var(--bl-text-color-light);
|
||||
font-size: 12px;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.repository {
|
||||
@ -139,29 +158,29 @@ const developer = [
|
||||
height: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.shields {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.project-link {
|
||||
width: 100%;
|
||||
.about-item {
|
||||
margin-top: 40px;
|
||||
.iconbl {
|
||||
width: 80px;
|
||||
font-size: 40px;
|
||||
color: var(--bl-text-color-light);
|
||||
text-align: center;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.bl-preview {
|
||||
background-color: transparent;
|
||||
|
||||
ol {
|
||||
@include font(14px, 300);
|
||||
@include themeText(1px 1px 5px #b5b5b5, 1px 1px 5px #000000);
|
||||
padding-left: 55px;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 3px;
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 13px;
|
||||
color: var(--bl-text-color-light);
|
||||
}
|
||||
|
||||
.btns {
|
||||
@include flex(column, center, center);
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,19 +189,8 @@ const developer = [
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
@include font(14px, 300);
|
||||
@include themeText(1px 1px 5px #b5b5b5, 1px 1px 5px #000000);
|
||||
text-indent: 30px;
|
||||
line-height: 25px;
|
||||
|
||||
a {
|
||||
text-indent: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.developer {
|
||||
width: 100%;
|
||||
width: 630px;
|
||||
@include flex(row, flex-start, flex-start);
|
||||
align-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
@ -209,11 +217,11 @@ const developer = [
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reference {
|
||||
width: 630px;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
margin-top: 30px;
|
||||
margin-top: 50px;
|
||||
border: 1px dashed #8b8b8b;
|
||||
border-radius: 5px;
|
||||
color: #8b8b8b;
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<div class="server-config-root">
|
||||
<el-tabs tab-position="left" type="card" style="height: 100%" class="config-tabs">
|
||||
<el-tab-pane label="客户端配置">
|
||||
<el-tabs v-model="curTab" style="height: 100%" class="config-tabs" tab-position="left" type="card" @tab-change="handleChange">
|
||||
<el-tab-pane label="客户端配置" name="client">
|
||||
<div class="tab-content"><ConfigClient></ConfigClient></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="服务器配置" :lazy="true" v-if="userStore.userinfo.type === 1">
|
||||
<div class="tab-content"><ConfigServer></ConfigServer></div>
|
||||
<el-tab-pane label="服务器配置" name="server" :lazy="true" v-if="userStore.userinfo.type === 1">
|
||||
<div class="tab-content"><ConfigServer ref="ConfigServerRef"></ConfigServer></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="博客配置" :lazy="true">
|
||||
<div class="tab-content"><ConfigBlog></ConfigBlog></div>
|
||||
<el-tab-pane label="博客配置" name="blog" :lazy="true">
|
||||
<div class="tab-content"><ConfigBlog ref="ConfigBlogRef"></ConfigBlog></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="修改个人信息" :lazy="true">
|
||||
<div class="tab-content"><ConfigUserinfo></ConfigUserinfo></div>
|
||||
<el-tab-pane label="修改个人信息" name="userinfo" :lazy="true">
|
||||
<div class="tab-content"><ConfigUserinfo ref="ConfigUserinfoRef"></ConfigUserinfo></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="修改登录密码" :lazy="true">
|
||||
<el-tab-pane label="修改登录密码" name="password" :lazy="true">
|
||||
<div class="tab-content"><ConfigUpdPwd></ConfigUpdPwd></div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="添加使用账号" :lazy="true" v-if="userStore.userinfo.type === 1">
|
||||
<el-tab-pane label="添加使用账号" name="adduser" :lazy="true" v-if="userStore.userinfo.type === 1">
|
||||
<div class="tab-content">
|
||||
<ConfigAddUser></ConfigAddUser>
|
||||
</div>
|
||||
@ -26,6 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import ConfigUserinfo from './SettingConfigUserinfo.vue'
|
||||
import ConfigUpdPwd from './SettingConfigUpdPwd.vue'
|
||||
import ConfigAddUser from './SettingConfigAddUser.vue'
|
||||
@ -34,6 +35,23 @@ import ConfigServer from './SettingConfigServer.vue'
|
||||
import ConfigBlog from './SettingConfigBlog.vue'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const curTab = ref('client')
|
||||
const ConfigServerRef = ref()
|
||||
const ConfigBlogRef = ref()
|
||||
const ConfigUserinfoRef = ref()
|
||||
|
||||
const handleChange = (name: string) => {
|
||||
if (name === 'server') {
|
||||
nextTick(() => ConfigServerRef.value.reload())
|
||||
}
|
||||
if (name === 'blog') {
|
||||
nextTick(() => ConfigBlogRef.value.reload())
|
||||
}
|
||||
if (name === 'userinfo') {
|
||||
nextTick(() => ConfigUserinfoRef.value.reload())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -64,21 +64,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onActivated, onMounted, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import { userParamListApi, userParamUpdApi, userParamRefreshApi } from '@renderer/api/blossom'
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
import { openExtenal } from '@renderer/assets/utils/electron'
|
||||
|
||||
onMounted(() => {
|
||||
getParamList()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
getParamList()
|
||||
})
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { auth } = storeToRefs(userStore)
|
||||
@ -139,6 +131,12 @@ const genWebLinksTemplate = () => {
|
||||
updParam('WEB_BLOG_LINKS', userParamForm.value.WEB_BLOG_LINKS)
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
getParamList()
|
||||
}
|
||||
|
||||
defineExpose({ reload })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div class="config-root">
|
||||
<div class="title">
|
||||
客户端配置<span class="version">{{ CONFIG.SYS.VERSION }}</span>
|
||||
</div>
|
||||
<div class="title">客户端配置</div>
|
||||
<div class="desc">客户端配置</div>
|
||||
|
||||
<el-form label-position="right" label-width="130px" style="max-width: 800px">
|
||||
@ -85,30 +83,12 @@
|
||||
</bl-row>
|
||||
<div class="conf-tip">设置日间/夜间主题颜色。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="检查更新">
|
||||
<bl-row>
|
||||
<el-button @click="checkUpdate">检查更新</el-button>
|
||||
</bl-row>
|
||||
<div class="conf-tip">获取最新的版本和更新信息</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
draggable
|
||||
v-model="isShowUpdateLog"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="true"
|
||||
width="550">
|
||||
{{ updateLog }}
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import CONFIG from '@renderer/assets/constants/system'
|
||||
import { useConfigStore } from '@renderer/stores/config'
|
||||
import type { EditorStyle, ViewStyle, PicStyle } from '@renderer/stores/config'
|
||||
import { openDevTools } from '@renderer/assets/utils/electron'
|
||||
@ -131,14 +111,6 @@ const changeViewStyle = () => {
|
||||
const changePicStyle = () => {
|
||||
configStore.setPicStyle(configPicStyleForm.value)
|
||||
}
|
||||
|
||||
const isShowUpdateLog = ref(false)
|
||||
const updateLoading = ref(false)
|
||||
const updateLog = ref<{ version: ''; content: '' }>()
|
||||
const checkUpdate = () => {
|
||||
isShowUpdateLog.value = true
|
||||
updateLoading.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -1,16 +1,9 @@
|
||||
<template>
|
||||
<div class="config-root" v-loading="auth.status !== '已登录'" element-loading-spinner="none" element-loading-text="请登录后使用服务端设置...">
|
||||
<div class="title">
|
||||
服务器配置<span class="version" v-if="isNotBlank(serverParamForm.serverVersion)">{{ 'v' + serverParamForm.serverVersion }}</span>
|
||||
</div>
|
||||
<div class="title">服务器配置</div>
|
||||
<div class="desc">服务器各项参数配置,若无内容请点击右侧刷新。<el-button @click="refreshParam" text bg>刷新</el-button></div>
|
||||
|
||||
<el-form v-if="auth.status == '已登录'" :model="serverParamForm" label-position="right" label-width="130px" style="max-width: 800px">
|
||||
<!-- <el-form-item label="网页端地址">
|
||||
<el-input size="default" v-model="serverParamForm.WEB_ARTICLE_URL" @change="(cur: any) => updParam('WEB_ARTICLE_URL', cur)"></el-input>
|
||||
<div class="conf-tip">网页端博客的访问地址,如果不使用博客可不配置。需以<code>/#/articles?articleId=</code>结尾。</div>
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item label="文件访问地址" :required="true">
|
||||
<el-input
|
||||
size="default"
|
||||
@ -85,25 +78,25 @@
|
||||
<el-form-item label="服务器到期时间">
|
||||
<div class="conf-tip">如果你使用云服务器或其他有时限的环境,可在此配置到期提示,其他环境可无视,(<code>yyyy-MM-dd</code>格式)。</div>
|
||||
<el-input size="default" v-model="serverParamForm.SERVER_MACHINE_EXPIRE" @change="(cur: any) => updParam('SERVER_MACHINE_EXPIRE', cur)">
|
||||
<template #append> {{ serverExpire.machine }} 天后到期 </template>
|
||||
<template #append> {{ serverExpire.machine }} </template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="数据库到期时间">
|
||||
<el-input size="default" v-model="serverParamForm.SERVER_DATABASE_EXPIRE" @change="(cur: any) => updParam('SERVER_DATABASE_EXPIRE', cur)">
|
||||
<template #append> {{ serverExpire.database }} 天后到期 </template>
|
||||
<template #append> {{ serverExpire.database }} </template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="域名到期时间">
|
||||
<el-input size="default" v-model="serverParamForm.SERVER_DOMAIN_EXPIRE" @change="(cur: any) => updParam('SERVER_DOMAIN_EXPIRE', cur)">
|
||||
<template #append> {{ serverExpire.domain }} 天后到期 </template>
|
||||
<template #append> {{ serverExpire.domain }} </template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="证书到期时间">
|
||||
<el-input size="default" v-model="serverParamForm.SERVER_HTTPS_EXPIRE" @change="(cur: any) => updParam('SERVER_HTTPS_EXPIRE', cur)">
|
||||
<template #append> {{ serverExpire.https }} 天后到期 </template>
|
||||
<template #append> {{ serverExpire.https }} </template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@ -114,22 +107,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useServerStore } from '@renderer/stores/server'
|
||||
import { KEY_BLOSSOM_OBJECT_STORAGE_DOMAIN, useUserStore } from '@renderer/stores/user'
|
||||
import { paramListApi, paramUpdApi, paramRefreshApi } from '@renderer/api/blossom'
|
||||
import { getDateTimeFormat, betweenDay } from '@renderer/assets/utils/util'
|
||||
import { isNotBlank } from '@renderer/assets/utils/obj'
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
|
||||
onMounted(() => {
|
||||
getParamList()
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
getParamList()
|
||||
})
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const serverStore = useServerStore()
|
||||
const userStore = useUserStore()
|
||||
@ -138,16 +123,16 @@ const { userinfo, auth } = storeToRefs(userStore)
|
||||
const serverParamForm = ref({
|
||||
WEB_ARTICLE_URL: '',
|
||||
BACKUP_PATH: '',
|
||||
ARTICLE_LOG_EXP_DAYS: '',
|
||||
ARTICLE_RECYCLE_EXP_DAYS: '',
|
||||
BACKUP_EXP_DAYS: '',
|
||||
ARTICLE_LOG_EXP_DAYS: 0,
|
||||
ARTICLE_RECYCLE_EXP_DAYS: 0,
|
||||
BACKUP_EXP_DAYS: 0,
|
||||
HEFENG_KEY: '',
|
||||
BLOSSOM_OBJECT_STORAGE_DOMAIN: '',
|
||||
SERVER_MACHINE_EXPIRE: '',
|
||||
SERVER_DATABASE_EXPIRE: '',
|
||||
SERVER_DOMAIN_EXPIRE: '',
|
||||
SERVER_HTTPS_EXPIRE: '',
|
||||
serverVersion: ''
|
||||
SERVER_VERSION: ''
|
||||
})
|
||||
|
||||
/**
|
||||
@ -157,10 +142,18 @@ const serverExpire = computed(() => {
|
||||
let now = getDateTimeFormat()
|
||||
try {
|
||||
return {
|
||||
machine: betweenDay(now, serverParamForm.value.SERVER_MACHINE_EXPIRE),
|
||||
database: betweenDay(now, serverParamForm.value.SERVER_DATABASE_EXPIRE),
|
||||
domain: betweenDay(now, serverParamForm.value.SERVER_DOMAIN_EXPIRE),
|
||||
https: betweenDay(now, serverParamForm.value.SERVER_HTTPS_EXPIRE)
|
||||
machine: dayjs().isBefore(serverParamForm.value.SERVER_MACHINE_EXPIRE)
|
||||
? betweenDay(now, serverParamForm.value.SERVER_MACHINE_EXPIRE) + '天后到期'
|
||||
: '已到期',
|
||||
database: dayjs().isBefore(serverParamForm.value.SERVER_DATABASE_EXPIRE)
|
||||
? betweenDay(now, serverParamForm.value.SERVER_DATABASE_EXPIRE) + '天后到期'
|
||||
: '已到期',
|
||||
domain: dayjs().isBefore(serverParamForm.value.SERVER_DOMAIN_EXPIRE)
|
||||
? betweenDay(now, serverParamForm.value.SERVER_DOMAIN_EXPIRE) + '天后到期'
|
||||
: '已到期',
|
||||
https: dayjs().isBefore(serverParamForm.value.SERVER_HTTPS_EXPIRE)
|
||||
? betweenDay(now, serverParamForm.value.SERVER_HTTPS_EXPIRE) + '天后到期'
|
||||
: '已到期'
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
@ -172,7 +165,14 @@ const serverExpire = computed(() => {
|
||||
*/
|
||||
const getParamList = () => {
|
||||
paramListApi().then((resp) => {
|
||||
serverParamForm.value = resp.data
|
||||
serverParamForm.value = {
|
||||
...resp.data,
|
||||
...{
|
||||
ARTICLE_LOG_EXP_DAYS: Number(resp.data.ARTICLE_LOG_EXP_DAYS),
|
||||
ARTICLE_RECYCLE_EXP_DAYS: Number(resp.data.ARTICLE_RECYCLE_EXP_DAYS),
|
||||
BACKUP_EXP_DAYS: Number(resp.data.BACKUP_EXP_DAYS)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -201,6 +201,12 @@ const autuUpdBlossomOSDomain = () => {
|
||||
Notify.success('配置成功', '配置成功')
|
||||
})
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
getParamList()
|
||||
}
|
||||
|
||||
defineExpose({ reload })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div class="config-root">
|
||||
<div class="title">修改用户信息</div>
|
||||
<div class="desc">
|
||||
天气预报使用<a target="_blank" href="https://dev.qweather.com/">和风天气API</a>,您可以在其上免费创建您的令牌以使用天气预报功能。
|
||||
</div>
|
||||
<div class="desc">用户的个人信息,若无内容请点击右侧刷新。<el-button @click="refreshUserinfo" text bg>刷新</el-button></div>
|
||||
<el-form :model="userinfoForm" :rules="rules" label-position="right" label-width="130px" style="max-width: 800px" ref="UserinfoFormRef">
|
||||
<el-form-item label="ID" prop="username">
|
||||
<el-input v-model="userinfoForm.id" size="default" disabled>
|
||||
@ -18,6 +16,7 @@
|
||||
<div class="iconbl bl-user-line" style="font-size: 20px"></div>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="conf-tip">用于登录。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nickName">
|
||||
<el-input v-model="userinfoForm.nickName" size="default">
|
||||
@ -44,6 +43,7 @@
|
||||
<el-button @click="openExtenal('https://github.com/qwd/LocationList/blob/master/China-City-List-latest.csv')">查看城市代码</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="conf-tip">填写和风天气的城市代码。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<el-input v-model="userinfoForm.avatar" size="default" placeholder="请输入图片地址, http://...">
|
||||
@ -71,14 +71,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { userUpdApi } from '@renderer/api/auth'
|
||||
import { userUpdApi, userinfoApi } from '@renderer/api/auth'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
import { openExtenal } from '@renderer/assets/utils/electron'
|
||||
import Notify from '@renderer/scripts/notify'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { userinfo } = storeToRefs(userStore)
|
||||
|
||||
interface UserinfoForm {
|
||||
id: string
|
||||
@ -90,12 +88,12 @@ interface UserinfoForm {
|
||||
}
|
||||
const UserinfoFormRef = ref<FormInstance>()
|
||||
const userinfoForm = ref<UserinfoForm>({
|
||||
id: userinfo.value.id,
|
||||
username: userinfo.value.username,
|
||||
nickName: userinfo.value.nickName,
|
||||
remark: userinfo.value.remark,
|
||||
location: userinfo.value.location,
|
||||
avatar: userinfo.value.avatar
|
||||
id: '',
|
||||
username: '',
|
||||
nickName: '',
|
||||
remark: '',
|
||||
location: '',
|
||||
avatar: ''
|
||||
})
|
||||
const rules = ref<FormRules<UserinfoForm>>({
|
||||
username: [{ required: true, message: '请填写用户名', trigger: 'blur' }],
|
||||
@ -104,6 +102,19 @@ const rules = ref<FormRules<UserinfoForm>>({
|
||||
avatar: [{ required: true, message: '请填写头像', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const getUserinfo = () => {
|
||||
userinfoApi().then((resp) => {
|
||||
userinfoForm.value = resp.data
|
||||
})
|
||||
}
|
||||
|
||||
const refreshUserinfo = () => {
|
||||
userinfoApi().then((resp) => {
|
||||
userinfoForm.value = resp.data
|
||||
Notify.success('刷新用户信息成功', '刷新成功')
|
||||
})
|
||||
}
|
||||
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, _fields) => {
|
||||
@ -118,6 +129,12 @@ const save = async (formEl: FormInstance | undefined) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
getUserinfo()
|
||||
}
|
||||
|
||||
defineExpose({ reload })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -14,13 +14,13 @@
|
||||
<el-tab-pane label="关于">
|
||||
<SettingAboutVue></SettingAboutVue>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="流量监控" :lazy="true">
|
||||
<el-tab-pane label="访问流量" :lazy="true">
|
||||
<SentinelResources></SentinelResources>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div class="version">
|
||||
<span>{{ CONFIG.SYS.NAME + ' | ' + CONFIG.SYS.VERSION }}</span>
|
||||
<span>{{ CONFIG.SYS.NAME + ' | ' + CONFIG.SYS.VERSION + getServerVersion() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -30,6 +30,16 @@ import SettingConfig from './SettingConfig.vue'
|
||||
import SettingAboutVue from './SettingAbout.vue'
|
||||
import SentinelResources from '@renderer/views/statistic/SentinelResources.vue'
|
||||
import CONFIG from '@renderer/assets/constants/system'
|
||||
import { useUserStore } from '@renderer/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const getServerVersion = () => {
|
||||
if (userStore.sysParams && userStore.sysParams.SERVER_VERSION) {
|
||||
return ' | v' + userStore.sysParams.SERVER_VERSION.replaceAll('-SNAPSHOT', '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
<el-button size="default" type="primary" @click="saveBlog" plain>
|
||||
确认使用{{ blogType === 'backend' ? '自带博客' : '独立部署' }}
|
||||
</el-button>
|
||||
<el-button size="default" @click="saveBlog" text>我不关心</el-button>
|
||||
<el-button size="default" @click="saveBlog" text>我不使用博客</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="iframe-container">
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
|
||||
<bl-row class="prop-row" just="space-between">
|
||||
<div class="prop">
|
||||
<div class="prop-name">隐藏试用按钮</div>
|
||||
<div class="prop-name">显示试用按钮</div>
|
||||
</div>
|
||||
<el-switch v-model="viewStyle.isShowTryuseBtn" size="default" @change="changeSubjectStype" />
|
||||
</bl-row>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 150px;
|
||||
padding-bottom: 50px;
|
||||
|
||||
a {
|
||||
color: var(--el-color-primary);
|
||||
|
||||
@ -12,12 +12,12 @@
|
||||
<template #content>
|
||||
显示排序<br />
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="TitleColor.ONE">一级</bl-tag>
|
||||
<bl-tag :bgColor="TitleColor.TWO">二级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.ONE">一级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.TWO">二级</bl-tag>
|
||||
</bl-row>
|
||||
<bl-row>
|
||||
<bl-tag :bgColor="TitleColor.THREE">三级</bl-tag>
|
||||
<bl-tag :bgColor="TitleColor.FOUR">四级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.THREE">三级</bl-tag>
|
||||
<bl-tag :bgColor="SortLevelColor.FOUR">四级</bl-tag>
|
||||
</bl-row>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
@ -44,7 +44,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, inject } from 'vue'
|
||||
import { provideKeyDocInfo, TitleColor } from '@renderer/views/doc/doc'
|
||||
import { provideKeyDocInfo, SortLevelColor } from '@renderer/views/doc/doc'
|
||||
import PictureInfo from '@renderer/views/picture/PictureInfo.vue'
|
||||
|
||||
// 当前菜单中选择的文档
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
padding: 5px;
|
||||
font-size: 30px;
|
||||
text-shadow: var(--bl-text-shadow);
|
||||
transition: 0.3s;
|
||||
transition: color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@ -152,7 +152,10 @@
|
||||
@include flex(column, flex-start, center);
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s;
|
||||
transition:
|
||||
height 0.3s,
|
||||
width 0.3s,
|
||||
box-shadow 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user