十年河東,十年河西,莫欺少年窮
學無止境,精益求精
這是一個后端開發人員的倔強
因項目需要,我從一個純后端變成了前端+后端,也俗稱全棧。
在實際項目中,上傳附件幾乎是必不可少的,因此,我們有必要將上傳控件進行組件化。
設計理念,通過vue的父子傳值進行封裝,上傳組件為子組件,調用方為父組件,由父組件傳值給子組件,用來校驗上傳的文件擴展名及大小。
子組件
<template> <a-upload :beforeUpload="beforeUpload" :multiple="false" @change="filechange" :customRequest="customRequest" :fileList="fileList" :remove="fileRemove" > <a-button> <a-icon type="upload" /> 選擇文件 </a-button> </a-upload> </template> <script> export default { props: ["fileinfo"], created() {}, data() { return { fileList: [], }; }, methods: { beforeUpload(file) { var that = this; //console.log(file); return new Promise((resolve, reject) => { const isLt100M = file.size / 1024 / 1024 > that.fileinfo.maxsize; if (isLt100M) { this.$message.warning( "上傳附件大小不能超過" + that.fileinfo.maxsize + "M。" ); return reject(false); } var index = file.name.lastIndexOf("."); var suffix = file.name.substring(index + 1); if( that.fileinfo.filetype.indexOf(suffix)<0){ this.$message.warning( "上傳的文件格式不符合要求" ); return reject(false); } return resolve(true); }); }, filechange(info) { //console.log(info); this.fileList = info.fileList; //console.log(this.fileList); if (info.file.status == "uploading") { } if (info.file.status === "done") { } else if (info.file.status === "error") { } }, customRequest(data) { // 上傳提交 const formData = new FormData(); formData.append("file", data.file); this.saveFile(formData); }, saveFile(formData) { let that = this; this.$axios({ url: "/api/File/UploadFileStream", method: "post", data: formData, }) .then(function (result) { //console.log(result); //that.fileList.push(result.Data) if (result.IsSuccess) { for (var i = 0; i < that.fileList.length; i++) { that.fileList[i].status = "done"; } console.log(that.fileinfo.uploadfiles); that.fileinfo.uploadfiles.push(result.Data); that.$message.success("上傳附件成功。"); } else { //that.fileList = []; for (var i = 0; i < that.fileList.length; i++) { if (that.fileList[i].status != "done") { that.fileList.splice(i, 1); } } that.$message.warning(result.ResultMessage); } }) .catch(function (error) { console.log(error); }); }, fileRemove(file) { //console.log(file); var that = this; for (var i = 0; i < that.fileinfo.uploadfiles.length; i++) { if ( that.fileinfo.uploadfiles[i].oldfilename == file.name && that.fileinfo.uploadfiles[i].filesize == file.size ) { that.fileinfo.uploadfiles.splice(i, 1); //alert(1) } } }, }, }; </script>
父組件
<template>
<div>
<a-row align="middle" class="arowLat">
<a-col :span="12">
<a-checkable-tag v-model="checked" @change="handleChange">
OTA固件包
</a-checkable-tag>
<file-upload v-bind:fileinfo="fileinfo"></file-upload>
</a-col>
<a-col :span="12">
<a-button @click="submit">提交</a-button>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
name: "Echarts",
data() {
return {
checked:false,
fileinfo: {
maxsize: 1024 * 1024 * 100,
filetype: ["jpg", "png"],
uploadfiles: [],
},
};
},
methods: {
submit() {
console.log(this.fileinfo.uploadfiles);
}
},
mounted() {},
};
</script>
<style scoped>
.img {
width: 100%;
height: auto;
}
.arow {
text-align: left;
}
.arowLat {
text-align: left;
margin-top: 25px;
}
</style>
下面來簡單分析下上述源碼
父組件Data中fileinfo的屬性用來校驗文件大小,擴展名
data() { return { checked:false, fileinfo: { maxsize: 1024 * 1024 * 100, filetype: ["jpg", "png"], uploadfiles: [], }, }; }
注:uploadfiles 用於接收子組件請求后台的結果,我的后台返回值為:
{ Data { LocalFilePath: "E:\IOT\IotApi\Content\OTA\4dccdff9bc17413c96d98031a8a451ec.bin", filedata: [80, 19, 0, 32, 1, 33, 0, 8, 1, 139, 0, 8, 69, 114, 0, 8, 229, 138, 0, 8, 43, 74, 0, 8, 219, 182, 0, 8, 0, 0...], filename: "4dccdff9bc17413c96d98031a8a451ec.bin", filepath: "~/Content/OTA/4dccdff9bc17413c96d98031a8a451ec.bin", filesize: 49336, oldfilename: "LBS-1522-YQ4501.jpg", }
IsSuccess: true, ResultCode: 0, ResultMessage: "請求成功...", }
fileinfo 用於傳值給子組件,子組件中有對應的 props,如下
props: ["fileinfo"],
a-upload 組件中有個名叫 beforeUpload 的鈎子函數,我們用來校驗上傳的文件,如果通過校驗,則上傳
beforeUpload(file) { var that = this; //console.log(file); return new Promise((resolve, reject) => { const isLt100M = file.size / 1024 / 1024 > that.fileinfo.maxsize; if (isLt100M) { this.$message.warning( "上傳附件大小不能超過" + that.fileinfo.maxsize + "M。" ); return reject(false); } var index = file.name.lastIndexOf("."); var suffix = file.name.substring(index + 1); if( that.fileinfo.filetype.indexOf(suffix)<0){ this.$message.warning( "上傳的文件格式不符合要求" ); return reject(false); } return resolve(true); }); },
fileRemove 方法用來同步移除 uploadfiles 的元素,保持和組件顯示的上傳文件一致。
fileRemove(file) { //console.log(file); var that = this; for (var i = 0; i < that.fileinfo.uploadfiles.length; i++) { if ( that.fileinfo.uploadfiles[i].oldfilename == file.name && that.fileinfo.uploadfiles[i].filesize == file.size ) { that.fileinfo.uploadfiles.splice(i, 1); //alert(1) } } },
在組件開發階段,遇到一個問題,上傳成功后,上傳的狀態一直是 uploading ,為了解決這個問題,我們需要在 filechange、customRequest、saveFile 一起解決。
主要是在 filechange 中給filelist賦值,在 saveFile 中當文件上傳成功后,修改狀態為done,如果上傳失敗,我們需要將filelist中上傳失敗的元素移除。
filechange(info) { //console.log(info); this.fileList = info.fileList; //console.log(this.fileList); if (info.file.status == "uploading") { } if (info.file.status === "done") { } else if (info.file.status === "error") { } }, customRequest(data) { // 上傳提交 const formData = new FormData(); formData.append("file", data.file); this.saveFile(formData); }, saveFile(formData) { let that = this; this.$axios({ url: "/api/File/UploadFileStream", method: "post", data: formData, }) .then(function (result) { //console.log(result); //that.fileList.push(result.Data) if (result.IsSuccess) { for (var i = 0; i < that.fileList.length; i++) { that.fileList[i].status = "done"; } console.log(that.fileinfo.uploadfiles); that.fileinfo.uploadfiles.push(result.Data); that.$message.success("上傳附件成功。"); } else { //that.fileList = []; for (var i = 0; i < that.fileList.length; i++) { if (that.fileList[i].status != "done") { that.fileList.splice(i, 1); } } that.$message.warning(result.ResultMessage); } }) .catch(function (error) { console.log(error); }); },
以上便是整個上傳組件。效果圖有點丑

在父組件,點擊提交,即可得到請求上傳接口的返回值

這樣整個組件也就做完了,有了這個組件,以后我們只需在父組件中引用子組件,並添加Props傳值對象fileinfo即可,從而節省了大量的重復代碼。
using Iot.Common; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Http.Cors; namespace Iot.WebSite.Controllers.Apis { [RoutePrefix("api/file")] [EnableCors(origins: "*", headers: "*", methods: "GET,POST,PUT,DELETE")] public class FileController : ApiController { /// <summary> /// 上傳文件 /// </summary> /// <returns></returns> [HttpPost] public async Task<BaseResponse> UploadFileStream() { var returns = CommonBaseResponse.SetResponse<fileInfoModel>(null, false); string fileType = "OTA";//要創建的子文件夾的名字 var uploadPath = "~/Content"; string filePath = System.Web.HttpContext.Current.Server.MapPath(uploadPath + "/" + fileType + "/");//絕對路徑 //string filePath = uploadPath + "\\" + fileType + "\\"; //E:\Fileup 居家 if (Directory.Exists(filePath) == false) { Directory.CreateDirectory(filePath); } try { var provider = new ReNameMultipartFormDataStreamProvider(filePath); await Request.Content.ReadAsMultipartAsync(provider).ContinueWith(o => { foreach (var file in provider.FileData) { string orfilename = file.Headers.ContentDisposition.FileName.TrimStart('"').TrimEnd('"');//待上傳的文件名 FileInfo fileinfo = new FileInfo(file.LocalFileName); //判斷開始 string oldName = orfilename;//選擇的文件的名稱 string fileExt = orfilename.Substring(orfilename.LastIndexOf('.')); string Extension = fileExt; string CreateTime = DateTime.Now.ToString("yyyyMMddHHmmss"); fileInfoModel fileResult = new fileInfoModel() { oldfilename = oldName, LocalFilePath = file.LocalFileName, filesize = fileinfo.Length, filename = fileinfo.Name, filepath = uploadPath + "/" + fileType + "/" + fileinfo.Name }; var fs = fileinfo.OpenRead(); byte[] buffur = new byte[fs.Length]; fs.Read(buffur, 0, (int)fs.Length); fileResult.filedata = buffur.ToList(); returns = CommonBaseResponse.SetResponse<fileInfoModel>(fileResult, true); } }); } catch (Exception ex) { LogHelper.WriteLog("上傳附件出錯:" + ex.ToString()); } return returns; } } /// <summary> /// 重命名上傳的文件 /// </summary> public class ReNameMultipartFormDataStreamProvider : MultipartFormDataStreamProvider { public ReNameMultipartFormDataStreamProvider(string root) : base(root) { } public override string GetLocalFileName(System.Net.Http.Headers.HttpContentHeaders headers) { string extension = !string.IsNullOrWhiteSpace(headers.ContentDisposition.FileName) ? Path.GetExtension(GetValidFileName(headers.ContentDisposition.FileName)) : ""; return Guid.NewGuid().ToString().Replace("-", "") + extension; } private string GetValidFileName(string filePath) { char[] invalids = System.IO.Path.GetInvalidFileNameChars(); return String.Join("_", filePath.Split(invalids, StringSplitOptions.RemoveEmptyEntries)).TrimEnd('.'); } } public class fileInfoModel { /// <summary> /// 新文件名稱 /// </summary> public string filename { get; set; } /// <summary> /// 老文件名稱 /// </summary> public string oldfilename { get; set; } /// <summary> /// 服務器絕對地址 /// </summary> public string LocalFilePath { get; set; } /// <summary> /// 文件大小 字節 /// </summary> public long filesize { get; set; } /// <summary> /// 問價數據 /// </summary> public List<byte> filedata { get; set; } /// <summary> /// 文件相對路徑 /// </summary> public string filepath { get; set; } } }
增刪改查代碼,只做記錄,方便自己以后查看
<template>
<div>
<a-card
hoverable="true"
title=""
headStyle="text-align:left;color:#606266;font-size:14px"
bodyStyle="border:none"
>
<a slot="extra" href="#" style="float: left">
<a-breadcrumb separator=">">
<a-breadcrumb-item>OTA升級</a-breadcrumb-item>
<a-breadcrumb-item>電池OTA升級 </a-breadcrumb-item>
</a-breadcrumb>
</a>
<div>
<a-row align="middle" class="arow">
<a-col :span="6">
<a-checkable-tag v-model="checked" @change="handleChange">
OTA版本號
</a-checkable-tag>
<a-input
placeholder="請輸入OTA版本號"
style="width: 180px"
v-model="SearchInfo.SoftWare"
/>
</a-col>
<a-col :span="6">
<a-checkable-tag v-model="checked" @change="handleChange">
BMS版本號
</a-checkable-tag>
<a-input
placeholder="請輸入BMS版本號"
style="width: 180px"
v-model="SearchInfo.OldSoftWare"
/>
</a-col>
<a-col :span="6"> </a-col>
<a-col :span="6">
<a-button type="primary" @click="Search"> 查詢 </a-button>
<a-button type="danger" @click="() => setModal1Visible(true)">
新增
</a-button>
</a-col>
</a-row>
<a-table
:columns="columns"
:data-source="data"
style="margin-top: 20px"
>
<template slot="action" slot-scope="text, record">
<a slot="action" href="javascript:;" @click="deleterow(record)"
>刪除</a
>
<a slot="action" href="javascript:;" @click="onEdit(record)" >編輯</a>
<!-- <span slot="action" slot-scope="text" href="javascript:;" @click="onEdit()" >刪除</span> -->
</template>
</a-table>
</div>
</a-card>
<a-modal
title="新增OTA升級信息"
:width="760"
:okText="提交"
:cancelText="取消"
:dialog-style="{ top: '20px' }"
:visible="modal1Visible"
@ok="() => Save()"
@cancel="() => setModal1Visible(false)"
>
<a-card
hoverable="true"
title=""
headStyle="text-align:left;color:#606266;font-size:14px"
bodyStyle="border:none"
>
<a-row align="middle" class="arow">
<a-col :span="12">
<a-checkable-tag v-model="checked" @change="handleChange">
OTA版本號
</a-checkable-tag>
<a-input
placeholder="請輸入OTA版本號"
style="width: 180px"
v-model="OtaModel.SoftWare"
/>
</a-col>
<a-col :span="12">
<a-checkable-tag v-model="checked" @change="handleChange">
單包大小
</a-checkable-tag>
<a-input-number
v-model="OtaModel.SigPackSize"
placeholder="請輸入單包大小"
:min="64"
:max="2048"
style="width: 180px"
/>
</a-col>
</a-row>
<a-row align="middle" class="arowLat">
<a-col :span="12">
<a-checkable-tag v-model="checked" @change="handleChange">
BMS版本號
</a-checkable-tag>
<a-input
placeholder="請輸入BMS版本號"
style="width: 180px"
v-model="OtaModel.OldSoftWare"
/>
</a-col>
<a-col :span="12">
<a-checkable-tag v-model="checked" @change="handleChange">
升級描述
</a-checkable-tag>
<a-input
placeholder="請輸入升級描述"
style="width: 180px"
v-model="OtaModel.OtaFileInfo"
/>
</a-col>
</a-row>
<a-row align="middle" class="arowLat">
<a-col :span="12">
<a-checkable-tag v-model="checked" @change="handleChange">
OTA固件包
</a-checkable-tag>
<file-upload v-bind:fileinfo="fileinfo"></file-upload>
</a-col>
<a-col :span="12"> </a-col>
</a-row>
</a-card>
</a-modal>
</div>
</template>
<script>
import moment from "moment";
import "moment/locale/zh-cn";
import locale from "ant-design-vue/es/date-picker/locale/zh_CN";
const formatterTime = (val) => {
alert(val);
return val ? moment(val).format("YYYY-MM-DD HH:mm:ss") : "";
};
const columns = [
{
title: "升級描述",
width: 200,
dataIndex: "OtaFileInfo",
key: "OtaFileInfo",
align: "center",
},
{ title: "固件大小(字節)", dataIndex: "FileSize", key: "1", align: "center" },
{ title: "BMS版本號", dataIndex: "OldSoftWare", key: "2", align: "center" },
{ title: "OTA版本號", dataIndex: "SoftWare", key: "3", align: "center" },
{
title: "單包大小(字節)",
dataIndex: "SigPackSize",
key: "4",
align: "center",
},
{
title: "創建日期",
dataIndex: "CreateTime",
key: "8",
align: "center",
customRender: function (val) {
return val ? moment(val).format("YYYY-MM-DD") : "";
},
},
{
title: "操作",
key: "operation",
align: "center",
width: 100,
scopedSlots: { customRender: "action"},
},
];
const data = [];
export default {
name: "Echarts",
data() {
return {
locale,
checked: false,
checkedmodal: true,
dateFormat: "YYYY/MM/DD",
monthFormat: "YYYY/MM",
data,
columns,
modal1Visible: false,
fileinfo: {
maxsize: 1024 * 1024 * 10,
filetype: ["bin"],
uploadfiles: [],
},
OtaModel: {
OldSoftWare: "",
SoftWare: "",
SigPackSize: 512,
OtaFileInfo: "",
FileName: "",
oldfilename: "",
LocalFilePath: "",
filesize: 0,
filedata: [],
FilePath: "",
},
SearchInfo: {
SoftWare: "",
OldSoftWare: "",
PageNumber: 1,
PageSize: 1,
Stime: "2020-01-01",
Etime: "2030-01-01",
},
};
},
methods: {
moment,
onChange(dates, dateStrings) {
console.log("From: ", dates[0], ", to: ", dates[1]);
console.log("From: ", dateStrings[0], ", to: ", dateStrings[1]);
},
handleChange(checked) {},
setModal1Visible(modal1Visible) {
this.modal1Visible = modal1Visible;
},
Save() {
let that = this;
if (!that.OtaModel.OldSoftWare) {
that.$message.error("請輸入BMS版本號。");
return;
} else if (!that.OtaModel.SoftWare) {
that.$message.error("請輸入OTA版本號。");
return;
} else if (!that.OtaModel.SigPackSize) {
that.$message.error("請輸入單包大小。");
return;
} else if (!that.OtaModel.OtaFileInfo) {
that.$message.error("請輸入OTA版本描述");
return;
} else if (
!that.fileinfo.uploadfiles ||
that.fileinfo.uploadfiles.length != 1
) {
that.$message.error("請上傳升級固件包【有且僅能上傳一個固件包】");
return;
} else {
that.OtaModel.FileName = that.fileinfo.uploadfiles[0].filename;
that.OtaModel.oldfilename = that.fileinfo.uploadfiles[0].oldfilename;
that.OtaModel.LocalFilePath =
that.fileinfo.uploadfiles[0].LocalFilePath;
that.OtaModel.filesize = that.fileinfo.uploadfiles[0].filesize;
that.OtaModel.filedata = that.fileinfo.uploadfiles[0].filedata;
that.OtaModel.FilePath = that.fileinfo.uploadfiles[0].filepath;
//console.log(that.OtaModel);
that
.$axios({
url: "/api/BatteryOta/CreateBatteryOtaSoftWareInfo",
method: "post",
data: that.OtaModel,
})
.then(function (result) {
if (result.IsSuccess) {
that.OtaModel = {
OldSoftWare: "",
SoftWare: "",
SigPackSize: 512,
OtaFileInfo: "",
FileName: "",
oldfilename: "",
LocalFilePath: "",
filesize: 0,
filedata: [],
FilePath: "",
};
that.modal1Visible = false;
} else {
that.$message.error(result.ResultMessage);
that.OtaModel = {
OldSoftWare: "",
SoftWare: "",
SigPackSize: 512,
OtaFileInfo: "",
FileName: "",
oldfilename: "",
LocalFilePath: "",
filesize: 0,
filedata: [],
FilePath: "",
};
}
})
.catch(function (error) {
console.log(error);
});
}
},
Search() {
let that = this;
that
.$axios({
url: "/api/BatteryOta/SearchOtaSoftWareInfo",
method: "post",
data: that.SearchInfo,
})
.then(function (result) {
console.log(result);
if (result.IsSuccess) {
that.data = result.Data.Data;
} else {
that.$message.error(result.ResultMessage);
}
})
.catch(function (error) {
console.log(error);
});
},
deleterow(record) {
console.log(record);
},
},
mounted() {},
created() {
this.Search();
},
};
</script>
<style scoped>
.img {
width: 100%;
height: auto;
}
.arow {
text-align: left;
}
.arowLat {
text-align: left;
margin-top: 25px;
}
</style>
@天才卧龍的博客
