借鉴文章:上传按钮<input type="file" />智能多效美化
先摆放一张图片来说明在React项目中的使用:
其中【上传照片】按钮是 将<input type="file" />与<label>配合,以保持语义化+可访问性为目标,辅以少量Javascript来实现美化的。
下面按照实际开发的项目来逐一讲解(只针对长处方申请业务):
1、Antd Pro2.x中业务代码目录
2、在父组件ResidentContract.js中获取 长处方申请 中已上传的数据
/** * 4、打开对应的签约操作页面 * 7-长处方申请 */ openTabByBtnType = async (item, type) => { const {tabPanes, keyList} = this.state; const _tabPanes = tabPanes; const _keyList = keyList; // 7-长处方申请 if (type === '7') { const {dispatch} = this.props; //<1>查询已上传的长处方图片 const {SIGN_ID} = item; let recipeCheckedArr = [], fileDetailArray = [], fileTypeFor11 = false; await dispatch({ type: 'residentRecipePhotoModel/selectUploadedRecipePhotoList', payload: SIGN_ID, callback: (res) => { let recipeTypeStr = ''; if (res.status === 200) { const {data} = res; for (let i = 0, length = data.length; i < length; i++) { if (i == length -1) { recipeTypeStr += data[i].fileType; } else { recipeTypeStr += data[i].fileType + ','; } } // ▲删除签约人员中的重复的长处方类型 recipeCheckedArr = recipeTypeStr.split(',').filter((element, index, self) => { return self.indexOf(element) === index; }); /*查询是否有 '其他' 类型*/ recipeCheckedArr.map((ele) => { if(ele === '11') { fileTypeFor11 = true; } }); // <1>处理查询结果中fileType对应的attachId, attachId为保存照片内容表的外键 // <2>过滤出有图片内容的集合 const fileList = {}; let n = 0; data.forEach(function (element) { const {fileType, attachId, fileContentDetail} = element; if (!!fileContentDetail) { // 图片内容不为空,说明有上传长处方类型图片 fileList[fileType] ? fileDetailArray[fileList[fileType] - 1].IdAndUrl.push({attachId, fileContentDetail}) : fileList[fileType] = ++n && fileDetailArray.push({ fileType: fileType, IdAndUrl: [{attachId, fileContentDetail}], signId: SIGN_ID }); } }); } } }); //<1>(从数据字典中)查询慢病长处方类型 dispatch({ type: 'residentRecipePhotoModel/getRecipeType', payload: '', callback: (res) => { const {status, data, msg} = res; if (status === 0) { _tabPanes.push({title: '长处方申请', id: '7', key: '7', state: '7', closable: false, tabsDetail: data, loadedPhotoArray: fileDetailArray, // 签约人员长处方上传图片的相关信息 recipeCheckedArr, // 过滤后的内容,用于匹配哪个checkbox选中 signPersonDetail: item, fileTypeFor11: fileTypeFor11 }); _keyList.push('7'); this.setState({ tabPanes: _tabPanes, activeKey: '7', keyList: _keyList }); } else { Modal.info({title: '提示信息', content: msg, okText: '确定'}); return; } } }); } }
注:针对 【// <2>过滤出有图片内容的集合】的处理代码,可参考本人写过的 如何把数组对象中相同的key值合并,并且把对应的id放到一个数组中。
type: 'residentRecipePhotoModel/selectUploadedRecipePhotoList' 获取的数据如下图:
type: 'residentRecipePhotoModel/getRecipeType' 获取的数据:
3、在ResidentContractMiddle.js组件render()中,长处方申请组件接收参数
4、ResidentContractMedApply.js
import React, {Component} from 'react'; import {connect} from "dva"; import {Row, Col, Button, Form, Checkbox, Modal, Upload, message} from 'antd'; import PubSub from "pubsub-js"; import {openCaptureVideo} from '@/utils/getSignPatient'; import styles from './ContractMiddleTab.less'; const FormItem = Form.Item; let globalSignId = 0; /** * @Description: 居民签约-长处方申请 * @author wanglong * @date 2020/7/10 13:59 */ @connect(({ residentRecipePhotoModel }) => ({residentRecipePhotoModel})) @Form.create() export default class ResidentContractMedApply extends Component { // 1、初始化状态 state = { previewVisible: false, // 图片是否预览 previewImageUrl: '', // 预览图片的URL(实际为api请求) recipeCheckedArray: this.props.recipeCheckedArr, fileMap: new Map(), // 长处方类型与对应的图片 的集合 loadedPhotoArray: !!this.props.loadedPhotoArray ? this.props.loadedPhotoArray : [], // 数据库已经上传的图片集合 delFlag: true, // 控制删除一张图片导致图片集合全部清空的问题 attachIdArray: [], // 用来保存保存长处方类型图片子表外键值 recipePhotoDisabled: false, // 长处方 } //<1>checkbox勾选事件 2019/2/24 handleRecipeIsCheck = (event) => { const {checked, id} = event.target; const {recipeCheckedArr, signPersonDetail: {EHR_ID}} = this.props; const checkId = id.split('_')[1]; if (checked) { recipeCheckedArr.push(checkId); this.setState({recipeCheckedArray: recipeCheckedArr}, () => { PubSub.publish( 'changeCheckedDiseases', {ehrId: EHR_ID, fileType: checkId, flag: true}); }); } else { const photoList = this.state.fileMap.get(checkId * 1); if (photoList && photoList.length > 0) { Modal.confirm({title: '你确定要清空图片吗?', okText: '确定', cancelText: '取消', onOk: () => { recipeCheckedArr.splice(recipeCheckedArr.findIndex(v => v === checkId), 1); this.setState({ recipeCheckedArray: recipeCheckedArr }, () => { PubSub.publish('changeCheckedDiseases', {ehrId: EHR_ID, fileType: checkId, flag: false}); }); }, }); } else { recipeCheckedArr.splice(recipeCheckedArr.findIndex(v => v === checkId), 1); this.setState({ recipeCheckedArray: recipeCheckedArr }, () => { PubSub.publish('changeCheckedDiseases', {ehrId: EHR_ID, fileType: checkId, flag: false}); }); } } } /*<2>自定义文件上传*/ // 过渡方法:间接打开上传文件窗口 myBtnUploadClick = (id, signId) => { globalSignId = signId; const fileInput = document.getElementById(`refFile_${id}`); fileInput.click(); } inputFileChange = (e) => { const {dispatch} = this.props; const {fileMap, attachIdArray} = this.state; const fileType = e.target.id.split('_')[1]; const fileObj = e.target.files[0]; const fileName = e.target.name; //检验文件类型是否正确 const isJPG = fileObj.type === 'image/jpg'; const isJPEG = fileObj.type === 'image/jpeg'; const isPNG = fileObj.type === 'image/png'; const isPic = isJPG || isJPEG || isPNG; if (!isPic) { message.error('只能上传jpg/jpeg/png格式的图片!'); e.target.value = ''; // 还原 return; } if(fileObj) { const formData = { recipeFile: fileObj, tableName: "ehr_cli_registry", paramId: globalSignId, recipeType: fileType }; dispatch({ type:'residentRecipePhotoModel/uploadRecipePhotoByAntd', payload: formData, callback: (res) => { if (res.status === 200) { const { attachId, photoBase64, file_type} = res.data; const list = !!fileMap.get(file_type) ? fileMap.get(file_type) : []; list.push({ uid: attachId, status: 'done', url: attachId, thumbUrl: "data:image/jpg;base64," + photoBase64, fileType: file_type, }); fileMap.set(file_type, list); this.setState({ fileMap, loadedPhotoArray: [], delFlag: false, attachIdArray: [...attachIdArray, attachId], }); } } }); } e.target.value = ''; // 上传之后还原 } /*自定义文件上传END*/ //<3>删除图片 handlePhotoRemove = (file) => { const {dispatch} = this.props; dispatch({ type: 'residentRecipePhotoModel/deleteRecipePhoto', payload: file.uid }); let {fileMap} = this.state; const index = fileMap.get(file.fileType).indexOf(file); const newFileList = fileMap.get(file.fileType).slice(); newFileList.splice(index, 1); fileMap.set(file.fileType, newFileList); this.setState({fileMap, loadedPhotoArray: [], delFlag: false}); } //<4>图片预览 2019/2/24 handlePreview = (file) => { const {dispatch} = this.props; const {uid} = file; dispatch({ type:'residentRecipePhotoModel/selectPhotoThumbUrl', payload: uid, callback: (res) => { if (!!res) { this.setState({previewImageUrl: res, previewVisible: true}); } } }); } //<5>取消图片预览 2019/2/24 handlePreviewCancel = () => { this.setState({ previewVisible: false }); } // 渲染组件 render() { const {previewVisible, previewImageUrl, recipeCheckedArray, loadedPhotoArray, delFlag, recipePhotoDisabled} = this.state; let {fileMap} = this.state; //loadedPhotoArray: 签约人员已有的长处方类型图片; recipeCheckedArr: 过滤去重后的长处方类型 const {form: { getFieldDecorator }, tabsDetail, signPersonDetail, fileTypeFor11} = this.props; /*已经上传的图片回显处理 wanglong */ if (loadedPhotoArray && loadedPhotoArray.length > 0) { loadedPhotoArray.map((element) => ( tabsDetail.map(tab => { const tempList = []; if (element.fileType === tab.dictId * 1) { element.IdAndUrl.map(obj => { tempList.push({ uid: obj.attachId, status: 'done', url: `${obj.attachId}`, thumbUrl: "data:image/jpg;base64," + obj.fileContentDetail, fileType: element.fileType, // 额外添加的参数 }); }); fileMap.set(element.fileType, tempList); } }) )); } // 清空之前的数据 else if(delFlag) { fileMap = new Map(); } return ( <div> <Form.Item style={{ marginTop: -5, marginBottom: -2}}> <Button type="primary" style={{float: 'right'}} onClick={() => this.handleSaveRecipePhoto(signPersonDetail)} disabled={recipePhotoDisabled}> 保存 </Button> </Form.Item> <hr style={{ border: '0.5px solid #1890FF', clear:"both" }} /> { tabsDetail.map((recipe, index) => { const fileList = !!fileMap.get(recipe.dictId * 1) ? fileMap.get(recipe.dictId * 1) : []; if(!fileTypeFor11){ if (recipe.dictId !== '11' ) { // '其他'项 return ( <Row key={index} style={{ marginBottom: 5 }}> <Col span={5}> <FormItem> {getFieldDecorator(`recipe_${recipe.dictId}`, { initialValue: recipe.dictId })( <Checkbox checked={recipeCheckedArray.includes(recipe.dictId)} onChange={this.handleRecipeIsCheck}> {recipe.dictName} </Checkbox> )} </FormItem> </Col> {/*针对按钮的处理*/} <Col span={19} style={{ display: recipeCheckedArray.includes(recipe.dictId) ? 'block' : 'none' }} className={styles["upload-col"]}> <Button type="primary" style={{width: '100px'}} onClick={() => openCaptureVideo(recipe.dictId)} >打开高拍仪</Button> <label className={styles["label-upload"]} > <input type="button" className={styles["btn-input-upload"]} value="上传照片" onClick={() => this.myBtnUploadClick(recipe.dictId, signPersonDetail.SIGN_ID)} /> <input type="file" name="recipeFile" className={styles["file-input"]} accept="image/jpg,image/png,image/jpeg,image/bmp" id={`refFile_${recipe.dictId}`} onChange={this.inputFileChange} /> </label> <Upload className="upload-list-inline" listType="picture" onPreview={this.handlePreview} onRemove={this.handlePhotoRemove} fileList={fileList}></Upload> </Col> </Row> ) } } else { return ( <Row key={index} style={{ marginBottom: 5 }}> <Col span={5}> <FormItem> {getFieldDecorator(`recipe_${recipe.dictId}`, { initialValue: recipe.dictId })( <Checkbox checked={recipeCheckedArray.includes(recipe.dictId)} onChange={this.handleRecipeIsCheck}> {recipe.dictName} </Checkbox> )} </FormItem> </Col> {/*针对按钮的处理*/} <Col span={19} style={{ display: recipeCheckedArray.includes(recipe.dictId) ? 'block' : 'none' }} className={styles["upload-col"]}> <Button type="primary" style={{width: '100px'}} onClick={() => openCaptureVideo(recipe.dictId)} >打开高拍仪</Button> <label className={styles["label-upload"]} > <input type="button" className={styles["btn-input-upload"]} value="上传照片" onClick={() => this.myBtnUploadClick(recipe.dictId, signPersonDetail.SIGN_ID)} /> <input type="file" name="recipeFile" className={styles["file-input"]} accept="image/jpg,image/png,image/jpeg,image/bmp" id={`refFile_${recipe.dictId}`} onChange={this.inputFileChange} /> </label> <Upload className="upload-list-inline" listType="picture" onPreview={this.handlePreview} onRemove={this.handlePhotoRemove} fileList={fileList}></Upload> </Col> </Row> ) } }) } <Modal visible={previewVisible} footer={null} onCancel={this.handlePreviewCancel} style={{top: 20}}> <img alt="example" style={{ width: '100%' }} src={previewImageUrl}/> </Modal> </div> ); } }
5、ContractMiddleTab.less
/*针对长处方图片上传*/ /*上传外层label样式*/ .label-upload { position: relative; margin-left: 10px; zoom: 1 } .btn-input-upload { padding: 4px 10px; width: 100px; height: 32px; //大小 text-align: center; color: #fff; //文字系列 border: 1px solid #1890FF; background-color: #1890FF; border-radius: 4px; //背景 cursor: pointer; zoom: 1; } .btn-input-upload:hover { color: #fff; background-color: #40a9ff; border-color: #40a9ff; } .file-input { position: absolute; left: 0; top: -2px; width: 100px; opacity: 0; display: none; } .upload-col { :global { /*上传图片的样式*/ .upload-list-inline .ant-upload-list-item { float: left; width: 120px; margin-right: 8px; } } }
说明:因为业务需求的原因,此处文件上传并没有直接利用<label>中的 for 属性的值(锚点链接的方式) 与 <input type="file" name="file" id="xxx" /> 中 id 属性的值进行关联。而是起了一个包裹美化的作用,让第一个input起到button的作用,进而再去调用上传文件的input。