完成帖子图片插入操作
This commit is contained in:
parent
e6c765c1ca
commit
b84cbdc380
@ -4,6 +4,7 @@ import com.example.entity.RestBean;
|
||||
import com.example.service.ImageService;
|
||||
import com.example.utils.Const;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@ -32,4 +33,21 @@ public class ImageController {
|
||||
return RestBean.failure(400, "头像上传失败!");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/cache")
|
||||
public RestBean<String> uploadImage(@RequestParam("file") MultipartFile file,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int id,
|
||||
HttpServletResponse response) throws IOException {
|
||||
if(file.getSize() > 1024 * 1024 * 5)
|
||||
return RestBean.failure(400, "图片不能大于5MB");
|
||||
log.info("正在进行图片上传操作...");
|
||||
String url = service.uploadImage(id, file);
|
||||
if(url != null) {
|
||||
log.info("成功上传图片,大小: "+file.getSize());
|
||||
return RestBean.success(url);
|
||||
} else {
|
||||
response.setStatus(400);
|
||||
return RestBean.failure(400, "图片上传失败!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ public class ObjectController {
|
||||
@Resource
|
||||
ImageService service;
|
||||
|
||||
@GetMapping("/images/avatar/**")
|
||||
@GetMapping("/images/**")
|
||||
public void imagesFetch(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
response.setHeader("Cache-Control", "max-age=2592000");
|
||||
response.setHeader("Content-Type", "image/jpg");
|
||||
this.fetchImage(request, response);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
package com.example.entity.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@TableName("db_image_store")
|
||||
public class StoreImage {
|
||||
Integer uid;
|
||||
String name;
|
||||
Date time;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.entity.dto.StoreImage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ImageStoreMapper extends BaseMapper<StoreImage> {
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.example.entity.dto.StoreImage;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface ImageService {
|
||||
public interface ImageService extends IService<StoreImage> {
|
||||
String uploadAvatar(int id, MultipartFile file) throws IOException;
|
||||
String uploadImage(int id, MultipartFile file) throws IOException;
|
||||
void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception;
|
||||
}
|
||||
|
@ -1,26 +1,30 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.dto.StoreImage;
|
||||
import com.example.mapper.AccountMapper;
|
||||
import com.example.mapper.ImageStoreMapper;
|
||||
import com.example.service.ImageService;
|
||||
import io.minio.GetObjectArgs;
|
||||
import io.minio.GetObjectResponse;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.PutObjectArgs;
|
||||
import io.minio.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ImageServiceImpl implements ImageService {
|
||||
public class ImageServiceImpl extends ServiceImpl<ImageStoreMapper, StoreImage> implements ImageService {
|
||||
|
||||
@Resource
|
||||
MinioClient client;
|
||||
@ -28,6 +32,11 @@ public class ImageServiceImpl implements ImageService {
|
||||
@Resource
|
||||
AccountMapper mapper;
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate template;
|
||||
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd");
|
||||
|
||||
@Override
|
||||
public void fetchImageFromMinio(OutputStream stream, String imagePath) throws Exception {
|
||||
GetObjectArgs args = GetObjectArgs.builder()
|
||||
@ -38,6 +47,30 @@ public class ImageServiceImpl implements ImageService {
|
||||
IOUtils.copy(object, stream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadImage(int id, MultipartFile file) throws IOException {
|
||||
if(!limitUpload(id)) return null;
|
||||
String imageName = UUID.randomUUID().toString().replace("-", "");
|
||||
Date date = new Date();
|
||||
imageName = "/cache/" + format.format(date) + "/" + imageName;
|
||||
PutObjectArgs args = PutObjectArgs.builder()
|
||||
.bucket("test")
|
||||
.stream(file.getInputStream(), file.getSize(), -1)
|
||||
.object(imageName)
|
||||
.build();
|
||||
try {
|
||||
client.putObject(args);
|
||||
if(this.save(new StoreImage(id, imageName, date))){
|
||||
return imageName;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}catch (Exception e) {
|
||||
log.error("图片上传出现问题: "+e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadAvatar(int id, MultipartFile file) throws IOException {
|
||||
//使用随机UUID作为图片名字,而不是直接用户ID是因为用户更改头像之后可以立即更新缓存
|
||||
@ -53,6 +86,8 @@ public class ImageServiceImpl implements ImageService {
|
||||
if(mapper.update(null, Wrappers.<Account>update()
|
||||
.eq("id", id)
|
||||
.set("avatar", imageName)) > 0) {
|
||||
String avatar = mapper.selectById(id).getAvatar();
|
||||
this.deleteOldAvatar(avatar);
|
||||
return imageName;
|
||||
} else {
|
||||
return null;
|
||||
@ -62,4 +97,28 @@ public class ImageServiceImpl implements ImageService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteOldAvatar(String avatar) throws Exception {
|
||||
if(avatar == null || avatar.isEmpty()) return;
|
||||
RemoveObjectArgs remove = RemoveObjectArgs.builder()
|
||||
.bucket("test")
|
||||
.object(avatar)
|
||||
.build();
|
||||
client.removeObject(remove);
|
||||
}
|
||||
|
||||
private boolean limitUpload(int id){
|
||||
String key = "image:upload:"+id;
|
||||
String s = template.opsForValue().get(key);
|
||||
if(s == null) {
|
||||
template.opsForValue().set(key, "1", 1, TimeUnit.HOURS);
|
||||
} else {
|
||||
if(Integer.parseInt(s) > 20) {
|
||||
return false;
|
||||
} else {
|
||||
template.opsForValue().increment(key);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
22
my-project-frontend/package-lock.json
generated
22
my-project-frontend/package-lock.json
generated
@ -14,6 +14,8 @@
|
||||
"axios": "^1.4.0",
|
||||
"element-plus": "^2.3.9",
|
||||
"pinia": "^2.1.6",
|
||||
"quill-image-resize-vue": "^1.0.4",
|
||||
"quill-image-super-solution-module": "^2.0.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
@ -1825,6 +1827,21 @@
|
||||
"lodash.isequal": "^4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-image-resize-vue": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/quill-image-resize-vue/-/quill-image-resize-vue-1.0.4.tgz",
|
||||
"integrity": "sha512-/Nakepctw5PWzY7ebQ0GQgZh6uA0RtW8wLUrkiLRTl0ql7DJ0OhRCEHThsjJO+8HgsfTu3ohLUbOthnsImStJg==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.4",
|
||||
"quill": "^1.2.2",
|
||||
"raw-loader": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/quill-image-super-solution-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/quill-image-super-solution-module/-/quill-image-super-solution-module-2.0.1.tgz",
|
||||
"integrity": "sha512-mXN7uN0R3CtnF1m0MkMxTKNVSUXPWmnJmQ+kC/RNa9dlJoC4NS0xQhHB+JRoR5eKZzywBxkVSHHygrskwBMSug=="
|
||||
},
|
||||
"node_modules/quill/node_modules/fast-diff": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||
@ -1843,6 +1860,11 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-loader": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/raw-loader/-/raw-loader-0.5.1.tgz",
|
||||
"integrity": "sha512-sf7oGoLuaYAScB4VGr0tzetsYlS8EJH6qnTCfQ/WVEa89hALQ4RQfCKt5xCyPQKPDUbVUAIP1QsxAwfAjlDp7Q=="
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
|
||||
|
@ -14,6 +14,8 @@
|
||||
"axios": "^1.4.0",
|
||||
"element-plus": "^2.3.9",
|
||||
"pinia": "^2.1.6",
|
||||
"quill-image-resize-vue": "^1.0.4",
|
||||
"quill-image-super-solution-module": "^2.0.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
|
@ -1,16 +1,23 @@
|
||||
<script setup>
|
||||
import {Document} from "@element-plus/icons-vue";
|
||||
import {QuillEditor} from "@vueup/vue-quill";
|
||||
import {reactive} from "vue";
|
||||
import {QuillEditor, Quill} from "@vueup/vue-quill";
|
||||
import {reactive, ref} from "vue";
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
import {accessHeader} from "@/net";
|
||||
import axios from "axios";
|
||||
import {ElMessage} from "element-plus";
|
||||
import ImageResize from "quill-image-resize-vue";
|
||||
import { ImageExtend, QuillWatch } from "quill-image-super-solution-module";
|
||||
|
||||
defineProps({ show: Boolean })
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const refEditor = ref()
|
||||
const editor = reactive({
|
||||
type: 1,
|
||||
title: '',
|
||||
text: ''
|
||||
text: '',
|
||||
uploading: false
|
||||
})
|
||||
const types = [
|
||||
{id: 1, name: '日常闲聊', desc: '在这里分享你的各种日常'},
|
||||
@ -19,12 +26,76 @@ const types = [
|
||||
{id: 4, name: '恋爱官宣', desc: '向大家展示你的恋爱成果'},
|
||||
{id: 5, name: '踩坑记录', desc: '将你遇到的坑分享给大家,防止其他人再次入坑'},
|
||||
]
|
||||
|
||||
function initEditor() {
|
||||
refEditor.value.setContents('', 'user')
|
||||
editor.title = ''
|
||||
editor.type = 1
|
||||
}
|
||||
|
||||
function submitTopic(){
|
||||
console.info(editor.text)
|
||||
}
|
||||
|
||||
Quill.register('modules/imageResize', ImageResize);
|
||||
Quill.register('modules/ImageExtend', ImageExtend)
|
||||
const editorOption = {
|
||||
placeholder: "今天分享点什么呢?",
|
||||
modules: {
|
||||
toolbar: {
|
||||
container: [
|
||||
"bold", "italic", "underline", "strike","clean",
|
||||
{color: []}, {'background': []},
|
||||
{size: ["small", false, "large", "huge"]},
|
||||
{ header: [1, 2, 3, 4, 5, 6, false] },
|
||||
{list: "ordered"}, {list: "bullet"}, {align: []},
|
||||
"blockquote", "code-block", "link", "image",
|
||||
{ indent: '-1' }, { indent: '+1' }
|
||||
],
|
||||
handlers: {
|
||||
'image': function () {
|
||||
QuillWatch.emit(this.quill.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
imageResize: {
|
||||
modules: [ 'Resize', 'DisplaySize' ]
|
||||
},
|
||||
ImageExtend: {
|
||||
action: axios.defaults.baseURL + '/api/image/cache',
|
||||
name: 'file',
|
||||
size: 5,
|
||||
loading: true,
|
||||
accept: 'image/png, image/jpeg',
|
||||
response: (resp) => {
|
||||
if(resp.data) {
|
||||
return axios.defaults.baseURL + '/images' + resp.data
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
methods: 'POST',
|
||||
headers: xhr => {
|
||||
xhr.setRequestHeader('Authorization', accessHeader().Authorization);
|
||||
},
|
||||
start: () => editor.uploading = true,
|
||||
success: () => {
|
||||
ElMessage.success('图片上传成功!')
|
||||
editor.uploading = false
|
||||
},
|
||||
error: () => {
|
||||
ElMessage.warning('图片上传失败,请联系管理员!')
|
||||
editor.uploading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-drawer direction="btt" :model-value="show"
|
||||
:size="650"
|
||||
:size="650" @open="initEditor"
|
||||
@close="emit('close')"
|
||||
:show-close="false">
|
||||
<template #header>
|
||||
@ -43,18 +114,21 @@ const types = [
|
||||
<el-input placeholder="请输入帖子标题..." :prefix-icon="Document" v-model="editor.title"/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 15px;height: 410px">
|
||||
<quill-editor ref="refEditor" v-model:content="editor.text" :options="editorOption" content-type="delta"/>
|
||||
<div style="margin-top: 15px;height: 450px;border-radius: 5px;overflow: hidden"
|
||||
v-loading="editor.uploading"
|
||||
element-loading-text="正在上传图片,请稍后...">
|
||||
<quill-editor ref="refEditor" v-model:content="editor.text"
|
||||
:options="editorOption" content-type="delta"
|
||||
style="height: calc(100% - 44px)"/>
|
||||
</div>
|
||||
<div style="display: flex;justify-content: space-between;margin-top: 50px">
|
||||
<div style="display: flex;justify-content: space-between;margin-top: 10px">
|
||||
<div style="color: grey;font-size: 13px">
|
||||
当前字数: 666(最大支持20000字)
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="success" plain>立即发布</el-button>
|
||||
<el-button @click="submitTopic" type="success" plain>立即发布</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@ -76,4 +150,11 @@ const types = [
|
||||
border-radius: 0 0 5px 5px;
|
||||
border-color: var(--el-border-color);
|
||||
}
|
||||
:deep(.ql-editor.ql-blank::before){
|
||||
color: grey;
|
||||
font-style: normal;
|
||||
}
|
||||
:deep(.ql-editor) {
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user