fix: 支持 base64 图片

This commit is contained in:
xiaozzzi 2024-02-05 16:54:00 +08:00
parent f17fbaab26
commit 096e561b94
6 changed files with 109 additions and 57 deletions

View File

@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blossom.backend.server.article.reference.pojo.ArticleReferenceEntity;
import com.blossom.backend.server.article.reference.pojo.ArticleReferenceReq;
import com.blossom.common.base.util.BeanUtil;
import com.blossom.common.base.util.security.Base64Util;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Data;
@ -48,6 +49,9 @@ public class ArticleReferenceService extends ServiceImpl<ArticleReferenceMapper,
ref.setUserId(userId);
ref.setSourceId(sourceId);
ref.setSourceName(sourceName);
if (Base64Util.isBase64Img(ref.getTargetUrl())) {
ref.setTargetUrl("");
}
}
baseMapper.insertList(refs);
}

View File

@ -1,20 +1,34 @@
package com.blossom.common.base.util.security;
import java.io.IOException;
import cn.hutool.core.util.StrUtil;
import java.util.Base64;
/**
* 消息编码算法
*
* @author xzzz
* @since 0.0.1
*/
public class Base64Util {
public static String encrypt(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
public static String encrypt(byte[] data) {
return Base64.getEncoder().encodeToString(data);
}
public static String decrypt(String data) throws IOException {
return new String(Base64.getDecoder().decode(data));
}
public static String decrypt(String data) {
return new String(Base64.getDecoder().decode(data));
}
/**
* 是否 base64 图片, 只校验格式, 不判断内容是否为正确的图片
* 例如: 传入 data:image/png;base64,a, 将会返回 true
*/
public static boolean isBase64Img(String image) {
if (StrUtil.isBlank(image)) {
return false;
}
String prefix = image.substring(0, Math.max(image.indexOf(','), 0));
return prefix.startsWith("data:image") && prefix.endsWith("base64");
}
}

View File

@ -411,3 +411,25 @@ export const getFilePrefix = (name: string): string => {
}
return prefix
}
/**
* http/https url
* @param url
* @returns
*/
export const isHttp = (url: string) => {
return url.startsWith('http://')
}
/**
* base64
* @param image
* @returns
*/
export const isBase64Img = (image: string) => {
if (isBlank(image)) {
return false
}
let prefix = image.substring(0, Math.max(image.indexOf(','), 0))
return prefix.startsWith('data:image') && prefix.endsWith('base64')
}

View File

