Merge branch 'refctor-param' into dev

This commit is contained in:
xiaozzzi 2023-12-27 12:21:21 +08:00
commit 281d55b0ee
50 changed files with 1373 additions and 220 deletions

View File

@ -80,7 +80,7 @@ docker compose -f docker/compose/blossom-mysql8.yaml up -d
| Mr_tg000 | Lucky | egil | Glimpse |
| 支付宝用户-\*\*衡 | 支付宝用户-\*\*福 | 支付宝用户-\*\*盼 | 何其正 |
| -A 明 | | | |
wweeesssssssssssssswwwweewwweesssssssssssssssssssddddxzzxzxaasdasdqweqqqwertffghyq11231232
---
<h4 align="center">你可以通过以下几种方式赞助 Blossom。</h4>

View File

@ -12,62 +12,67 @@ public enum ParamEnum {
/**
* 文章的 web 端访问路径
*/
WEB_ARTICLE_URL(false,0),
WEB_ARTICLE_URL(false, 0,""),
/**
* 文章日志过期天数
*/
ARTICLE_LOG_EXP_DAYS(false,0),
ARTICLE_LOG_EXP_DAYS(false, 0,""),
/**
* 文章回收站过期天数
*/
ARTICLE_RECYCLE_EXP_DAYS(false,0),
ARTICLE_RECYCLE_EXP_DAYS(false, 0,""),
/**
* 和风天气KEY
*/
HEFENG_KEY(true,20),
HEFENG_KEY(true, 20,""),
/**
* GITEE key
*/
GITEE_ACCESS_TOKEN(true,20),
GITEE_ACCESS_TOKEN(true, 20,""),
/**
* 备份路径
*/
BACKUP_PATH(false,0),
BACKUP_PATH(false, 0,""),
/**
* 备份过期天数
*/
BACKUP_EXP_DAYS(false,0),
BACKUP_EXP_DAYS(false, 0,""),
/**
* BLOSSOM 对象存储地址
*/
BLOSSOM_OBJECT_STORAGE_DOMAIN(false, 0,"http://www.xxx.com/"),
/**
* 服务器JWT加密字符串
*/
SERVER_JWT_SECRET(true,9999),
SERVER_JWT_SECRET(true, 9999,""),
/**
* 过期时间 - 服务器
*/
SERVER_MACHINE_EXPIRE(false,0),
SERVER_MACHINE_EXPIRE(false, 0,""),
/**
* 过期时间 - 域名
*/
SERVER_DOMAIN_EXPIRE(false,0),
SERVER_DOMAIN_EXPIRE(false, 0,""),
/**
* 过期时间 - HTTPS 证书
*/
SERVER_HTTPS_EXPIRE(false,0),
SERVER_HTTPS_EXPIRE(false, 0,""),
/**
* 过期时间 - 数据库
*/
SERVER_DATABASE_EXPIRE(false,0),
SERVER_DATABASE_EXPIRE(false, 0,""),
;
/**
@ -82,8 +87,15 @@ public enum ParamEnum {
@Getter
private final Integer maskingLength;
ParamEnum(Boolean masking, Integer maskingLength) {
/**
* 默认值
*/
@Getter
private final String defaultValue;
ParamEnum(Boolean masking, Integer maskingLength, String defaultValue) {
this.masking = masking;
this.maskingLength = maskingLength;
this.defaultValue = defaultValue;
}
}

View File

@ -10,6 +10,8 @@ import com.blossom.backend.base.param.pojo.ParamEntity;
import com.blossom.backend.base.param.pojo.ParamUpdReq;
import com.blossom.common.base.exception.XzException500;
import com.blossom.common.base.util.BeanUtil;
import com.blossom.common.iaas.IaasEnum;
import com.blossom.common.iaas.OSManager;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;
@ -32,7 +34,11 @@ import java.util.Map;
public class ParamService extends ServiceImpl<ParamMapper, ParamEntity> {
private static final Map<String, ParamEntity> CACHE = new HashMap<>(20);
private final OSManager osManager;
/**
* 初始化系统参数
*/
@EventListener(ApplicationStartedEvent.class)
public void refresh() {
log.info("[ BASE] 初始化系统参数缓存");
@ -76,10 +82,39 @@ public class ParamService extends ServiceImpl<ParamMapper, ParamEntity> {
param.setParamValue(StrUtil.hide(param.getParamValue(), 0, Math.min(param.getParamValue().length(), name.getMaskingLength())));
}
result.put(name.name(), param.getParamValue());
// domain 涉及多个地方配置, 需要特别处理
if (name.equals(ParamEnum.BLOSSOM_OBJECT_STORAGE_DOMAIN)) {
result.put(name.name(), getDomain());
}
}
return result;
}
/**
* 获取对象存储的地址前缀, 优先从数据库中获取, 如果数据库中配置的是默认值,
*
* @return 对象存储的地址
* @since 1.12.0
*/
public String getDomain() {
if (osManager.getProp().getOsType().equals(IaasEnum.BLOSSOM.getType())) {
String domain = getValue(ParamEnum.BLOSSOM_OBJECT_STORAGE_DOMAIN).getParamValue();
// 如果后台配置的图片地址为默认值
if (ParamEnum.BLOSSOM_OBJECT_STORAGE_DOMAIN.getDefaultValue().equals(domain)) {
// 如果配置文件中未配置地址
if (StrUtil.isNotBlank(osManager.getDomain())) {
domain = osManager.getDomain();
}
}
if (StrUtil.isBlank(domain)) {
throw new XzException500("文件地址 [" + ParamEnum.BLOSSOM_OBJECT_STORAGE_DOMAIN.name() + "] 配置错误");
}
return domain;
}
return osManager.getDomain();
}
/**
* 修改参数
*/

View File

@ -0,0 +1,56 @@
package com.blossom.backend.base.paramu;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.paramu.pojo.UserParamUpdReq;
import com.blossom.common.base.pojo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 用户参数配置
*
* @since 1.12.0
*/
@Slf4j
@RestController
@RequestMapping("/user/param")
public class UserParamController {
@Autowired
private UserParamService baseService;
/**
* 用户参数列表
*
* @apiNote 敏感参数会进行脱敏
*/
@GetMapping("/list")
public R<Map<String, String>> list() {
Map<String, String> param = baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
return R.ok(param);
}
/**
* 修改用户参数
*/
@PostMapping("/upd")
public R<Map<String, String>> upd(@Validated @RequestBody UserParamUpdReq req) {
req.setUserId(AuthContext.getUserId());
baseService.update(req);
baseService.refresh();
return R.ok(baseService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values()));
}
/**
* 刷新用户配置
*/
@PostMapping("/refresh")
public R<?> paramRefresh() {
baseService.refresh();
return R.ok();
}
}

View File

@ -0,0 +1,60 @@
package com.blossom.backend.base.paramu;
import lombok.Getter;
/**
* 参数枚举
*
* @author xzzz
* @since 1.12.0
*/
public enum UserParamEnum {
/**
* 文章的 web 端访问路径
*/
WEB_ARTICLE_URL(false, 0, "https://www.domain.com/blossom/#/articles?articleId="),
/**
* 博客 LOGO 地址
*/
WEB_LOGO_URL(false, 0, ""),
/**
* 博客名称
*/
WEB_LOGO_NAME(false, 0, ""),
/**
* 博客的公网安备号
*/
WEB_GONG_WANG_AN_BEI(false, 0, ""),
/**
* 博客备案号
*/
WEB_IPC_BEI_AN_HAO(false, 0, ""),
/**
* 是否提示博客地址配置有误
*/
WEB_BLOG_URL_ERROR_TIP_SHOW(false, 0, ""),
;
/**
* 是否脱敏
*/
@Getter
private final Boolean masking;
/**
* 脱敏长度
*/
@Getter
private final Integer maskingLength;
@Getter
private final String defaultValue;
UserParamEnum(Boolean masking, Integer maskingLength, String defaultValue) {
this.masking = masking;
this.maskingLength = maskingLength;
this.defaultValue = defaultValue;
}
}

View File

@ -0,0 +1,32 @@
package com.blossom.backend.base.paramu;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.blossom.backend.base.paramu.pojo.UserParamEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 用户参数
*
* @since 1.12.0
*/
@Mapper
public interface UserParamMapper extends BaseMapper<UserParamEntity> {
/**
* 参数是否存在
*
* @param userId 用户ID
* @param paramName 参数值
*/
UserParamEntity selectByUserId(@Param("userId") Long userId, @Param("paramName") String paramName);
/**
* 修改用户参数
*
* @param userId 用户ID
* @param paramName 参数名称
* @param paramValue 参数值
*/
void updByParamName(@Param("userId") Long userId, @Param("paramName") String paramName, @Param("paramValue") String paramValue);
}

