@sheinoutmobile/shineoutmobile 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @sheinoutmobile/shineoutmobile might be problematic. Click here for more details.
- package/.eslintignore +20 -0
- package/.eslintrc +8 -0
- package/.gitlab-ci.yml +53 -0
- package/.yarnrc +1 -0
- package/CHANGELOG.md +8 -0
- package/README.md +14 -0
- package/build.config.js +6 -0
- package/doc.config.js +16 -0
- package/lib/index.esm.js +5414 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -0
- package/package.json +47 -0
- package/src/component/button/button.jsx +83 -0
- package/src/component/button/index.jsx +3 -0
- package/src/component/button/style.less +132 -0
- package/src/component/cell/cell.jsx +71 -0
- package/src/component/cell/index.jsx +3 -0
- package/src/component/cell/style.less +86 -0
- package/src/component/cell-group/cell-group.jsx +26 -0
- package/src/component/cell-group/index.jsx +3 -0
- package/src/component/cell-group/style.less +22 -0
- package/src/component/checkbox/checkbox.jsx +92 -0
- package/src/component/checkbox/index.js +3 -0
- package/src/component/checkbox/kind/checkbox-button.jsx +41 -0
- package/src/component/checkbox/style.less +49 -0
- package/src/component/dialog/dialog.jsx +58 -0
- package/src/component/dialog/index.js +3 -0
- package/src/component/dialog/kind/dialog-alert.jsx +22 -0
- package/src/component/dialog/kind/dialog-confirm.jsx +54 -0
- package/src/component/dialog/style.less +80 -0
- package/src/component/drawer/drawer.jsx +151 -0
- package/src/component/drawer/index.jsx +3 -0
- package/src/component/drawer/js/tools.js +1 -0
- package/src/component/drawer/style.less +17 -0
- package/src/component/field/field.jsx +49 -0
- package/src/component/field/index.js +3 -0
- package/src/component/icon/icon.jsx +55 -0
- package/src/component/icon/index.jsx +19 -0
- package/src/component/icon/style.less +9 -0
- package/src/component/icons/index.js +3 -0
- package/src/component/icons/mrp.js +3 -0
- package/src/component/image-previewer/image-previewer.jsx +54 -0
- package/src/component/image-previewer/index.js +3 -0
- package/src/component/index-sort-list/group.jsx +49 -0
- package/src/component/index-sort-list/index-bar.jsx +34 -0
- package/src/component/index-sort-list/index.js +3 -0
- package/src/component/index-sort-list/js/rules.js +14 -0
- package/src/component/index-sort-list/js/sort-rule.js +41 -0
- package/src/component/index-sort-list/list.jsx +110 -0
- package/src/component/index-sort-list/style.less +109 -0
- package/src/component/input/index.js +3 -0
- package/src/component/input/input.jsx +285 -0
- package/src/component/input/style.less +77 -0
- package/src/component/list-view/index.js +3 -0
- package/src/component/list-view/jsx/empty.jsx +23 -0
- package/src/component/list-view/list-view.jsx +165 -0
- package/src/component/list-view/style.less +79 -0
- package/src/component/loading/index.js +3 -0
- package/src/component/loading/loading.jsx +97 -0
- package/src/component/loading/styles.less +128 -0
- package/src/component/multiple-selector/index.jsx +3 -0
- package/src/component/multiple-selector/item.jsx +23 -0
- package/src/component/multiple-selector/multiple-selector.jsx +116 -0
- package/src/component/multiple-selector/selector.jsx +41 -0
- package/src/component/multiple-selector/style.less +78 -0
- package/src/component/notice-bar/index.jsx +3 -0
- package/src/component/notice-bar/notice-bar.jsx +86 -0
- package/src/component/notice-bar/style.less +72 -0
- package/src/component/notify/index.js +37 -0
- package/src/component/notify/notify.jsx +125 -0
- package/src/component/notify/style.less +48 -0
- package/src/component/picker/drawPicker.jsx +110 -0
- package/src/component/picker/index.js +3 -0
- package/src/component/picker/picker.jsx +380 -0
- package/src/component/picker/pickerItem.jsx +31 -0
- package/src/component/picker/style.less +80 -0
- package/src/component/pull-refresh/index.js +3 -0
- package/src/component/pull-refresh/pull-refresh.jsx +46 -0
- package/src/component/search-bar/index.jsx +3 -0
- package/src/component/search-bar/search-bar.jsx +97 -0
- package/src/component/search-bar/style.less +52 -0
- package/src/component/sign/index.jsx +3 -0
- package/src/component/sign/sign.jsx +33 -0
- package/src/component/sign/style.less +56 -0
- package/src/component/sort-item/index.jsx +3 -0
- package/src/component/sort-item/sort-item.jsx +52 -0
- package/src/component/sort-item/style.less +52 -0
- package/src/component/subscript/index.js +3 -0
- package/src/component/subscript/style.less +63 -0
- package/src/component/subscript/subscript.jsx +77 -0
- package/src/component/tab/index.jsx +6 -0
- package/src/component/tab/nav-child.jsx +63 -0
- package/src/component/tab/panel.jsx +22 -0
- package/src/component/tab/style.less +87 -0
- package/src/component/tab/tab.jsx +195 -0
- package/src/component/tag/index.jsx +3 -0
- package/src/component/tag/style.less +56 -0
- package/src/component/tag/tag.jsx +52 -0
- package/src/component/toast/content.jsx +31 -0
- package/src/component/toast/index.js +3 -0
- package/src/component/toast/style.less +50 -0
- package/src/component/toast/toast.jsx +62 -0
- package/src/component/touchable/index.js +3 -0
- package/src/component/touchable/style.less +33 -0
- package/src/component/touchable/touchable.jsx +43 -0
- package/src/component/uploader/index.jsx +445 -0
- package/src/component/uploader/js/get-image-base64.js +31 -0
- package/src/component/uploader/js/get-image-detail.js +26 -0
- package/src/component/uploader/js/index.js +77 -0
- package/src/component/uploader/js/merge-class-name.js +14 -0
- package/src/component/uploader/js/old-compress-function.js +64 -0
- package/src/component/uploader/js/request.js +62 -0
- package/src/component/uploader/js/tools.js +115 -0
- package/src/component/uploader/jsx/config.jsx +36 -0
- package/src/component/uploader/jsx/img-preview.jsx +41 -0
- package/src/component/uploader/jsx/thumbnails.jsx +27 -0
- package/src/component/uploader/styles/index.less +158 -0
- package/src/index.js +1 -0
- package/src/index.jsx +64 -0
- package/src/styles/color.less +51 -0
- package/src/styles/font.less +14 -0
- package/src/styles/format-theme.less +27 -0
- package/src/styles/index.less +26 -0
- package/src/styles/spacing.less +3 -0
- package/src/styles/var.less +3 -0
- package/src/tools/create-body-container.js +47 -0
- package/src/tools/limit-body-scroll.js +17 -0
- package/src/tools/limit-document-scroll.js +12 -0
- package/src/tools/merge-class-name.js +14 -0
- package/src/tools/object-key-check.js +3 -0
- package/src/tools/pinyin.js +6 -0
- package/src/tools/proptypes.js +51 -0
- package/src/tools/style.less +14 -0
- package/test/cases/case-button01.assert.js +6 -0
- package/test/cases/case-button01.source.jsx +28 -0
- package/test/cases/case-checkbox01.assert.js +6 -0
- package/test/cases/case-checkbox01.source.jsx +20 -0
- package/test/cases/case-dialog01.assert.js +6 -0
- package/test/cases/case-dialog01.source.jsx +45 -0
- package/test/cases/case-drawer01.assert.js +6 -0
- package/test/cases/case-drawer01.source.jsx +85 -0
- package/test/cases/case-icons01.assert.js +6 -0
- package/test/cases/case-icons01.source.jsx +25 -0
- package/test/cases/case-indexSortList01.assert.js +6 -0
- package/test/cases/case-indexSortList01.source.jsx +74 -0
- package/test/cases/case-input01.assert.js +6 -0
- package/test/cases/case-input01.source.jsx +24 -0
- package/test/cases/case-listView01.assert.js +6 -0
- package/test/cases/case-listView01.source.jsx +64 -0
- package/test/cases/case-multiplePicker01.assert.js +6 -0
- package/test/cases/case-multiplePicker01.source.jsx +60 -0
- package/test/cases/case-multipleSelector01.assert.js +6 -0
- package/test/cases/case-multipleSelector01.source.jsx +49 -0
- package/test/cases/case-noticebar01.assert.js +6 -0
- package/test/cases/case-noticebar01.source.jsx +21 -0
- package/test/cases/case-picker01.assert.js +6 -0
- package/test/cases/case-picker01.source.jsx +61 -0
- package/test/cases/case-refresh01.assert.js +6 -0
- package/test/cases/case-refresh01.source.jsx +33 -0
- package/test/cases/case-sign01.assert.js +6 -0
- package/test/cases/case-sign01.source.jsx +21 -0
- package/test/cases/case-subscript01.assert.js +6 -0
- package/test/cases/case-subscript01.source.jsx +30 -0
- package/test/cases/case-tab01.assert.js +6 -0
- package/test/cases/case-tab01.source.jsx +51 -0
- package/test/cases/case-toast01.assert.js +6 -0
- package/test/cases/case-toast01.source.jsx +32 -0
- package/test/cases/case-uploader01.assert.js +6 -0
- package/test/cases/case-uploader01.source.jsx +45 -0
- package/test.config.js +25 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import React, { PureComponent, isValidElement } from 'react';
|
|
2
|
+
import propTypes from 'prop-types';
|
|
3
|
+
import { parseFiles, setArray, checkFileType } from './js';
|
|
4
|
+
import getImageBase64, { getImageBase64List } from './js/get-image-base64';
|
|
5
|
+
import getImageDetail from './js/get-image-detail';
|
|
6
|
+
import merge from './js/merge-class-name';
|
|
7
|
+
import styles from './styles/index.less';
|
|
8
|
+
import Thumbnails from './jsx/thumbnails';
|
|
9
|
+
import { textConf, fontConf, fileTypeIcons, findIconByType } from './jsx/config';
|
|
10
|
+
import defaultRequest, { ERROR, UPLOADING } from './js/request';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @cn 图片压缩
|
|
14
|
+
* @desc 支持指定类型图片的压缩处理
|
|
15
|
+
*
|
|
16
|
+
* @author yan geliang
|
|
17
|
+
* @date 2019-07-16
|
|
18
|
+
*/
|
|
19
|
+
class Uploader extends PureComponent {
|
|
20
|
+
constructor(props) {
|
|
21
|
+
super(props);
|
|
22
|
+
|
|
23
|
+
this.inputRef = React.createRef();
|
|
24
|
+
this.uploaderBody = React.createRef();
|
|
25
|
+
|
|
26
|
+
this.timeout = null; // 定时器,用于过滤掉一些无谓的操作
|
|
27
|
+
this.state = {
|
|
28
|
+
processedFiles: [], // 被压缩或者其他处理后的file对象数组
|
|
29
|
+
compressStatus: 'default', // default: 无;pending: 压缩正在进行;resolve:压缩完成;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 为了动态计算到底多宽和多高,因为现在外围容器是inline-block,但内容需要宽度来进行grid布局
|
|
34
|
+
componentDidUpdate() {
|
|
35
|
+
const { limit, children } = this.props;
|
|
36
|
+
if (children) return; // 倘若是外部引入,则不在进行计算,也不会使用缩略图
|
|
37
|
+
|
|
38
|
+
const currentFiles = this.getCurrentFiles();
|
|
39
|
+
|
|
40
|
+
const width = 80 + (88 * currentFiles.length);
|
|
41
|
+
this.uploaderBody.current.style.width = `${width}px`;
|
|
42
|
+
const clientWidth = this.uploaderBody.current.clientWidth;
|
|
43
|
+
|
|
44
|
+
const extraCount = currentFiles.length < limit ? 1 : 0; // 如果超过了限制,那个选图片的图标会消失
|
|
45
|
+
const count = Math.floor((clientWidth + 8) / 88) ; // 一行能容纳多少个缩略图(含加号)
|
|
46
|
+
const rows = Math.ceil((currentFiles.length + extraCount) / count); // 有多少行
|
|
47
|
+
|
|
48
|
+
// console.log(`一行能容纳: ${count}`, `总共${rows}`);
|
|
49
|
+
|
|
50
|
+
if (width > clientWidth) {
|
|
51
|
+
this.uploaderBody.current.style.paddingBottom = `${80 + (rows - 1) * 88}px`;
|
|
52
|
+
} else {
|
|
53
|
+
this.uploaderBody.current.style.paddingBottom = '80px';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 处理文件
|
|
59
|
+
* @param {array} filesList 文件列表
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
previewFile(filesList) {
|
|
63
|
+
const self = this;
|
|
64
|
+
const { accept, onChange, imageCompression, onError, action, request } = this.props;
|
|
65
|
+
|
|
66
|
+
const currentFiles = this.getCurrentFiles();
|
|
67
|
+
|
|
68
|
+
// 检测指定的接受格式和实际传入文件的格式是否匹配
|
|
69
|
+
const errorDesc = checkFileType(filesList, accept);
|
|
70
|
+
if (errorDesc) {
|
|
71
|
+
console.error(errorDesc);
|
|
72
|
+
if (typeof onError === 'function') onError(errorDesc);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (action || request) {
|
|
77
|
+
if (filesList.length === 1) {
|
|
78
|
+
this.uploadFile(filesList[0], 0);
|
|
79
|
+
} else {
|
|
80
|
+
// TODO 多个文件的先不支持,等待重构
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// 此setTimeout出现问题,执行时间对不上,应该是由于下方parseFiles的异步导致的,等待排查
|
|
84
|
+
// this.timeout = setTimeout(() => this.setState({ compressStatus: 'pending' }), 100)
|
|
85
|
+
this.setState({ compressStatus: 'pending' });
|
|
86
|
+
|
|
87
|
+
// 开始处理文件
|
|
88
|
+
parseFiles(filesList, imageCompression).then(({ processedFileList }) => {
|
|
89
|
+
const newProcessedFiles = [...currentFiles, ...processedFileList];
|
|
90
|
+
|
|
91
|
+
self.setState({ processedFiles: newProcessedFiles, compressStatus: 'resolve' });
|
|
92
|
+
|
|
93
|
+
if (self.timeout) {
|
|
94
|
+
clearTimeout(self.timeout);
|
|
95
|
+
self.timeout = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 依次返回「处理过的(比如被压缩)file对象」
|
|
99
|
+
onChange(newProcessedFiles);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
transformAccept() {
|
|
105
|
+
const { accept } = this.props;
|
|
106
|
+
return accept.indexOf('image') >= 0 ? 'image' : 'default';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getAction(file) {
|
|
110
|
+
const { action } = this.props;
|
|
111
|
+
if (typeof action === 'string') return action;
|
|
112
|
+
if (typeof action === 'function') return action(file);
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
uploadFile(file, index) {
|
|
117
|
+
const {
|
|
118
|
+
request,
|
|
119
|
+
} = this.props;
|
|
120
|
+
const opts = typeof request === 'object' ? request : {};
|
|
121
|
+
const {
|
|
122
|
+
name,
|
|
123
|
+
htmlName,
|
|
124
|
+
cors,
|
|
125
|
+
params,
|
|
126
|
+
withCredentials,
|
|
127
|
+
headers,
|
|
128
|
+
onStart = () => null,
|
|
129
|
+
onProgress = () => null,
|
|
130
|
+
onSuccess = () => null,
|
|
131
|
+
onError = () => null,
|
|
132
|
+
} = opts;
|
|
133
|
+
|
|
134
|
+
const req = typeof request === 'function' ? request : defaultRequest;
|
|
135
|
+
let throttle = false;
|
|
136
|
+
const options = {
|
|
137
|
+
url: this.getAction(file),
|
|
138
|
+
name: htmlName || name,
|
|
139
|
+
cors,
|
|
140
|
+
params,
|
|
141
|
+
withCredentials,
|
|
142
|
+
file,
|
|
143
|
+
headers,
|
|
144
|
+
onStart,
|
|
145
|
+
onProgress: (e, msg) => {
|
|
146
|
+
const percent = typeof e.percent === 'number' ? e.percent : (e.loaded / e.total) * 100;
|
|
147
|
+
if (throttle) return;
|
|
148
|
+
throttle = true;
|
|
149
|
+
setTimeout(() => {
|
|
150
|
+
throttle = false;
|
|
151
|
+
}, 16);
|
|
152
|
+
|
|
153
|
+
this.setState((preState) => {
|
|
154
|
+
const processTask = (preState.processTask || []).slice();
|
|
155
|
+
if (processTask[index]) {
|
|
156
|
+
processTask[index].process = percent;
|
|
157
|
+
} else {
|
|
158
|
+
processTask[index] = { process: percent };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
processTask,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
onLoad: (xhr) => {
|
|
166
|
+
if (!/^2|1223/.test(xhr.status)) {
|
|
167
|
+
onError();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let response = xhr.responseText || xhr.response;
|
|
172
|
+
if (onSuccess) {
|
|
173
|
+
response = onSuccess(response, file, xhr);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (response instanceof Error) {
|
|
177
|
+
onError();
|
|
178
|
+
} else {
|
|
179
|
+
const values = (this.props.value || []).concat([response]);
|
|
180
|
+
this.props.onChange(values);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
onError: () => {
|
|
184
|
+
this.setState((preState) => {
|
|
185
|
+
const processTask = (preState.processTask || []).slice();
|
|
186
|
+
if (processTask[index]) {
|
|
187
|
+
processTask[index].process = undefined;
|
|
188
|
+
} else {
|
|
189
|
+
processTask[index] = { process: undefined };
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
processTask,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
onError();
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
if (onProgress === false || onProgress === null) {
|
|
199
|
+
delete options.onProgress;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return req(options);
|
|
203
|
+
}
|
|
204
|
+
renderCtrlView = (item, index, list) => {
|
|
205
|
+
const { compressStatus } = this.state;
|
|
206
|
+
const {
|
|
207
|
+
renderCtrlView,
|
|
208
|
+
children,
|
|
209
|
+
icon,
|
|
210
|
+
text,
|
|
211
|
+
disabled,
|
|
212
|
+
onBeforeSelect,
|
|
213
|
+
} = this.props;
|
|
214
|
+
|
|
215
|
+
const compressingFlag = compressStatus === 'pending'; // 是否处于正在压缩
|
|
216
|
+
|
|
217
|
+
const onClick = () => {
|
|
218
|
+
if (compressingFlag) return;
|
|
219
|
+
if (onBeforeSelect()) {
|
|
220
|
+
this.inputRef.current.value = null; // 清掉input中当前的图片,以便能选择重复的图片
|
|
221
|
+
this.inputRef.current.click();
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (typeof children === 'function') return children(item, index, list, onClick);
|
|
226
|
+
|
|
227
|
+
if (React.isValidElement(children) && React.Children.only(children)) {
|
|
228
|
+
return React.cloneElement(children, {
|
|
229
|
+
onClick
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (typeof renderCtrlView === 'function') return renderCtrlView(item, index, list, onClick);
|
|
233
|
+
|
|
234
|
+
let title = compressingFlag ? '正在压缩' : text || textConf[this.transformAccept()];
|
|
235
|
+
if (item.process !== undefined) title = (item.process !== 100 ? '上传中' : '上传完成');
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div
|
|
239
|
+
className={merge(styles.textCont, (compressingFlag || disabled) && styles.disabled)}
|
|
240
|
+
onClick={onClick}
|
|
241
|
+
>
|
|
242
|
+
<div>
|
|
243
|
+
{
|
|
244
|
+
isValidElement(icon)
|
|
245
|
+
? <span className={styles.extraIcon}>{icon}</span>
|
|
246
|
+
: fontConf[this.transformAccept()]
|
|
247
|
+
}
|
|
248
|
+
<span className={styles.text}>
|
|
249
|
+
{ title }
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
renderPreview(thumbnails) {
|
|
257
|
+
const { onChange } = this.props;
|
|
258
|
+
|
|
259
|
+
const currentFiles = this.getCurrentFiles();
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
thumbnails.map((thumbnail, index) => (
|
|
263
|
+
<Thumbnails
|
|
264
|
+
key={index}
|
|
265
|
+
name={fileTypeIcons[thumbnail]}
|
|
266
|
+
src={thumbnail}
|
|
267
|
+
deleteClick={() => {
|
|
268
|
+
this.inputRef.current.value = null; // 清掉input中当前的图片,以便能选择重复的图片
|
|
269
|
+
const newProcessedFiles = (currentFiles || []).filter((p, ind) => ind !== index);
|
|
270
|
+
this.setState((preState) => {
|
|
271
|
+
const newProcessTask = (preState.processTask || []).filter((p, idx) => idx !== index);
|
|
272
|
+
return {
|
|
273
|
+
processedFiles: newProcessedFiles,
|
|
274
|
+
processTask: newProcessTask,
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
// 依次返回「处理过的(比如被压缩)file对象」
|
|
278
|
+
onChange(newProcessedFiles);
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
))
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 获取外部传入的value,或者在外部没传入时,采用state里的files
|
|
286
|
+
getCurrentFiles() {
|
|
287
|
+
const { processedFiles } = this.state;
|
|
288
|
+
const { value } = this.props;
|
|
289
|
+
|
|
290
|
+
return this.props.hasOwnProperty('value') ? value : processedFiles;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 生成缩略图 || iconName
|
|
294
|
+
createThumbnails() {
|
|
295
|
+
const currentFiles = this.getCurrentFiles();
|
|
296
|
+
|
|
297
|
+
return currentFiles.map((f) => {
|
|
298
|
+
if (typeof f === 'string') return f;
|
|
299
|
+
if (f.type.indexOf('image') >= 0) return URL.createObjectURL(f);
|
|
300
|
+
return findIconByType(f.type);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
render() {
|
|
305
|
+
const {
|
|
306
|
+
accept,
|
|
307
|
+
multiple,
|
|
308
|
+
disabled,
|
|
309
|
+
limit = 10,
|
|
310
|
+
className,
|
|
311
|
+
onError,
|
|
312
|
+
showPreview,
|
|
313
|
+
children,
|
|
314
|
+
} = this.props;
|
|
315
|
+
|
|
316
|
+
const currentFiles = this.getCurrentFiles();
|
|
317
|
+
const thumbnails = this.createThumbnails();
|
|
318
|
+
|
|
319
|
+
let { processTask } = this.state;
|
|
320
|
+
processTask = processTask && processTask.length > 0 ? processTask : [{}];
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<div ref={this.uploaderBody} className={!children && styles['sm-uploader']}>
|
|
324
|
+
<span className={merge(!children && styles['sm-uploader-content'], className)}>
|
|
325
|
+
{
|
|
326
|
+
thumbnails.length < limit || !showPreview
|
|
327
|
+
? processTask.map(this.renderCtrlView)
|
|
328
|
+
: null
|
|
329
|
+
}
|
|
330
|
+
{
|
|
331
|
+
showPreview
|
|
332
|
+
? (
|
|
333
|
+
children
|
|
334
|
+
? (<div className={styles['sm-uploader-content-thumbnails']}>{this.renderPreview(thumbnails)}</div>)
|
|
335
|
+
: this.renderPreview(thumbnails)
|
|
336
|
+
)
|
|
337
|
+
: null
|
|
338
|
+
}
|
|
339
|
+
<input
|
|
340
|
+
ref={this.inputRef}
|
|
341
|
+
type="file"
|
|
342
|
+
accept={accept}
|
|
343
|
+
disabled={disabled}
|
|
344
|
+
multiple={multiple}
|
|
345
|
+
style={{ display: 'none' }}
|
|
346
|
+
onClick={(e) => {
|
|
347
|
+
e.stopPropagation();
|
|
348
|
+
}}
|
|
349
|
+
onChange={(e) => {
|
|
350
|
+
const fileList = e.files ? e.files : e.target.files;
|
|
351
|
+
const originFiles = fileList || this.inputRef.current.files;
|
|
352
|
+
if (originFiles.length + currentFiles.length > limit) {
|
|
353
|
+
console.warn('overdue the limit, 超出文件数量限制');
|
|
354
|
+
onError(new Error('超出文件数量限制'));
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 此举仅为了把原生的file对象变成数组,准确来说也就一个file
|
|
359
|
+
const filesArray = setArray(originFiles);
|
|
360
|
+
this.previewFile(filesArray); // 这个位置处理新的file
|
|
361
|
+
}}
|
|
362
|
+
/>
|
|
363
|
+
</span>
|
|
364
|
+
</div>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
Uploader.propTypes = {
|
|
370
|
+
/** 文件或文件链接列表 */
|
|
371
|
+
value: propTypes.arrayOf(propTypes.object),
|
|
372
|
+
/** 上传地址 */
|
|
373
|
+
action: propTypes.string,
|
|
374
|
+
/** 上传配置或者上传方法 */
|
|
375
|
+
request: propTypes.oneOfType([
|
|
376
|
+
propTypes.shape({
|
|
377
|
+
name: propTypes.string,
|
|
378
|
+
htmlName: propTypes.string,
|
|
379
|
+
cors: propTypes.bool,
|
|
380
|
+
params: propTypes.object,
|
|
381
|
+
withCredentials: propTypes.bool,
|
|
382
|
+
headers: propTypes.object,
|
|
383
|
+
onStart: propTypes.func,
|
|
384
|
+
onProgress: propTypes.func,
|
|
385
|
+
onSuccess: propTypes.func,
|
|
386
|
+
}),
|
|
387
|
+
propTypes.func,
|
|
388
|
+
]),
|
|
389
|
+
/** 是否支持多选 */
|
|
390
|
+
multiple: propTypes.bool,
|
|
391
|
+
/** 数据变化时触发
|
|
392
|
+
* 当配置 imageCompression 时,传入三个参数
|
|
393
|
+
* files: 更新后的files,differFiles: 包含特殊属性的如base64、图片width的value,originalFiles是原始文件
|
|
394
|
+
* 当配置了 action、request,传入一个参数,value: 更新后的value
|
|
395
|
+
* */
|
|
396
|
+
onChange: propTypes.func,
|
|
397
|
+
/** 最多文件数量 */
|
|
398
|
+
limit: propTypes.number,
|
|
399
|
+
/** 接受的文件类型,与原生input一样 */
|
|
400
|
+
accept: propTypes.string,
|
|
401
|
+
/** 图片压缩参数 */
|
|
402
|
+
imageCompression: propTypes.shape({
|
|
403
|
+
/** 最大内存,M为单位 */
|
|
404
|
+
maxSize: propTypes.number,
|
|
405
|
+
/** 宽度、高度(只设定其中一个的话,另一个会根据比例变化) */
|
|
406
|
+
width: propTypes.number,
|
|
407
|
+
height: propTypes.number,
|
|
408
|
+
}),
|
|
409
|
+
/** 最外层的className */
|
|
410
|
+
className: propTypes.string,
|
|
411
|
+
/** 不可用 */
|
|
412
|
+
disabled: propTypes.bool,
|
|
413
|
+
/** 发生错误时触发,比如文件格式不匹配 */
|
|
414
|
+
onError: propTypes.func,
|
|
415
|
+
/** 自定义图标 */
|
|
416
|
+
icon: propTypes.node,
|
|
417
|
+
/** 自定义文字 */
|
|
418
|
+
text: propTypes.node,
|
|
419
|
+
/** 渲染点击区域,传入参数 上传对象,下标,上传列表,onClick 方法 */
|
|
420
|
+
renderCtrlView: propTypes.oneOfType([
|
|
421
|
+
propTypes.element,
|
|
422
|
+
propTypes.func
|
|
423
|
+
]),
|
|
424
|
+
/* 是否显示图片预览 */
|
|
425
|
+
showPreview: propTypes.bool,
|
|
426
|
+
onBeforeSelect: propTypes.func,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
Uploader.defaultProps = {
|
|
430
|
+
accept: '*',
|
|
431
|
+
disabled: false,
|
|
432
|
+
multiple: false,
|
|
433
|
+
limit: 10,
|
|
434
|
+
onChange: () => null,
|
|
435
|
+
onError: () => null,
|
|
436
|
+
showPreview: true,
|
|
437
|
+
onBeforeSelect: () => true,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
Uploader.Img = Thumbnails;
|
|
441
|
+
Uploader.getImageBase64 = getImageBase64;
|
|
442
|
+
Uploader.getImageBase64List = getImageBase64List;
|
|
443
|
+
Uploader.getImageDetail = getImageDetail;
|
|
444
|
+
|
|
445
|
+
export default Uploader;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 图片转base64格式
|
|
2
|
+
const getImageBase64 = file => new Promise((resolve, reject) => {
|
|
3
|
+
const reader = new FileReader();
|
|
4
|
+
|
|
5
|
+
reader.onload = function onload() {
|
|
6
|
+
resolve({ code: 1, info: this.result });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
10
|
+
reader.onerror = e => reject({ code: 0, msg: e });
|
|
11
|
+
|
|
12
|
+
reader.readAsDataURL(file);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const getImageBase64List = files => new Promise((resolve, reject) => {
|
|
16
|
+
if (!files || files.length <= 0) resolve({ code: 1, info: [] });
|
|
17
|
+
|
|
18
|
+
const result = [];
|
|
19
|
+
|
|
20
|
+
files.forEach(async (file) => {
|
|
21
|
+
const base64Info = await getImageBase64(file);
|
|
22
|
+
if (base64Info.code) {
|
|
23
|
+
result.push(base64Info.info);
|
|
24
|
+
if (result.length === files.length) resolve({ code: 1, info: result });
|
|
25
|
+
} else {
|
|
26
|
+
reject(base64Info);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export default getImageBase64;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import getImageBase64 from './get-image-base64';
|
|
2
|
+
|
|
3
|
+
// 获取图片宽/高
|
|
4
|
+
async function getImageDetail(_base64, file) {
|
|
5
|
+
let base64 = _base64;
|
|
6
|
+
if (file) base64 = await getImageBase64(file);
|
|
7
|
+
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const ImgEl = new Image();
|
|
10
|
+
ImgEl.src = base64;
|
|
11
|
+
|
|
12
|
+
document.body.appendChild(ImgEl);
|
|
13
|
+
|
|
14
|
+
ImgEl.onload = () => {
|
|
15
|
+
const [width, height] = [ImgEl.offsetWidth, ImgEl.offsetHeight];
|
|
16
|
+
document.body.removeChild(ImgEl);
|
|
17
|
+
|
|
18
|
+
resolve({ code: 1, info: { width, height } });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
22
|
+
ImgEl.onerror = e => reject({ code: 0, msg: e });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default getImageDetail;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import compressImage from './tools';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 返回结果:
|
|
5
|
+
* processedFileList: [], // 压缩后的图片
|
|
6
|
+
* differFileList: [{ width: number, height: number, base64: string }], // 压缩后图片的各类参数,如宽、高、base64
|
|
7
|
+
*/
|
|
8
|
+
function parseFiles(fileList, imageCompression) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
try {
|
|
11
|
+
const processedFileList = [];
|
|
12
|
+
const differFileList = [];
|
|
13
|
+
|
|
14
|
+
const resolveFinally = () => {
|
|
15
|
+
if (!fileList) {
|
|
16
|
+
resolve([]);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (processedFileList.length === fileList.length) {
|
|
20
|
+
resolve({ processedFileList, differFileList });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
resolveFinally();
|
|
25
|
+
|
|
26
|
+
fileList.forEach(async (file) => {
|
|
27
|
+
if (imageCompression && Object.keys(imageCompression).length > 0 && file.type.indexOf('image') >= 0) {
|
|
28
|
+
const { fileCompressed, differFile } = await compressImage(
|
|
29
|
+
file, imageCompression.maxSize || 20, imageCompression.width, imageCompression.height,
|
|
30
|
+
);
|
|
31
|
+
processedFileList.push(fileCompressed);
|
|
32
|
+
differFileList.push(differFile);
|
|
33
|
+
} else {
|
|
34
|
+
processedFileList.push(file);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resolveFinally();
|
|
38
|
+
});
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error(e);
|
|
41
|
+
reject(e);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const setArray = (obj) => {
|
|
47
|
+
const list = [];
|
|
48
|
+
Object.keys(obj).forEach((key) => {
|
|
49
|
+
if (!Number.isNaN(Number(key))) {
|
|
50
|
+
list.push(obj[key]);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return list;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// 检测指定的accept格式和实际传入的文件格式是否匹对
|
|
57
|
+
// 匹配则返回false,不匹配则返回true
|
|
58
|
+
const checkFileType = (filesList, accept) => {
|
|
59
|
+
if (accept.indexOf('image/') < 0) return false; // 暂不检测image之外的其他格式
|
|
60
|
+
let desc = null;
|
|
61
|
+
filesList.forEach((file) => {
|
|
62
|
+
if (accept === 'image/*' && file.type.indexOf('image/') < 0) desc = `接受类型为${accept},而传入文件含有${file.type}`;
|
|
63
|
+
if (
|
|
64
|
+
accept !== 'image/*'
|
|
65
|
+
&& accept.indexOf('jpeg') < 0
|
|
66
|
+
&& accept.indexOf('jpg') < 0
|
|
67
|
+
&& file.type !== accept
|
|
68
|
+
) desc = `接受类型为${accept},而传入文件含有${file.type}`;
|
|
69
|
+
if (
|
|
70
|
+
(accept.indexOf('jpg') >= 0 || accept.indexOf('jpeg') >= 0)
|
|
71
|
+
&& ['image/jpg', 'image/jpeg'].indexOf(file.type) < 0
|
|
72
|
+
) desc = `接受类型为${accept},而传入文件含有${file.type}`;
|
|
73
|
+
});
|
|
74
|
+
return desc;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { parseFiles, setArray, checkFileType };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 此方法会把传入的className处理成className拼接字符串
|
|
2
|
+
function merge(...args) {
|
|
3
|
+
const finallyClassNameArr = [];
|
|
4
|
+
|
|
5
|
+
if (!args) return null;
|
|
6
|
+
|
|
7
|
+
Object.keys(args).forEach((key) => {
|
|
8
|
+
if (args[key]) finallyClassNameArr.push(args[key]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return finallyClassNameArr.join(' ');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default merge;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import getImageDetail from './get-image-detail';
|
|
2
|
+
import getImageBase64 from './get-image-base64';
|
|
3
|
+
|
|
4
|
+
const checkCode = (result) => {
|
|
5
|
+
if (!result.code) throw result.msg;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const getCompressSize = (size, maxSize) => size / maxSize;
|
|
9
|
+
|
|
10
|
+
// 利用canvas压缩图片
|
|
11
|
+
function compressImage(file, maxSize) {
|
|
12
|
+
return new Promise(async (resolve) => {
|
|
13
|
+
const base64res = await getImageBase64(file);
|
|
14
|
+
checkCode(base64res);
|
|
15
|
+
|
|
16
|
+
let base64 = base64res.info;
|
|
17
|
+
|
|
18
|
+
if (file.size <= (parseInt(maxSize, 10) * 1024 * 1024)) {
|
|
19
|
+
resolve({ fileCompressed: file, differFile: { base64 } });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const imageRes = await getImageDetail(base64);
|
|
24
|
+
checkCode(imageRes);
|
|
25
|
+
|
|
26
|
+
const { width, height } = imageRes.info;
|
|
27
|
+
const canvas = document.createElement('canvas');
|
|
28
|
+
const context = canvas.getContext('2d');
|
|
29
|
+
|
|
30
|
+
// 根据目前尺寸和最大尺寸,判断需要压缩多少倍
|
|
31
|
+
const multiple = getCompressSize(file.size, maxSize * 1024 * 1024);
|
|
32
|
+
const [compressedWidth, compressedHeight] = [
|
|
33
|
+
Math.floor(width / multiple),
|
|
34
|
+
Math.floor(height / multiple),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
canvas.width = compressedWidth;
|
|
38
|
+
canvas.height = compressedHeight;
|
|
39
|
+
|
|
40
|
+
const ImgEl = new Image();
|
|
41
|
+
ImgEl.src = base64;
|
|
42
|
+
|
|
43
|
+
context.drawImage(ImgEl, 0, 0, compressedWidth, compressedHeight);
|
|
44
|
+
|
|
45
|
+
canvas.toBlob(async (blob) => {
|
|
46
|
+
// 把blob对象转化成file
|
|
47
|
+
let fileCompressed = new File([blob], file.name, { type: file.type });
|
|
48
|
+
|
|
49
|
+
// 如果发现依然比maxsize要大,那么将递归本函数继续压缩
|
|
50
|
+
if (fileCompressed.size > (parseInt(maxSize, 10) * 1024 * 1024)) {
|
|
51
|
+
const {
|
|
52
|
+
fileCompressed: fc, differFile: { base64: b },
|
|
53
|
+
} = await compressImage(fileCompressed, maxSize);
|
|
54
|
+
|
|
55
|
+
fileCompressed = fc;
|
|
56
|
+
base64 = b;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
resolve({ fileCompressed, differFile: { base64, name: file.name } });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default compressImage;
|