@ -191,7 +191,7 @@ import { articleInfoApi, articleUpdContentApi, uploadFileApiUrl } from '@rendere
// utils
import { Local } from '@renderer/assets/utils/storage'
import { isBlank, isNull } from '@renderer/assets/utils/obj'
import { sleep, isElectron } from '@renderer/assets/utils/util'
import { sleep, isElectron, isBase64Img } from '@renderer/assets/utils/util'
import { openExtenal, writeText, readText, openNewArticleWindow } from '@renderer/assets/utils/electron'
import { formartMarkdownTable } from '@renderer/assets/utils/format-table'
// component
@ -510,6 +510,7 @@ const clickCurDoc = async (tree: DocTree) => {
})
}
}
/**
* 保存文章的正文, 并更新编辑器状态栏中的版本, 字数, 修改时间等信息.
*
@ -521,7 +522,7 @@ const saveCurArticleContent = async (auto: boolean = false) => {
}
const saveCallback = () => {
if (!auto) {
ElMessage.info({ message: '保存成功', duration: 1000, offset: 70, grouping: true })
ElMessage.success({ message: '保存成功', duration: 1000, offset: 70, grouping: true })
}
}
//
@ -541,7 +542,14 @@ const saveCurArticleContent = async (auto: boolean = false) => {
name: curArticle.value!.name,
markdown: cmw.getDocString(),
html: PreviewRef.value.innerHTML,
references: articleImg.value.concat(articleLink.value)
references: articleImg.value.concat(articleLink.value).map((item) => {
let refer: ArticleReference = { targetId: '', targetName: '', targetUrl: '', type: 10 }
Object.assign(refer, item)
if (isBase64Img(refer.targetUrl)) {
refer.targetUrl = ''
}
return refer
})
}
await articleUpdContentApi(data)
.then((resp) => {
@ -931,10 +939,10 @@ const unbindKeys = () => {
</script>
<style scoped lang="scss">
@import '@renderer/assets/styles/bl-loading-spinner.scss';
@import './styles/article-index.scss';
@import './styles/article-view-absolute.scss';
@import './styles/editor-right-menu.scss';
@import './styles/bl-preview-toc.scss';
@import './styles/article-backtop.scss';
@import '@renderer/assets/styles/bl-loading-spinner.scss';
</style>

View File

@ -84,9 +84,9 @@ import { useDark } from '@vueuse/core'
import { useServerStore } from '@renderer/stores/server'
import { useConfigStore } from '@renderer/stores/config'
import { useUserStore, AuthStatus } from '@renderer/stores/user'
import TryUse from './setting/TryUse.vue'
import SYSTEM from '@renderer/assets/constants/system'
import { isBlank } from '@renderer/assets/utils/obj'
import SYSTEM from '@renderer/assets/constants/system'
import TryUse from './setting/TryUse.vue'
onMounted(() => {
formLogin.value.serverUrl = serverUrl.value

View File

@ -5,53 +5,53 @@
<div class="container">
<bl-row align="flex-start">
<strong>图片名称</strong>
<div>{{ picInfo.name }}<span class="iconbl bl-copy-line" @click="writeText(picInfo.name)"></span>
</div>
<div>{{ picInfo!.name }}<span class="iconbl bl-copy-line" @click="writeText(picInfo!.name)"></span></div>
</bl-row>
<bl-row align="flex-start"><strong>图片大小</strong>{{ formatFileSize(picInfo.size) }}</bl-row>
<bl-row align="flex-start"><strong>上传时间</strong>{{ picInfo.creTime }}</bl-row>
<bl-row align="flex-start"><strong>图片路径</strong>{{ picInfo.pathName }}</bl-row>
<bl-row align="flex-start"><strong>图片大小</strong>{{ formatFileSize(picInfo!.size) }}</bl-row>
<bl-row align="flex-start"><strong>上传时间</strong>{{ picInfo!.creTime }}</bl-row>
<bl-row align="flex-start"><strong>图片路径</strong>{{ picInfo!.pathName }}</bl-row>
</div>
<el-divider></el-divider>
<div class="container">
<div><strong>使用该图片的文章</strong></div>
<bl-row v-if="!isEmpty(picInfo.articleNames)" align="flex-start">
<bl-row v-if="!isEmpty(picInfo!.articleNames)" align="flex-start">
<div>
<div v-for="aname in articleNamesToArray(picInfo.articleNames)">
{{ aname }}
</div>
<div v-for="aname in articleNamesToArray(picInfo!.articleNames)">{{ aname }}</div>
</div>
</bl-row>
</div>
<el-divider></el-divider>
<div class="container btns">
<el-tooltip content="强制删除会使该图片链接失效" placement="top" :hide-after="0">
<el-button type="primary" text style="--el-fill-color:#535353;--el-fill-color-light:#414141"
@click="deletePicture">
<el-button type="primary" text style="--el-fill-color: #535353; --el-fill-color-light: #414141" @click="deletePicture">
强制删除
</el-button>
</el-tooltip>
<el-tooltip placement="top" :hide-after="0">
<template #content>
<bl-row>
<svg style="height: 20px;width: 20px;margin-right: 10px;" aria-hidden="true">
<svg style="height: 20px; width: 20px; margin-right: 10px" aria-hidden="true">
<use xlink:href="#wl-jinggao"></use>
</svg>
<div>
将该图片替换为其他图片<br />
<span style="color: #FAAD14;">图片替换为立即生效旧图片将无法找回</span>
<span style="color: #faad14">图片替换为立即生效旧图片将无法找回</span>
</div>
</bl-row>
</template>
<el-upload :action="serverStore.serverUrl + uploadFileApiUrl" name="file"
:data="{ pid: picInfo.pid, filename: picInfo.name, repeatUpload: true }"
:headers="{ 'Authorization': 'Bearer ' + userStore.auth.token }" :show-file-list="false"
:before-upload="beforeUpload" :on-success="onUploadSeccess" :on-error="onError">
<el-button type="primary" text style="--el-fill-color:#535353;--el-fill-color-light:#414141">
<svg style="height: 15px;width: 25px;" aria-hidden="true">
<el-upload
:action="serverStore.serverUrl + uploadFileApiUrl"
name="file"
:data="{ pid: picInfo!.pid, filename: picInfo!.name, repeatUpload: true }"
:headers="{ Authorization: 'Bearer ' + userStore.auth.token }"
:show-file-list="false"
:before-upload="beforeUpload"
:on-success="onUploadSeccess"
:on-error="onError">
<el-button type="primary" text style="--el-fill-color: #535353; --el-fill-color-light: #414141">
<svg style="height: 15px; width: 25px" aria-hidden="true">
<use xlink:href="#wl-jinggao"></use>
</svg>
替换图片
@ -59,9 +59,9 @@
</el-upload>
</el-tooltip>
<el-button type="primary" text style="--el-fill-color:#535353;--el-fill-color-light:#414141"
@click="download(picInfo.url)">下载图片</el-button>
<el-button type="primary" text style="--el-fill-color: #535353; --el-fill-color-light: #414141" @click="download(picInfo!.url)"
>下载图片</el-button
>
</div>
</div>
</el-image-viewer>
@ -69,18 +69,18 @@
</template>
<script setup lang="ts">
import { ref } from "vue"
import { ElMessageBox, UploadProps } from "element-plus"
import { ref } from 'vue'
import { ElMessageBox, UploadProps } from 'element-plus'
import { WarnTriangleFilled } from '@element-plus/icons-vue'
import { useUserStore } from '@renderer/stores/user'
import { useServerStore } from '@renderer/stores/server'
import { pictureDelApi, pictureInfoApi, uploadFileApiUrl } from '@renderer/api/blossom'
import { articleNamesToArray, Picture, buildDefaultPicture, onError, beforeUpload, picCacheWrapper, picCacheRefresh } from "./scripts/picture"
import { formatFileSize } from "@renderer/assets/utils/util"
import { isNotNull } from "@renderer/assets/utils/obj"
import { articleNamesToArray, Picture, buildDefaultPicture, onError, beforeUpload, picCacheWrapper, picCacheRefresh } from './scripts/picture'
import { formatFileSize, isHttp } from '@renderer/assets/utils/util'
import { isNotNull } from '@renderer/assets/utils/obj'
import { isEmpty } from 'lodash'
import { download, writeText } from "@renderer/assets/utils/electron"
import Notify from "@renderer/scripts/notify"
import { download, writeText } from '@renderer/assets/utils/electron'
import Notify from '@renderer/scripts/notify'
const userStore = useUserStore()
const serverStore = useServerStore()
@ -90,12 +90,16 @@ const isShowPicInfo = ref(false)
//
const picUrl = ref('')
//
const picInfo = ref<Picture>(buildDefaultPicture())
const picInfo = ref<Picture | null>(buildDefaultPicture())
const showPicInfo = (url: string) => {
picUrl.value = url
isShowPicInfo.value = true
pictureInfoApi({ url: url }).then(resp => {
if (!isHttp(url)) {
picInfo.value = null
return
}
pictureInfoApi({ url: url }).then((resp) => {
picInfo.value = resp.data
})
}
@ -110,11 +114,13 @@ const closePicInfo = () => {
* @param pic 当前选中图片
*/
const deletePicture = () => {
ElMessageBox.confirm(
'强制删除图片后该图片访问链接将会失效, 是否继续删除?',
{ confirmButtonText: '我要删除', cancelButtonText: '取消', type: 'warning', icon: WarnTriangleFilled }
).then(() => {
pictureDelApi({ id: picInfo.value.id, ignoreCheck: true }).then(_resp => {
ElMessageBox.confirm('强制删除图片后该图片访问链接将会失效, 是否继续删除?', {
confirmButtonText: '我要删除',
cancelButtonText: '取消',
type: 'warning',
icon: WarnTriangleFilled
}).then(() => {
pictureDelApi({ id: picInfo.value.id, ignoreCheck: true }).then((_resp) => {
picCacheRefresh()
closePicInfo()
emits('saved')
@ -141,15 +147,13 @@ const onUploadSeccess: UploadProps['onSuccess'] = (resp, _file?) => {
defineExpose({ showPicInfo })
const emits = defineEmits(['saved'])
</script>
<style scoped lang="scss">
.picture-viewer-info-root {
.bl-image-viewer-infos {
@include themeBg(#1B1B1BBF, #1E1E1EBF);
@include themeColor(#A9A9A9, rgb(190, 190, 190));
@include themeBg(#1b1b1bbf, #1e1e1ebf);
@include themeColor(#a9a9a9, rgb(190, 190, 190));
width: 320px;
font-size: 13px;
position: absolute;
@ -200,7 +204,7 @@ const emits = defineEmits(['saved'])
}
.test {
color: #A9A9A9;
color: #a9a9a9;
}
}
</style>
</style>