(一)上传多张图片
前言:数据库单独有表存所有图片,在项目表中 所需要的图片字段是数组;
正确的格式:
<template>
<van-fieldlabel-align="top" v-model="photos" required
:rules="[{ required: true, message: '请上传照片'}]">
<template #label>
<div class="picture-label">
<span>图片上传</span>
<span>{{ fileList.length + "/9" }}</span>
</div>
</template>
<template #input>
<van-uploader v-model="fileList" multiple :max-count="9"
@delete="handelDelete"/>
</template>
</van-field>
</template>
<script setup lang="ts">
const photos = ref([])
const fileList = ref([])
// 将后台存的photo数据显示到fileList上
const refreshPhoto = () => {
fileList.value = []; // 清空fileList数组,避免重复添加数据
var list = {}
if (photos && Array.isArray(photos)) {
for (var i = 0; i < photos .length; i++) {
list = photos[i]
fileList.value.push({ url: list.url, realName: list.realName,name: list.name})
}
}else {
photos = null
fileList.value = []
}
}
// 页面初始化时 显示图片
onMounted(() => { refreshPhoto() // 刷新图片加载 })
// 上传图片
const ok = () =>{
let formDatas = new FormData()
if(fileList.value.length > 0){
fileList.value.forEach((item) => {
if ('file' in item) {
formDatas.append('file', item.file);
}
});
}
photos = null // 保存图片为null,以免传的格式错误,(会被转为[object Object])
formDatas.append('data',JSON.stringify(data)) // 其它需要修改的数据(省略)
updatePhotosAndDataById(formDatas).then(() => {
refreshPhoto() // 刷新图片加载
});
}
axios 中用特殊的upload 来传输
其中upload在定义的时候需要添加请求头类型 option.headersType = 'multipart/form-data'
import request from '@/config/axios';
import type { UnwrapRef } from "vue";
const MOCK = '';
//
export function updatePhotosAndDataById(data) {
return request.upload({
url: MOCK + '' +
'/url/url/url',
data: data,
});
}
// -----------------------请求.ts-------------------------------
import type {AxiosInstance, InternalAxiosRequestConfig} from 'axios';
import axios from 'axios';
import {LocalStorage, useCache} from '@/hooks/web/local_storage';
import {showFailToast} from 'vant';
import router from '@/router';
import {setToken} from '@/utils/token';
const {wsCache} = useCache();
interface requestInfo {
method: string,
params: any,
data: any
}
const service: AxiosInstance = axios.create({
baseURL: '/admin-api',
timeout: 30000,
} as any);
const whiteList = ['/login'];
let isRefreshToken = false;
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
let requestList: any[] = [];
const request = (option) => {
const {url, method, params, data, headersType, responseType, headers} = option;
const head = Object.assign({
'Content-Type': headersType || 'application/json',
}, headers);
return service({
url: url,
method,
params,
data,
responseType: responseType,
headers: head,
});
};
const map: Map<string, requestInfo> = new Map();
//重复请求拦截
const duplicateDetection = (config) => {
if (map.has(config.url)) {
const lastReq = map.get(config.url);
if (lastReq.method === config.method && lastReq.params === config.params && JSON.stringify(lastReq.data) === JSON.stringify(config.data)) {
return true;
}
} else {
map.set(config.url, {method: config.method, params: config.params, data: config.data});
}
};
const refreshToken = async () => {
axios.defaults.headers.common['tenant-id'] = wsCache.get(LocalStorage.tenantId);
return await axios.post('/admin-api/system/auth/refresh-token?refreshToken=' + wsCache.get(LocalStorage.refreshToken));
};
const sendLogin = async (url,tenantId)=>{
await axios.get('/admin-api/tpi/wechat/send/login?url='+url+'&tenantId='+tenantId);
};
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
if (duplicateDetection(config)) {
source.cancel();
}
// 是否需要设置 token
const headers: any = config!.headers || {};
let isToken = headers.isToken === false;
whiteList.some((v) => {
if (config.url === v) {
return (isToken = true);
}
});
if (!isToken && wsCache.get(LocalStorage.accessToken)) {
config.headers.Authorization = 'Bearer ' + wsCache.get(LocalStorage.accessToken);
}
if (wsCache.get(LocalStorage.tenantId)) {
config.headers['tenant-id'] = wsCache.get(LocalStorage.tenantId);
}
return config;
},
(error) => {
return Promise.reject(error);
});
service.interceptors.response.use(
async (response) => {
const code = response.data.code || 200;
console.log('code', code)
if (code !== 200) {
if (code === 401) {
console.log('code', isRefreshToken)
if (!isRefreshToken) {
isRefreshToken = true;
console.log('code', !wsCache.get(LocalStorage.refreshToken))
if (!wsCache.get(LocalStorage.refreshToken)) {
router.push('/login');
return;
}
try {
console.log('进入try')
const refreshTokenRes = await refreshToken();
// 2.1 刷新成功,则回放队列的请求 + 当前请求
setToken((await refreshTokenRes).data.data);
response.config.headers!.Authorization = 'Bearer ' + wsCache.get(LocalStorage.accessToken);
requestList.forEach((cb: any) => {
cb();
});
requestList = [];
return service(response.config);
} catch (e) {
console.log('进入catch')
console.log('catch', e)
// 刷新失败时,请求因为 Promise.reject 触发异常。
// 刷新失败,只回放队列的请求
requestList.forEach((cb: any) => {
cb();
});
wsCache.set(LocalStorage.accessToken, '');
wsCache.set(LocalStorage.refreshToken, '');
console.log('href',window.location.href);
// 提示是否要登出。即不回放当前请求!不然会形成递归
return Promise.reject(new Error(response.data.msg));
} finally {
requestList = [];
isRefreshToken = false;
}
}
}
showFailToast(response.data.msg);
return Promise.reject(new Error(response.data.msg));
}
if (map.has(<string>response.config.url)) {
map.delete(<string>response.config.url);
}
return response;
},
(error) => {
showFailToast(error);
return Promise.reject(error);
},
);
export default {
get: async (option) => {
const res = await request({method: 'GET', ...option});
return res.data;
},
post: async (option) => {
const res = await request({method: 'POST', ...option});
return res.data;
},
delete: async (option) => {
const res = await request({method: 'DELETE', ...option});
return res.data;
},
put: async (option) => {
const res = await request({method: 'PUT', ...option});
return res.data;
},
download: async (option) => {
// option.responseType = 'blob'
const res = await request({method: 'GET', ...option});
return res;
},
upload: async (option) => {
option.headersType = 'multipart/form-data'
const res = await request({method: 'POST', ...option});
return res;
}
};
—————————————————————后端——————————————————
@PostMapping("/url")
public CommonResult<Boolean> updateOneDetail(
@RequestParam("data") String data,
@Valid @RequestParam(value = "file", required = false) MultipartFile[] files
) throws Exception {
DO detailDO = JSON.parseObject(data, DO.class);
return success(updateDetailService.updatePhotos(detailDO, files));
}
// ---- 实现 Boolean updatePhotos(DO detailDO, MultipartFile[] files)throws Exception;
// ---- 实现类 photos 字段在DO中的类型是List
@TableField(value = "photos", typeHandler = FastjsonTypeHandler.class)
private List<Diagram> photos;
// 注:所有图片在存到表中相对应的字段中的同时会存到一张专门用于放文件流的fileService对应的表中
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updatePhotos(DO detailDO, MultipartFile[] files) throws Exception {
String name = "";
String fileUrl = "";
Diagram diagram = new Diagram();
List<Diagram> diagramSave = new ArrayList<>();
DO billDetailDO = baseMapper.selecById(detailDO.getId()); //查出photos字段
<!-- // mapper.xml中使用自定义结果集 映射出photos集合
<resultMap id="detailMap" type="cn.****.**.***.DO" autoMapping="true">
<result column="photos"property="photos"
typeHandler="com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler"/>
</resultMap> -->
if (billDetailDO.getCheckPhoto() != null) {
diagramSave = billDetailDO.getCheckPhoto(); // 保留原有图片
}
if (files != null) {
for (MultipartFile fileList : files) {
name = (IdWorker.getId()) + getExtension(fileList.getOriginalFilename());
//上传图片
fileUrl = fileService.createFile(fileList.getOriginalFilename(), name, IoUtil.readBytes(fileList.getInputStream()));
diagram = new Diagram();
diagram.setUrl(fileUrl);
diagram.setName(name);
diagram.setRealName(fileList.getOriginalFilename());
diagramSave.add(diagram);
}
detailDO.setPhotos(diagramSave); // 原图片和files
}else {
detailDO.setPhotos(billDetailDO.getCheckPhoto()); // 只保留原图片
}
int i = baseMapper.updateById(detailDO);
if(i > 0){
return true;
}else {
return false;
}
}
(二)删图片
<script>
const handelDelete = async (file) => {
if('file' in file){
// 还没有上传到后端的图片 直接删
}else {
// 调用删除图片接口 传此条数据的id
await deleteImage(id, file.url).then(() => {
fileList.value = fileList.value // 更新当前图片的显示
})
}
}
</script>
// ----------------------------delete-------------------
export const deleteImage = (id, fileName) => {
return request.delete({
url: '/url/url/?id=' + id + '&fileUrl=' + fileName
})
}
后端接收:
@DeleteMapping("delete-one-image")
public CommonResult<Boolean> deleteImage(
@Valid @RequestParam("id") Long id,
@RequestParam("fileUrl")String fileUrl
) throws Exception {
return success(baseService.deleteOnePhoto(id,fileUrl));
}
// 实现
Boolean deleteOnePhoto(Long id, String fileUrl) throws Exception;
// 实现类
@Override
public Boolean deleteOnePhoto(Long id, String fileUrl) throws Exception {
//删除图片
BaseDO detailDO = baseMapper.selectOneById(id);
List<Diagram> diagramList = new ArrayList<>();
diagramList = detailDO.getPhotos();
FilePageReqVO filePageReqVO = new FilePageReqVO();
PageResult<FileDO> fileDOList = new PageResult<>();
JSONArray diagramArr = JSONArray.parseArray(detailDO.getPhotos().toString());
JSONObject diagramObj = new JSONObject();
Diagram diagram = new Diagram();
diagramList = new ArrayList<>();
//删除指定文件
String fileName = FilenameUtils.getName(fileUrl);
filePageReqVO = new FilePageReqVO();
filePageReqVO.setPath(fileUrl);
fileDOList = fileService.getFilePage(filePageReqVO);
if (fileDOList.getList().size() > 0) {
for (FileDO fileDOListList : fileDOList.getList()) {
fileService.deleteFile(fileDOListList.getId());
}
}
for (int i = 0; i < diagramArr.size(); i++) {
diagramObj = diagramArr.getJSONObject(i);
if (!fileUrl.equals(diagramObj.getString("url"))) {
diagram.setName(diagramObj.getString("name"));
diagram.setUrl(diagramObj.getString("url"));
diagram.setRealName(diagramObj.getString("realName"));
diagramList.add(diagram);
}
}
detailDO.setPhotos(diagramList);
baseMapper.updateById(detailDO);
return true;
}