實(shí)現(xiàn)效果圖
功能預(yù)覽
- 【less】上傳文件的input框樣式的改造
- 獲取圖片列表
- 點(diǎn)擊上傳圖片 + 轉(zhuǎn)換成base64
- 上傳中l(wèi)oading
- 上傳失敗
- 刪除圖片
- 查看大圖
- 下載圖片 + Blob URL
- useState 與 useEffect的使用
- 跨域
- 靜態(tài)文件的查看
- 查詢/刪除/添加文件
- uuid
- 用同步的方式書寫異步的操作(封裝 async await promise集合體)
- axios
【前端】React
1.【less】上傳文件的input框樣式的改造
<div className="upload-container item">
<Icon type="plus" className="icon"/>
<div className="name">Upload</div>
<input id="upload-image"
type="file"
name="image"
accept="image/*"
onChange={()=>handleUploadImage(props)}/>
</div>
主要用到的是原生的input框:
type="file"
彈出選擇上傳文件的框潜圃;
accept="image/*"
來限制你選擇上傳的文件只能是圖片類型的;
當(dāng)你彈出選擇文件的框之后郑藏,無論是選擇還是取消都會(huì)觸發(fā)onChange
事件馁蒂。
原生input上傳文件的樣式很丑呵晚,于是換樣式成了重中之重!
思路:
1.把input框給隱藏掉
2.并且要設(shè)置一個(gè)與最終樣式同樣大小的寬高沫屡,通過子絕父相讓input框覆蓋在最上層饵隙,這樣才能夠命中點(diǎn)擊事件
.upload-container{
margin: 5px;
padding: 3px;
float: left;
border-style: dashed;
background-color: #f5f5f5;
position: relative; //父相
flex-direction: column;
.icon{
color: #e1e1e1;
margin-top: 10px;
font-size: 40px;
font-weight: bolder;
}
.name{
color: #a3a3a3;
}
input{
width: 100%;
height: 100%; // 與父樣式等寬高
cursor: pointer;
position: absolute; // 子絕
top: 0;
opacity: 0; // 全透明
}
}
2. 用同步的方式書寫異步的操作(封裝 async await promise集合體)+ 獲取圖片列表 + useState
const baseUrl = 'http://localhost:8080/image';
const getImageListUrl = `${baseUrl}/list`;
const [list, setList] = useState([]);
const ajax = (url, data={}, type='GET') =>{
return new Promise((resolve)=>{
const promise = type === 'GET' ? axios.get(url, {params: data}) : axios.post(url, data)
promise.then(res=>{
const data = res.data;
data.status !== 0 ? message.error(data.msg) : resolve(data);
}).catch(err => {
message.error('Network request Error: '+err);
})
})
};
const getAndUpdateImageList = async(setList) => {
const data = await ajax(getImageListUrl);
setList(data.data);
};
3. 點(diǎn)擊上傳圖片 + 轉(zhuǎn)換成base
出現(xiàn)上傳文件的框,無論是選擇還是取消沮脖,都會(huì)觸發(fā)onChange
事件金矛,所以要判斷你選擇的targetFile
是否存在。
通過FileReder將圖片轉(zhuǎn)成base64勺届,用同步的方式將最終結(jié)果拋出去:
const getBase64 = (file) => {
const fileReader = new FileReader();
fileReader.readAsDataURL(file);
return new Promise((resolve)=>{
fileReader.onload = (data) => {
resolve(data.target.result)
}
})
};
獲取到了base64之后再發(fā)送axios請(qǐng)求到后端:
const handleUploadImage= async ({list,action,uploadImage,setUploadLoading, setUploadErrorFileName})=>{
const input = document.getElementById('upload-image');
const targetFile = input.files[0];
if (targetFile){
setUploadLoading(true);
const { name } = targetFile;
const imageBase64 = await getBase64(targetFile);
await axios.post(action, {imageBase64, name})
.then(()=>{
uploadImage(action + "/" + name);
}).catch(() => {
setUploadErrorFileName(name);
});
setUploadLoading(false);
}
};
4. 上傳中l(wèi)oading
當(dāng)我們開始上傳圖片的時(shí)候驶俊,會(huì)將uploadLoading
設(shè)置成true,上傳成功之后會(huì)將uploadLoading
再設(shè)置成false涮因!
const handleUploadImage= async ({list,action,uploadImage,setUploadLoading, setUploadErrorFileName})=>{
const input = document.getElementById('upload-image');
const targetFile = input.files[0];
if (targetFile){
setUploadLoading(true);
...
setUploadLoading(false);
}
};
const [uploadLoading, setUploadLoading] = useState(false);
{uploadLoading && renderUploading()}
const renderUploading =()=> (
<div className="uploading item">
<Icon className="icon" type='loading' />
<span>Uploading...</span>
</div>
);
5. 上傳失敗
與loading其實(shí)同理, 其次废睦,上傳失敗之后設(shè)置uploadErrorFileName
來渲染失敗的樣式。
await axios.post(action, {imageBase64, name})
.then(()=>{
...
}).catch(() => {
setUploadErrorFileName(name);
});
const [uploadErrorFileName, setUploadErrorFileName] = useState(null);
{uploadErrorFileName && renderUploadError(uploadErrorFileName,setUploadErrorFileName)}
const renderUploadError =(uploadErrorFileName, setUploadErrorFileName)=> (
<div className="uploading item error">
<Icon className="icon" type='close-circle' />
<div className="error-message">Error!</div>
<div className="name">{uploadErrorFileName}</div>
<div className="config item">
<Icon className="delete-icon" type="delete" onClick={()=> setUploadErrorFileName(null)}/>
</div>
</div>
);
6. 刪除圖片
const deleteImage = async ({name}, setList)=>{
await ajax(deleteImageUrl,{name});
getAndUpdateImageList(setList);
};
7. 查看大圖
用到了Antd的上傳圖片一樣养泡,用到的Modal框去顯示大圖嗜湃, 用previewSrc
保存選擇的結(jié)果
const [previewSrc, setPreviewSrc] = useState(null);
<Modal
width={800}
className="preview-modal"
visible={previewSrc !== null}
title={null}
footer={null}
onCancel={()=>setPreviewSrc(null)} >
<img src={previewSrc} alt=""/>
</Modal>
8. 下載圖片 + blob
思路:
1.用a標(biāo)簽的download屬性來實(shí)現(xiàn)下載效果奈应,因?yàn)橄螺d按鈕是Icon,所以點(diǎn)擊download Icon之后觸發(fā)a標(biāo)簽的download
2.使用Blob: 使用URL.createObjectURL()函數(shù)可以創(chuàng)建一個(gè)Blob URL
<Icon type="download" className="icon" onClick={()=>downloadImage(item, onDownload)}/>
<a id="download-image" download={item.name}/>
const downloadImage=(item, onDownload)=>{
const target = document.getElementById('download-image');
const blob = new Blob([item.src]);
target.href = URL.createObjectURL(blob);
target.click();
onDownload(item);
};
【后端】Nest
1.跨域
app.enableCors();
2.靜態(tài)文件暴露
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(join(__dirname, '..', 'data/images'), {
prefix: '/images/',
});
3.寫文件
- 解析base64,生成buffer
- fs.writeFile(buffer)
@Post('/image/add')
getImage(@Req() req, @Res() res): void {
const {imageBase64, name} = req.body;
const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, '');
const dataBuffer = new Buffer(base64Data, 'base64');
fs.writeFile(`data/images/${name}`, dataBuffer, (err) => {
if (err) {
res.send(err);
} else {
res.send({status: 0 });
}
});
}
4.讀文件
fs.readdirSync(文件名)
@Get('/image/list')
getProductList(@Res() res): void {
const data = fs.readdirSync('data/images');
const url = 'http://localhost:8080/images/';
res.send({
data: data.map(item => ({
name: item,
id: uuid(),
src: url + item,
})),
status: 0,
});
}
5.刪文件
fs.unlinkSync(文件名)
@Get('/image/delete')
deleteImage(@Query() query, @Res() res): void {
const files = fs.readdirSync('data/images');
const target = files.filter(item => item === query.name);
if (target) {
fs.unlinkSync('data/images/' + target);
res.send({status: 0 });
}
res.send({status: 1 });
}
案例
用useEffect代替了didMount請(qǐng)求圖片列表
const [list, setList] = useState([]);
useEffect(() => {
getAndUpdateImageList(setList);
},[]);
import React,{useState, useEffect} from 'react';
import UploadImage from "./component/upload-image/upload-image";
import axios from "axios";
import {message} from "antd";
const baseUrl = 'http://localhost:8080/image';
const getImageListUrl = `${baseUrl}/list`;
const addImageUrl = `${baseUrl}/add`;
const deleteImageUrl = `${baseUrl}/delete`;
const ajax = (url, data={}, type='GET') =>{
return new Promise((resolve)=>{
const promise = type === 'GET' ? axios.get(url, {params: data}) : axios.post(url, data)
promise.then(res=>{
const data = res.data;
data.status !== 0 ? message.error(data.msg) : resolve(data);
}).catch(err => {
message.error('Network request Error: '+err);
})
})
};
const getAndUpdateImageList = async(setList) => {
const data = await ajax(getImageListUrl);
setList(data.data);
};
const deleteImage = async ({name}, setList)=>{
await ajax(deleteImageUrl,{name});
getAndUpdateImageList(setList);
};
const uploadImage = (item, setList) => {
getAndUpdateImageList(setList);
};
const downloadImage = (item) => {
console.log('downloadImage', item)
};
function App() {
const [list, setList] = useState([]);
useEffect(() => {
getAndUpdateImageList(setList);
},[]);
return (
<div className="App">
<UploadImage
action={addImageUrl}
list={list}
onUpload={(item)=>uploadImage(item, setList)}
onDelete={(item)=>deleteImage(item, setList)}
onDownload={(item)=>downloadImage(item)}
/>
</div>
);
}
export default App;