From d31851c719361c3f962cec0f7123e9db3b44bfc0 Mon Sep 17 00:00:00 2001 From: liaoxuezhi Date: Mon, 21 Oct 2019 19:03:56 +0800 Subject: [PATCH] =?UTF-8?q?Image=20=E6=9B=B4=E6=94=B9=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scss/_variables.scss | 89 +++++--- scss/components/form/_image.scss | 183 +++++++++++++++- src/components/icons.tsx | 9 + src/icons/remove.svg | 9 + src/icons/retry.svg | 4 + src/icons/view.svg | 7 + src/renderers/Form/Image.tsx | 364 ++++++++++++++++++------------- 7 files changed, 478 insertions(+), 187 deletions(-) create mode 100644 src/icons/remove.svg create mode 100644 src/icons/retry.svg create mode 100644 src/icons/view.svg diff --git a/scss/_variables.scss b/scss/_variables.scss index e7058382..8eb7dc90 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -35,10 +35,28 @@ $dark: $gray800 !default; $remFactor: 16px !default; // 字体相关 -$fontFamilySansSerif: -apple-system, BlinkMacSystemFont, 'SF Pro SC', 'SF Pro Text', 'Helvetica Neue', Helvetica, - 'PingFang SC', 'Segoe UI', Roboto, 'Hiragino Sans GB', 'Arial', 'microsoft yahei ui', 'Microsoft YaHei', SimSun, - sans-serif !default; -$fontFamilyMonospace: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !default; +$fontFamilySansSerif: -apple-system, +BlinkMacSystemFont, +'SF Pro SC', +'SF Pro Text', +'Helvetica Neue', +Helvetica, +'PingFang SC', +'Segoe UI', +Roboto, +'Hiragino Sans GB', +'Arial', +'microsoft yahei ui', +'Microsoft YaHei', +SimSun, +sans-serif !default; +$fontFamilyMonospace: SFMono-Regular, +Menlo, +Monaco, +Consolas, +'Liberation Mono', +'Courier New', +monospace !default; $fontFamilyBase: $fontFamilySansSerif !default; $fontSizeBase: px2rem(14px) !default; // Assumes the browser default, typically `16px` @@ -81,13 +99,11 @@ $boxShadow: 0 0.5rem 1rem rgba($black, 0.15) !default; $boxShadowLg: 0 1rem 3rem rgba($black, 0.175) !default; // 窗口适配 -$breakpoints: ( - xs: 0, +$breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, - xl: 1200px -) !default; + xl: 1200px) !default; // 段落间距 $paragraph-marginBottom: 1rem !default; @@ -166,7 +182,8 @@ $Layout-brand-color: lighten($Layout-brandBar-color, 25%) !default; $Layout-header-height: px2rem(50px) !default; $Layout-headerBar-borderBottom: none !default; $Layout-header-bg: $white !default; -$Layout-header-boxShadow: 0 px2rem(2px) px2rem(2px) rgba(0, 0, 0, 0.05), 0 1px 0 rgba(0, 0, 0, 0.05) !default; +$Layout-header-boxShadow: 0 px2rem(2px) px2rem(2px) rgba(0, 0, 0, 0.05), +0 1px 0 rgba(0, 0, 0, 0.05) !default; $Layout-nav-height: px2rem(40px) !default; $Layout-nav-lgHeight: px2rem(50px) !default; $Layout-nav--folded-height: px2rem(50px) !default; @@ -376,7 +393,8 @@ $Tabs--line-borderWidth: px2rem(2px) !default; $Tabs--line-linkPadding: 10px 0 !default; $Tabs--line-linkMargin: 0 40px 0 0 !default; $Tabs--line-content-bg: transparent !default; -$Tabs--line-content-padding: 20px 0 !default;; +$Tabs--line-content-padding: 20px 0 !default; +; $Tabs--card-padding: px2rem(6px) 0 0 px2rem(10px); $Tabs--card-bg: darken($body-bg, 5%) !default; @@ -737,7 +755,8 @@ $Button--lg-lineHeight: 24 / 20 !default; $Button--lg-paddingX: px2rem(16px) !default; $Button--lg-paddingY: ($Button--lg-height - $Button-borderWidth * 2 - $Button--lg-lineHeight * $Button--lg-fontSize)/2 !default; -$Button-boxShadow: inset 0 1px 0 rgba($white, 0.15), 0 1px 1px rgba($black, 0.075) !default; +$Button-boxShadow: inset 0 1px 0 rgba($white, 0.15), +0 1px 1px rgba($black, 0.075) !default; $Button-onFocus-boxShadow: none !default; $Button-onActive-boxShadow: inset 0 3px 5px rgba($black, 0.125) !default; $Button-onDisabled-opacity: 0.65 !default; @@ -749,8 +768,10 @@ $Button-borderRadius: $borderRadius !default; $Button--lg-borderRadius: $borderRadius !default; $Button--sm-borderRadius: $borderRadius !default; -$Button-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, - box-shadow 0.15s ease-in-out !default; +$Button-transition: color 0.15s ease-in-out, +background-color 0.15s ease-in-out, +border-color 0.15s ease-in-out, +box-shadow 0.15s ease-in-out !default; $Button--primary-bg: $primary !default; $Button--primary-border: $Button--primary-bg !default; @@ -908,8 +929,7 @@ $ColorPicker-height: $Form-input-height !default; $ColorPicker-lineHeight: $Form-input-lineHeight !default; $ColorPicker-fontSize: $Form-input-fontSize !default; $ColorPicker-paddingX: px2rem(12px) !default; -$ColorPicker-paddingY: ($ColorPicker-height - $ColorPicker-lineHeight * $ColorPicker-fontSize)/2 - - $ColorPicker-borderWidth !default; +$ColorPicker-paddingY: ($ColorPicker-height - $ColorPicker-lineHeight * $ColorPicker-fontSize)/2 - $ColorPicker-borderWidth !default; $ColorPicker-placeholderColor: $Form-input-placeholderColor !default; $ColorPicker-onFocused-borderColor: $Form-input-onFocused-borderColor !default; $DatePicker-onHover-borderColor: $Form-input-borderColor !default; @@ -1039,9 +1059,7 @@ $Combo-addBtn-borderRadius: $Button-borderRadius; $Combo-addBtn-height: px2rem(26px) !default; $Combo-addBtn-lineHeight: $Button--sm-lineHeight !default; $Combo-addBtn-paddingX: $Button--sm-paddingX !default; -$Combo-addBtn-paddingY: ( - $Combo-addBtn-height - $Button-borderWidth * 2 - $Combo-addBtn-lineHeight * $Combo-addBtn-fontSize - )/2 !default; +$Combo-addBtn-paddingY: ($Combo-addBtn-height - $Button-borderWidth * 2 - $Combo-addBtn-lineHeight * $Combo-addBtn-fontSize)/2 !default; $Combo--vertical-item-gap: px2rem(5px); $Combo--vertical-item-borderColor: $borderColor !default; @@ -1080,9 +1098,7 @@ $SubForm--addBtn-borderRadius: $Button-borderRadius; $SubForm--addBtn-height: $Button--sm-height !default; $SubForm--addBtn-lineHeight: $Button--sm-lineHeight !default; $SubForm--addBtn-paddingX: $Button--sm-paddingX !default; -$SubForm--addBtn-paddingY: ( - $SubForm--addBtn-height - $Button-borderWidth * 2 - $SubForm--addBtn-lineHeight * $SubForm--addBtn-fontSize - )/2 !default; +$SubForm--addBtn-paddingY: ($SubForm--addBtn-height - $Button-borderWidth * 2 - $SubForm--addBtn-lineHeight * $SubForm--addBtn-fontSize)/2 !default; // InputRange $InputRange-fontFamily: $fontFamilyBase !default; @@ -1095,11 +1111,11 @@ $InputRange-onDisabled-color: #cccccc !default; $InputRange-slider-bg: $InputRange-primaryColor !default; $InputRange-slider-border: px2rem(1px) solid $InputRange-primaryColor !default; $InputRange-slider-onFocus-borderRadius: $borderRadiusMd !default; -$InputRange-slider-onFocus-boxShadow: 0 0 0 $InputRange-slider-onFocus-borderRadius - transparentize($InputRange-slider-bg, 0.8) !default; +$InputRange-slider-onFocus-boxShadow: 0 0 0 $InputRange-slider-onFocus-borderRadius transparentize($InputRange-slider-bg, 0.8) !default; $InputRange-slider-height: px2rem(24px) !default; $InputRange-slider-width: px2rem(18px) !default; -$InputRange-slider-transition: transform 0.3s ease-out, box-shadow 0.3s ease-out !default; +$InputRange-slider-transition: transform 0.3s ease-out, +box-shadow 0.3s ease-out !default; $InputRange-sliderContainer-transition: left 0.3s ease-out !default; $InputRange-slider-onActive-transform: scale(1.3) !default; $InputRange-slider-onDisabled-bg: $InputRange-onDisabled-color !default; @@ -1115,10 +1131,26 @@ $InputRange-label--value-positionTop: px2rem(-40px) !default; // input-range-track $InputRange-track-bg: $InputRange-neutralLightColor !default; $InputRange-track-height: px2rem(12px) !default; -$InputRange-track-transition: left 0.3s ease-out, width 0.3s ease-out !default; +$InputRange-track-transition: left 0.3s ease-out, +width 0.3s ease-out !default; $InputRange-track-onActive-bg: $InputRange-primaryColor !default; $InputRange-track-onDisabled-bg: $InputRange-neutralLightColor !default; +// ImageControl +$ImageControl-addBtn-bg: $Button--default-bg !default; +$ImageControl-addBtn-border: $Button--default-border !default; +$ImageControl-addBtn-color: $Button--default-color !default; +$ImageControl-addBtn-onHover-bg: darken($ImageControl-addBtn-bg, 7.5%) !default; +$ImageControl-addBtn-onHover-border: darken($ImageControl-addBtn-border, 10%) !default; +$ImageControl-addBtn-onHover-color: $Button--default-color !default; +$ImageControl-addBtn-onActive-bg: darken($ImageControl-addBtn-bg, 10%) !default; +$ImageControl-addBtn-onActive-border: darken($ImageControl-addBtn-border, 12.5%) !default; +$ImageControl-addBtn-onActive-color: $ImageControl-addBtn-color !default; +$ImageControl-addBtn-onDisabled-bg: $Form-input-onDisabled-bg !default; +$ImageControl-addBtn-onDisabled-border: $Form-input-onDisabled-borderColor !default; +$ImageControl-addBtn-onDisabled-color: $text--muted-color !default; + + // Tag $TagControl-sugTip-color: $info !default; @@ -1138,10 +1170,7 @@ $TagControl-sugBtn-borderRadius: $Button-borderRadius !default; $TagControl-sugBtn-height: $Button--sm-height !default; $TagControl-sugBtn-lineHeight: $Button--sm-lineHeight !default; $TagControl-sugBtn-paddingX: $Button--sm-paddingX !default; -$TagControl-sugBtn-paddingY: ( - $TagControl-sugBtn-height - $Button-borderWidth * 2 - $TagControl-sugBtn-lineHeight * - $TagControl-sugBtn-fontSize - )/2 !default; +$TagControl-sugBtn-paddingY: ($TagControl-sugBtn-height - $Button-borderWidth * 2 - $TagControl-sugBtn-lineHeight * $TagControl-sugBtn-fontSize)/2 !default; // Wizard $Wizard-steps-bg: $gray100 !default; @@ -1312,4 +1341,4 @@ $Picker-iconColor: $gray600 !default; $Picker-onHover-iconColor: darken($Picker-iconColor, 10%) !default; $Picker-btn-vendor: 'FontAwesome' !default; $Picker-btn-fontSize: $Form-fontSize !default; -$Picker-btn-icon: '\f2d2' !default; +$Picker-btn-icon: '\f2d2' !default; \ No newline at end of file diff --git a/scss/components/form/_image.scss b/scss/components/form/_image.scss index 05cfd07d..f83b54d5 100644 --- a/scss/components/form/_image.scss +++ b/scss/components/form/_image.scss @@ -1,9 +1,179 @@ -// todo - .#{$ns}ImageControl { outline: none; + + &-addBtn { + margin: 0; + width: px2rem(120px); + height: px2rem(120px); + display: inline-flex; + justify-content: center; + align-items: center; + border: $borderWidth solid $borderColor; + cursor: pointer; + + @include button-variant($ImageControl-addBtn-bg, + $ImageControl-addBtn-border, + $ImageControl-addBtn-color, + $ImageControl-addBtn-onHover-bg, + $ImageControl-addBtn-onHover-border, + $ImageControl-addBtn-onHover-color, + $ImageControl-addBtn-onActive-bg, + $ImageControl-addBtn-onActive-border, + $ImageControl-addBtn-onActive-color); + + >svg { + width: px2rem(50px); + height: px2rem(50px); + top: 0; + } + + &.is-disabled { + pointer-events: none; + border: px2rem(1px) solid $ImageControl-addBtn-onDisabled-border; + background: $ImageControl-addBtn-onDisabled-bg; + color: $ImageControl-addBtn-onDisabled-color; + } + } + + &-item { + border: $borderWidth solid $borderColor; + vertical-align: top; + padding: px2rem(5px); + display: inline-block; + margin-right: px2rem(15px); + margin-right: px2rem(15px); + position: relative; + } + + &-itemImageWrap { + width: px2rem(108px); + height: px2rem(108px); + overflow: hidden; + position: relative; + + >img { + position: absolute; + left: 50%; + top: 50%; + height: 100%; + width: auto; + transform: translate(-50%, -50%); + } + } + + &-itemOverlay { + background: rgba(0, 0, 0, 0.6); + position: absolute; + width: px2rem(108px); + height: px2rem(108px); + display: none; + top: px2rem(5px); + left: px2rem(5px); + + justify-content: center; + align-items: center; + align-content: center; + flex-wrap: wrap; + color: #fff; + + >div { + width: 100%; + text-align: center; + margin-bottom: 5px; + } + + >a { + cursor: pointer; + color: #fff; + display: inline-block; + padding: 0 5px; + line-height: 1; + font-size: px2rem(16px); + } + } + + &-item:hover &-itemOverlay { + display: flex; + } + + &-itemClear { + position: absolute; + cursor: pointer; + + color: #999; + top: 5px; + right: 5px; + line-height: 1; + + >svg { + top: 0; + width: 10px; + height: 10px; + } + } + + &-itemInfo { + display: inline-flex; + width: 110px; + height: 110px; + justify-content: center; + align-items: center; + align-content: center; + flex-wrap: wrap; + + >p { + width: 100%; + text-align: center; + font-size: 12px; + margin-bottom: 5px; + } + } + + &-progress { + width: 70px; + height: 5px; + background: #ebebeb; + } + + &-progressValue { + height: 5px; + display: block; + background: $info; + min-width: 10%; + transition: ease-out width 0.3s; + } + + &-retryBtn { + margin: 0; + width: px2rem(108px); + height: px2rem(108px); + display: inline-flex; + cursor: pointer; + justify-content: center; + align-items: center; + align-content: center; + flex-wrap: wrap; + color: #666; + + &:hover { + color: #333; + text-decoration: none; + } + + + + >p { + width: 100%; + text-align: center; + color: $danger; + margin: 10px 0 0; + } + } } + +// todo + .drop-zone { border: $Form-input-borderWidth * 2 dashed $Form-input-borderColor; height: 70px; @@ -14,7 +184,7 @@ position: relative; cursor: pointer; - > div:not(.image-list) { + >div:not(.image-list) { display: table-cell; vertical-align: middle; } @@ -99,8 +269,8 @@ align-content: center; padding: 10px; - > a, - > button { + >a, + >button { flex: 1; outline: none; display: flex; @@ -181,9 +351,10 @@ } @media (min-width: 768px) { + .amis-image-control.form-contorl-inline, .form-group-inline .amis-image-control { display: inline-block; min-width: 280px; } -} +} \ No newline at end of file diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 6bea474c..9e73319f 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -33,6 +33,12 @@ import PlusIcon from '../icons/plus.svg'; import MinusIcon from '../icons/minus.svg'; // @ts-ignore import PencilIcon from '../icons/pencil.svg'; +// @ts-ignore +import ViewIcon from '../icons/view.svg'; +// @ts-ignore +import RemoveIcon from '../icons/remove.svg'; +// @ts-ignore +import RetryIcon from '../icons/retry.svg'; // 兼容原来的用法,后续不直接试用。 // @ts-ignore @@ -82,6 +88,9 @@ registerIcon('check', CheckIcon); registerIcon('plus', PlusIcon); registerIcon('minus', MinusIcon); registerIcon('pencil', PencilIcon); +registerIcon('view', ViewIcon); +registerIcon('remove', RemoveIcon); +registerIcon('retry', RetryIcon); export function Icon({ icon, diff --git a/src/icons/remove.svg b/src/icons/remove.svg new file mode 100644 index 00000000..d1e9d47c --- /dev/null +++ b/src/icons/remove.svg @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/src/icons/retry.svg b/src/icons/retry.svg new file mode 100644 index 00000000..3971a512 --- /dev/null +++ b/src/icons/retry.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/icons/view.svg b/src/icons/view.svg new file mode 100644 index 00000000..3d07b583 --- /dev/null +++ b/src/icons/view.svg @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/src/renderers/Form/Image.tsx b/src/renderers/Form/Image.tsx index 8d1f0bdd..40f40131 100644 --- a/src/renderers/Form/Image.tsx +++ b/src/renderers/Form/Image.tsx @@ -1,17 +1,22 @@ import React from 'react'; import {FormItem, FormControlProps} from './Item'; -import cx from 'classnames'; +// @require 'cropperjs/dist/cropper.css'; import Cropper from 'react-cropper'; import DropZone from 'react-dropzone'; import 'blueimp-canvastoblob'; -// @require 'cropperjs/dist/cropper.css'; -// jest 不能支持这种写法 -// import 'cropperjs/dist/cropper.css'; 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'; + +let id = 1; +function gennerateId() { + return id++; +} export interface ImageProps extends FormControlProps { placeholder?: string; @@ -67,8 +72,10 @@ export interface FileValue { } export interface FileX extends File { + id?: string | number; preview?: string; state?: 'init' | 'error' | 'pending' | 'uploading' | 'uploaded' | 'invalid'; + progress?: number; [propName: string]: any; } @@ -81,7 +88,7 @@ export default class ImageControl extends React.Component item.state === 'pending'); + const file = find(this.state.files, item => item.state === 'pending') as FileX; if (file) { this.current = file; @@ -309,44 +317,61 @@ export default class ImageControl extends React.Component - this.sendFile(file as FileX, (error, file, obj) => { - const files = this.state.files.concat(); - const idx = files.indexOf(file); + this.sendFile( + file as FileX, + (error, file, obj) => { + const files = this.state.files.concat(); + const idx = files.indexOf(file); - if (!~idx) { - return; - } - - let newFile: FileX | FileValue = file; - - if (error) { - newFile.state = file.state !== 'uploading' ? file.state : 'error'; - newFile.error = error; - - if (!this.props.multiple && newFile.state === 'invalid') { - files.splice(idx, 1); - this.current = null; - - return this.setState( - { - files: files, - error: error - }, - this.tick - ); + if (!~idx) { + return; } - } else { - newFile = obj as FileValue; + + let newFile: FileX | FileValue = file; + + if (error) { + newFile.state = file.state !== 'uploading' ? file.state : 'error'; + newFile.error = error; + + if (!this.props.multiple && newFile.state === 'invalid') { + files.splice(idx, 1); + this.current = null; + + return this.setState( + { + files: files, + error: error + }, + this.tick + ); + } + } else { + newFile = obj as FileValue; + } + files.splice(idx, 1, newFile); + this.current = null; + this.setState( + { + 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 + }); } - files.splice(idx, 1, newFile); - this.current = null; - this.setState( - { - files: files - }, - this.tick - ); - }) + ) ); } else { this.setState( @@ -437,7 +462,7 @@ export default class ImageControl extends React.Component) { - const event = e.nativeEvent; + const event = e.nativeEvent as any; const files: Array = []; const items = event.clipboardData.items; @@ -449,6 +474,7 @@ export default class ImageControl extends React.Component void) { + sendFile( + file: FileX, + cb: (error: null | string, file: FileX, obj?: FileValue) => void, + onProgress: (progress: number) => void + ) { const {limit} = this.props; if (!limit) { - return this._upload(file, cb); + return this._upload(file, cb, onProgress); } const image = new Image(); @@ -564,13 +595,17 @@ export default class ImageControl extends React.Component void) { + _upload( + file: Blob, + cb: (error: null | string, file: Blob, obj?: FileValue) => void, + onProgress: (progress: number) => void + ) { let compressOptions = this.state.compressOptions; if (this.props.showCompressOptions) { @@ -581,7 +616,7 @@ export default class ImageControl extends React.Component { if (ret.status) { throw new Error(ret.msg || '上传失败, 请重试'); @@ -598,29 +633,35 @@ export default class ImageControl extends React.Component cb(error.message || '上传失败,请重试', file)); } - _send(file: Blob, reciever: string, params: object): Promise { + _send(file: Blob, reciever: string, params: object, onProgress: (progress: number) => void): Promise { const fd = new FormData(); const data = this.props.data; - reciever = filter(reciever, data); + const api = buildApi(reciever, createObject(data, params), { + method: 'post' + }); const fileField = this.props.fileField || 'file'; fd.append(fileField, file, (file as File).name); - const idx = reciever.indexOf('?'); + const idx = api.url.indexOf('?'); if (~idx && params) { params = { ...qs.parse(reciever.substring(idx + 1)), ...params }; - reciever = reciever.substring(0, idx) + '?' + qs.stringify(params); + api.url = api.url.substring(0, idx) + '?' + qsstringify(params); } else if (params) { - reciever += '?' + qs.stringify(params); + api.url += '?' + qsstringify(params); } - // params && Object.keys(params).forEach(key => { - // const value = (params as any)[key]; - // fd.append(key, value); - // }); + if (api.data) { + qsstringify(api.data) + .split('&') + .forEach(item => { + let parts = item.split('='); + fd.append(parts[0], parts[1]); + }); + } const env = this.props.env; @@ -628,8 +669,9 @@ export default class ImageControl extends React.Component onProgress(event.loaded / event.total) }); } @@ -751,7 +793,7 @@ export default class ImageControl extends React.Component file.state == 'pending'); return ( -
+
{cropFile ? (
@@ -782,11 +824,11 @@ export default class ImageControl extends React.Component - {files && files.length ? ( -
( +
+ {file.error ? ( + + +

重新上传

+
+ ) : file.state === 'uploading' ? ( + <> + + + +
+

文件上传中

+
+ +
+
+ + ) : ( + <> +
+ {file.name} +
+ +
+ {file.info ? ( + [ +
+ {file.info.width} x {file.info.height} +
, + file.info.len ? ( +
+ {ImageControl.formatFileSize(file.info.len)} +
+ ) : null + ] + ) : ( +
...
+ )} + + {!disabled ? ( + + + + ) : null} + {!!crop && !disabled ? ( + + + + ) : null} + {!disabled ? ( + + + + ) : null} +
+ + )} +
+ )) + : null} + + {(multiple && (!maxLength || files.length < maxLength)) || (!multiple && !files.length) ? ( +
- ) : ( -
- {error || placeholder} - -
- )} + + + ) : null} )}