View File

@ -0,0 +1,146 @@
package com.blossom.backend.base.paramu;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamMapper;
import com.blossom.backend.base.param.pojo.ParamEntity;
import com.blossom.backend.base.paramu.pojo.UserParamEntity;
import com.blossom.backend.base.paramu.pojo.UserParamUpdReq;
import com.blossom.backend.base.user.UserService;
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 org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用户参数
*/
@Service
@AllArgsConstructor
public class UserParamService extends ServiceImpl<UserParamMapper, UserParamEntity> {
private final ParamMapper paramMapper;
private final UserService userService;
private static final Map<Long, Map<String, UserParamEntity>> CACHE = new HashMap<>(20);
/**
* 启动时刷新数据
*/
@EventListener(ApplicationStartedEvent.class)
public void refresh() {
CACHE.clear();
List<UserEntity> users = userService.listAll();
// 初始化所有用户的配置参数
for (UserEntity user : users) {
initUserParams(user.getId());
}
}
/**
* 获取博客地址
* <p> 1.12.0 版本前, 博客的地址配置在 SysParam
* <p>如果 SysParam 配置中有值, 则使用 SysParam 中的值, 否则使用默认值
*/
public String getWebArticleUrl() {
List<ParamEntity> sysParams = paramMapper.selectList(new QueryWrapper<>());
String WEB_ARTICLE_URL = "";
// 获取数据库中的配置的博客地址
for (ParamEntity param : sysParams) {
if (param.getParamName().equals(ParamEnum.WEB_ARTICLE_URL.name())) {
WEB_ARTICLE_URL = param.getParamValue();
break;
}
}
// 如果数据库中没有配置, 则使用默认值
if (StrUtil.isBlank(WEB_ARTICLE_URL)) {
WEB_ARTICLE_URL = UserParamEnum.WEB_ARTICLE_URL.getDefaultValue();
}
return WEB_ARTICLE_URL;
}
/**
* 初始化用户参数
*
* @param userId 用户ID
*/
public void initUserParams(Long userId) {
Map<String, UserParamEntity> params = new HashMap<>(UserParamEnum.values().length);
for (UserParamEnum param : UserParamEnum.values()) {
// TODO 一次性查出数据以提高效率
UserParamEntity storeParam = baseMapper.selectByUserId(userId, param.name());
// 该用户不存在该参数, 则新增参数
if (storeParam == null) {
UserParamEntity istParam = new UserParamEntity();
istParam.setUserId(userId);
istParam.setParamName(param.name());
istParam.setParamValue(param.getDefaultValue());
// 博客的地址需要特别兼容, 用于适配 1.12.0 之前的版本
if (param.name().equals(UserParamEnum.WEB_ARTICLE_URL.name())) {
istParam.setParamValue(getWebArticleUrl());
}
baseMapper.insert(istParam);
params.put(param.name(), istParam);
} else {
params.put(param.name(), storeParam);
}
}
CACHE.put(userId, params);
}
/**
* 根据参数名称获取参数
*
* @param name 参数名称
*/
public UserParamEntity getValue(Long userId, UserParamEnum name) {
UserParamEntity param = CACHE.get(userId).get(name.name());
XzException500.throwBy(ObjUtil.isNull(param), String.format("缺失系统参数[%s], 请检查系统参数配置", name.name()));
return param;
}
/**
* 根据多个名称查询
*
* @param masking 返回数据是否脱敏
* @param names 参数名称
* @return 返回参数 map
*/
public Map<String, String> selectMap(Long userId, boolean masking, UserParamEnum... names) {
if (ArrayUtil.isEmpty(names)) {
return new HashMap<>(0);
}
Map<String, String> result = new HashMap<>(names.length);
for (UserParamEnum name : names) {
ParamEntity param = BeanUtil.toObj(CACHE.get(userId).get(name.name()), ParamEntity.class);
XzException500.throwBy(ObjUtil.isNull(param), "缺失所有系统参数, 请检查用户参数配置[BASE_USER_PARAM]中是否包含数据");
if (masking && name.getMasking()) {
param.setParamValue(StrUtil.hide(param.getParamValue(), 0, Math.min(param.getParamValue().length(), name.getMaskingLength())));
}
result.put(name.name(), param.getParamValue());
}
return result;
}
/**
* 修改参数
*/
@Transactional(rollbackFor = Exception.class)
public void update(UserParamUpdReq req) {
baseMapper.updByParamName(req.getUserId(), req.getParamName(), req.getParamValue());
}
}

View File

@ -0,0 +1,39 @@
package com.blossom.backend.base.paramu.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 用户参数
*
* @since 1.12.0
*/
@Data
@TableName("base_user_param")
public class UserParamEntity {
/**
* ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 参数名称
*/
private String paramName;
/**
* 参数值
*/
private String paramValue;
/**
* 修改时间
*/
private Date updTime;
}

View File

@ -0,0 +1,30 @@
package com.blossom.backend.base.paramu.pojo;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 修改参数
*
* @since 1.12.0
*/
@Data
public class UserParamUpdReq {
/**
* 参数名称
*/
@NotBlank(message = "参数名称为必填项")
private String paramName;
/**
* 参数值
*/
private String paramValue;
/**
* 用户ID
*/
private Long userId;
}

View File

@ -1,15 +1,17 @@
package com.blossom.backend.base.user;
import cn.hutool.core.util.ObjUtil;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.paramu.UserParamEnum;
import com.blossom.backend.base.paramu.UserParamService;
import com.blossom.backend.base.sys.SysService;
import com.blossom.backend.base.user.pojo.*;
import com.blossom.backend.config.BlConstants;
import com.blossom.backend.server.article.draft.pojo.ArticleStatRes;
import com.blossom.backend.server.article.stat.ArticleStatService;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.common.base.exception.XzException400;
import com.blossom.common.base.exception.XzException404;
import com.blossom.common.base.pojo.R;
@ -32,6 +34,7 @@ public class UserController {
private final ArticleStatService articleService;
private final SysService sysService;
private final ParamService paramService;
private final UserParamService userParamService;
/**
* 用户信息
@ -42,6 +45,8 @@ public class UserController {
user.setOsRes(sysService.getOsConfig());
Map<String, String> paramMap = paramService.selectMap(true, ParamEnum.values());
user.setParams(paramMap);
Map<String, String> userParamMap = userParamService.selectMap(AuthContext.getUserId(), true, UserParamEnum.values());
user.setUserParams(userParamMap);
return R.ok(user);
}

View File

@ -63,4 +63,9 @@ public class BlossomUserRes extends AbstractPOJO implements Serializable {
* 系统参数, paramName: paramValue
*/
private Map<String, String> params;
/**
* 用户参数, paramName: paramValue
*/
private Map<String, String> userParams;
}

View File

@ -96,7 +96,8 @@ public class WebConfigurer implements WebMvcConfigurer {
.addPathPatterns("/**")
.excludePathPatterns(
"/blog/**",
"/editor/**"
"/editor/**",
"/error"
);
}
@ -110,7 +111,7 @@ public class WebConfigurer implements WebMvcConfigurer {
registry.addViewController("/editor/").setViewName("/editor/index.html");
}
/***
/**
* 静态文件处理
*/
@Override

View File

