Compare commits

...

11 Commits

Author SHA1 Message Date
xiaozzzi
83127b625b release: 1.16.0 2024-04-17 20:21:23 +08:00
xiaozzzi
d48048d306 pref: 优化临时访问中的目录样样式 2024-04-17 16:13:13 +08:00
xiaozzzi
e59bd36110 pref: 优化新窗口访问中的目录样样式 2024-04-17 15:53:34 +08:00
xiaozzzi
61ca570a23 pref: 博客配置中的关于文章临时访问的说明内容 2024-04-17 15:27:19 +08:00
xiaozzzi
82acbc5745 fix: 修改访问IP最大长度 2024-04-17 15:20:18 +08:00
xiaozzzi
7d706fe969 fix: 回收站文章无法还原 2024-04-17 15:15:18 +08:00
xiaozzzi
2233c2d8bd fix: 无图片时备份会报错的问题 2024-04-17 00:42:17 +08:00
xiaozzzi
64b9a61276 feat: 临时访问增加水印 2024-04-17 00:41:54 +08:00
xiaozzzi
c5866839fd feat: 临时访问主题色配置 2024-04-16 18:24:01 +08:00
xiaozzzi
f999b2176b pref: 优化默认主题色 2024-04-16 18:23:24 +08:00
xiaozzzi
9374688aa2 fix: 链接使用默认浏览器打开 2024-04-13 14:11:30 +08:00
32 changed files with 1792 additions and 1327 deletions

View File

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

View File

