UNPKG

jobsys-newbie

Version:

Enhanced component based on ant-design-vue

414 lines (359 loc) 10.7 kB
import { computed, defineComponent, inject, onMounted, reactive, watch } from "vue" import { Form, message, Upload, Image, ImagePreviewGroup } from "ant-design-vue" import { every, isArray, isEqual, map, pick, random } from "lodash-es" import { NEWBIE_UPLOADER } from "../provider/NewbieProvider.jsx" import { STATUS, useFetch, useT } from "../../hooks" import Resumable from "resumablejs" import "./index.less" import { CloudUploadOutlined } from "@ant-design/icons-vue" import NewbieButton from "../button/NewbieButton.jsx" /** * 上传组件 * * @version 1.0.0 * */ export default defineComponent({ name: "NewbieUploader", props: { value: { type: [Object, Array, String], default: () => ({}) }, /** * 上传文件字段名 */ name: { type: String, default: "file" }, /** * 设置上传的请求头部,IE10 以上有效 */ headers: { type: Object, default: () => ({}) }, /** * 接受上传的文件类型, 详见 [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) */ accept: { type: String, default: "" }, /** * 上传列表的内建样式,支持三种基本样式 text/file, picture 和 picture-card * * @values text|file, picture, picture-card */ type: { type: String, default: "file" }, /** * 是否禁用 */ disabled: { type: Boolean, default: false }, /** * 单个文件大小上限,单位为 MB */ maxSize: { type: Number, default: 20 }, /** * 上传文件个数上限 */ maxNum: { type: Number, default: 1 }, /** * 是否支持多选文件 */ multiple: { type: Boolean, default: false }, /** * 是否使用分块上传 */ multipart: { type: Boolean, default: false }, /** * 上传文件的服务器地址 */ action: { type: String, default: "" }, /** * 上传时的附加参数 */ extraData: { type: Object, default: () => ({}) }, /** * 上传按钮文本 */ uploadText: { type: String, default: "" }, /** * 上传盘符标志,可以灵活配合后台使用 */ disk: { type: String, default: "" }, /** * 原生 [Uploader](https://www.antdv.com/components/upload-cn#api) 配置 */ uploadProps: { type: Object, default: () => ({}) }, }, emits: [ "update:value", /** * 上传成功时触发 * * @event success * @param {Array} fileList 文件列表 * */ "success", ], setup(props, { emit }) { const { STATE_CODE_SUCCESS } = STATUS const formItemContext = Form.useInjectFormItemContext() const uploaderProvider = inject(NEWBIE_UPLOADER, () => ({})) const defaultUploadUrl = uploaderProvider.uploadUrl || "" const defaultFileItem = uploaderProvider.defaultFileItem || {} const { url: urlKey, path: pathKey, name: nameKey } = defaultFileItem const state = reactive({ fileList: [], previewVisible: false, previewCurrent: 0, progress: props.multipart ? { strokeColor: { "0%": "#108ee9", "100%": "#87d068", }, strokeWidth: 3, format: (percent) => `${parseFloat(percent.toFixed(2))}%`, } : null, }) const isSignle = computed(() => !props.maxNum || props.maxNum === 1) const isOverflow = computed(() => { return state.fileList.length >= props.maxNum }) const isImage = computed(() => { return props.type === "picture-card" || props.type === "picture" }) /** * submit 前处理文件列表 * @param list * @returns {{[p: string]: *}[]} */ const processFileList = (list) => { if (!isArray(list)) { list = [list] } const fileList = list .filter((item) => item.done || !!item[pathKey] || !!item[nameKey]) .map((item) => ({ ...pick(item, Object.values(defaultFileItem)), _type: "file", _disk: props.disk, })) return isSignle.value ? fileList[0] || null : fileList } /** * 将文件列表处理成符合文件结构的数组 * @param {Array|Object} fileList */ const prepareFileList = (fileList) => { if (!fileList) { return [] } fileList = isArray(fileList) ? fileList : [fileList] state.fileList = fileList .filter((item) => item[urlKey] || item[nameKey] || item[pathKey]) .map((item) => ({ uid: random(1, 10000000), done: true, name: item[nameKey] || useT("form.attachment"), url: item[urlKey], _type: "file", ...item, })) //由于初始值可能不符合文件结构,处理后再次触发更新 emit("update:value", processFileList(state.fileList)) } onMounted(() => { // 控制这个组件的渲染时间 prepareFileList(props.value) }) watch( () => props.value, (fileList) => { if (fileList && !isArray(fileList)) { fileList = [fileList] } //如果文件列表相同应该避免重复处理 if (!isEqual(map(fileList, pathKey).sort(), map(state.fileList, pathKey).sort())) { prepareFileList(fileList) } }, ) const handlePreview = (file) => { state.previewCurrent = state.fileList.findIndex((item) => item.uid === file.uid) state.previewVisible = true } const submitFile = (list) => { let fileList = processFileList(list) emit("update:value", fileList) emit("success", fileList) formItemContext.onFieldChange() } const handleChange = ({ file, fileList }) => { fileList = fileList .map((item) => { if (item.status === "done" && item.response) { const res = item.response if (res.result[pathKey]) { item = { ...item, ...pick(res.result, Object.values(defaultFileItem)) } item.done = true } else { item.isRemoved = true } } else if (item.status === "error") { item.isRemoved = true } return item }) .filter((item) => !item.isRemoved) if (file.status === "removed") { submitFile(fileList) } else if (file.status === "done" && file.response?.status !== STATE_CODE_SUCCESS) { message.error(file.response?.result || useT("form.upload-fail")) } else if (file.status === "error" && file.error) { if (file.error.status === 413) { message.error(`${file.error.status}: ${useT("form.upload-size-limit")}`) } else { message.error(file.error.message || useT("form.upload-fail")) } } else if (!file.status) { let index = -1 fileList.forEach((item, i) => { if (item.uid === file.uid) { index = i } }) if (index >= 0) { fileList.splice(index, 1) } } //仅当所有的文件状态都为 “done” 才 submitFile, 主要是在多文件上传时不能仅判断当前文件状态 if (every(fileList, (item) => item.done)) { submitFile(fileList) } state.fileList = fileList } const handleBeforeUpload = (file) => { if (file.size > props.maxSize * 1024 * 1024) { message.error(useT("form.upload-size-max", { size: props.maxSize })) return false } if (isOverflow.value) { message.error(useT("form.upload-num-max", { num: props.maxNum })) return false } return true } // 普通上传 const uploadAction = async ({ action, data, file, filename, headers, onError, onProgress, onSuccess, withCredentials }) => { console.log("Using uploadAction") const formData = new FormData() if (data) { Object.keys(data).forEach((key) => { formData.append(key, data[key]) }) } formData.append(filename, file) if (props.disk) { formData.append("_disk", props.disk) } try { let res = await useFetch().post(action, formData, { withCredentials, headers, onUploadProgress: ({ total, loaded }) => { onProgress({ percent: parseInt(Math.round((loaded / total) * 100).toFixed(2)) }, file) }, }) onSuccess(res, file) } catch (e) { onError(e) } return { abort() { console.log("upload progress is aborted.") }, } } // 分块上传 const multipartUploadAction = async ({ action, data, file, headers, onError, onProgress, onSuccess }) => { data = data || {} if (props.disk) { data["_disk"] = props.disk } const resumable = new Resumable({ // Use chunk size that is smaller than your maximum limit due a resumable issue // https://github.com/23/resumable.js/issues/51 chunkSize: 1 * 1024 * 1024, //https://github.com/23/resumable.js/issues/559#issuecomment-622429803 forceChunkSize: false, simultaneousUploads: 3, testChunks: false, throttleProgressCallbacks: 1, // Get the url from data-url tag target: action, query: data, headers, }) resumable.on("fileAdded", () => { // trigger when file picked resumable.upload() // to actually start uploading. }) resumable.on("fileProgress", (uploadFile) => { onProgress({ percent: uploadFile.progress() * 100 }) }) resumable.on("fileSuccess", (uploadFile, uploadMessage) => { onSuccess(JSON.parse(uploadMessage), uploadFile) }) resumable.on("fileError", (uploadMessage) => { onError(uploadMessage) }) resumable.addFile(file) } /********** render **********/ const uploadBtn = () => { if (props.disabled) { return null } if (isImage.value) { if (!isOverflow.value) { return [<CloudUploadOutlined />, <div class="newbie-upload-text">{useT("form.upload")}</div>] } return null } else if (!isImage.value && !isOverflow.value) { return [<NewbieButton label={props.uploadText || useT("form.upload")} type="primary" icon={<CloudUploadOutlined />}></NewbieButton>] } return null } return () => ( <div class="newbie-uploader"> <Upload v-model={[state.fileList, "fileList"]} class={`newbie-upload ${isImage.value ? "is-image" : ""}`} name={props.name} listType={props.type} disabled={props.disabled} accept={props.accept} action={props.action || defaultUploadUrl} headers={props.headers} data={props.extraData} multiple={props.multiple} progress={state.progress} beforeUpload={handleBeforeUpload} customRequest={props.multipart ? multipartUploadAction : uploadAction} onPreview={isImage.value ? handlePreview : null} onChange={handleChange} {...props.uploadProps} > {uploadBtn()} </Upload> {isImage.value ? ( <ImagePreviewGroup preview={{ visible: state.previewVisible, onVisibleChange: (vis) => (state.previewVisible = vis), current: state.previewCurrent, }} > {state.fileList.map((item) => ( <Image style={{ display: "none" }} src={item.url}></Image> ))} </ImagePreviewGroup> ) : null} </div> ) }, })