@ -28,6 +28,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"async": "2.6.0",
|
||||
"attr-accept": "1.1.3",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"blueimp-canvastoblob": "2.1.0",
|
||||
"classnames": "2.2.5",
|
||||
@ -53,26 +54,26 @@
|
||||
"rc-input-number": "4.4.5",
|
||||
"react": "^16.8.6",
|
||||
"react-addons-update": "15.6.2",
|
||||
"react-overlays": "0.8.3",
|
||||
"react-color": "2.13.8",
|
||||
"react-cropper": "1.0.0",
|
||||
"react-date-range": "0.9.4",
|
||||
"react-datetime": "2.16.0",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-dropzone": "4.2.1",
|
||||
"react-dropzone": "10.1.10",
|
||||
"react-input-range": "1.2.1",
|
||||
"react-json-tree": "0.11.0",
|
||||
"react-overlays": "0.8.3",
|
||||
"react-progress-2": "^4.4.2",
|
||||
"react-select": "1.2.1",
|
||||
"react-textarea-autosize": "5.1.0",
|
||||
"react-transition-group": "2.2.1",
|
||||
"react-visibility-sensor": "3.11.0",
|
||||
"redux": "^3.7.2",
|
||||
"setimmediate": "1.0.5",
|
||||
"sortablejs": "1.10.0",
|
||||
"tslib": "^1.10.0",
|
||||
"uncontrollable": "4.1.0",
|
||||
"video-react": "0.9.4",
|
||||
"redux": "^3.7.2"
|
||||
"video-react": "0.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/async": "^2.0.45",
|
||||
|
155
scss/components/form/_file.scss
Normal file
@ -0,0 +1,155 @@
|
||||
.#{$ns}FileControl {
|
||||
&-dropzone {
|
||||
outline: none;
|
||||
|
||||
}
|
||||
|
||||
&-selectBtn {
|
||||
width: px2rem(120px);
|
||||
|
||||
|
||||
>svg {
|
||||
margin-right: 10px;
|
||||
width: pxrem(16px);
|
||||
height: pxrem(16px);
|
||||
}
|
||||
}
|
||||
|
||||
// &-dropzone:focus {
|
||||
// .#{$ns}FileControl-selectBtn {
|
||||
// background: $Button--default-onHover-bg;
|
||||
// border-color: $Button--default-onHover-border;
|
||||
// color: $Button--default-onHover-color;
|
||||
// }
|
||||
|
||||
// &:after {
|
||||
// content: '当前状态接受从剪切板中粘贴文件。';
|
||||
// color: $text--muted-color;
|
||||
// font-size: 11px;
|
||||
// margin-top: 10px;
|
||||
// }
|
||||
// }
|
||||
|
||||
&-description {
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-list {
|
||||
list-style: none;
|
||||
margin: 10px 0;
|
||||
padding: 0;
|
||||
width: 250px;
|
||||
|
||||
>li {
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
color: #108cee;
|
||||
background: #f3f3f3;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
&-itemInfo {
|
||||
padding: 0px 6px;
|
||||
line-height: 26px;
|
||||
height: 26px;
|
||||
|
||||
&.is-invalid {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
>svg:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
>svg:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
width: px2rem(16px);
|
||||
height: px2rem(16px);
|
||||
top: px2rem(5px);
|
||||
}
|
||||
}
|
||||
|
||||
&-itemInfoText {
|
||||
white-space: nowrap;
|
||||
max-width: 180px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&-clear {
|
||||
float: right;
|
||||
color: #999;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
&-list>li:hover &-clear {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&-progressInfo {
|
||||
display: inline-flex;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
transform: translateY(-3px);
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
>span {
|
||||
display: inline-block;
|
||||
padding: 0 4px 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
>svg {
|
||||
display: inline-block;
|
||||
margin: 0 4px 0 10px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
&-progress {
|
||||
height: 5px;
|
||||
flex: 1;
|
||||
background: #ebebeb;
|
||||
|
||||
>span {
|
||||
display: block;
|
||||
background: $info;
|
||||
height: 100%;
|
||||
min-width: 10%;
|
||||
transition: ease-out width 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&-acceptTip {
|
||||
height: 120px;
|
||||
color: #999;
|
||||
border: 2px dashed $info;
|
||||
border-radius: $borderRadius;
|
||||
background: #f3f9fe;
|
||||
line-height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
.#{$ns}ImageControl {
|
||||
outline: none;
|
||||
&-dropzone {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-addBtn {
|
||||
margin: 0;
|
||||
@ -35,7 +37,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&-dropzone.is-active &-addBtn {
|
||||
&-pasteTip {
|
||||
display: block;
|
||||
color: $text--muted-color;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&-dropzone:focus &-addBtn {
|
||||
border-color: $ImageControl-addBtn-onHover-border;
|
||||
background: $ImageControl-addBtn-onHover-bg;
|
||||
color: $ImageControl-addBtn-onHover-color;
|
||||
@ -184,4 +193,46 @@
|
||||
&-uploadBtn {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&-cropperWrapper {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
&-croperToolbar {
|
||||
display: inline-flex;
|
||||
width: 50px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
>a {
|
||||
color: #fff;
|
||||
padding: 2px 5px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-acceptTip {
|
||||
height: 120px;
|
||||
color: #999;
|
||||
border: 2px dashed $borderColor;
|
||||
|
||||
// &.is-accept {
|
||||
border-color: $info;
|
||||
background: #f3f9fe;
|
||||
// }
|
||||
|
||||
border-radius: $borderRadius;
|
||||
|
||||
line-height: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -533,6 +533,7 @@ $Card-actions-onChecked-onHover-bg: $white;
|
||||
@import "../components/form/date";
|
||||
@import "../components/form/date-range";
|
||||
@import "../components/form/image";
|
||||
@import "../components/form/file";
|
||||
@import "../components/form/editor";
|
||||
@import "../components/form/rich-text";
|
||||
@import "../components/form/range";
|
||||
|
@ -215,6 +215,7 @@ pre {
|
||||
@import '../components/form/date';
|
||||
@import '../components/form/date-range';
|
||||
@import '../components/form/image';
|
||||
@import "../components/form/file";
|
||||
@import '../components/form/editor';
|
||||
@import '../components/form/rich-text';
|
||||
@import '../components/form/range';
|
||||
@ -233,4 +234,4 @@ pre {
|
||||
@import '../components/form/nested-select';
|
||||
@import '../components/form/icon-picker';
|
||||
|
||||
@import '../utilities';
|
||||
@import '../utilities';
|
@ -80,6 +80,7 @@ $Form-input-borderColor: #cfdadd;
|
||||
@import "../components/form/date";
|
||||
@import "../components/form/date-range";
|
||||
@import "../components/form/image";
|
||||
@import "../components/form/file";
|
||||
@import "../components/form/editor";
|
||||
@import "../components/form/rich-text";
|
||||
@import "../components/form/range";
|
||||
|
@ -39,6 +39,14 @@ import ViewIcon from '../icons/view.svg';
|
||||
import RemoveIcon from '../icons/remove.svg';
|
||||
// @ts-ignore
|
||||
import RetryIcon from '../icons/retry.svg';
|
||||
// @ts-ignore
|
||||
import UploadIcon from '../icons/upload.svg';
|
||||
// @ts-ignore
|
||||
import FileIcon from '../icons/file.svg';
|
||||
// @ts-ignore
|
||||
import SuccessIcon from '../icons/success.svg';
|
||||
// @ts-ignore
|
||||
import FailIcon from '../icons/fail.svg';
|
||||
|
||||
// 兼容原来的用法,后续不直接试用。
|
||||
// @ts-ignore
|
||||
@ -91,6 +99,10 @@ registerIcon('pencil', PencilIcon);
|
||||
registerIcon('view', ViewIcon);
|
||||
registerIcon('remove', RemoveIcon);
|
||||
registerIcon('retry', RetryIcon);
|
||||
registerIcon('upload', UploadIcon);
|
||||
registerIcon('file', FileIcon);
|
||||
registerIcon('success', SuccessIcon);
|
||||
registerIcon('fail', FailIcon);
|
||||
|
||||
export function Icon({
|
||||
icon,
|
||||
|
7
src/icons/fail.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 34 34" version="1.1">
|
||||
<g transform="translate(1.000000, 1.000000)">
|
||||
<circle stroke="#EA2E2E" cx="16" cy="16" r="16" fill="none"></circle>
|
||||
<polygon fill="#EA2E2E" fill-rule="nonzero" points="24 10.1052632 21.8947368 8 16 14.0350877 10.1052632 8 8 10.1052632 14.0350877 16 8 21.8947368 10.1052632 24 16 17.9649123 21.8947368 24 24 21.8947368 17.9649123 16"></polygon>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 502 B |
8
src/icons/file.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 14 16" version="1.1">
|
||||
<g>
|
||||
<path d="M0,0 L0,16 L14,16 L14,4.001 L9.939,0 L0,0 Z M1,1 L9,1 L9,4.001 L9,5 L10,5 L13,5 L13,15 L1,15 L1,1 Z M10,1.464 L12.575,4.001 L10,4.001 L10,1.464 Z" id="Fill-1"></path>
|
||||
<polygon points="4 12.0002 10 12.0002 10 10.9992 4 10.9992"></polygon>
|
||||
<polygon points="4 9.0002 10 9.0002 10 8.0002 4 8.0002"></polygon>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 485 B |
@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1024 1024" version="1.1" p-id="1463">
|
||||
<path d="M852.727563 392.447107C956.997809 458.473635 956.941389 565.559517 852.727563 631.55032L281.888889 993.019655C177.618644 1059.046186 93.090909 1016.054114 93.090909 897.137364L93.090909 126.860063C93.090909 7.879206 177.675064-35.013033 281.888889 30.977769L852.727563 392.447107 852.727563 392.447107Z" p-id="4494" fill="#606670" />
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 14 16" version="1.1" p-id="1463">
|
||||
<path d="M13.5722,7.254 L1.2838,0.115 C1.019,-0.038 0.6926,-0.038 0.4278,0.115 C0.163,0.269 -1.83725092e-07,0.554 -1.83725092e-07,0.861 L-1.83725092e-07,15.139 C-0.0002,15.446 0.1629,15.731 0.4278,15.885 C0.6927,16.039 1.019,16.038 1.2838,15.884 L13.5721,8.746 C13.8368,8.592 13.9999998,8.308 13.9999998,8 C13.9999998,7.692 13.837,7.408 13.5722,7.254 Z" id="path-1"></path>
|
||||
</svg>
|
Before Width: | Height: | Size: 492 B After Width: | Height: | Size: 519 B |
@ -1,4 +1,8 @@
|
||||
<svg viewBox="0 0 1024 1024" version="1.1"
|
||||
<svg viewBox="0 0 15 17" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M972.8 102.4c-30.72 0-51.2 20.48-51.2 51.2v51.2c-51.2-71.68-122.88-128-204.8-158.72C460.8-66.56 158.72 51.2 46.08 307.2S51.2 865.28 307.2 977.92 865.28 972.8 977.92 716.8H972.8c0-30.72-20.48-51.2-51.2-51.2s-51.2 20.48-51.2 51.2h-5.12c-46.08 76.8-112.64 138.24-199.68 174.08-209.92 87.04-445.44-15.36-532.48-225.28S148.48 215.04 358.4 133.12c189.44-81.92 404.48 0 506.88 174.08H768c-30.72 0-51.2 20.48-51.2 51.2s20.48 51.2 51.2 51.2h204.8c30.72 0 51.2-20.48 51.2-51.2V153.6c0-30.72-20.48-51.2-51.2-51.2z"></path>
|
||||
</svg>
|
||||
<g transform="translate(1.000000, 0.000000)">
|
||||
<polygon id="Fill-1" fill="#666666" points="5.0003 0.0003 5.0003 7.0703 9.5353 3.5353"></polygon>
|
||||
<path fill="none" d="M13,9.5355 C13,13.1255 10.09,16.0355 6.5,16.0355 C2.91,16.0355 0,13.1255 0,9.5355 C0,5.9455 2.91,3.0355 6.5,3.0355" stroke="#666666" stroke-width="2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 615 B After Width: | Height: | Size: 440 B |
9
src/icons/success.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" version="1.1">
|
||||
<g id="Group-5">
|
||||
<circle stroke="#5FB333" fill="#FFFFFF" cx="16" cy="16" r="15.5"></circle>
|
||||
<g transform="translate(5.647059, 7.529412)" fill="#5FB333" fill-rule="nonzero">
|
||||
<polygon id="Shape" points="21.1764706 2.76408669 18.7058824 0.26749226 7.41176471 11.6804954 2.47058824 6.50897833 0 9.18390093 4.94117647 14.1770898 4.94117647 14.1770898 7.41176471 16.6736842 9.88235294 14.1770898 9.88235294 14.1770898"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 608 B |
11
src/icons/upload.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16 16" version="1.1" p-id="1463">
|
||||
<g stroke="#666666" stroke-width="2" fill="none" fill-rule="evenodd">
|
||||
<path d="M8,12.2426 L8,1.2426"></path>
|
||||
<path d="M4.4648,4.9496 L8.7068,0.7076"></path>
|
||||
<path d="M11.5352,4.9496 L7.2932,0.7076"></path>
|
||||
<path d="M0,14.2426 L16,14.2426"></path>
|
||||
<path d="M1,9.2426 L1,15.2426"></path>
|
||||
<path d="M15,9.2426 L15,15.2426"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 529 B |
@ -6,14 +6,16 @@ import find = require('lodash/find');
|
||||
import isPlainObject = require('lodash/isPlainObject');
|
||||
import {mapLimit} from 'async';
|
||||
import ImageControl from './Image';
|
||||
import {Payload} from '../../types';
|
||||
import {Payload, ApiObject, ApiString} from '../../types';
|
||||
import {filter} from '../../utils/tpl';
|
||||
import Alert from '../../components/Alert2';
|
||||
import {qsstringify} from '../../utils/helper';
|
||||
import {qsstringify, createObject} from '../../utils/helper';
|
||||
import {buildApi} from '../../utils/api';
|
||||
import Button from '../../components/Button';
|
||||
import {Icon} from '../../components/icons';
|
||||
import DropZone from 'react-dropzone';
|
||||
|
||||
export interface FileProps extends FormControlProps {
|
||||
btnClassName: string;
|
||||
btnUploadClassName: string;
|
||||
maxSize: number;
|
||||
maxLength: number;
|
||||
placeholder?: string;
|
||||
@ -48,6 +50,8 @@ export interface FileProps extends FormControlProps {
|
||||
|
||||
export interface FileX extends File {
|
||||
state?: 'init' | 'error' | 'pending' | 'uploading' | 'uploaded' | 'invalid' | 'ready';
|
||||
progress?: number;
|
||||
id?: any;
|
||||
}
|
||||
|
||||
export interface FileValue {
|
||||
@ -56,6 +60,7 @@ export interface FileValue {
|
||||
name?: string;
|
||||
url?: string;
|
||||
state: 'init' | 'error' | 'pending' | 'uploading' | 'uploaded' | 'invalid' | 'ready';
|
||||
id?: any;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
@ -65,14 +70,19 @@ export interface FileState {
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
let id = 1;
|
||||
function gennerateId() {
|
||||
return id++;
|
||||
}
|
||||
|
||||
let preventEvent = (e: any) => e.stopPropagation();
|
||||
|
||||
export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
static defaultProps: Partial<FileProps> = {
|
||||
btnClassName: 'btn-sm btn-info',
|
||||
btnUploadClassName: 'btn-sm btn-success',
|
||||
maxSize: 0,
|
||||
maxLength: 0,
|
||||
placeholder: '',
|
||||
btnLabel: '请选择文件',
|
||||
btnLabel: '文件上传',
|
||||
reciever: '/api/upload/file',
|
||||
fileField: 'file',
|
||||
joinValues: true,
|
||||
@ -116,7 +126,8 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
state: 'ready',
|
||||
value: value,
|
||||
name: value.name,
|
||||
url: ''
|
||||
url: '',
|
||||
id: gennerateId()
|
||||
}
|
||||
: {
|
||||
...(typeof value === 'string'
|
||||
@ -124,6 +135,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
state: file && file.state ? file.state : 'init',
|
||||
value,
|
||||
name: /^data:/.test(value) ? (file && file.name) || 'base64数据' : '',
|
||||
id: gennerateId(),
|
||||
url:
|
||||
typeof props.downloadUrl === 'string' && value && !/^data:/.test(value)
|
||||
? `${props.downloadUrl}${value}`
|
||||
@ -134,6 +146,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
: undefined;
|
||||
}
|
||||
|
||||
dropzone = React.createRef<any>();
|
||||
constructor(props: FileProps) {
|
||||
super(props);
|
||||
|
||||
@ -163,6 +176,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
this.removeFile = this.removeFile.bind(this);
|
||||
this.clearError = this.clearError.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
this.handleDropRejected = this.handleDropRejected.bind(this);
|
||||
this.startUpload = this.startUpload.bind(this);
|
||||
this.stopUpload = this.stopUpload.bind(this);
|
||||
this.toggleUpload = this.toggleUpload.bind(this);
|
||||
@ -170,6 +184,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.uploadFile = this.uploadFile.bind(this);
|
||||
this.uploadBigFile = this.uploadBigFile.bind(this);
|
||||
this.handleSelect = this.handleSelect.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: FileProps) {
|
||||
@ -199,7 +214,8 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
) {
|
||||
obj = {
|
||||
...org,
|
||||
...obj
|
||||
...obj,
|
||||
id: obj.id || org!.id
|
||||
};
|
||||
}
|
||||
|
||||
@ -214,30 +230,29 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e: React.ChangeEvent<any>) {
|
||||
const files = e.currentTarget.files;
|
||||
|
||||
handleDrop(files: Array<FileX>) {
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {maxSize, multiple, maxLength} = this.props;
|
||||
const allowed =
|
||||
(multiple ? (maxLength ? maxLength : files.length + this.state.files.length) : 1) - this.state.files.length;
|
||||
let allowed = multiple && maxLength ? maxLength - this.state.files.length : files.length;
|
||||
|
||||
const inputFiles: Array<FileX> = [];
|
||||
|
||||
[].slice.call(files, 0, allowed).forEach((file: FileX) => {
|
||||
if (maxSize && file.size > maxSize) {
|
||||
alert(
|
||||
this.props.env.alert(
|
||||
`您选择的文件 ${file.name} 大小为 ${ImageControl.formatFileSize(
|
||||
file.size
|
||||
)} 超出了最大为 ${ImageControl.formatFileSize(maxSize)} 的限制,请重新选择`
|
||||
);
|
||||
return;
|
||||
file.state = 'invalid';
|
||||
} else {
|
||||
file.state = 'pending';
|
||||
}
|
||||
|
||||
file.state = 'pending';
|
||||
file.id = gennerateId();
|
||||
inputFiles.push(file);
|
||||
});
|
||||
|
||||
@ -248,7 +263,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
this.setState(
|
||||
{
|
||||
error: null,
|
||||
files: this.state.files.concat(inputFiles)
|
||||
files: multiple ? this.state.files.concat(inputFiles) : inputFiles
|
||||
},
|
||||
() => {
|
||||
const {autoUpload} = this.props;
|
||||
@ -260,6 +275,36 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
);
|
||||
}
|
||||
|
||||
handleDropRejected(rejectedFiles: any, evt: React.DragEvent<any>) {
|
||||
if (evt.type !== 'change' && evt.type !== 'drop') {
|
||||
return;
|
||||
}
|
||||
const {multiple, env, accept} = this.props;
|
||||
|
||||
const files = rejectedFiles.map((file: any) => ({
|
||||
...file,
|
||||
state: 'invalid',
|
||||
id: gennerateId(),
|
||||
name: file.name
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
files: multiple
|
||||
? this.state.files.concat(files)
|
||||
: this.state.files.length
|
||||
? this.state.files
|
||||
: files.slice(0, 1)
|
||||
});
|
||||
|
||||
env.alert(
|
||||
`您添加的文件${files.map((item: any) => `【${item.name}】`)}不符合类型的\`${accept}\`设定,请仔细检查。`
|
||||
);
|
||||
}
|
||||
|
||||
handleSelect() {
|
||||
this.dropzone.current && this.dropzone.current.open();
|
||||
}
|
||||
|
||||
startUpload() {
|
||||
if (this.state.uploading) {
|
||||
return;
|
||||
@ -311,32 +356,49 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
files: this.state.files.concat()
|
||||
},
|
||||
() =>
|
||||
this.sendFile(file, (error, file, obj) => {
|
||||
const files = this.state.files.concat();
|
||||
const idx = files.indexOf(file as FileX);
|
||||
this.sendFile(
|
||||
file,
|
||||
(error, file, obj) => {
|
||||
const files = this.state.files.concat();
|
||||
const idx = files.indexOf(file as FileX);
|
||||
|
||||
if (!~idx) {
|
||||
return;
|
||||
if (!~idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newFile: FileValue = file as FileValue;
|
||||
|
||||
if (error) {
|
||||
newFile.state = 'error';
|
||||
newFile.error = error;
|
||||
} else {
|
||||
newFile = obj as FileValue;
|
||||
}
|
||||
files.splice(idx, 1, newFile);
|
||||
this.current = null;
|
||||
this.setState(
|
||||
{
|
||||
error: error ? error : null,
|
||||
files: files
|
||||
},
|
||||
this.tick
|
||||
);
|
||||
},
|
||||
progress => {
|
||||
const files = this.state.files.concat();
|
||||
const idx = files.indexOf(file);
|
||||
|
||||
if (!~idx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// file 是个非 File 对象,先不copy了直接改。
|
||||
file.progress = progress;
|
||||
this.setState({
|
||||
files
|
||||
});
|
||||
}
|
||||
|
||||
let newFile: FileValue = file as FileValue;
|
||||
|
||||
if (error) {
|
||||
newFile.state = 'error';
|
||||
newFile.error = error;
|
||||
} else {
|
||||
newFile = obj as FileValue;
|
||||
}
|
||||
files.splice(idx, 1, newFile);
|
||||
this.current = null;
|
||||
this.setState(
|
||||
{
|
||||
error: error ? error : null,
|
||||
files: files
|
||||
},
|
||||
this.tick
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.setState(
|
||||
@ -357,7 +419,11 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
}
|
||||
}
|
||||
|
||||
sendFile(file: FileX, cb: (error: null | string, file?: FileX, obj?: FileValue) => void) {
|
||||
sendFile(
|
||||
file: FileX,
|
||||
cb: (error: null | string, file?: FileX, obj?: FileValue) => void,
|
||||
onProgress: (progress: number) => void
|
||||
) {
|
||||
const {
|
||||
reciever,
|
||||
fileField,
|
||||
@ -368,7 +434,8 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
chunkApi,
|
||||
finishChunkApi,
|
||||
asBase64,
|
||||
asBlob
|
||||
asBlob,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
if (asBase64) {
|
||||
@ -379,7 +446,8 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
value: reader.result as string,
|
||||
name: file.name,
|
||||
url: '',
|
||||
state: 'ready'
|
||||
state: 'ready',
|
||||
id: file.id
|
||||
});
|
||||
};
|
||||
reader.onerror = (error: any) => cb(error.message);
|
||||
@ -391,7 +459,8 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
name: file.name,
|
||||
value: file,
|
||||
url: '',
|
||||
state: 'ready'
|
||||
state: 'ready',
|
||||
id: file.id
|
||||
}),
|
||||
4
|
||||
);
|
||||
@ -412,14 +481,17 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
chunkSize,
|
||||
startChunkApi,
|
||||
chunkApi,
|
||||
finishChunkApi
|
||||
}
|
||||
finishChunkApi,
|
||||
data
|
||||
},
|
||||
onProgress
|
||||
)
|
||||
.then(ret => {
|
||||
if (ret.status || !ret.data) {
|
||||
throw new Error(ret.msg || '上传失败, 请重试');
|
||||
}
|
||||
|
||||
onProgress(1);
|
||||
const value = (ret.data as any).value || ret.data;
|
||||
|
||||
cb(null, file, {
|
||||
@ -431,7 +503,8 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
: ret.data
|
||||
? (ret.data as any).url
|
||||
: null,
|
||||
state: 'uploaded'
|
||||
state: 'uploaded',
|
||||
id: file.id
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
@ -481,32 +554,42 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
onChange(value);
|
||||
}
|
||||
|
||||
uploadFile(file: FileX, reciever: string, params: object, config: Partial<FileProps> = {}): Promise<Payload> {
|
||||
uploadFile(
|
||||
file: FileX,
|
||||
reciever: string,
|
||||
params: object,
|
||||
config: Partial<FileProps> = {},
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<Payload> {
|
||||
const fd = new FormData();
|
||||
const api = buildApi(reciever, createObject(config.data, params), {
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
qsstringify({...api.data, ...params})
|
||||
.split('&')
|
||||
.forEach(item => {
|
||||
const parts = item.split('=');
|
||||
fd.append(parts[0], parts[1]);
|
||||
});
|
||||
|
||||
reciever = filter(reciever, this.props.data);
|
||||
fd.append(config.fieldName || 'file', file);
|
||||
|
||||
const idx = reciever.indexOf('?');
|
||||
|
||||
if (~idx && params) {
|
||||
params = {
|
||||
...qs.parse(reciever.substring(idx + 1)),
|
||||
...params
|
||||
};
|
||||
reciever = reciever.substring(0, idx) + '?' + qsstringify(params);
|
||||
} else if (params) {
|
||||
reciever += '?' + qsstringify(params);
|
||||
}
|
||||
|
||||
return this._send(reciever, fd, {
|
||||
withCredentials: true
|
||||
});
|
||||
return this._send(api, fd, {}, onProgress);
|
||||
}
|
||||
|
||||
uploadBigFile(file: FileX, reciever: string, params: object, config: Partial<FileProps> = {}): Promise<Payload> {
|
||||
uploadBigFile(
|
||||
file: FileX,
|
||||
reciever: string,
|
||||
params: object,
|
||||
config: Partial<FileProps> = {},
|
||||
onProgress: (progress: number) => void
|
||||
): Promise<Payload> {
|
||||
const chunkSize = config.chunkSize || 5 * 1024 * 1024;
|
||||
const self = this;
|
||||
let startProgress = 0.2;
|
||||
let endProgress = 0.9;
|
||||
let progressArr: Array<number>;
|
||||
|
||||
interface ObjectState {
|
||||
key: string;
|
||||
@ -527,13 +610,26 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let state: ObjectState;
|
||||
const startApi = buildApi(
|
||||
config.startChunkApi!,
|
||||
createObject(config.data, {
|
||||
...params,
|
||||
filename: file.name
|
||||
}),
|
||||
{
|
||||
method: 'post',
|
||||
autoAppend: true
|
||||
}
|
||||
);
|
||||
|
||||
self._send(config.startChunkApi as string, {filename: file.name})
|
||||
self._send(startApi)
|
||||
.then(startChunk)
|
||||
.catch(reject);
|
||||
|
||||
function startChunk(ret: Payload) {
|
||||
onProgress(startProgress);
|
||||
const tasks = getTasks(file);
|
||||
progressArr = tasks.map(() => 0);
|
||||
|
||||
if (!ret.data) {
|
||||
throw new Error('接口返回错误,请仔细检查');
|
||||
@ -555,25 +651,53 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
});
|
||||
}
|
||||
|
||||
function updateProgress(partNumber: number, progress: number) {
|
||||
progressArr[partNumber - 1] = progress;
|
||||
onProgress(
|
||||
startProgress +
|
||||
(endProgress - startProgress) *
|
||||
(progressArr.reduce((count, progress) => count + progress, 0) / progressArr.length)
|
||||
);
|
||||
}
|
||||
|
||||
function finishChunk(partList: Array<any> | undefined, state: ObjectState) {
|
||||
self._send(config.finishChunkApi as string, {
|
||||
...params,
|
||||
uploadId: state.uploadId,
|
||||
key: state.key,
|
||||
filename: file.name,
|
||||
partList
|
||||
})
|
||||
onProgress(endProgress);
|
||||
const endApi = buildApi(
|
||||
config.finishChunkApi!,
|
||||
createObject(config.data, {
|
||||
...params,
|
||||
uploadId: state.uploadId,
|
||||
key: state.key,
|
||||
filename: file.name,
|
||||
partList
|
||||
}),
|
||||
{
|
||||
method: 'post',
|
||||
autoAppend: true
|
||||
}
|
||||
);
|
||||
|
||||
self._send(endApi)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
|
||||
function uploadPartFile(state: ObjectState, conf: Partial<FileProps>) {
|
||||
reciever = conf.chunkApi as string;
|
||||
|
||||
return (task: Task, callback: (error: any, value?: any) => void) => {
|
||||
const api = buildApi(conf.chunkApi!, createObject(config.data, params), {
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
const fd = new FormData();
|
||||
let blob = task.file.slice(task.start, task.stop + 1);
|
||||
|
||||
qsstringify({...api.data, ...params})
|
||||
.split('&')
|
||||
.forEach(item => {
|
||||
const parts = item.split('=');
|
||||
fd.append(parts[0], parts[1]);
|
||||
});
|
||||
|
||||
fd.append('key', state.key);
|
||||
fd.append('uploadId', state.uploadId);
|
||||
fd.append('partNumber', task.partNumber.toString());
|
||||
@ -581,9 +705,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
fd.append(config.fieldName || 'file', blob, file.name);
|
||||
|
||||
return self
|
||||
._send(reciever, fd, {
|
||||
withCredentials: true
|
||||
})
|
||||
._send(api, fd, {}, progress => updateProgress(task.partNumber, progress))
|
||||
.then(ret => {
|
||||
state.loaded++;
|
||||
callback(null, {
|
||||
@ -622,17 +744,25 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
});
|
||||
}
|
||||
|
||||
_send(reciever: string, data: any, options?: object): Promise<Payload> {
|
||||
_send(
|
||||
api: ApiObject | ApiString,
|
||||
data?: any,
|
||||
options?: object,
|
||||
onProgress?: (progress: number) => void
|
||||
): Promise<Payload> {
|
||||
const env = this.props.env;
|
||||
|
||||
if (!env || !env.fetcher) {
|
||||
throw new Error('fetcher is required');
|
||||
}
|
||||
|
||||
reciever = filter(reciever, this.props.data);
|
||||
return env.fetcher(reciever, data, {
|
||||
return env.fetcher(api, data, {
|
||||
method: 'post',
|
||||
...options
|
||||
...options,
|
||||
withCredentials: true,
|
||||
onUploadProgress: onProgress
|
||||
? (event: {loaded: number; total: number}) => onProgress(event.loaded / event.total)
|
||||
: undefined
|
||||
});
|
||||
}
|
||||
|
||||
@ -652,85 +782,137 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
btnLabel,
|
||||
accept,
|
||||
disabled,
|
||||
btnClassName,
|
||||
btnUploadClassName,
|
||||
maxLength,
|
||||
multiple,
|
||||
autoUpload,
|
||||
stateTextMap,
|
||||
description,
|
||||
hideUploadButton,
|
||||
className,
|
||||
asBlob,
|
||||
joinValues
|
||||
classnames: cx,
|
||||
render
|
||||
} = this.props;
|
||||
let {files, uploading, error} = this.state;
|
||||
|
||||
const hasPending = files.some(file => file.state == 'pending');
|
||||
|
||||
return (
|
||||
<div className={cx('amis-file-control', className)}>
|
||||
{error ? (
|
||||
<Alert level="danger" showCloseButton onClose={this.clearError}>
|
||||
{error}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{files && files.length ? (
|
||||
<ul className="list-group no-bg m-b-sm">
|
||||
{files.map((file, key) => (
|
||||
<li key={key} className="list-group-item clearfix">
|
||||
<a
|
||||
className="text-danger pull-right"
|
||||
onClick={() => this.removeFile(file, key)}
|
||||
href="javascript:void 0"
|
||||
data-tooltip="移除"
|
||||
>
|
||||
<i className="fa fa-times" />
|
||||
</a>
|
||||
<span className="pull-right text-muted text-xs m-r-sm">
|
||||
{(stateTextMap && stateTextMap[file.state as string]) || ''}
|
||||
</span>
|
||||
<i className="fa fa-file fa-fw m-r-xs" />
|
||||
{(file as FileValue).url ? (
|
||||
<a href={(file as FileValue).url} target="_blank">
|
||||
{file.name || (file as FileValue).filename || (file as FileValue).value}
|
||||
</a>
|
||||
) : (
|
||||
<span>{file.name || (file as FileValue).filename}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
<div className="clear">
|
||||
{(multiple && (!maxLength || files.length < maxLength)) || (!multiple && !files.length) ? (
|
||||
<label className={cx('btn m-r-xs', btnClassName, {disabled})}>
|
||||
<input
|
||||
type="file"
|
||||
accept={accept}
|
||||
disabled={disabled}
|
||||
multiple={multiple}
|
||||
className="invisible"
|
||||
onChange={this.handleDrop}
|
||||
/>
|
||||
{btnLabel}
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{!autoUpload && !hideUploadButton && files.length ? (
|
||||
<button
|
||||
type="button"
|
||||
className={cx('btn m-r-xs', btnUploadClassName)}
|
||||
disabled={!hasPending}
|
||||
onClick={this.toggleUpload}
|
||||
<div className={cx('FileControl', className)}>
|
||||
<DropZone
|
||||
key="drop-zone"
|
||||
ref={this.dropzone}
|
||||
onDrop={this.handleDrop}
|
||||
onDropRejected={this.handleDropRejected}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
>
|
||||
{({getRootProps, getInputProps, isDragActive}) => (
|
||||
<div
|
||||
{...getRootProps({
|
||||
onClick: preventEvent
|
||||
})}
|
||||
className={cx('FileControl-dropzone', {
|
||||
disabled,
|
||||
'is-empty': !files.length,
|
||||
'is-active': isDragActive
|
||||
})}
|
||||
>
|
||||
{uploading ? '暂停上传' : '开始上传'}
|
||||
</button>
|
||||
) : null}
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{this.state.uploading ? <i className="fa fa-spinner fa-spin fa-2x fa-fw" /> : null}
|
||||
</div>
|
||||
{isDragActive ? (
|
||||
<div className={cx('FileControl-acceptTip')}>把文件拖到这,然后松完成添加!</div>
|
||||
) : (
|
||||
<>
|
||||
{(multiple && (!maxLength || files.length < maxLength)) || !multiple ? (
|
||||
<Button
|
||||
level="default"
|
||||
className={cx('FileControl-selectBtn')}
|
||||
onClick={this.handleSelect}
|
||||
>
|
||||
<Icon icon="upload" className="icon" />
|
||||
{!multiple && files.length
|
||||
? '重新上传'
|
||||
: multiple && files.length
|
||||
? '继续添加'
|
||||
: '上传文件'}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{description
|
||||
? render('desc', description!, {
|
||||
className: cx('FileControl-description')
|
||||
})
|
||||
: null}
|
||||
|
||||
{Array.isArray(files) ? (
|
||||
<ul className={cx('FileControl-list')}>
|
||||
{files.map((file, index) => (
|
||||
<li key={file.id}>
|
||||
<div
|
||||
className={cx('FileControl-itemInfo', {
|
||||
'is-invalid':
|
||||
file.state === 'invalid' || file.state === 'error'
|
||||
})}
|
||||
>
|
||||
<Icon icon="file" className="icon" />
|
||||
<span className={cx('FileControl-itemInfoText')}>
|
||||
{file.name || (file as FileValue).filename}
|
||||
</span>
|
||||
{file.state === 'invalid' || file.state === 'error' ? (
|
||||
<Icon icon="fail" className="icon" />
|
||||
) : null}
|
||||
{file.state !== 'uploading' ? (
|
||||
<a
|
||||
data-tooltip="移除"
|
||||
className={cx('FileControl-clear')}
|
||||
onClick={() => this.removeFile(file, index)}
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
{file.state === 'uploading' || file.state === 'uploaded' ? (
|
||||
<div className={cx('FileControl-progressInfo')}>
|
||||
<div className={cx('FileControl-progress')}>
|
||||
<span
|
||||
style={{
|
||||
width: `${
|
||||
file.state === 'uploaded'
|
||||
? 100
|
||||
: (file.progress || 0) * 100
|
||||
}%`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{file.state === 'uploaded' ? (
|
||||
<Icon icon="success" className="icon" />
|
||||
) : (
|
||||
<span>{Math.round((file.progress || 0) * 100)}%</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DropZone>
|
||||
|
||||
{error ? <div className={cx('FileControl-errorMsg')}>{error}</div> : null}
|
||||
|
||||
{!autoUpload && !hideUploadButton && files.length ? (
|
||||
<Button
|
||||
level="default"
|
||||
disabled={!hasPending}
|
||||
className={cx('FileControl-uploadBtn')}
|
||||
onClick={this.handleSelect}
|
||||
>
|
||||
{uploading ? '暂停上传' : '开始上传'}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -738,6 +920,7 @@ export default class FileControl extends React.Component<FileProps, FileState> {
|
||||
|
||||
@FormItem({
|
||||
type: 'file',
|
||||
sizeMutable: false
|
||||
sizeMutable: false,
|
||||
renderDescription: false
|
||||
})
|
||||
export class FileControlRenderer extends FileControl {}
|
||||
|
@ -7,17 +7,18 @@ import 'blueimp-canvastoblob';
|
||||
import find = require('lodash/find');
|
||||
import qs from 'qs';
|
||||
import {Payload} from '../../types';
|
||||
import {filter} from '../../utils/tpl';
|
||||
import {Switch} from '../../components';
|
||||
import {buildApi} from '../../utils/api';
|
||||
import {createObject, qsstringify} from '../../utils/helper';
|
||||
import {Icon} from '../../components/icons';
|
||||
import Button from '../../components/Button';
|
||||
// @ts-ignore
|
||||
import accepts from 'attr-accept';
|
||||
|
||||
let id = 1;
|
||||
function gennerateId() {
|
||||
return id++;
|
||||
}
|
||||
let preventEvent = (e: any) => e.stopPropagation();
|
||||
|
||||
export interface ImageProps extends FormControlProps {
|
||||
placeholder?: string;
|
||||
@ -32,9 +33,14 @@ export interface ImageProps extends FormControlProps {
|
||||
aspectRatio?: number;
|
||||
aspectRatioLabel?: string;
|
||||
};
|
||||
crop?:
|
||||
| boolean
|
||||
| {
|
||||
aspectRatio?: number;
|
||||
[propName: string]: any;
|
||||
};
|
||||
accept?: string;
|
||||
btnUploadClassName?: string;
|
||||
btnClassName?: string;
|
||||
|
||||
hideUploadButton?: boolean;
|
||||
joinValues?: boolean;
|
||||
extractValue?: boolean;
|
||||
@ -47,13 +53,8 @@ export interface ImageState {
|
||||
uploading: boolean;
|
||||
locked: boolean;
|
||||
lockedReason?: string;
|
||||
compress: boolean;
|
||||
compressOptions: {
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
};
|
||||
files: Array<FileValue | FileX>;
|
||||
crop?: object;
|
||||
crop?: any;
|
||||
error?: string;
|
||||
cropFile?: FileValue;
|
||||
submitOnChange?: boolean;
|
||||
@ -85,10 +86,7 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
limit: undefined,
|
||||
accept: 'image/jpeg, image/jpg, image/png, image/gif',
|
||||
reciever: '/api/upload',
|
||||
btnUploadClassName: 'btn-success',
|
||||
btnClassName: 'btn-info btn-sm',
|
||||
hideUploadButton: false,
|
||||
compressOptions: {},
|
||||
placeholder: '点击选择图片或者将图片拖入该区域',
|
||||
joinValues: true,
|
||||
extractValue: false,
|
||||
@ -136,11 +134,11 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
state: ImageState = {
|
||||
uploading: false,
|
||||
locked: false,
|
||||
compress: false,
|
||||
files: [],
|
||||
compressOptions: {}
|
||||
files: []
|
||||
};
|
||||
|
||||
cropper = React.createRef<Cropper>();
|
||||
dropzone = React.createRef<any>();
|
||||
current: FileValue | FileX | null = null;
|
||||
resolve?: (value?: any) => void;
|
||||
|
||||
@ -167,9 +165,7 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
this.state = {
|
||||
...this.state,
|
||||
files: files,
|
||||
crop: this.buildCrop(props),
|
||||
compress: !!props.compress,
|
||||
compressOptions: props.compressOptions
|
||||
crop: this.buildCrop(props)
|
||||
};
|
||||
|
||||
this.sendFile = this.sendFile.bind(this);
|
||||
@ -216,7 +212,8 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
if (obj && (org = find(this.state.files, item => (item as FileValue).value === obj.value))) {
|
||||
obj = {
|
||||
...org,
|
||||
...obj
|
||||
...obj,
|
||||
id: org.id || obj.id
|
||||
};
|
||||
}
|
||||
|
||||
@ -265,7 +262,29 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
}
|
||||
|
||||
handleDropRejected(rejectedFiles: any, evt: React.DragEvent<any>) {
|
||||
evt.type === 'change' && alert('您选择的文件类型不符已被过滤!');
|
||||
if (evt.type !== 'change' && evt.type !== 'drop') {
|
||||
return;
|
||||
}
|
||||
const {multiple, env, accept} = this.props;
|
||||
|
||||
const files = rejectedFiles.map((file: any) => ({
|
||||
...file,
|
||||
state: 'invalid',
|
||||
id: gennerateId(),
|
||||
name: file.name
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
files: multiple
|
||||
? this.state.files.concat(files)
|
||||
: this.state.files.length
|
||||
? this.state.files
|
||||
: files.slice(0, 1)
|
||||
});
|
||||
|
||||
env.alert(
|
||||
`您添加的文件${files.map((item: any) => `【${item.name}】`)}不符合类型的\`${accept}\`设定,请仔细检查。`
|
||||
);
|
||||
}
|
||||
|
||||
startUpload() {
|
||||
@ -445,7 +464,7 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
}
|
||||
|
||||
handleSelect() {
|
||||
this.refs.dropzone && (this.refs.dropzone as any).open();
|
||||
this.dropzone.current && this.dropzone.current.open();
|
||||
}
|
||||
|
||||
handleDrop(files: Array<FileX>) {
|
||||
@ -466,15 +485,15 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
const event = e.nativeEvent as any;
|
||||
const files: Array<FileX> = [];
|
||||
const items = event.clipboardData.items;
|
||||
const accept = this.props.accept;
|
||||
|
||||
[].slice.call(items).forEach((item: DataTransferItem) => {
|
||||
let blob: FileX;
|
||||
|
||||
if (item.kind !== 'file' || !(blob = item.getAsFile() as File) || !/^image/i.test(blob.type)) {
|
||||
if (item.kind !== 'file' || !(blob = item.getAsFile() as File) || !accepts(blob, accept)) {
|
||||
return;
|
||||
}
|
||||
|
||||
blob.preview = window.URL.createObjectURL(blob);
|
||||
blob.id = gennerateId();
|
||||
files.push(blob);
|
||||
});
|
||||
@ -483,7 +502,7 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
}
|
||||
|
||||
handleCrop() {
|
||||
(this.refs.cropper as any).getCroppedCanvas().toBlob((file: File) => {
|
||||
this.cropper.current!.getCroppedCanvas().toBlob((file: File) => {
|
||||
this.addFiles([file]);
|
||||
this.setState({
|
||||
cropFile: undefined,
|
||||
@ -607,17 +626,7 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
cb: (error: null | string, file: Blob, obj?: FileValue) => void,
|
||||
onProgress: (progress: number) => void
|
||||
) {
|
||||
let compressOptions = this.state.compressOptions;
|
||||
|
||||
if (this.props.showCompressOptions) {
|
||||
compressOptions = {
|
||||
maxWidth: 800,
|
||||
maxHeight: 600,
|
||||
...compressOptions
|
||||
};
|
||||
}
|
||||
|
||||
this._send(file, this.props.reciever as string, {compress: this.state.compress, compressOptions}, onProgress)
|
||||
this._send(file, this.props.reciever as string, {}, onProgress)
|
||||
.then((ret: Payload) => {
|
||||
if (ret.status) {
|
||||
throw new Error(ret.msg || '上传失败, 请重试');
|
||||
@ -724,73 +733,6 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
}
|
||||
}
|
||||
|
||||
renderCompressOptions() {
|
||||
const showCompressOptions = this.props.showCompressOptions;
|
||||
const cx = this.props.classnames;
|
||||
const classPrefix = this.props.classPrefix;
|
||||
|
||||
if (!showCompressOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key="options" className="m-t">
|
||||
<Switch
|
||||
classPrefix={classPrefix}
|
||||
checked={!!this.state.compress}
|
||||
onChange={checked => this.setState({compress: checked})}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
|
||||
<span className="m-l-xs">开启缩放?</span>
|
||||
|
||||
{this.state.compress && (
|
||||
<div className="inline">
|
||||
<input
|
||||
className="form-control w-xs inline m-l-xs m-r-xs"
|
||||
type="text"
|
||||
value={
|
||||
typeof this.state.compressOptions.maxWidth === 'undefined'
|
||||
? 800
|
||||
: this.state.compressOptions.maxWidth
|
||||
}
|
||||
onChange={e =>
|
||||
this.setState({
|
||||
compressOptions: {
|
||||
...this.state.compressOptions,
|
||||
maxWidth: parseInt(e.currentTarget.value, 10) || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
|
||||
<span className=" m-l-xs m-r-xs">X</span>
|
||||
|
||||
<input
|
||||
className="form-control w-xs inline m-l-xs m-r-xs"
|
||||
type="text"
|
||||
value={
|
||||
typeof this.state.compressOptions.maxHeight === 'undefined'
|
||||
? 600
|
||||
: this.state.compressOptions.maxHeight
|
||||
}
|
||||
onChange={e =>
|
||||
this.setState({
|
||||
compressOptions: {
|
||||
...this.state.compressOptions,
|
||||
maxHeight: parseInt(e.currentTarget.value, 10) || 0
|
||||
}
|
||||
})
|
||||
}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
@ -801,167 +743,223 @@ export default class ImageControl extends React.Component<ImageProps, ImageState
|
||||
accept,
|
||||
maxLength,
|
||||
autoUpload,
|
||||
btnUploadClassName,
|
||||
btnClassName,
|
||||
hideUploadButton
|
||||
} = this.props;
|
||||
|
||||
const {files, error, crop, uploading, cropFile} = this.state;
|
||||
|
||||
const hasPending = files.some(file => file.state == 'pending');
|
||||
|
||||
return (
|
||||
<div className={cx(`ImageControl`, className)} tabIndex={-1} onPaste={this.handlePaste}>
|
||||
<div className={cx(`ImageControl`, className)}>
|
||||
{cropFile ? (
|
||||
<div className="cropper-wrapper">
|
||||
<Cropper {...crop} ref="cropper" src={cropFile.preview} />
|
||||
<button type="button" className="btn-sm btn btn-link" onClick={this.handleCrop}>
|
||||
<i className="fa fa-2x fa-check text-warning" />
|
||||
</button>
|
||||
<button type="button" className="btn-sm btn btn-link" onClick={this.cancelCrop}>
|
||||
<i className="fa fa-2x fa-times text-white" />
|
||||
</button>
|
||||
<div className={cx('ImageControl-cropperWrapper')}>
|
||||
<Cropper {...crop} ref={this.cropper} src={cropFile.preview} />
|
||||
<div className={cx('ImageControl-croperToolbar')}>
|
||||
<a
|
||||
className={cx('ImageControl-cropCancel')}
|
||||
onClick={this.cancelCrop}
|
||||
data-tooltip="取消"
|
||||
data-position="left"
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
<a
|
||||
className={cx('ImageControl-cropConfirm')}
|
||||
onClick={this.handleCrop}
|
||||
data-tooltip="确认"
|
||||
data-position="left"
|
||||
>
|
||||
<Icon icon="check" className="icon" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<DropZone
|
||||
key="drop-zone"
|
||||
className={cx('ImageControl-dropzone', {
|
||||
disabled,
|
||||
'is-empty': !files.length
|
||||
})}
|
||||
activeClassName="is-active"
|
||||
ref="dropzone"
|
||||
ref={this.dropzone}
|
||||
onDrop={this.handleDrop}
|
||||
onDropRejected={this.handleDropRejected}
|
||||
disableClick
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
>
|
||||
{files && files.length
|
||||
? files.map((file, key) => (
|
||||
<div
|
||||
key={file.id || key}
|
||||
className={cx('ImageControl-item', {
|
||||
'is-uploaded': file.state !== 'uploading',
|
||||
'is-invalid': file.state === 'error' || file.state === 'invalid'
|
||||
})}
|
||||
>
|
||||
{file.state === 'invalid' || file.state === 'error' ? (
|
||||
<a
|
||||
className={cx('ImageControl-retryBtn', {'is-disabled': disabled})}
|
||||
onClick={this.handleSelect}
|
||||
>
|
||||
<Icon icon="retry" className="icon" />
|
||||
<p className="ImageControl-itemInfoError">重新上传</p>
|
||||
</a>
|
||||
) : file.state === 'uploading' ? (
|
||||
<>
|
||||
<a
|
||||
onClick={this.removeFile.bind(this, file, key)}
|
||||
key="clear"
|
||||
className={cx('ImageControl-itemClear')}
|
||||
data-tooltip="移除"
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
<div key="info" className={cx('ImageControl-itemInfo')}>
|
||||
<p>文件上传中</p>
|
||||
<div className={cx('ImageControl-progress')}>
|
||||
<span
|
||||
style={{width: `${Math.round(file.progress * 100)}%`}}
|
||||
className={cx('ImageControl-progressValue')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div key="image" className={cx('ImageControl-itemImageWrap')}>
|
||||
<img
|
||||
onLoad={this.handleImageLoaded.bind(this, key)}
|
||||
src={file.url || file.preview}
|
||||
alt={file.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div key="overlay" className={cx('ImageControl-itemOverlay')}>
|
||||
{file.info ? (
|
||||
[
|
||||
<div key="1">
|
||||
{file.info.width} x {file.info.height}
|
||||
</div>,
|
||||
file.info.len ? (
|
||||
<div key="2">
|
||||
{ImageControl.formatFileSize(file.info.len)}
|
||||
</div>
|
||||
) : null
|
||||
]
|
||||
) : (
|
||||
<div>...</div>
|
||||
)}
|
||||
|
||||
{!disabled ? (
|
||||
<a
|
||||
data-tooltip="查看大图"
|
||||
data-position="bottom"
|
||||
target="_blank"
|
||||
href={file.url || file.preview}
|
||||
>
|
||||
<Icon icon="view" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
{!!crop && !disabled ? (
|
||||
<a
|
||||
data-tooltip="裁剪图片"
|
||||
data-position="bottom"
|
||||
onClick={this.editImage.bind(this, key)}
|
||||
>
|
||||
<Icon icon="pencil" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
{!disabled ? (
|
||||
<a
|
||||
data-tooltip="移除"
|
||||
data-position="bottom"
|
||||
onClick={this.removeFile.bind(this, file, key)}
|
||||
>
|
||||
<Icon icon="remove" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
|
||||
{(multiple && (!maxLength || files.length < maxLength)) || (!multiple && !files.length) ? (
|
||||
<label
|
||||
className={cx('ImageControl-addBtn', {'is-disabled': disabled})}
|
||||
onClick={this.handleSelect}
|
||||
data-tooltip={placeholder}
|
||||
data-position="right"
|
||||
{({getRootProps, getInputProps, isDragActive, isDragAccept, isDragReject, isFocused}) => (
|
||||
<div
|
||||
{...getRootProps({
|
||||
onClick: preventEvent,
|
||||
onPaste: this.handlePaste,
|
||||
className: cx('ImageControl-dropzone', {
|
||||
disabled,
|
||||
'is-empty': !files.length,
|
||||
'is-active': isDragActive
|
||||
})
|
||||
})}
|
||||
>
|
||||
<Icon icon="plus" className="icon" />
|
||||
</label>
|
||||
) : null}
|
||||
<input {...getInputProps()} />
|
||||
|
||||
{isDragActive || isDragAccept || isDragReject ? (
|
||||
<div
|
||||
className={cx('ImageControl-acceptTip', {
|
||||
'is-accept': isDragAccept,
|
||||
'is-reject': isDragReject
|
||||
})}
|
||||
>
|
||||
把图片拖到这,然后松完成添加!
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{files && files.length
|
||||
? files.map((file, key) => (
|
||||
<div
|
||||
key={file.id || key}
|
||||
className={cx('ImageControl-item', {
|
||||
'is-uploaded': file.state !== 'uploading',
|
||||
'is-invalid':
|
||||
file.state === 'error' || file.state === 'invalid'
|
||||
})}
|
||||
>
|
||||
{file.state === 'invalid' || file.state === 'error' ? (
|
||||
<a
|
||||
className={cx('ImageControl-retryBtn', {
|
||||
'is-disabled': disabled
|
||||
})}
|
||||
onClick={this.handleSelect}
|
||||
>
|
||||
<Icon icon="retry" className="icon" />
|
||||
<p className="ImageControl-itemInfoError">重新上传</p>
|
||||
</a>
|
||||
) : file.state === 'uploading' ? (
|
||||
<>
|
||||
<a
|
||||
onClick={this.removeFile.bind(this, file, key)}
|
||||
key="clear"
|
||||
className={cx('ImageControl-itemClear')}
|
||||
data-tooltip="移除"
|
||||
>
|
||||
<Icon icon="close" className="icon" />
|
||||
</a>
|
||||
<div key="info" className={cx('ImageControl-itemInfo')}>
|
||||
<p>文件上传中</p>
|
||||
<div className={cx('ImageControl-progress')}>
|
||||
<span
|
||||
style={{
|
||||
width: `${Math.round(
|
||||
file.progress * 100
|
||||
)}%`
|
||||
}}
|
||||
className={cx('ImageControl-progressValue')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
key="image"
|
||||
className={cx('ImageControl-itemImageWrap')}
|
||||
>
|
||||
<img
|
||||
onLoad={this.handleImageLoaded.bind(this, key)}
|
||||
src={file.url || file.preview}
|
||||
alt={file.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
key="overlay"
|
||||
className={cx('ImageControl-itemOverlay')}
|
||||
>
|
||||
{file.info ? (
|
||||
[
|
||||
<div key="1">
|
||||
{file.info.width} x {file.info.height}
|
||||
</div>,
|
||||
file.info.len ? (
|
||||
<div key="2">
|
||||
{ImageControl.formatFileSize(
|
||||
file.info.len
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
]
|
||||
) : (
|
||||
<div>...</div>
|
||||
)}
|
||||
|
||||
{!disabled ? (
|
||||
<a
|
||||
data-tooltip="查看大图"
|
||||
data-position="bottom"
|
||||
target="_blank"
|
||||
href={file.url || file.preview}
|
||||
>
|
||||
<Icon icon="view" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
{!!crop && !disabled ? (
|
||||
<a
|
||||
data-tooltip="裁剪图片"
|
||||
data-position="bottom"
|
||||
onClick={this.editImage.bind(this, key)}
|
||||
>
|
||||
<Icon icon="pencil" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
{!disabled ? (
|
||||
<a
|
||||
data-tooltip="移除"
|
||||
data-position="bottom"
|
||||
onClick={this.removeFile.bind(
|
||||
this,
|
||||
file,
|
||||
key
|
||||
)}
|
||||
>
|
||||
<Icon icon="remove" className="icon" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
|
||||
{(multiple && (!maxLength || files.length < maxLength)) ||
|
||||
(!multiple && !files.length) ? (
|
||||
<label
|
||||
className={cx('ImageControl-addBtn', {'is-disabled': disabled})}
|
||||
onClick={this.handleSelect}
|
||||
data-tooltip={placeholder}
|
||||
data-position="right"
|
||||
>
|
||||
<Icon icon="plus" className="icon" />
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{isFocused ? (
|
||||
<span className={cx('ImageControl-pasteTip')}>
|
||||
当前状态支持从剪切板中粘贴图片文件。
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{!autoUpload && !hideUploadButton && files.length ? (
|
||||
<Button
|
||||
level="default"
|
||||
className={cx('ImageControl-uploadBtn')}
|
||||
disabled={!hasPending}
|
||||
onClick={this.toggleUpload}
|
||||
>
|
||||
{uploading ? '暂停上传' : '开始上传'}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{error ? <div className={cx('ImageControl-errorMsg')}>{error}</div> : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DropZone>
|
||||
)}
|
||||
|
||||
{this.renderCompressOptions()}
|
||||
|
||||
{!autoUpload && !hideUploadButton && files.length ? (
|
||||
<Button
|
||||
level="default"
|
||||
className={cx('ImageControl-uploadBtn', btnUploadClassName)}
|
||||
disabled={!hasPending}
|
||||
onClick={this.toggleUpload}
|
||||
>
|
||||
{uploading ? '暂停上传' : '开始上传'}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{error ? <div className={cx('ImageControl-errorMsg')}>{error}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ export interface FormItemBasicConfig extends Partial<RendererConfig> {
|
||||
type?: string;
|
||||
wrap?: boolean;
|
||||
renderLabel?: boolean;
|
||||
renderDescription?: boolean;
|
||||
test?: RegExp | TestFunc;
|
||||
storeType?: string;
|
||||
validations?: string;
|
||||
@ -48,6 +49,7 @@ export interface FormControlProps extends RendererProps {
|
||||
|
||||
renderControl?: (props: RendererProps) => JSX.Element;
|
||||
renderLabel?: boolean;
|
||||
renderDescription?: boolean;
|
||||
sizeMutable?: boolean;
|
||||
wrap?: boolean;
|
||||
hint?: string;
|
||||
@ -165,6 +167,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
env,
|
||||
formItem: model,
|
||||
renderLabel,
|
||||
renderDescription,
|
||||
hint
|
||||
} = this.props;
|
||||
|
||||
@ -250,7 +253,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
{description
|
||||
{renderDescription !== false && description
|
||||
? render('description', description, {
|
||||
className: cx(`Form-description`, descriptionClassName)
|
||||
})
|
||||
@ -278,6 +281,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
captionClassName,
|
||||
formItem: model,
|
||||
renderLabel,
|
||||
renderDescription,
|
||||
hint,
|
||||
formMode
|
||||
} = this.props;
|
||||
@ -339,7 +343,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
{description
|
||||
{renderDescription !== false && description
|
||||
? render('description', description, {
|
||||
className: cx(`Form-description`, descriptionClassName)
|
||||
})
|
||||
@ -366,7 +370,8 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
labelRemark,
|
||||
env,
|
||||
hint,
|
||||
renderLabel
|
||||
renderLabel,
|
||||
renderDescription
|
||||
} = this.props;
|
||||
|
||||
description = description || desc;
|
||||
@ -427,7 +432,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
{description
|
||||
{renderDescription !== false && description
|
||||
? render('description', description, {
|
||||
className: cx(`Form-description`, descriptionClassName)
|
||||
})
|
||||
@ -455,6 +460,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
captionClassName,
|
||||
formItem: model,
|
||||
renderLabel,
|
||||
renderDescription,
|
||||
hint,
|
||||
formMode
|
||||
} = this.props;
|
||||
@ -518,7 +524,7 @@ export class FormItemWrap extends React.Component<FormControlProps, FormControlS
|
||||
</ul>
|
||||
) : null}
|
||||
|
||||
{description
|
||||
{description && renderDescription !== false
|
||||
? render('description', description, {
|
||||
className: cx(`Form-description`, descriptionClassName)
|
||||
})
|
||||
@ -577,6 +583,7 @@ export function registerFormItem(config: FormItemConfig): RendererConfig {
|
||||
static defaultProps = {
|
||||
className: '',
|
||||
renderLabel: config.renderLabel,
|
||||
renderDescription: config.renderDescription,
|
||||
sizeMutable: config.sizeMutable,
|
||||
wrap: config.wrap,
|
||||
strictMode: config.strictMode,
|
||||
|
@ -192,10 +192,10 @@ export function validate(
|
||||
|
||||
const fn = validations[ruleName];
|
||||
|
||||
if (!fn(values, value, ...rules[ruleName])) {
|
||||
if (!fn(values, value, ...(Array.isArray(rules[ruleName]) ? rules[ruleName] : [rules[ruleName]]))) {
|
||||
errors.push(
|
||||
filter((messages && messages[ruleName]) || validateMessages[ruleName], {
|
||||
...['', ...rules[ruleName]]
|
||||
...[''].concat(rules[ruleName])
|
||||
})
|
||||
);
|
||||
}
|
||||
|