@ -9,6 +9,8 @@ import cn.hutool.core.util.ZipUtil;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.param.pojo.ParamEntity;
import com.blossom.backend.base.paramu.UserParamEnum;
import com.blossom.backend.base.paramu.UserParamService;
import com.blossom.backend.base.user.UserService;
import com.blossom.backend.base.user.pojo.UserEntity;
import com.blossom.backend.server.article.backup.pojo.BackupFile;
@ -56,6 +58,9 @@ public class ArticleBackupService {
@Autowired
private ParamService paramService;
@Autowired
private UserParamService userParamService;
@Autowired
private UserService userService;
@ -126,6 +131,13 @@ public class ArticleBackupService {
// 用户信息
UserEntity user = userService.selectById(userId);
final String BLOG_COLOR = userParamService.getValue(userId, UserParamEnum.WEB_BLOG_COLOR).getParamValue();
final String WATER_ENABLED = userParamService.getValue(userId, UserParamEnum.WEB_BLOG_WATERMARK_ENABLED).getParamValue();
final String WATERMARK_CONTENT = userParamService.getValue(userId, UserParamEnum.WEB_BLOG_WATERMARK_CONTENT).getParamValue();
final String WATERMARK_FONTSIZE = userParamService.getValue(userId, UserParamEnum.WEB_BLOG_WATERMARK_FONTSIZE).getParamValue();
final String WATERMARK_COLOR = userParamService.getValue(userId, UserParamEnum.WEB_BLOG_WATERMARK_COLOR).getParamValue();
final String WATERMARK_GAP = userParamService.getValue(userId, UserParamEnum.WEB_BLOG_WATERMARK_GAP).getParamValue();
final File backLogFile = new File(backupFile.getRootPath() + "/" + "log.txt");
final List<String> backLogs = new ArrayList<>();
log.info("[文章备份] 开始备份, 本次备份文件名称 [{}], 用户ID [{}]", backupFile.getFilename(), userId);
@ -186,7 +198,9 @@ public class ArticleBackupService {
articleDetail.setName(article.getN());
// 文章 markdown 内容
String content = getContentByType(articleDetail, type, user);
String content = getContentByType(articleDetail, type, user,
BLOG_COLOR, WATER_ENABLED, WATERMARK_CONTENT, WATERMARK_FONTSIZE, WATERMARK_COLOR, WATERMARK_GAP
);
content = formatContent(content, toLocal, article.getI(), article.getN());
String id = String.valueOf(articleDetail.getId());
String version = String.valueOf(articleDetail.getVersion());
@ -231,17 +245,19 @@ public class ArticleBackupService {
}
// 备份全部图片
else {
List<File> files = FileUtil.loopFiles(FileUtil.newFile(iaasProperties.getBlos().getDefaultPath() + "/U" + userId), null);
backLogs.add("[图片备份] 图片个数: " + files.size());
List<File> pics = FileUtil.loopFiles(FileUtil.newFile(iaasProperties.getBlos().getDefaultPath() + "/U" + userId), null);
backLogs.add("[图片备份] 图片个数: " + pics.size());
backLogs.add("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↓↓ 图片列表 ↓↓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
for (File file : files) {
for (File file : pics) {
backLogs.add(file.getPath());
}
FileUtil.copy(
iaasProperties.getBlos().getDefaultPath() + "/U" + userId,
backupFile.getRootPath() + "/" + iaasProperties.getBlos().getDefaultPath(),
true);
if (CollUtil.isNotEmpty(pics)) {
FileUtil.copy(
iaasProperties.getBlos().getDefaultPath() + "/U" + userId,
backupFile.getRootPath() + "/" + iaasProperties.getBlos().getDefaultPath(),
true);
}
}
backLogs.add("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↑↑ 图片列表 ↑↑ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
@ -353,18 +369,31 @@ public class ArticleBackupService {
/**
* 根据类型判断是获取 markdown 内容还是 html 内容
*
* @param article 文章
* @param type 类型
* @return 对应的内容
* @param article 文章
* @param type 类型
* @param user 用户信息
* @param blogColor 主题色
* @param watermarkEnabled 开启水印
* @param watermarkContent 水印内容
* @param waterFontSize 水印字体大小
* @param waterColor 水印颜色
* @param waterGap 水印密集度
* @return
*/
private String getContentByType(ArticleEntity article, BackupTypeEnum type, UserEntity user) {
private String getContentByType(ArticleEntity article, BackupTypeEnum type, UserEntity user,
String blogColor,
String watermarkEnabled,
String watermarkContent,
String waterFontSize,
String waterColor,
String waterGap) {
if (type == BackupTypeEnum.MARKDOWN) {
return StrUtil.isBlank(article.getMarkdown()) ? "文章无内容" : article.getMarkdown();
} else if (type == BackupTypeEnum.HTML) {
ArticleEntity export = new ArticleEntity();
export.setName(article.getName().substring(article.getName().lastIndexOf("/")));
export.setHtml(article.getHtml());
return ArticleUtil.toHtml(article, user);
return ArticleUtil.toHtml(article, user, blogColor, watermarkEnabled, watermarkContent, waterFontSize, waterColor, waterGap);
}
return "";
}

View File

@ -7,6 +7,8 @@ import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.paramu.UserParamEnum;
import com.blossom.backend.base.paramu.UserParamService;
import com.blossom.backend.base.user.UserService;
import com.blossom.backend.server.article.draft.pojo.*;
import com.blossom.backend.server.article.open.ArticleOpenService;
@ -60,6 +62,7 @@ public class ArticleController {
private final DocService docService;
private final DocSortChecker docSortChecker;
private final ImportManager importManager;
private final UserParamService userParamService;
/**
* 查询列表
@ -260,7 +263,15 @@ public class ArticleController {
if (StrUtil.isBlank(article.getHtml())) {
article.setHtml("<span>文章无内容</span>");
}
String reportHtml = ArticleUtil.toHtml(article, userService.selectById(AuthContext.getUserId()));
String reportHtml = ArticleUtil.toHtml(article,
userService.selectById(AuthContext.getUserId()),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_COLOR).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_ENABLED).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_CONTENT).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_FONTSIZE).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_COLOR).getParamValue(),
userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_GAP).getParamValue()
);
try (InputStream is = new ByteArrayInputStream(reportHtml.getBytes(StandardCharsets.UTF_8));
BufferedInputStream bis = new BufferedInputStream(is)) {
String filename = URLEncodeUtil.encode(article.getName() + ".html");
@ -331,6 +342,15 @@ public class ArticleController {
XzException404.throwBy(ObjUtil.isNull(visit), "文章不存在或您无权限查看");
ArticleEntity article = baseService.selectById(visit.getArticleId(), false, false, true, visit.getUserId());
resp.setContentType("text/html");
return ArticleUtil.toHtml(article, userService.selectById(visit.getUserId()));
return ArticleUtil.toHtml(
article,
userService.selectById(visit.getUserId()),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_COLOR).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_ENABLED).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_CONTENT).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_FONTSIZE).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_COLOR).getParamValue(),
userParamService.getValue(visit.getUserId(), UserParamEnum.WEB_BLOG_WATERMARK_GAP).getParamValue()
);
}
}

View File

@ -1,18 +1,16 @@
package com.blossom.backend.server.utils;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.blossom.backend.base.user.pojo.UserEntity;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.common.base.util.DateUtils;
import com.blossom.common.base.enums.YesNo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
/**
* 文章工具类
@ -102,32 +100,265 @@ public class ArticleUtil {
}
/**
*
*/
private static final String HEAD_SCRIPT_BLOG_COLOR = " <script>\n" +
" window.addEventListener('load', function() {\n" +
" const rgb = '{BLOSSOM_WEB_BLOG_COLOR}'\n" +
" if (rgb && !rgb.toLowerCase().startsWith('rgb(')) {\n" +
" return\n" +
" }\n" +
" const prefix = rgb.substring(4, rgb.length - 1)\n" +
" let text = `:root {\n" +
" --bl-color-primary: ${rgb};\n" +
" }`\n" +
"\n" +
" let themeStyleTag = document.createElement('style')\n" +
" themeStyleTag.type = 'text/css'\n" +
" themeStyleTag.id = 'BLOSSOM_TEMPVISIT_STYLE_TAG'\n" +
" themeStyleTag.innerHTML = text\n" +
" document\n" +
" .getElementsByTagName('head')\n" +
" .item(0)\n" +
" .appendChild(themeStyleTag)\n" +
" });\n" +
" </script>\n";
private static final String prefix = "\n" +
"<body><div class=\"header\">\n" +
" <div class=\"copyright\">本文作者:{BLOSSOM_EXPORT_HTML_AUTHOR}。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</div><a\n" +
private static final String HEAD_SCRIPT_WATERMARK = " <script>\n" +
" window.addEventListener(\"load\", function () {\n" +
" const FontGap = 3\n" +
"\n" +
" function prepareCanvas(width, height, ratio = 1){\n" +
" const canvas = document.createElement('canvas')\n" +
" const ctx = canvas.getContext('2d')\n" +
" const realWidth = width * ratio\n" +
" const realHeight = height * ratio\n" +
" canvas.setAttribute('width', `${realWidth}px`)\n" +
" canvas.setAttribute('height', `${realHeight}px`)\n" +
" ctx.save()\n" +
" return [ctx, canvas, realWidth, realHeight]\n" +
" }\n" +
"\n" +
" // Get single clips\n" +
" function getClips(content, rotate, ratio, width, height, font, gapX, gapY) {\n" +
" // ================= Text =================\n" +
" const [ctx, canvas, contentWidth, contentHeight] = prepareCanvas(width, height, ratio)\n" +
" const {color, fontSize, fontStyle, fontWeight, fontFamily } = font\n" +
" const mergedFontSize = Number(fontSize) * ratio\n" +
" ctx.font = `${fontStyle} normal ${fontWeight} ${mergedFontSize}px/${height}px ${fontFamily}`\n" +
" ctx.fillStyle = color\n" +
" ctx.textAlign = 'center'\n" +
" ctx.textBaseline = 'top'\n" +
" const contents = Array.isArray(content) ? content : [content]\n" +
" contents?.forEach((item, index) => {\n" +
" ctx.fillText(\n" +
" item ?? 'ccccc',\n" +
" contentWidth / 2,\n" +
" index * (mergedFontSize + FontGap * ratio)\n" +
" )\n" +
" })\n" +
"\n" +
" // ==================== Rotate ====================\n" +
" const angle = (Math.PI / 180) * Number(rotate)\n" +
" const maxSize = Math.max(width, height)\n" +
" const [rCtx, rCanvas, realMaxSize] = prepareCanvas(maxSize, maxSize, ratio)\n" +
"\n" +
" // Copy from `ctx` and rotate\n" +
" rCtx.translate(realMaxSize / 2, realMaxSize / 2)\n" +
" rCtx.rotate(angle)\n" +
" if (contentWidth > 0 && contentHeight > 0) {\n" +
" rCtx.drawImage(canvas, -contentWidth / 2, -contentHeight / 2)\n" +
" }\n" +
"\n" +
" // Get boundary of rotated text\n" +
" function getRotatePos(x, y) {\n" +
" const targetX = x * Math.cos(angle) - y * Math.sin(angle)\n" +
" const targetY = x * Math.sin(angle) + y * Math.cos(angle)\n" +
" return [targetX, targetY]\n" +
" }\n" +
"\n" +
" let left = 0\n" +
" let right = 0\n" +
" let top = 0\n" +
" let bottom = 0\n" +
"\n" +
" const halfWidth = contentWidth / 2\n" +
" const halfHeight = contentHeight / 2\n" +
" const points = [\n" +
" [0 - halfWidth, 0 - halfHeight],\n" +
" [0 + halfWidth, 0 - halfHeight],\n" +
" [0 + halfWidth, 0 + halfHeight],\n" +
" [0 - halfWidth, 0 + halfHeight],\n" +
" ]\n" +
" points.forEach(([x, y]) => {\n" +
" const [targetX, targetY] = getRotatePos(x, y)\n" +
" left = Math.min(left, targetX)\n" +
" right = Math.max(right, targetX)\n" +
" top = Math.min(top, targetY)\n" +
" bottom = Math.max(bottom, targetY)\n" +
" })\n" +
"\n" +
" const cutLeft = left + realMaxSize / 2\n" +
" const cutTop = top + realMaxSize / 2\n" +
" const cutWidth = right - left\n" +
" const cutHeight = bottom - top\n" +
"\n" +
" // ================ Fill Alternate ================\n" +
" const realGapX = gapX * ratio\n" +
" const realGapY = gapY * ratio\n" +
" const filledWidth = (cutWidth + realGapX) * 2\n" +
" const filledHeight = cutHeight + realGapY\n" +
"\n" +
" const [fCtx, fCanvas] = prepareCanvas(filledWidth, filledHeight)\n" +
"\n" +
" function drawImg(targetX = 0, targetY = 0) {\n" +
" fCtx.drawImage(rCanvas, cutLeft, cutTop, cutWidth, cutHeight, targetX, targetY, cutWidth, cutHeight)\n" +
" }\n" +
" drawImg()\n" +
" drawImg(cutWidth + realGapX, -cutHeight / 2 - realGapY / 2)\n" +
" drawImg(cutWidth + realGapX, +cutHeight / 2 + realGapY / 2)\n" +
" return [fCanvas.toDataURL(), filledWidth / ratio, filledHeight / ratio]\n" +
" }\n" +
" \n" +
" const getMarkSize = (ctx, content) => {\n" +
" let defaultWidth = 120\n" +
" let defaultHeight = 64\n" +
" const width = 120\n" +
" const height = 64\n" +
" if (ctx.measureText) {\n" +
" ctx.font = `${Number('15')}px sans-serif`\n" +
" const contents = Array.isArray(content) ? content : [content]\n" +
" const sizes = contents.map((item) => {\n" +
" const metrics = ctx.measureText(item)\n" +
"\n" +
" return [metrics.width,\n" +
" metrics.fontBoundingBoxAscent !== undefined\n" +
" ? metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent\n" +
" : metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,\n" +
" ]\n" +
" })\n" +
" defaultWidth = Math.ceil(Math.max(...sizes.map((size) => size[0])))\n" +
" defaultHeight =\n" +
" Math.ceil(Math.max(...sizes.map((size) => size[1]))) * contents.length +\n" +
" (contents.length - 1) * FontGap\n" +
" }\n" +
" return [width ?? defaultWidth, height ?? defaultHeight]\n" +
" }\n" +
" \n" +
" function toLowercaseSeparator(key) {\n" +
" return key.replace(/([A-Z])/g, '-$1').toLowerCase()\n" +
" }\n" +
"\n" +
" function getStyleStr(style) {\n" +
" return Object.keys(style)\n" +
" .map(\n" +
" (key) =>\n" +
" `${toLowercaseSeparator(key)}: ${style[key]};`\n" +
" )\n" +
" .join(' ')\n" +
" }\n" +
"\n" +
" const getMarkStyle = (rotate) => {\n" +
" const markStyle = {zIndex: 9, position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', pointerEvents: 'none', backgroundRepeat: 'repeat' }\n" +
"\n" +
" /** Calculate the style of the offset */\n" +
" let positionLeft = rotate / 2 - rotate / 2\n" +
" let positionTop = rotate / 2 - rotate / 2\n" +
" if (positionLeft > 0) {\n" +
" markStyle.left = `${positionLeft}px`\n" +
" markStyle.width = `calc(100% - ${positionLeft}px)`\n" +
" positionLeft = 0\n" +
" }\n" +
" if (positionTop > 0) {\n" +
" markStyle.top = `${positionTop}px`\n" +
" markStyle.height = `calc(100% - ${positionTop}px)`\n" +
" positionTop = 0\n" +
" }\n" +
" markStyle.backgroundPosition = `${positionLeft}px ${positionTop}px`\n" +
" return markStyle\n" +
" }\n" +
" \n" +
" let stopObservation = false\n" +
" const observerContainer = document.getElementsByClassName('bl-preview')[0]\n" +
" const observerConfig = { attributes: true, childList: true, subtree: true };\n" +
" const observer = new MutationObserver(() => {\n" +
" renderWatermark()\n" +
" });\n" +
"\n" +
" const renderWatermark = () => {\n" +
" const canvas = document.createElement(\"canvas\")\n" +
" const ctx = canvas.getContext(\"2d\")\n" +
" const rotate = -22\n" +
" const content = '{WEB_BLOG_WATERMARK_CONTENT}'\n" +
" const color = '{WEB_BLOG_WATERMARK_COLOR}'\n" +
" const fontSize = {WEB_BLOG_WATERMARK_FONTSIZE}\n" +
" const gap = {WEB_BLOG_WATERMARK_GAP}\n" +
"\n" +
" if (ctx) {\n" +
" const ratio = window.devicePixelRatio || 1\n" +
" const [markWidth, markHeight] = getMarkSize(ctx, content)\n" +
"\n" +
" const drawCanvas = (drawContent) => {\n" +
" const [textClips, clipWidth] = getClips(drawContent, rotate, ratio, markWidth, markHeight, \n" +
" {color: color, fontSize: fontSize, fontStyle: 'normal', fontWeight: 'normal'}, gap, gap\n" +
" )\n" +
" let watermark = document.createElement('div')\n" +
" let container = document.getElementsByClassName('bl-preview')[0]\n" +
" stopObservation = true\n" +
" observer.disconnect()\n" +
" watermark.setAttribute('style',getStyleStr({\n" +
" ...getMarkStyle(rotate),\n" +
" backgroundImage: `url('${textClips}')`,\n" +
" backgroundSize: `${Math.floor(clipWidth)}px`,\n" +
" })\n" +
" )\n" +
" container.append(watermark)\n" +
" setTimeout(() => {\n" +
" observer.observe(observerContainer, observerConfig)\n" +
" })\n" +
" };\n" +
" \n" +
" drawCanvas(content);\n" +
" }\n" +
" };\n" +
"\n" +
" renderWatermark()\n" +
" })\n" +
" </script>\n";
private static final String BODY_HEADER = "\n" +
"<body>\n" +
" <div class=\"header\">\n" +
" <div class=\"copyright\">本文作者:{BLOSSOM_EXPORT_HTML_AUTHOR}。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</div><a\n" +
" href=\"https://github.com/blossom-editor/blossom\" target=\"_blank\"><span>Export by Blossom</span><svg\n" +
" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-github\"\n" +
" viewBox=\"0 0 16 16\">\n" +
" <path\n" +
" <path\n" +
" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z\" />\n" +
"</svg></a>\n" +
"</div><div class=\"content \">\n" +
" </svg></a>\n" +
" </div>\n" +
" <div class=\"content \">\n" +
" <div class=\"main\">\n" +
" <div class=\"bl-preview\" id=\"blossom-view\">";
private static final String BODY_CONTENT_TOC = "\n" +
" <div class=\"toc\" id=\"blossom-toc\">\n" +
" <div style=\"font-size: 15px;color:#727272;padding:10px 0\">《{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}》</div>\n" +
" <div style=\"font-size: 20px;color:#727272;border-bottom:2px solid #eaeaea;padding-bottom: 10px\">目录</div>\n" +
" </div><div class=\"main bl-preview\" id=\"blossom-view\">";
" <div style=\"font-size: 20px;color:#727272;border-bottom:2px solid #eaeaea;padding-bottom: 10px;margin-bottom: 10px;\">目录</div>\n" +
" </div>\n";
private static final String suffix = "</div></div></body></html>";
private static final String suffix = "</div></body></html>";
private static String htmlTag;
private static String htmlTemplate;
static {
Resource resource = new ClassPathResource("exportTemplate.html");
try (InputStream is = resource.getInputStream()) {
byte[] bytes = new byte[is.available()];
is.read(bytes);
htmlTag = new String(bytes);
htmlTemplate = new String(bytes);
} catch (IOException e) {
e.printStackTrace();
}
@ -136,44 +367,90 @@ public class ArticleUtil {
/**
* 将文章转换为 html 格式
*
* @param article 文章
* @param user 用户, 用户获取作者
* @param article 文章
* @param user 用户, 用户获取作者
* @param blogColor 主颜色
* @param WEB_BLOG_WATERMARK_ENABLED 开启水印
* @param WEB_BLOG_WATERMARK_CONTENT 水印内容
* @param WEB_BLOG_WATERMARK_FONTSIZE 水印字体大小
* @param WEB_BLOG_WATERMARK_COLOR 水印颜色
* @param WEB_BLOG_WATERMARK_GAP 水印密集度
* @return html 内容
*/
public static String toHtml(ArticleEntity article, UserEntity user) {
return htmlTag + prefix
// 替换作者, 文章名称
.replaceAll("\\{BLOSSOM_EXPORT_HTML_AUTHOR}", user.getNickName())
.replaceAll("\\{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}", article.getName()) +
article.getHtml() + suffix;
public static String toHtml(ArticleEntity article,
UserEntity user,
String blogColor,
String WEB_BLOG_WATERMARK_ENABLED,
String WEB_BLOG_WATERMARK_CONTENT,
String WEB_BLOG_WATERMARK_FONTSIZE,
String WEB_BLOG_WATERMARK_COLOR,
String WEB_BLOG_WATERMARK_GAP) {
return htmlTemplate
+ appendHeadScript(blogColor, WEB_BLOG_WATERMARK_ENABLED, WEB_BLOG_WATERMARK_CONTENT, WEB_BLOG_WATERMARK_FONTSIZE, WEB_BLOG_WATERMARK_COLOR, WEB_BLOG_WATERMARK_GAP)
+ appendBodyHeader(user)
+ appendArticleContent(article)
+ appendToc(article)
+ suffix;
}
private static void genHeatmap() {
Date begin = DateUtils.parse("2023-04-01", DateUtils.PATTERN_YYYYMMDD);
Date end = DateUtils.parse("2023-06-30", DateUtils.PATTERN_YYYYMMDD);
for (; DateUtils.compare(begin, end) <= 0; begin = DateUtils.offsetDay(begin, 1)) {
String dt = DateUtils.format(begin, DateUtils.PATTERN_YYYYMMDD);
int value = RandomUtil.randomInt(0, 10);
System.out.println(String.format("insert into blossom_stat values(null, 1, '%s' ,%s);", dt, value));
/**
* 添加 head 下的动态 script
*
* @param blogColor 主题色
* @param WEB_BLOG_WATERMARK_ENABLED 开启水印
* @param WEB_BLOG_WATERMARK_CONTENT 水印内容
* @param WEB_BLOG_WATERMARK_FONTSIZE 水印字体大小
* @param WEB_BLOG_WATERMARK_COLOR 水印颜色
* @param WEB_BLOG_WATERMARK_GAP 水印密集度
* @return head 下的所有动态 script
*/
private static String appendHeadScript(String blogColor,
String WEB_BLOG_WATERMARK_ENABLED,
String WEB_BLOG_WATERMARK_CONTENT,
String WEB_BLOG_WATERMARK_FONTSIZE,
String WEB_BLOG_WATERMARK_COLOR,
String WEB_BLOG_WATERMARK_GAP) {
String script = HEAD_SCRIPT_BLOG_COLOR.replaceAll("\\{BLOSSOM_WEB_BLOG_COLOR}", blogColor);
if (YesNo.YES.getValue().toString().equals(WEB_BLOG_WATERMARK_ENABLED)) {
script += HEAD_SCRIPT_WATERMARK
.replaceAll("\\{WEB_BLOG_WATERMARK_CONTENT}", WEB_BLOG_WATERMARK_CONTENT)
.replaceAll("\\{WEB_BLOG_WATERMARK_FONTSIZE}", WEB_BLOG_WATERMARK_FONTSIZE)
.replaceAll("\\{WEB_BLOG_WATERMARK_COLOR}", WEB_BLOG_WATERMARK_COLOR)
.replaceAll("\\{WEB_BLOG_WATERMARK_GAP}", WEB_BLOG_WATERMARK_GAP);
}
return script + "</head>";
}
private static void genWords() {
int value = 203012;
Date begin = DateUtils.parse("2019-01-01", DateUtils.PATTERN_YYYYMMDD);
Date end = DateUtils.parse("2023-06-30", DateUtils.PATTERN_YYYYMMDD);
for (; DateUtils.compare(begin, end) <= 0; begin = DateUtils.offsetMonth(begin, 1)) {
String dt = DateUtils.format(begin, DateUtils.PATTERN_YYYYMMDD);
value = value + RandomUtil.randomInt(0, 10000);
System.out.println(String.format("insert into blossom_stat values(null, 2, '%s' ,%s);", dt, value));
}
/**
* 添加页面顶部的文章作者和目录顶部的文章名称
*
* @param article 文章
* @param user 用户, 用户获取作者
* @return 页面顶部的文章作者和文章名称
*/
private static String appendBodyHeader(UserEntity user) {
return BODY_HEADER
.replaceAll("\\{BLOSSOM_EXPORT_HTML_AUTHOR}", user.getNickName());
}
public static void main(String[] args) {
// ArticleEntity a = new ArticleEntity();
// a.setName("123123213");
// a.setHtml("asdasd");
// System.out.println(exportHtml(a));
/**
* 添加正文
*
* @param article 文章
*/
private static String appendArticleContent(ArticleEntity article) {
return article.getHtml()
+ "\n </div>"
+ "\n </div>";
}
/**
* 添加目录
*
* @param article 文章
*/
private static String appendToc(ArticleEntity article) {
return BODY_CONTENT_TOC
.replaceAll("\\{BLOSSOM_EXPORT_HTML_ARTICLE_NAME}", article.getName());
}
}

View File

@ -1358,6 +1358,9 @@
color: #c5c5c5;
border-bottom: 1px solid #eaeaea;
text-align: right;
position: fixed;
z-index: 999;
background-color: #fff;
}
.header .copyright {
@ -1380,7 +1383,7 @@
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start
align-items: flex-start;
}
.content .main {
@ -1388,16 +1391,19 @@
width: calc(100% - 300px);
padding: 20px;
overflow-x: hidden;
overflow-y: scroll
overflow-y: scroll;
margin-top: 30px;
}
.toc {
height: 100%;
width: 300px;
padding-left: 15px;
border-right: 1px solid #eeeeee;
padding-left: 10px;
padding-bottom: 10px;
border-left: 1px solid #eeeeee;
overflow-x: hidden;
overflow-y: scroll
overflow-y: scroll;
margin-top: 30px;
}
.toc > h1,
@ -1406,11 +1412,32 @@
.toc > h4,
.toc > h5,
.toc > h6 {
margin: 0;
margin-top: 0;
margin-bottom: 0;
line-height: 27px;
font-size: 15px;
font-weight: 300;
cursor: pointer;
color: #727272
color: #727272;
position: relative;
}
.toc > h1::after,
.toc > h2::after,
.toc > h3::after,
.toc > h4::after,
.toc > h5::after,
.toc > h6::after {
content: "";
position: absolute;
top: 20%;
left: -5px;
width: 2px;
height: 60%;
background: var(--bl-color-primary);
border-radius: 10px;
opacity: 0;
transition: opacity 0.1s;
}
.toc > h1:hover,
@ -1419,37 +1446,42 @@
.toc > h4:hover,
.toc > h5:hover,
.toc > h6:hover {
color: #3b3b3b
color: var(--bl-color-primary);
}
.toc-h1 {
margin-top: 5px !important;
padding-top: 5px
.toc > h1:hover::after,
.toc > h2:hover::after,
.toc > h3:hover::after,
.toc > h4:hover::after,
.toc > h5:hover::after,
.toc > h6:hover::after {
opacity: 1;
}
.toc-h2 {
padding-left: 10px
margin-left: 10px;
}
.toc-h3 {
padding-left: 20px
margin-left: 20px;
}
.toc-h4 {
padding-left: 30px
margin-left: 30px;
}
.toc-h5 {
padding-left: 40px
margin-left: 40px;
}
.toc-h6 {
padding-left: 50px
margin-left: 50px;
}
</style>
<!--blossom styles-->
<style type="text/css">
:root {
--bl-color-primary: rgb(104, 104, 104);
--bl-editor-color: #707070;
--bl-editor-bg-color: #FFFFFF;
--bl-editor-gutters-bg-color: #f5f5f5;
@ -1587,6 +1619,7 @@
background-color: var(--bl-preview-bg-color);
line-height: 23px;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
position: relative;
}
.bl-preview * {
@ -1594,7 +1627,7 @@
}
.bl-preview a {
color: #ad8cf2;
color: var(--bl-color-primary);
font-weight: bold;
}
@ -2006,7 +2039,6 @@
for (let i = 0; i < heads.length; i++) {
let head = heads[i];
let tocHead = document.createElement(head.localName);
console.log(head.id);
tocHead.setAttribute('class', `toc-${head.localName}`);
tocHead.innerText = head.innerText;
tocHead.onclick = function () {
@ -2014,26 +2046,23 @@
if (ele == null || ele == undefined) {
return
}
ele.parentNode.scrollTop = ele.offsetTop - 30
// ele.parentNode.scrollTop = ele.offsetTop - 30
ele.scrollIntoView()
};
tocContainer.appendChild(tocHead)
}
}
function onHtmlEventDispatch(t, _ty, event, type, data) {
// copy pre code
if (type === 'copyPreCode') {
let code = document.getElementById(data)
if (code) {
// navigator.clipboard.writeText(code.innerText)
// navigator clipboard 需要https等安全上下文
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard 向剪贴板写文本
return navigator.clipboard.writeText(code.innerText)
} else {
// 创建text area
let textArea = document.createElement('textarea')
textArea.value = code.innerText
// 使text area不在viewport同时设置不可见
textArea.style.position = 'absolute'
textArea.style.opacity = '0'
textArea.style.left = '-999999px'
@ -2051,6 +2080,5 @@
}
}
</script>
</head>

View File

@ -61,7 +61,8 @@
'',
cre_time,
now(),
user_id
user_id,
now()
from blossom_article_recycle
where id = #{id})
</insert>

View File

@ -422,6 +422,11 @@ CREATE TABLE IF NOT EXISTS `blossom_article_view`
ROW_FORMAT = DYNAMIC COMMENT ='文章访问记录ArticleView';
-- ----------------------------
-- since: 1.16.0
-- ----------------------------
alter table blossom_article_view
modify ip varchar(51) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL comment '地址,IPV4/IPV6';
-- ----------------------------
-- Records of blossom_article_view
-- ----------------------------

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,11 +14,11 @@
<groupId>com.blossom</groupId>
<artifactId>blossom-backend</artifactId>
<version>1.15.0-SNAPSHOT</version>
<version>1.16.0-SNAPSHOT</version>
<!-- 版本控制 -->
<properties>
<revision>1.15.0-SNAPSHOT</revision>
<revision>1.16.0-SNAPSHOT</revision>
<!-- 编译设置 -->
<java.version>1.8</java.version>

View File

@ -1,12 +1,12 @@
{
"name": "blossom",
"version": "1.15.0",
"version": "1.16.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "blossom",
"version": "1.15.0",
"version": "1.16.0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {

View File

@ -1,7 +1,7 @@
{
"name": "blossom",
"productName": "Blossom",
"version": "1.15.0",
"version": "1.16.0",
"description": "A markdown editor",
"license": "MIT",
"main": "./out/main/index.js",
@ -75,4 +75,4 @@
"vue": "^3.4.5",
"vue-tsc": "^2.0.10"
}
}
}

View File

@ -385,7 +385,7 @@
<br />
<span style="font-size: 13px">加载中...</span>
</div>
<div class="html-loading-version">Blossom | v1.15.0</div>
<div class="html-loading-version">Blossom | v1.16.0</div>
</div>
</div>
</div>

View File

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

View File

@ -20,9 +20,9 @@ const DEFAULT_LIGHT = {
'--el-color-primary-light-7': 'rgba(119, 150, 73, 0.3)',
'--el-color-primary-light-8': 'rgba(119, 150, 73, 0.2)',
'--el-color-primary-light-9': 'rgba(119, 150, 73, 0.1)',
'--bl-tag-color-open': '#A9BC8E',
'--bl-tag-color-subject': '#89A85A',
'--bl-tag-color-toc': '#C5C5C5',
'--bl-tag-color-open': '#bdc3c7',
'--bl-tag-color-subject': '#7f8c8d',
'--bl-tag-color-toc': '#c5c5c5',
'--bl-todo-wait-color': '#C7D0D6',
'--bl-todo-proc-color': '#ecd452',
'--bl-todo-comp-color': '#c0d695',

View File

@ -3,27 +3,29 @@
<AppHeader simple></AppHeader>
</div>
<div class="article-view-window-root">
<div class="preview bl-preview" :style="editorStyle" v-html="article?.html" ref="WindowPreviewRef" style="margin-right: 5px"></div>
<el-backtop target=".preview" :right="350" :bottom="50">
<div class="iconbl bl-a-doubleonline-line backtop"></div>
</el-backtop>
<!-- the toc -->
<div class="bl-preview-toc-block">
<div class="toc-subtitle">{{ article?.name }}</div>
<div class="toc-subtitle">
<span class="iconbl bl-pen-line"></span> {{ article?.words }} | <span class="iconbl bl-read-line"></span> {{ article?.uv }} |
<span class="iconbl bl-like-line"></span> {{ article?.likes }}
<div class="doc-info">
<div class="doc-name">{{ article?.name }}</div>
<div class="doc-subtitle">
<span class="iconbl bl-pen-line"></span> {{ article?.words }} | <span class="iconbl bl-read-line"></span> {{ article?.uv }} |
<span class="iconbl bl-like-line"></span> {{ article?.likes }}
</div>
<div class="doc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 公开 {{ article?.openTime }}</div>
<div class="doc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 修改 {{ article?.updTime }}</div>
<div class="toc-title">目录</div>
</div>
<div class="toc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 公开 {{ article?.openTime }}</div>
<div class="toc-subtitle"><span class="iconbl bl-a-clock3-line"></span> 修改 {{ article?.updTime }}</div>
<div class="toc-title">目录</div>
<div class="toc-content">
<div v-for="toc in tocs" :key="toc.id" :class="[toc.clazz]" @click="toScroll(toc.id)">
<div v-for="toc in tocs" :key="toc.id" :class="['toc-item', toc.clazz]" @click="toScroll(toc.id)">
{{ toc.content }}
</div>
</div>
</div>
<div class="preview bl-preview" :style="editorStyle" v-html="article?.html" ref="WindowPreviewRef"></div>
<el-backtop target=".preview" :right="50" :bottom="50">
<div class="iconbl bl-a-doubleonline-line backtop"></div>
</el-backtop>
</div>
</template>
<script setup lang="ts">

View File

@ -4,10 +4,148 @@
transition:
width 0.2s,
height 0.2s;
}
// 块的目录, 用在新窗口中
.bl-preview-toc-block {
@include box(400px, 100%);
color: #5e5e5e;
border-left: 1px solid var(--bl-preview-toc-border-color);
padding-right: 5px;
.doc-info {
.doc-name {
color: #ababab;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-bottom: 10px;
}
.doc-subtitle {
@include flex(row, flex-start, center);
@include font(12px);
font-family: 'Jetbrains Mono';
width: 100%;
color: #ababab;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
white-space: pre;
margin-top: 5px;
}
}
.toc-title {
@include font(25px, 700);
width: 100%;
border-top: 1px solid var(--bl-preview-toc-border-color);
margin-top: 10px;
padding-top: 8px;
margin-bottom: 8px;
}
.toc-content {
@include box(100%, calc(100% - 130px));
@include font(14px);
padding-bottom: 20px;
padding-left: 5px;
overflow-y: scroll;
.toc-item {
width: auto;
padding: 4px 0;
color: #5e5e5e;
position: relative;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
&::after {
content: '';
position: absolute;
top: 20%;
left: -5px;
width: 2px;
height: 60%;
background: var(--el-color-primary-light-4);
border-radius: 10px;
opacity: 0;
transition: opacity 0.1s;
}
&:hover::after {
opacity: 1;
}
}
.toc-2 {
margin-left: 10px;
}
.toc-3 {
margin-left: 20px;
}
.toc-4 {
margin-left: 30px;
}
.toc-5 {
margin-left: 40px;
}
.toc-6 {
margin-left: 50px;
}
}
}
@media screen and (max-height: 800px) {
.bl-preview-toc-absolute {
max-height: 500px !important;
background-color: red;
.toc-content {
max-height: calc(500px - 150px - 60px - 20px) !important;
}
}
}
// 浮动的目录, 用在编辑器中
.bl-preview-toc-absolute {
@include font(12px);
@include themeShadow(2px 4px 7px 2px rgba(49, 49, 49, 0.3), 2px 4px 7px 2px rgb(28, 28, 28));
position: absolute;
top: 50px;
right: 50px;
padding-top: 0px;
background-color: #4d4d4dbd;
border-radius: 10px;
z-index: 2002;
max-height: 700px;
overflow: hidden;
user-select: none;
.toc-title {
@include box(100%, 40px);
@include font(15px, 700);
@include themeColor(#ffffff, #cdcdcd);
padding-top: 10px;
cursor: move;
}
.toc-content {
@include themeColor(#e2e2e2, #bcbcbc);
overflow-y: scroll;
width: 100%;
max-height: calc(700px - 150px - 60px - 20px);
.toc-1 {
@include themeBorder(2px, #a3a3a3, #a3a3a3, 'top');
}
.toc-1,
.toc-2,
.toc-3,
@ -56,86 +194,6 @@
padding-left: 50px;
}
}
}
// 块的目录, 用在新窗口中
.bl-preview-toc-block {
@include box(400px, 100%);
color: #5e5e5e;
border-right: 3px solid var(--bl-preview-toc-border-color);
.toc-title {
@include box(100%, 50px);
@include font(30px, 700);
border-top: 3px solid var(--bl-preview-toc-border-color);
margin-top: 10px;
padding: 10px 0;
}
.toc-subtitle {
@include box(100%, 20px);
@include font(12px);
color: #ababab;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
white-space: pre;
}
.toc-content {
@include box(100%, calc(100% - 130px));
@include font(13px);
padding-bottom: 10px;
.toc-1 {
border-top: 2px solid var(--bl-preview-toc-border-color);
}
}
}
@media screen and (max-height: 800px) {
.bl-preview-toc-absolute {
max-height: 500px !important;
background-color: red;
.toc-content {
max-height: calc(500px - 150px - 60px - 20px) !important;
}
}
}
// 浮动的目录, 用在编辑器中
.bl-preview-toc-absolute {
@include font(12px);
@include themeShadow(2px 4px 7px 2px rgba(49, 49, 49, 0.3), 2px 4px 7px 2px rgb(28, 28, 28));
position: absolute;
top: 50px;
right: 50px;
padding-top: 0px;
background-color: #4d4d4dbd;
border-radius: 10px;
z-index: 2002;
max-height: 700px;
overflow: hidden;
user-select: none;
.toc-title {
@include box(100%, 40px);
@include font(15px, 700);
@include themeColor(#ffffff, #cdcdcd);
padding-top: 10px;
cursor: move;
}
.toc-content {
@include themeColor(#e2e2e2, #bcbcbc);
width: 100%;
max-height: calc(700px - 150px - 60px - 20px);
.toc-1 {
@include themeBorder(2px, #a3a3a3, #a3a3a3, 'top');
}
}
.img-title {
@include box(100%, 20px);
@ -196,9 +254,4 @@
.bl-preview-toc-absolute.is-expand-close {
@include box(50px, 40px);
// .toc-title {
// span {
// display: none;
// }
// }
}

View File

@ -80,7 +80,7 @@
<li v-for="ref in references">
<bl-row>
<span style="width: 180px">{{ ref.name }}</span
>: <a :href="ref.url">{{ ref.url }}</a>
>: <a :href="ref.url" target="_blank">{{ ref.url }}</a>
</bl-row>
</li>
</ol>

View File

@ -46,7 +46,7 @@
@change="changeBlogColor" />
<div class="color-item" v-for="color in colors" :key="color" :style="{ backgroundColor: color }" @click="changeBlogColor(color)"></div>
</bl-row>
<div class="conf-tip">博客主题色主要影响专题样式颜色以及文章中的链接颜色</div>
<div class="conf-tip">博客主题色主要影响专题样式颜色以及文章中的链接颜色以及文章临时访问中的链接颜色</div>
</el-form-item>
<el-form-item label="博客名称">
@ -88,7 +88,7 @@
v-model="userParamForm.WEB_BLOG_WATERMARK_ENABLED"
@change="(cur: boolean) => updParam('WEB_BLOG_WATERMARK_ENABLED', cur ? '1' : '0')" />
</bl-row>
<div class="conf-tip">是否开启博客文章背景文字水印</div>
<div class="conf-tip">是否开启博客文章以及文章临时访问中的背景文字水印</div>
<div class="conf-watermark" v-if="userParamForm.WEB_BLOG_WATERMARK_ENABLED">
<bl-row>
<bl-row width="60%">

View File

@ -73,7 +73,7 @@
<el-form-item label="和风天气 Key">
<el-input size="default" v-model="serverParamForm.HEFENG_KEY" @change="(cur: any) => updParam('HEFENG_KEY', cur)"></el-input>
<div class="conf-tip">
和风天气的 API KEY申请方式请查看<a href="https://www.wangyunf.com/blossom-doc/guide/hefeng.html">和风天气配置文档</a
和风天气的 API KEY申请方式请查看<a href="https://www.wangyunf.com/blossom-doc/guide/hefeng.html" target="_blank">和风天气配置文档</a
>修改后点击首页天气右上角的刷新按钮 <span class="iconbl bl-refresh-smile"></span> 获取最新天气
</div>
</el-form-item>

View File

@ -1,12 +1,12 @@
{
"name": "blossom-web",
"version": "1.15.0",
"version": "1.16.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "blossom-web",
"version": "1.15.0",
"version": "1.16.0",
"dependencies": {
"axios": "^1.6.8",
"echarts": "^5.5.0",

View File

@ -1,6 +1,6 @@
{
"name": "blossom-web",
"version": "1.15.0",
"version": "1.16.0",
"private": true,
"scripts": {
"dev": "vite",

View File

@ -1,5 +1,5 @@
const blossom = {
SYS: { VERSION: 'v1.15.0' }
SYS: { VERSION: 'v1.16.0' }
}
export default blossom

File diff suppressed because it is too large Load Diff