@ -6,9 +6,9 @@ import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
import com.blossom.backend.base.auth.AuthContext;
import com.blossom.backend.base.auth.annotation.AuthIgnore;
import com.blossom.backend.base.param.ParamEnum;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.base.param.pojo.ParamEntity;
import com.blossom.backend.base.paramu.UserParamEnum;
import com.blossom.backend.base.paramu.UserParamService;
import com.blossom.backend.base.paramu.pojo.UserParamEntity;
import com.blossom.backend.server.article.draft.ArticleService;
import com.blossom.backend.server.article.draft.pojo.ArticleEntity;
import com.blossom.backend.server.article.draft.pojo.ArticleInfoRes;
@ -46,7 +46,7 @@ public class ArticleOpenController {
private final ArticleService articleService;
private final ArticleOpenService openService;
private final ParamService paramService;
private final UserParamService userParamService;
/**
@ -96,7 +96,7 @@ public class ArticleOpenController {
* @param req 文章对象
*/
@PostMapping("/sync")
public R<ArticleOpenRes> sync(@Validated @RequestBody ArticleOpenSyncReq req) {
protected R<ArticleOpenRes> sync(@Validated @RequestBody ArticleOpenSyncReq req) {
openService.sync(req.getId());
ArticleOpenEntity openEntity = openService.selectById(req.getId(), false, false, false);
return R.ok(openEntity.to(ArticleOpenRes.class));
@ -110,8 +110,8 @@ public class ArticleOpenController {
*/
@GetMapping("/qrcode")
public void qrcode(@RequestParam("id") Long id, HttpServletResponse response) {
ParamEntity param = paramService.getValue(ParamEnum.WEB_ARTICLE_URL);
XzException404.throwBy(ObjUtil.isNull(param), "未配置文章公网访问链接,无法生成二维码,请在服务端配置参数 [BaseParam.WEB_ARTICLE_URL]");
UserParamEntity param = userParamService.getValue(AuthContext.getUserId(), UserParamEnum.WEB_ARTICLE_URL);
XzException404.throwBy(ObjUtil.isNull(param), "请先在设置中配置博客访问路径");
final String url = param.getParamValue() + id;
BufferedImage bfi = QrCodeUtil.generate(url, new QrConfig(200, 200));
response.setContentType("image/png");

View File

@ -22,7 +22,6 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
@ -82,15 +81,26 @@ public class PictureBlosController {
return filename;
}
/**
* 检查文件路径
*
* @param filename 文件名称
*/
private void checkFilename(String filename) {
if (StrUtil.isBlank(filename)) {
throw new XzException400("未知文件");
}
if (!filename.startsWith(osManager.getDefaultPath())) {
// 如果图片前缀不是配置的前缀则去数据库查询文件是否上传过
log.error("路径必须以配置的前缀开头");
throw new XzException400("无法访问");
}
if (!filename.startsWith("/")) {
log.error("路径必须是绝对路径");
throw new XzException400("无法访问");
}
if (!FileUtil.exist(filename)) {
log.error("文件不存在");
throw new XzException400("未知文件");
}
}

View File

@ -5,6 +5,7 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.base.param.ParamService;
import com.blossom.backend.server.article.reference.ArticleReferenceService;
import com.blossom.backend.server.folder.FolderService;
import com.blossom.backend.server.folder.pojo.FolderEntity;
@ -42,6 +43,7 @@ public class PictureService extends ServiceImpl<PictureMapper, PictureEntity> {
private FolderService folderService;
private ArticleReferenceService articleReferenceService;
private OSManager osManager;
private ParamService paramService;
@Autowired
public void setFolderService(FolderService folderService) {
@ -58,6 +60,11 @@ public class PictureService extends ServiceImpl<PictureMapper, PictureEntity> {
this.osManager = osManager;
}
@Autowired
public void setParamService(ParamService paramService) {
this.paramService = paramService;
}
/**
* 分页列表
*/
@ -149,7 +156,7 @@ public class PictureService extends ServiceImpl<PictureMapper, PictureEntity> {
}
pic.setSize(file.getSize());
final String domain = osManager.getDomain();
final String domain = paramService.getDomain();
final String rootPath = osManager.getDefaultPath();
final String uid = "/U" + userId;
final String pname = "/" + pic.getName();
@ -245,4 +252,5 @@ public class PictureService extends ServiceImpl<PictureMapper, PictureEntity> {
public PictureStatRes stat(Long userId, Long pid) {
return baseMapper.stat(userId, pid);
}
}

View File

@ -16,6 +16,15 @@ logging:
com.baomidou.dynamic.datasource.DynamicRoutingDataSource: warn
com.zaxxer.hikari.HikariDataSource: warn
org.apache.coyote.http11.Http11NioProtocol: warn
com:
blossom:
backend:
server:
article:
log: info
draft:
ArticleMapper:
updContentById: info
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ project ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
project:
@ -75,7 +84,7 @@ project:
os-type: blossom
blos:
# 请以 /pic 结尾, 如果你在 nginx 中配置有代理, 注意别忘了添加你的代理路径
domain: "http://localhost:9999/pic/"
domain:
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
default-path: "/home/bl/img/"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -76,7 +76,7 @@ project:
blos:
# 请以 /pic 结尾, 如果你在 nginx 中配置有代理, 注意别忘了添加你的代理路径
# 注意:在下方示例中, /bl 即为 nginx 反向代理路径, 如果你的访问路径中不包含反向代理或路径不同, 请酌情删除或修改
domain: "https://www.wangyunf.com/bl/pic/"
domain: "http://www.xxx.com/"
# 请以 / 开头, / 结尾, 简短的路径在文章中有更好的显示效果, 过长一定程度会使文章内容混乱
default-path: "/home/bl/img/"
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ 全文搜索 ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blossom.backend.base.paramu.UserParamMapper">
<select id="selectByUserId" resultType="com.blossom.backend.base.paramu.pojo.UserParamEntity">
select *
from base_user_param
where user_id = #{userId}
and param_name = #{paramName}
</select>
<update id="updByParamName">
update base_user_param
set param_value = #{paramValue}
where param_name = #{paramName}
and user_id = #{userId}
</update>
</mapper>

View File

@ -152,6 +152,22 @@ SELECT 904,
WHERE id = 904);
-- ----------------------------
-- since: 1.12.0
-- ----------------------------
INSERT INTO base_sys_param (id, param_name, param_value, param_desc, open_state, cre_time, upd_time)
SELECT 101,
'BLOSSOM_OBJECT_STORAGE_DOMAIN',
'http://www.xxx.com/',
'BLOSSOM 对象存储地址',
1,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
WHERE NOT EXISTS (SELECT 1
FROM base_sys_param
WHERE id = 101);
-- ----------------------------
-- Table structure for base_user
-- ----------------------------

View File

@ -27,7 +27,7 @@ public enum RCode implements IRCode {
SERVER_DENIED_CUR_ENV ("40302", "当前环境无法访问该资源"),
/* ──────────────────────────────────────────── 404 ────────────────────────────────────────────────────────────*/
NOT_FOUND ("40400", "到您的请求"),
NOT_FOUND ("40400", "找到您的请求"),
/* ──────────────────────────────────────────── 500 ────────────────────────────────────────*/
INTERNAL_SERVER_ERROR ("50000", "服务器处理错误"),

View File

@ -60,6 +60,13 @@ public class BLOSManager extends AbstractOSManager implements OSManager {
return domain;
}
/**
* 上传图片, 并返回文件的访问地址
* <p>但需要注意的是, 调用方可以保存自己的文件地址前缀, 而不使用 {@link BLOSManager#getDomain()},
*
* @param filename 文件名
* @param inputStream 输入流
*/
@Override
public String put(String filename, InputStream inputStream) {
File file = FileUtil.newFile(filename);

View File

@ -1,4 +1,4 @@
import { app, shell, ipcMain, BrowserWindow, Menu, IpcMainEvent, Tray, HandlerDetails } from 'electron'
import { app, shell, ipcMain, BrowserWindow, Menu, IpcMainEvent, Tray, HandlerDetails, session } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is, platform } from '@electron-toolkit/utils'
import icon from '../../resources/imgs/icon.ico?asset'
@ -51,6 +51,15 @@ if (!gotTheLock) {
createMainWindow()
}, 300)
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['frame-ancestors *']
}
})
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
@ -432,8 +441,8 @@ export const initOnWindow = (window: BrowserWindow) => {
*/
window.webContents.setWindowOpenHandler((details: HandlerDetails): any => {
let url = details.url as string
if (blossomUserinfo && url.startsWith(blossomUserinfo.params.WEB_ARTICLE_URL)) {
let articleId: string = url.replaceAll(blossomUserinfo.params.WEB_ARTICLE_URL, '')
if (blossomUserinfo && url.startsWith(blossomUserinfo.userParams.WEB_ARTICLE_URL)) {
let articleId: string = url.replaceAll(blossomUserinfo.userParams.WEB_ARTICLE_URL, '')
createNewWindow('article', articleId, Number(articleId))
} else {
shell.openExternal(url)
@ -450,8 +459,8 @@ export const initOnWindow = (window: BrowserWindow) => {
const interceptorATag = (e: Event, url: string): boolean => {
e.preventDefault()
let innerUrl = url
if (blossomUserinfo && innerUrl.startsWith(blossomUserinfo.params.WEB_ARTICLE_URL)) {
let articleId: string = innerUrl.replaceAll(blossomUserinfo.params.WEB_ARTICLE_URL, '')
if (blossomUserinfo && innerUrl.startsWith(blossomUserinfo.userParams.WEB_ARTICLE_URL)) {
let articleId: string = innerUrl.replaceAll(blossomUserinfo.userParams.WEB_ARTICLE_URL, '')
createNewWindow('article', articleId, Number(articleId))
} else if (!is.dev) {
shell.openExternal(url)

View File

@ -29,6 +29,33 @@ export const paramRefreshApi = (): Promise<R<any>> => {
return rq.post<R<any>>('/sys/param/refresh', {})
}
// 用户参数
/**
*
* @returns
*/
export const userParamListApi = (): Promise<R<any>> => {
return rq.get<R<any>>('/user/param/list', {})
}
/**
*
* @returns
*/
export const userParamUpdApi = (data: object): Promise<R<any>> => {
return rq.post<R<any>>('/user/param/upd', data)
}
/**
*
* @param data form
* @returns
*/
export const userParamRefreshApi = (): Promise<R<any>> => {
return rq.post<R<any>>('/user/param/refresh', {})
}
/**
*
* @param data form
@ -506,8 +533,8 @@ export const pictureStatApi = (params?: object): Promise<R<any>> => {
/**
*
* @param data
* @returns
* @param data
* @returns
*/
export const pictureTransferApi = (data?: object): Promise<R<any>> => {
return rq.post<R<any>>('/picture/transfer', data)

View File

@ -102,21 +102,28 @@ export class Request {
* @returns
*/
(err: any) => {
console.log(err)
let errorMsg = err.message
let url = ''
if (err.config) {
url = ':' + err.config.url
}
let code = err.code
let resp = err.response
if (resp && resp.data) {
errorMsg = resp.data.msg
}
if (code === 'ERR_NETWORK') {
errorMsg = '网络错误,请检查您的网络是否通畅'
}
if (err.request && err.request.status === 404) {
Notify.error('未找到您的请求, 请您检查服务器地址或应用版本!', '请求失败(404)')
Notify.error(resp.data.msg, '请求失败')
return Promise.reject(err)
}
if (code === 'ERR_NETWORK') {
Notify.error('网络错误,请检查您的网络是否通畅', '请求失败')
return Promise.reject(err)
}
if (err.request && err.request.status === 404) {
Notify.error('未找到您的请求, 请您检查服务器地址!', '请求失败(404)')
return Promise.reject(err)
}
if (err.request && err.request.status === 405) {
Notify.error(`您的请求地址可能有误, 请检查请求地址${url}`, '请求失败(405)')
return Promise.reject(err)
}
Notify.error(errorMsg, '请求失败')
return Promise.reject(err)
}
)

View File

@ -9,7 +9,7 @@ const blossom = {
//
DOC: 'https://www.wangyunf.com/blossom-doc/index',
CONTACT: 'QQ群:522359970 / Email: kuamax888@qq.com',
CONTACT: 'https://www.wangyunf.com/blossom-doc/guide/about/all.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'

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconbl"; /* Project id 4118609 */
src: url('iconfont.woff2?t=1702393359707') format('woff2'),
url('iconfont.woff?t=1702393359707') format('woff'),
url('iconfont.ttf?t=1702393359707') format('truetype');
src: url('iconfont.woff2?t=1703588816023') format('woff2'),
url('iconfont.woff?t=1703588816023') format('woff'),
url('iconfont.ttf?t=1703588816023') format('truetype');
}
.iconbl {
@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale;
}
.bl-blog:before {
content: "\e7c6";
}
.bl-caution-line:before {
content: "\ea4c";
}
.bl-a-picturecracked-line:before {
content: "\ea4b";
}
.bl-xiaoxie:before {
content: "\e603";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,27 @@
"css_prefix_text": "bl-",
"description": "",
"glyphs": [
{
"icon_id": "12579639",
"name": "blog",
"font_class": "blog",
"unicode": "e7c6",
"unicode_decimal": 59334
},
{
"icon_id": "24341306",
"name": "caution-line",
"font_class": "caution-line",
"unicode": "ea4c",
"unicode_decimal": 59980
},
{
"icon_id": "24342104",
"name": "picture cracked-line",
"font_class": "a-picturecracked-line",
"unicode": "ea4b",
"unicode_decimal": 59979
},
{
"icon_id": "11179871",
"name": "小写",

View File

@ -2,6 +2,43 @@
<div class="app-header-root">
<div class="drag">{{ tryuseComment }}</div>
<div class="window-workbench">
<el-popover
popper-class="header-popover"
ref="PopoverRef"
trigger="click"
width="350px"
:virtual-ref="ButtonRef"
:hide-after="10"
:tabindex="100"
:offset="5"
:persistent="false"
virtual-triggering>
<div class="header-config-popover">
<bl-row class="url-container" just="space-between">
<bl-col width="60px" height="60px" class="iconbl bl-blog" just="center"></bl-col>
<bl-col width="calc(100% - 70px)" height="60px" align="flex-start" just="flex-start">
<div class="name"><span>博客地址</span> <span class="iconbl bl-sendmail-line"></span></div>
<div class="url">
{{ userStore.userParams.WEB_ARTICLE_URL }}123123123123123123123123123123123123123123123123123123123123123123123123123123123123
</div>
</bl-col>
</bl-row>
<bl-row class="url-container" height="100%" style="margin-bottom: 6px">
<bl-col width="60px" height="60px" class="iconbl bl-image--line" just="center"></bl-col>
<bl-col width="calc(100% - 70px)" height="60px" align="flex-start" just="flex-start">
<div class="name">图片地址</div>
<div class="url">{{ userStore.sysParams.BLOSSOM_OBJECT_STORAGE_DOMAIN }}</div>
</bl-col>
</bl-row>
<bl-row just="space-between">
<el-button text>关闭闪烁提示</el-button>
<el-button type="primary" plain @click="showQuickSetting">快捷配置</el-button>
</bl-row>
</div>
</el-popover>
<div class="iconbl bl-blog warn-heightlight" ref="ButtonRef" v-click-outside="onClickOutside"></div>
<div class="iconbl bl-a-colorpalette-line" @click="themeStrore.show()"></div>
<el-tooltip content="查看图标" effect="light" placement="top" :show-after="1000" :hide-after="0" :auto-close="2000">
@ -27,19 +64,35 @@
<el-drawer class="web-collect-drawer" size="420" v-model="isShowWebDrawer">
<WebCollect></WebCollect>
</el-drawer>
<el-dialog
v-model="isShowQuickSetting"
width="80%"
style="height: fit-content; max-width: 800px"
:align-center="true"
:append-to-body="false"
:destroy-on-close="true"
:close-on-click-modal="false"
draggable>
<QuickSetting ref="PlanDayInfoRef"></QuickSetting>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, unref } from 'vue'
import { ClickOutside as vClickOutside } from 'element-plus'
import { toRoute } from '@renderer/router'
import { useThemeStore } from '@renderer/stores/theme'
import { windowMin, windowMax, windowHide, setBestSize } from '@renderer/assets/utils/electron'
import { windowMin, windowMax, windowHide, setBestSize, openExtenal } from '@renderer/assets/utils/electron'
import WebCollect from './WebCollect.vue'
import { isWindows, isElectron } from '@renderer/assets/utils/util'
import { isTryuse } from '@renderer/scripts/env'
import SYSTEM from '@renderer/assets/constants/system'
import { useUserStore } from '@renderer/stores/user'
import QuickSetting from '@renderer/views/index/setting/QuickSetting.vue'
const themeStrore = useThemeStore()
const userStore = useUserStore()
onMounted(() => {
window.addEventListener('resize', handleResize)
@ -72,6 +125,22 @@ const isFullScreen = ref(checkFullScreen())
const handleResize = () => {
isFullScreen.value = checkFullScreen()
}
//#region
const ButtonRef = ref()
const PopoverRef = ref()
const isShowQuickSetting = ref(false)
const onClickOutside = () => {
unref(PopoverRef).popperRef?.delayHide?.()
}
const showQuickSetting = () => {
unref(PopoverRef).popperRef?.delayHide?.()
isShowQuickSetting.value = true
}
//#endregion
</script>
<style lang="scss" scoped>
@ -79,7 +148,7 @@ const handleResize = () => {
@include box(100%, 100%);
@include flex(row, space-between, center);
$width-workbench: 220px;
$width-workbench: 240px;
$width-drag: calc(100% - #{$width-workbench});
.drag {
@ -94,14 +163,16 @@ const handleResize = () => {
.window-workbench {
@include box($width-workbench, 100%);
@include flex(row, flex-end, center);
@include flex(row, flex-end, flex-end);
color: var(--el-color-primary);
padding-right: 4px;
div {
@include box(40px, 100%);
@include box(40px, 90%);
@include flex(row, center, center);
cursor: pointer;
transition: 0.3s;
border-radius: 4px;
&:hover {
color: #fff;
@ -123,6 +194,72 @@ const handleResize = () => {
}
}
}
.warn-heightlight {
animation: animated-border 1.5s infinite;
text-shadow: 0 0 3px #e3a300;
background-color: #e3a300;
color: var(--bl-html-color);
}
@keyframes animated-border {
0% {
// box-shadow: 0 0 0 0 rgba(227, 163, 0, 0.7);
filter: drop-shadow(0 0 0 #e3a300);
}
100% {
// box-shadow: 0 0 0 10px rgba(227, 163, 0, 0);
filter: drop-shadow(0 0 30px #e3a300);
}
}
}
.header-config-popover {
@include font(13px, 300);
margin: 6px;
.url-container {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
.bl-blog,
.bl-image--line {
@include themeBg(#f5f5f5, #060404);
font-size: 28px;
border-radius: 4px;
margin-right: 10px;
}
.bl-image--line {
font-size: 33px;
}
.bl-sendmail-line {
font-size: 14px;
}
&:hover {
border: 1px solid var(--el-border-color);
}
}
.name {
width: 100%;
min-height: 20px;
text-decoration: none;
color: var(--el-text-color);
}
.url {
@include box(100%, 40px);
font-size: 12px;
padding: 2px 5px 0 5px;
overflow-y: scroll;
border-radius: 4px;
color: var(--bl-text-color-light);
}
}
</style>
@ -137,4 +274,11 @@ const handleResize = () => {
--el-drawer-padding-primary: 0;
}
}
.header-popover {
/* inset: 35.3333px 10px auto auto !important; */
padding: 0 !important;
.el-popper__arrow {
/* left: 240.333px !important; */
}
}
</style>

View File

@ -7,7 +7,7 @@ const isDemo = import.meta.env.MODE === 'tryuse'
export const storeKey = 'serverUrl'
export const usernameKey = 'username'
const initServerUrl = () => {
const initServerUrl = (): string => {
const defaultUrl = isDemo ? SYSTEM.TRY_USE.serverUrl : ''
Local.set(storeKey, defaultUrl)
return defaultUrl
@ -15,7 +15,7 @@ const initServerUrl = () => {
export const useServerStore = defineStore('serverStore', {
state: () => ({
serverUrl: Local.get(storeKey) || initServerUrl(),
serverUrl: (Local.get(storeKey) as string) || initServerUrl(),
serverUsername: Local.get(usernameKey) || ''
}),
actions: {

View File

@ -34,42 +34,110 @@ const initAuth = () => {
return auth
}
const initUserinfo = () => {
let userinfo = {
id: '',
username: '暂未登录',
nickName: '暂未登录',
avatar: '',
remark: '',
articleCount: 0,
articleWords: 0,
osRes: {
osType: '',
bucketName: '',
domain: '',
defaultPath: ''
},
params: {
WEB_ARTICLE_URL: '',
BACKUP_PATH: '',
BACKUP_EXP_DAYS: '',
ARTICLE_LOG_EXP_DAYS: '',
ARTICLE_RECYCLE_EXP_DAYS: '',
SERVER_MACHINE_EXPIRE: '',
SERVER_DATABASE_EXPIRE: '',
SERVER_HTTPS_EXPIRE: '',
SERVER_DOMAIN_EXPIRE: ''
}
const DEFAULT_USER_INFO = {
id: '',
username: '暂未登录',
nickName: '暂未登录',
avatar: '',
remark: '',
articleCount: 0,
articleWords: 0,
osRes: {
osType: '',
bucketName: '',
domain: '',
defaultPath: ''
},
params: {
/**
* @deprecated 使, userParams.WEB_ARTICLE_URL, 使
*/
WEB_ARTICLE_URL: '',
BACKUP_PATH: '',
BACKUP_EXP_DAYS: '',
ARTICLE_LOG_EXP_DAYS: '',
ARTICLE_RECYCLE_EXP_DAYS: '',
HEFENG_KEY: '',
BLOSSOM_OBJECT_STORAGE_DOMAIN: '',
SERVER_MACHINE_EXPIRE: '',
SERVER_DATABASE_EXPIRE: '',
SERVER_HTTPS_EXPIRE: '',
SERVER_DOMAIN_EXPIRE: ''
},
userParams: {
WEB_ARTICLE_URL: '',
WEB_IPC_BEI_AN_HAO: '',
WEB_LOGO_NAME: '',
WEB_LOGO_URL: '',
WEB_GONG_WANG_AN_BEI: '',
WEB_BLOG_URL_ERROR_TIP_SHOW: ''
}
Local.set(userinfoKey, userinfo)
return userinfo
}
const timeoutMs = 800
export type Userinfo = typeof DEFAULT_USER_INFO
/**
*
*/
const initUserinfo = (): Userinfo => {
// let userinfo = {
// id: '',
// username: '暂未登录',
// nickName: '暂未登录',
// avatar: '',
// remark: '',
// articleCount: 0,
// articleWords: 0,
// osRes: {
// osType: '',
// bucketName: '',
// domain: '',
// defaultPath: ''
// },
// params: {
// /**
// * @deprecated 该字段已不使用, 博客地址改用 userParams.WEB_ARTICLE_URL, 使用该地址会报错
// */
// WEB_ARTICLE_URL: '',
// BACKUP_PATH: '',
// BACKUP_EXP_DAYS: '',
// ARTICLE_LOG_EXP_DAYS: '',
// ARTICLE_RECYCLE_EXP_DAYS: '',
// HEFENG_KEY: '',
// BLOSSOM_OBJECT_STORAGE_DOMAIN: '',
// SERVER_MACHINE_EXPIRE: '',
// SERVER_DATABASE_EXPIRE: '',
// SERVER_HTTPS_EXPIRE: '',
// SERVER_DOMAIN_EXPIRE: ''
// },
// userParams: {
// WEB_ARTICLE_URL: '',
// WEB_IPC_BEI_AN_HAO: '',
// WEB_LOGO_NAME: '',
// WEB_LOGO_URL: '',
// WEB_GONG_WANG_AN_BEI: '',
// WEB_BLOG_URL_ERROR_TIP_SHOW: ''
// }
// }
Local.set(userinfoKey, { ...DEFAULT_USER_INFO })
return { ...DEFAULT_USER_INFO }
}
export const useUserStore = defineStore('userStore', {
state: () => ({
auth: Local.get(storeKey) || initAuth(),
userinfo: Local.get(userinfoKey) || initUserinfo()
/** @type { Userinfo } */
userinfo: (Local.get(userinfoKey) as Userinfo) || initUserinfo()
}),
getters: {
/** 获取系统个人配置信息 */
sysParams(state) {
return state.userinfo.params
},
/** 获取用户个人配置信息 */
userParams(state) {
return state.userinfo.userParams
}
},
actions: {
/**
*
@ -84,20 +152,16 @@ export const useUserStore = defineStore('userStore', {
*/
await loginApi({ username: username, password: password, clientId: 'blossom', grantType: 'password' })
.then((resp: any) => {
setTimeout(() => {
let auth = { token: resp.data.token, status: AuthStatus.Succ }
this.auth = auth
Local.set(storeKey, auth)
this.getUserinfo()
}, timeoutMs)
let auth = { token: resp.data.token, status: AuthStatus.Succ }
this.auth = auth
Local.set(storeKey, auth)
this.getUserinfo()
})
.catch((_e) => {
setTimeout(() => {
this.reset()
// 登录失败的状态需要特别更改
let auth = { token: '', status: AuthStatus.Fail }
this.auth = auth
}, timeoutMs)
this.reset()
// 登录失败的状态需要特别更改
let auth = { token: '', status: AuthStatus.Fail }
this.auth = auth
})
},
async logout() {
@ -112,22 +176,18 @@ export const useUserStore = defineStore('userStore', {
this.auth.status = AuthStatus.Checking
await checkApi()
.then((resp) => {
setTimeout(() => {
let auth = { token: resp.data.token, status: AuthStatus.Succ }
this.auth = auth
Local.set(storeKey, auth)
this.getUserinfo()
succ()
}, timeoutMs)
let auth = { token: resp.data.token, status: AuthStatus.Succ }
this.auth = auth
Local.set(storeKey, auth)
this.getUserinfo()
succ()
})
.catch((_error) => {
setTimeout(() => {
this.reset()
// 登录失败的状态需要特别更改
let auth = { token: '', status: AuthStatus.Wait }
this.auth = auth
fail()
}, timeoutMs)
this.reset()
// 登录失败的状态需要特别更改
let auth = { token: '', status: AuthStatus.Wait }
this.auth = auth
fail()
})
},
/**

View File

@ -0,0 +1,35 @@
<template>
<div class="article-backup-root">
<!-- 标题 -->
<div class="info-title">
<div class="iconbl bl-dizhicuowu"></div>
博客地址配置方式
</div>
<div class="content">
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { formatFileSize } from '@renderer/assets/utils/util'
import { articleBackupListApi, articleBackupApi, articleBackupDownloadFragmentApi, articleBackupDownloadFragmentHeadApi } from '@renderer/api/blossom'
import type { BackupFile } from '@renderer/api/blossom'
import Notify from '@renderer/scripts/notify'
</script>
<style scoped lang="scss">
@import '@renderer/assets/styles/bl-dialog-info';
.article-backup-root {
border-radius: 10px;
@include box(100%, 100%);
.content {
@include box(100%, calc(100% - 50px));
padding: 20px;
}
}
</style>

View File

@ -166,8 +166,8 @@ const renderChart = () => {
if (!params.data.inner) {
url = `<div>地址: <a target="_blank" href="${params.data.artUrl}">${params.data.artUrl}</a></div>`
} else {
url = `<div>地址: <a target="_blank" href="${userStore.userinfo.params.WEB_ARTICLE_URL + params.data.artId}">${
userStore.userinfo.params.WEB_ARTICLE_URL + params.data.artId
url = `<div>地址: <a target="_blank" href="${userStore.userinfo.userParams.WEB_ARTICLE_URL + params.data.artId}">${
userStore.userinfo.userParams.WEB_ARTICLE_URL + params.data.artId
}</a></div>`
}
return `<div class="chart-graph-article-ref-tooltip">

View File

@ -184,14 +184,7 @@
</el-dialog>
<!-- 二维码 -->
<el-dialog
v-model="isShowQrCodeDialog"
width="335"
top="100px"
:append-to-body="true"
:destroy-on-close="true"
:close-on-click-modal="false"
draggable>
<el-dialog v-model="isShowQrCodeDialog" width="335" :append-to-body="true" :destroy-on-close="true" :close-on-click-modal="false" draggable>
<ArticleQrCode ref="ArticleQrCodeRef"></ArticleQrCode>
</el-dialog>
@ -253,7 +246,7 @@ import ArticleImport from './ArticleImport.vue'
import { useLifecycle } from '@renderer/scripts/lifecycle'
const server = useServerStore()
const { userinfo } = useUserStore()
const user = useUserStore()
const { viewStyle } = useConfigStore()
const route = useRoute()
@ -443,13 +436,13 @@ const openArticleWindow = () => {
* @param open: 生成链接后是否直接打开
*/
const createUrl = (type: 'open' | 'copy' | 'link' | 'tempVisit', open: boolean = false) => {
let url: string = userinfo.params.WEB_ARTICLE_URL + curDoc.value.i
let url: string = user.userParams.WEB_ARTICLE_URL + curDoc.value.i
if (type === 'open') {
openExtenal(url)
} else if (type === 'copy') {
writeText(url)
} else if (type === 'link') {
url = `[${curDoc.value.n}](${userinfo.params.WEB_ARTICLE_URL + curDoc.value.i} "${grammar}${curDoc.value.i}${grammar}")`
url = `[${curDoc.value.n}](${user.userParams.WEB_ARTICLE_URL + curDoc.value.i} "${grammar}${curDoc.value.i}${grammar}")`
writeText(url)
} else if (type === 'tempVisit') {
articleTempKey({ id: curDoc.value.i }).then((resp) => {
@ -536,13 +529,16 @@ const syncDoc = () => {
/** 删除文档 */
const delDoc = () => {
let type = curDoc.value.ty === 3 ? '文章' : '文件夹'
ElMessageBox.confirm(`是否确定删除${type}: <span style="color:#C02B2B;text-decoration: underline;">${curDoc.value.n}</span>?删除后的文章可在回收站中查看。`, {
confirmButtonText: '确定删除',
cancelButtonText: '我再想想',
type: 'info',
draggable: true,
dangerouslyUseHTMLString: true
}).then(() => {
ElMessageBox.confirm(
`是否确定删除${type}: <span style="color:#C02B2B;text-decoration: underline;">${curDoc.value.n}</span>?删除后的文章可在回收站中查看。`,
{
confirmButtonText: '确定删除',
cancelButtonText: '我再想想',
type: 'info',
draggable: true,
dangerouslyUseHTMLString: true
}
).then(() => {
if (curDoc.value.ty === 3) {
articleDelApi({ id: curDoc.value.i }).then((_resp) => {
Notify.success(`删除文章成功`)

View File

@ -19,7 +19,7 @@ export const keymaps = {
blockquote: isMac ? '>' : '>',
code: isMac ? 'Cmd + E' : 'Alt + E',
pre: isMac ? 'Ctrl + Cmd + S' : 'Ctrl + Alt + E',
pre: isMac ? 'Ctrl + Cmd + E' : 'Ctrl + Alt + E',
table: isMac ? 'Cmd + T' : 'Alt + T',
image: isMac ? 'Cmd + M' : 'Alt + M',

View File

@ -62,7 +62,7 @@ const toRoute = (articleId: number) => {
const toWebview = (article: any) => {
if (article.openStatus === 1) {
openExtenal(userStore.userinfo.params.WEB_ARTICLE_URL + article.id)
openExtenal(userStore.userinfo.userParams.WEB_ARTICLE_URL + article.id)
}
}

View File

@ -1,58 +0,0 @@
<template>
<div class="index-footer-root">
<el-popover placement="top-start" :width="350" trigger="hover" :show-after="100" :hide-after="300">
<template #reference>
<div class="operator-buttons">
<el-radio-group v-model="copyType" class="copy-type-radio">
<el-radio-button label="http">HT</el-radio-button>
<el-radio-button label="markdown">MD</el-radio-button>
<el-radio-button label="binary">01</el-radio-button>
</el-radio-group>
<el-button class="iconbl bl-copy-line" />
</div>
</template>
<el-input :suffix-icon="Link" placeholder="截图后可查看图片链接..." disabled>
<template #prepend>
<el-button class="iconbl bl-copy-line" />
</template>
</el-input>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Link } from '@element-plus/icons-vue';
const copyType = ref('markdown')
</script>
<style scoped lang="scss">
.index-footer-root {
@include box(100%, 100%);
@include flex(row, space-around, center);
font-size: 12px;
-webkit-app-region: drag;
.operator-buttons {
@include flex(row, flex-end, center);
.copy-type-radio {
margin-right: 10px;
:deep(.el-radio-button__inner) {
height: 24px;
}
}
.copy-button {
height: 24px;
}
.el-input {
width: 300px;
}
}
}
</style>

View File

@ -57,8 +57,11 @@
</ol>
<p class="paragraph">
更多内容可前往<a :href="CONFIG.SYS.GITHUB_REPO" target="_blank">源码仓库</a><a :href="CONFIG.SYS.DOC" target="_blank">查看文档</a
>或联系作者<span style="font-size: 13px">({{ CONFIG.SYS.CONTACT }})</span>
更多内容可前往<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>
</div>
@ -120,8 +123,8 @@ const developer = [
.project-name {
@include font(50px, 500);
@include themeColor(#2b2b2b, #e4e4e4);
@include themeText(2px 2px 5px #878787, 2px 2px 5px #000000);
color: var(--el-color-primary);
width: 100%;
text-align: center;
}

View File

@ -7,6 +7,9 @@
<el-tab-pane label="服务器配置" :lazy="true">
<div class="tab-content"><ConfigServer></ConfigServer></div>
</el-tab-pane>
<el-tab-pane label="博客配置" :lazy="true">
<div class="tab-content"><ConfigBlog></ConfigBlog></div>
</el-tab-pane>
<el-tab-pane label="修改个人信息" :lazy="true">
<div class="tab-content"><ConfigUserinfo></ConfigUserinfo></div>
</el-tab-pane>
@ -28,6 +31,7 @@ import ConfigUpdPwd from './SettingConfigUpdPwd.vue'
import ConfigAddUser from './SettingConfigAddUser.vue'
import ConfigClient from './SettingConfigClient.vue'
import ConfigServer from './SettingConfigServer.vue'
import ConfigBlog from './SettingConfigBlog.vue'
</script>
<style scoped lang="scss">

View File

@ -0,0 +1,111 @@
<template>
<div class="config-root" v-loading="auth.status !== '已登录'" element-loading-spinner="none" element-loading-text="请登录后使用博客设置...">
<div class="title">博客配置</div>
<div class="desc">博客配置若无内容请点击右侧刷新<el-button @click="refreshParam" text bg>刷新</el-button></div>
<el-form v-if="auth.status == '已登录'" :model="userParamForm" label-position="right" label-width="130px" style="max-width: 800px">
<el-form-item label="文章查看地址" :required="true">
<el-input
size="default"
v-model="userParamForm.WEB_ARTICLE_URL"
@change="(cur: any) => updParam('WEB_ARTICLE_URL', cur)"
style="width: calc(100% - 100px)"></el-input>
<el-button size="default" style="width: 90px; margin-left: 10px">访问测试</el-button>
<div class="conf-tip">网页端博客的访问地址如果不使用博客可不配置需以<code>/#/articles?articleId=</code>结尾</div>
</el-form-item>
<el-form-item label="博客名称">
<el-input size="default" v-model="userParamForm.WEB_LOGO_NAME" @change="(cur: any) => updParam('WEB_LOGO_NAME', cur)"></el-input>
<div class="conf-tip">博客左上角名称</div>
</el-form-item>
<el-form-item label="博客LOGO地址">
<el-input size="default" v-model="userParamForm.WEB_LOGO_URL" @change="(cur: any) => updParam('WEB_LOGO_URL', cur)"></el-input>
<div class="conf-tip">博客左上角 Logo 的访问地址</div>
</el-form-item>
<el-form-item label="IPC备案号">
<el-input size="default" v-model="userParamForm.WEB_IPC_BEI_AN_HAO" @change="(cur: any) => updParam('WEB_IPC_BEI_AN_HAO', cur)"></el-input>
<div class="conf-tip">如果博客作为你的域名首页你可能需要配置 IPC 备案号</div>
</el-form-item>
<el-form-item label="公网安备号">
<el-input
size="default"
v-model="userParamForm.WEB_GONG_WANG_AN_BEI"
@change="(cur: any) => updParam('WEB_GONG_WANG_AN_BEI', cur)"></el-input>
<div class="conf-tip">如果博客作为你的域名首页你可能需要配置公网安备号</div>
</el-form-item>
</el-form>
<div class="server-config">
{{ userinfoJson }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onActivated, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@renderer/stores/user'
import { userParamListApi, userParamUpdApi, userParamRefreshApi } from '@renderer/api/blossom'
import { formatJson } from '@renderer/assets/utils/util'
import Notify from '@renderer/scripts/notify'
onMounted(() => {
getParamList()
})
onActivated(() => {
getParamList()
})
const userStore = useUserStore()
const { userinfo, auth } = storeToRefs(userStore)
const userParamForm = ref({
WEB_ARTICLE_URL: '',
WEB_IPC_BEI_AN_HAO: '',
WEB_LOGO_NAME: '',
WEB_LOGO_URL: '',
WEB_GONG_WANG_AN_BEI: '',
WEB_BLOG_URL_ERROR_TIP_SHOW: ''
})
/**
* 获取参数列表
*/
const getParamList = () => {
userParamListApi().then((resp) => {
userParamForm.value = resp.data
})
}
const refreshParam = () => {
userParamRefreshApi().then((_) => {
Notify.success('刷新参数成功', '刷新成功')
getParamList()
userStore.getUserinfo()
})
}
const updParam = (paramName: string, paramValue: string) => {
userParamUpdApi({ paramName: paramName, paramValue: paramValue }).then((_resp) => {
userStore.getUserinfo()
})
}
const userinfoJson = computed(() => {
return formatJson(userinfo.value)
})
</script>
<style scoped lang="scss">
@import './styles/config-root.scss';
.server-config {
padding: 10px;
font-size: 12px;
white-space: pre;
color: var(--bl-text-color-light);
}
</style>

View File

@ -3,14 +3,14 @@
<div class="title">
客户端配置<span class="version">{{ CONFIG.SYS.VERSION }}</span>
</div>
<div class="desc">Blossom 桌面客户端配置</div>
<div class="desc">桌面客户端配置</div>
<el-form label-position="right" label-width="130px" style="max-width: 800px">
<bl-row just="flex-start" class="config-module-titile"><span class="iconbl bl-a-texteditorhighlightcolor-line"></span>文章设置</bl-row>
<el-form-item label="编辑器字体">
<el-input v-model="configEditorStyleForm.fontFamily" size="default" @input="changeEditorStyle"></el-input>
<div class="conf-tip">
会影响 Markdown 编辑器预览页面新窗口预览编辑历史中的正文字体样式中英文等宽字体在表格中会有更好的样式表现:
Markdown 编辑器预览页面新窗口预览编辑历史中的正文字体样式中英文等宽字体在表格中会有更好的样式表现:
<a href="https://github.com/be5invis/Sarasa-Gothic" target="_blank">更纱黑体(Sarasa Fixed CL)</a>
</div>
</el-form-item>
@ -19,19 +19,19 @@
<el-input v-model="configEditorStyleForm.fontSize" size="default" @input="changeEditorStyle">
<template #append>单位 px</template>
</el-input>
<div class="conf-tip">会影响 Markdown 编辑器预览页面新窗口预览编辑历史中的正文字体大小</div>
<div class="conf-tip">Markdown 编辑器预览页面新窗口预览编辑历史中的正文字体大小</div>
</el-form-item>
<el-form-item label="文档菜单字体大小">
<el-input v-model="configViewStyleForm.treeDocsFontSize" size="default" @input="changeViewStyle">
<template #append>单位 px</template>
</el-input>
<div class="conf-tip">会影响文章照片墙功能中左侧树状菜单的字体大小</div>
<div class="conf-tip">文章照片墙功能中左侧树状菜单的字体大小</div>
</el-form-item>
<el-form-item label="代码块默认语言">
<el-input v-model="configEditorStyleForm.defaultPreLanguage" size="default" @input="changeEditorStyle"> </el-input>
<div class="conf-tip">通过快捷键或者操作按钮生成多行代码块<code>```</code>时的默认语言。</div>
<div class="conf-tip">通过快捷键或者工具栏按钮生成多行代码块<code>```</code>时的默认语言。</div>
</el-form-item>
<el-form-item label="显示代码块行数">

View File

@ -3,12 +3,20 @@
<div class="title">
服务器配置<span class="version" v-if="isNotBlank(serverParamForm.serverVersion)">{{ 'v' + serverParamForm.serverVersion }}</span>
</div>
<div class="desc">Blossom 服务器配置若无内容请点击右侧刷新<el-button @click="refreshParam" text bg>刷新</el-button></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-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"
v-model="serverParamForm.BLOSSOM_OBJECT_STORAGE_DOMAIN"
@change="(cur: any) => updParam('BLOSSOM_OBJECT_STORAGE_DOMAIN', cur)"></el-input>
<div class="conf-tip">文件访问地址需以<code>/pic</code>结尾</div>
</el-form-item>
<el-form-item label="备份文件路径">
@ -124,6 +132,7 @@ const serverParamForm = ref({
ARTICLE_RECYCLE_EXP_DAYS: '',
BACKUP_EXP_DAYS: '',
HEFENG_KEY: '',
BLOSSOM_OBJECT_STORAGE_DOMAIN: '',
SERVER_MACHINE_EXPIRE: '',
SERVER_DATABASE_EXPIRE: '',
SERVER_DOMAIN_EXPIRE: '',

View File

@ -35,7 +35,7 @@ import CONFIG from '@renderer/assets/constants/system'
<style scoped lang="scss">
.setting-index-root {
@include box(100%, 100%);
background-image: linear-gradient(145deg, var(--bl-html-color) 0%, var(--bl-html-color) 55%, var(--el-color-primary-light-5));
background-image: linear-gradient(145deg, transparent 0%, transparent 55%, var(--el-color-primary-light-5));
padding: 50px 0 0 50px;
z-index: 2;
@ -68,7 +68,7 @@ import CONFIG from '@renderer/assets/constants/system'
}
.version {
@include themeColor(#FFFFFF, #9B9B9B);
@include themeColor(#ffffff, #9b9b9b);
@include font(13px, 300);
@include absolute('', 10px, 10px, '');
z-index: 2;

View File

@ -0,0 +1,251 @@
<template>
<div class="quick-setting-root">
<el-steps :active="activeSetp" align-center>
<el-step title="文件" description="文件访问地址配置">
<template #icon>
<div class="iconbl bl-image--line"></div>
</template>
</el-step>
<el-step title="博客" description="博客地址配置">
<template #icon>
<div class="iconbl bl-blog"></div>
</template>
</el-step>
<el-step title="完成">
<template #icon>
<div class="iconbl bl-check"></div>
</template>
</el-step>
</el-steps>
<!--
-->
<div class="step-detail" v-if="activeSetp === 1">
<div class="row">下方是否为您在登录页面添加的地址</div>
<div class="row">若不是请在登录页重新登录</div>
<div class="row server-url">{{ server.serverUrl }}</div>
<div class="row">
<el-button size="default" type="primary" plain @click="savePicture"></el-button>
</div>
</div>
<!--
-->
<div class="step-detail blog-detail" v-if="activeSetp === 2">
<div class="detail">
<div class="row">您选择使用后台自带博客, 还是自己独立部署博客</div>
<div class="row" just="center">
<el-button size="default" :type="blogType == 1 ? 'primary' : ''" @click="setBlogType(1)">自带博客</el-button>
<el-button size="default" :type="blogType == 2 ? 'primary' : ''" @click="setBlogType(2)">独立部署</el-button>
</div>
<div v-if="blogType == 1" class="row blog-type">
<div>
自带博客只能作为用户ID为1的用户使用若想修改请查看<a
href="https://www.wangyunf.com/blossom-doc/guide/deploy/blog.html#update-config"
target="_blank"
>修改方法<span class="iconbl bl-sendmail-line"></span></a
>
</div>
</div>
<div v-if="blogType == 2" class="row blog-type">
<div style="font-size: 13px">
填写博客地址并以<code>/#/articles?articleId=</code>结尾
<el-tooltip effect="light" content="复制地址结尾" placement="top">
<span class="iconbl bl-copy-line" @click="writeText(URL_SUFFIX)"></span>
</el-tooltip>
</div>
<bl-row>
<el-input type="textarea" :rows="2" resize="none" v-model="blogUrl" placeholder="请填写博客地址..." @input="blogUrlChange"></el-input>
</bl-row>
<div class="blog-url-error">
<span v-show="blogUrlError">博客地址格式错误!</span>
</div>
</div>
<div class="row" style="margin-top: 20px">
<el-button size="default" text @click="last(1)">上一步</el-button>
<el-button size="default" type="primary" @click="saveBlog" plain>确认使用{{ blogType === 1 ? '自带博客' : '独立部署' }}</el-button>
</div>
</div>
<div class="iframe-container">
<bl-row v-if="isBlank(blogUrlPreview)" class="iframe-placeholder" just="center">请填写博客地址</bl-row>
<iframe v-else :src="blogUrlPreview" width="100%"></iframe>
</div>
</div>
<div class="step-detail" v-if="activeSetp === 3">
<img style="width: 200px" src="@renderer/assets/imgs/plant/杨桃.svg" />
<div class="row">配置完成,</div>
<div class="row">
<el-button size="default" type="primary" plain @click="savePicture">开始</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import { useServerStore } from '@renderer/stores/server'
import { isBlank } from '@renderer/assets/utils/obj'
import { writeText } from '@renderer/assets/utils/electron'
const server = useServerStore()
const activeSetp = ref(1)
const last = (active: number) => {
activeSetp.value = active
}
const savePicture = () => {
activeSetp.value = 2
}
const URL_SUFFIX_SERVER = '/blog/#/articles?articleId='
const URL_SUFFIX = '/#/articles?articleId='
const previewId = -123
const blogType = ref<1 | 2>(1)
const blogUrl = ref('')
const blogUrlError = ref(false)
/**
* 预览页面
*/
const blogUrlPreview = computed(() => {
console.log(blogUrl.value)
if (blogType.value == 1) {
return server.serverUrl + URL_SUFFIX_SERVER + previewId
} else if (isBlank(blogUrl.value) || (!blogUrl.value.startsWith('http://') && !blogUrl.value.startsWith('https://'))) {
return ''
} else if (blogUrl.value.endsWith(URL_SUFFIX)) {
return blogUrl.value + previewId
}
return blogUrl.value
})
const setBlogType = (type: 1 | 2) => {
blogType.value = type
}
const blogUrlChange = (val) => {
blogUrl.value = ''
blogUrlError.value = false
nextTick(() => {
blogUrl.value = val
})
}
const saveBlog = () => {
if (blogType.value === 2 && !blogUrl.value.endsWith(URL_SUFFIX)) {
blogUrlError.value = true
return
}
activeSetp.value = 3
}
</script>
<style lang="scss">
.quick-setting-root {
padding: 20px 10px 0 10px;
.el-steps {
--el-bg-color: var(--bl-dialog-bg-color);
.iconbl {
font-size: 30px;
}
}
.row {
margin-bottom: 15px;
&:last-child {
margin-bottom: 5px;
}
}
.step-detail {
@include font(14px, 300);
padding: 35px 20px 20px 20px;
text-align: center;
.server-url {
padding: 10px;
border-radius: 8px;
background-color: var(--bl-preview-code-bg-color);
color: var(--bl-preview-code-color);
}
.blog-type {
@include flex(column, space-around, center);
@include box(100%, 100px);
padding: 5px;
border-radius: 6px;
border: 1px dashed var(--el-border-color);
background-color: var(--bl-preview-code-bg-color);
}
.blog-url-error {
height: 16px;
font-size: 12px;
color: var(--el-color-danger);
}
.bl-sendmail-line {
font-size: 14px;
}
.bl-copy-line {
font-size: 15px;
cursor: pointer;
}
.bl-refresh-smile {
font-size: 50px;
}
code {
color: var(--el-color-primary);
font-size: 13px;
margin: 0 5px;
}
}
.blog-detail {
@include flex(row, space-around, center);
padding-top: 0;
.detail {
@include flex(column, flex-start, center);
@include box(400px, auto, 400px, 400px);
}
.iframe-container {
@include box(240px, 480px);
position: relative;
.iframe-placeholder {
@include box(240px, 480px);
color: var(--bl-text-color-light);
border: 1px solid var(--el-border-color);
border-radius: 6px;
}
iframe {
height: 800px;
width: 400px;
transform: scale(0.6);
position: absolute;
left: -80px;
top: -160px;
border: none;
border-radius: 6px;
}
}
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="picture-cant-show-tip-root">
<div class="info-title">
<div class="iconbl bl-delete-line"></div>
<div class="iconbl bl-a-picturecracked-line"></div>
图片无法显示的解决方式
</div>
<div class="content">

View File

@ -0,0 +1,23 @@
import { onActivated, onMounted } from 'vue'
/**
* , mounted activated
* @param mounted
* @param activated
*/
export const useLifecycle = (mounted: Function, activated: Function) => {
let isMounted = false
onMounted(() => {
if (!isMounted) {
mounted()
}
})
onActivated(() => {
if (isMounted) {
activated()
}
isMounted = true
})
}

View File

@ -125,28 +125,33 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { ref, onActivated, onUnmounted, onMounted } from 'vue'
import { ref, onUnmounted } from 'vue'
import { ArrowDownBold, ArrowRightBold } from '@element-plus/icons-vue'
import { articleInfoOpenApi, articleInfoApi, docTreeOpenApi, docTreeApi } from '@/api/blossom'
import { useUserStore } from '@/stores/user'
import { isNull, isEmpty, isNotNull } from '@/assets/utils/obj'
import DocTitle from './DocTitle.vue'
import { useLifecycle } from '@/scripts/lifecycle'
import 'katex/dist/katex.min.css'
const userStore = useUserStore()
useLifecycle(
() => {
window.onHtmlEventDispatch = onHtmlEventDispatch
getRouteQueryParams()
window.addEventListener('resize', onresize)
initStyle()
},
() => {
getRouteQueryParams()
window.addEventListener('resize', onresize)
initStyle()
}
)
onMounted(() => {
window.onHtmlEventDispatch = onHtmlEventDispatch
getRouteQueryParams()
window.addEventListener('resize', onresize)
initStyle()
})
// onMounted(() => {})
onActivated(() => {
getRouteQueryParams()
window.addEventListener('resize', onresize)
initStyle()
})
// onActivated(() => {})
onUnmounted(() => {
window.removeEventListener('resize', onresize)
@ -227,6 +232,12 @@ const clickCurDoc = async (tree: DocTree) => {
* 如果点击的是文章, 则查询文章信息和正文, 并在编辑器中显示.
*/
const getCurEditArticle = async (id: number) => {
if (id == -123) {
article.value.html = `<div style="color:#C6C6C6;font-weight: 300;width:100%;height:300px;padding:0 20px;display:flex;justify-content: center;
align-items: center;text-align:center;font-size:25px;">如果您看到这句话, 证明博客验证成功</div>`
return
}
const then = (resp: any) => {
if (isNull(resp.data)) return
article.value = resp.data