mirror of
https://github.com/blossom-editor/blossom
synced 2024-11-17 14:39:21 +08:00
fix: 支持 base64 图片
This commit is contained in:
parent
f17fbaab26
commit
096e561b94
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user