Browse Source

放大镜类型实现

RaoMeng 6 years ago
parent
commit
d1b491e21f

+ 309 - 129
uas-office-web/wxuasapproval/src/components/approvalAdd/FormInput.jsx

@@ -7,10 +7,19 @@ import React, { Component } from 'react'
 import './formCommon.css'
 import ApprovalBean from '../../model/ApprovalBean'
 import BillModel from '../../model/BillModel'
-import { TextareaItem, InputItem, DatePicker, List, Modal } from 'antd-mobile'
-import { isObjEmpty } from '../../utils/common'
+import {
+  TextareaItem,
+  InputItem,
+  DatePicker,
+  List,
+  Modal,
+  Toast,
+} from 'antd-mobile'
+import { isObjEmpty, isObjNull } from '../../utils/common'
 import moment from 'moment'
-import { fetchPost } from '../../utils/fetchRequest'
+import { Upload, Button, Icon } from 'antd'
+
+let uploadFail = false
 
 export default class FormInput extends Component {
 
@@ -45,136 +54,306 @@ export default class FormInput extends Component {
     let valueItem
 
     let type = billModel.type
-    if (type === 'N') {
-      valueItem =
-        <InputItem className='form-input-value' clear
-                   placeholder={this.isSelect(billModel)
-                     ? '请选择'
-                     : ((billModel.readOnly === 'T' ||
-                       billModel.editable === 'F')
-                       ? ''
-                       : '请输入')}
-                   editable={(billModel.readOnly === 'T' ||
-                     billModel.editable === 'F') ? false : (this.isSelect(
-                     billModel)
-                     ? false
-                     : true)}
-          // extra={(billModel.readOnly === 'T' &&
-          //   billModel.editable === 'F' && this.isSelect(billModel))
-          //   ? '>'
-          //   : ''}
-                   onChange={this.onTextChange}
-                   onClick={this.onInputClick}
-                   type={'number'}
-                   value={billModel.getValue()}
-        />
-    } else if (type === 'DT' || type === 'D' || type === 'T') {
-      valueItem = <DatePicker
-        style={{ width: '100%' }}
-        locale={{
-          okText: '确定',
-          dismissText: '取消',
-        }}
-        mode={type === 'DT' ? 'datetime' : 'date'}
-        extra={(billModel.readOnly === 'T' ||
-          billModel.editable === 'F') ? '' : type === 'DT' ? '选择时间' : '选择日期'}
-        disabled={(billModel.readOnly === 'T' ||
-          billModel.editable === 'F') ? true : false}
-        value={!isObjEmpty(billModel.getValue())
-          ? new Date(billModel.getValue().replace(/\-/g, '/'))
-          : ''}
-        onChange={this.onDateChange}
-      >
-        <DatePickerCustom>
-          <div className='form-input-caption'>{billModel.caption}</div>
-          <div className={billModel.allowBlank == 'F'
-            ? 'form-input-fill'
-            : 'visibleHidden'}>*
-          </div>
-          <div style={{ flex: 1 }}></div>
-        </DatePickerCustom>
-      </DatePicker>
-    } else if (type === 'HTML') {
-      valueItem =
-        <div className='form-input-value'
-             style={{ minHeight: '32px' }}
-             dangerouslySetInnerHTML={{ __html: billModel.getValue() }}></div>
-    } else {
-      valueItem = <TextareaItem className='form-input-value' autoHeight
-                                placeholder={this.isSelect(billModel)
-                                  ? '请选择'
-                                  : ((billModel.readOnly === 'T' ||
-                                    billModel.editable === 'F')
-                                    ? ''
-                                    : '请输入')}
-                                clear={true}
-                                editable={(billModel.readOnly === 'T' ||
-                                  billModel.editable === 'F')
-                                  ? false
-                                  : (this.isSelect(billModel)
-                                    ? false
-                                    : true)}
-                                disabled={false}
-        // extra={(billModel.readOnly === 'T' &&
-        //   billModel.editable === 'F' && this.isSelect(billModel))
-        //   ? '>'
-        //   : ''}
-                                onChange={this.onTextChange}
-                                onClick={this.onInputClick}
-                                value={billModel.getValue()}
-      />
+    switch (type) {
+      case 'N':
+        valueItem =
+          this.getNumCom(billModel)
+        break
+      case 'DT':
+      case 'D':
+      case 'T':
+        valueItem =
+          this.getDateCom(type, billModel)
+        break
+      case 'HTML':
+        valueItem =
+          this.getHtmlcom(billModel)
+        break
+      // case 'MF':
+      //   valueItem =
+      //     this.getEnclosureCom(billModel)
+      //   break
+      default:
+        valueItem =
+          this.getTextCom(billModel)
+        break
     }
     return (
       (type === 'DT' || type === 'D' || type === 'T') ? <div>
-        {valueItem}
-      </div> : (type === 'MS') ? (<div className={'form-textarea-layout'}>
-        <div className='form-common-layout'
-             style={{ borderBottom: 'none' }}>
-          <div style={{
-            minWidth: '90px',
-            fontSize: '14px',
-            color: '#333333',
-          }}>{billModel.caption}</div>
-          <div className={billModel.allowBlank == 'F'
-            ? 'form-input-fill'
-            : 'visibleHidden'}>*
-          </div>
-        </div>
-        {valueItem}
-      </div>) : (
-        <div className='form-common-layout'
-             style={{ minHeight: '32px' }}>
-          <div className='form-input-caption'>{billModel.caption}</div>
-          <div className={billModel.allowBlank == 'F'
-            ? 'form-input-fill'
-            : 'visibleHidden'}>*
-          </div>
           {valueItem}
-          {this.isSelect(billModel) &&
-          <Modal visible={modalOpen}
-                 animationType={'slide-up'}
-                 onClose={() => {
-                   this.setState({
-                     modalOpen: false,
-                   })
-                 }}
-                 title={billModel.caption}
-                 popup
-          >
-            <List className='form-common-modal-root'>
-              {modalList && (
-                modalList.map((modalObj, index) => (
-                  <List.Item key={index}
-                             onClick={this.onModalSelect.bind(this,
-                               index)}>{modalObj.value}</List.Item>
-                ))
-              )}
-            </List>
-          </Modal>}
-        </div>)
+        </div> :
+        (type === 'MS'
+          // || type === 'MF'
+        ) ? (this.renderTwoLines(
+          billModel,
+          valueItem)) :
+          (this.renderNormal(billModel, valueItem, modalOpen, modalList))
     )
   }
 
+  /**
+   * 加载普通类型
+   * @param billModel
+   * @param valueItem
+   * @param modalOpen
+   * @param modalList
+   * @returns {*}
+   */
+  renderNormal (billModel, valueItem, modalOpen, modalList) {
+    return <div className='form-common-layout'
+                style={{ minHeight: '32px' }}>
+      <div className='form-input-caption'>{billModel.caption}</div>
+      <div className={billModel.allowBlank == 'F'
+        ? 'form-input-fill'
+        : 'visibleHidden'}>*
+      </div>
+      {valueItem}
+      {this.isSelect(billModel) &&
+      <Modal visible={modalOpen}
+             animationType={'slide-up'}
+             onClose={() => {
+               this.setState({
+                 modalOpen: false,
+               })
+             }}
+             title={billModel.caption}
+             popup
+      >
+        <List className='form-common-modal-root'>
+          {modalList && (
+            modalList.map((modalObj, index) => (
+              <List.Item key={index}
+                         onClick={this.onModalSelect.bind(this,
+                           index)}>{modalObj.value}</List.Item>
+            ))
+          )}
+        </List>
+      </Modal>}
+    </div>
+  }
+
+  /**
+   * 加载标题和内容单独分行
+   * @param billModel
+   * @param valueItem
+   * @returns {*}
+   */
+  renderTwoLines (billModel, valueItem) {
+    return <div className={'form-textarea-layout'}>
+      <div className='form-common-layout'
+           style={{ borderBottom: 'none' }}>
+        <div style={{
+          minWidth: '90px',
+          fontSize: '14px',
+          color: '#333333',
+        }}>{billModel.caption}</div>
+        <div className={billModel.allowBlank == 'F'
+          ? 'form-input-fill'
+          : 'visibleHidden'}>*
+        </div>
+      </div>
+      {valueItem}
+    </div>
+  }
+
+  /**
+   * 文本输入类型
+   * @param billModel
+   * @returns {*}
+   */
+  getTextCom (billModel) {
+    return <TextareaItem className='form-input-value' autoHeight
+                         placeholder={this.isSelect(billModel)
+                           ? '请选择'
+                           : ((billModel.readOnly === 'T' ||
+                             billModel.editable === 'F')
+                             ? ''
+                             : '请输入')}
+                         clear={true}
+                         editable={(billModel.readOnly === 'T' ||
+                           billModel.editable === 'F')
+                           ? false
+                           : (this.isSelect(billModel)
+                             ? false
+                             : true)}
+                         disabled={false}
+      // extra={(billModel.readOnly === 'T' &&
+      //   billModel.editable === 'F' && this.isSelect(billModel))
+      //   ? '>'
+      //   : ''}
+                         onChange={this.onTextChange}
+                         onClick={this.onInputClick}
+                         value={billModel.getValue()}
+    />
+  }
+
+  /**
+   * html类型字符串
+   * @param billModel
+   * @returns {*}
+   */
+  getHtmlcom (billModel) {
+    return <div className='form-input-value'
+                style={{ minHeight: '32px' }}
+                dangerouslySetInnerHTML={{ __html: billModel.getValue() }}></div>
+  }
+
+  /**
+   * 日期类型
+   * @param type
+   * @param billModel
+   * @returns {*}
+   */
+  getDateCom (type, billModel) {
+    return <DatePicker
+      style={{ width: '100%' }}
+      locale={{
+        okText: '确定',
+        dismissText: '取消',
+      }}
+      mode={type === 'DT' ? 'datetime' : 'date'}
+      extra={(billModel.readOnly === 'T' ||
+        billModel.editable === 'F') ? '' : type === 'DT' ? '选择时间' : '选择日期'}
+      disabled={(billModel.readOnly === 'T' ||
+        billModel.editable === 'F') ? true : false}
+      value={!isObjEmpty(billModel.getValue())
+        ? new Date(billModel.getValue().replace(/\-/g, '/'))
+        : ''}
+      onChange={this.onDateChange}
+    >
+      <DatePickerCustom>
+        <div className='form-input-caption'>{billModel.caption}</div>
+        <div className={billModel.allowBlank == 'F'
+          ? 'form-input-fill'
+          : 'visibleHidden'}>*
+        </div>
+        <div style={{ flex: 1 }}></div>
+      </DatePickerCustom>
+    </DatePicker>
+  }
+
+  /**
+   * 数字类型输入框
+   * @returns {*}
+   */
+  getNumCom (billModel) {
+    return <InputItem className='form-input-value' clear
+                      placeholder={this.isSelect(billModel)
+                        ? '请选择'
+                        : ((billModel.readOnly === 'T' ||
+                          billModel.editable === 'F')
+                          ? ''
+                          : '请输入')}
+                      editable={(billModel.readOnly === 'T' ||
+                        billModel.editable === 'F') ? false : (this.isSelect(
+                        billModel)
+                        ? false
+                        : true)}
+      // extra={(billModel.readOnly === 'T' &&
+      //   billModel.editable === 'F' && this.isSelect(billModel))
+      //   ? '>'
+      //   : ''}
+                      onChange={this.onTextChange}
+                      onClick={this.onInputClick}
+                      type={'digit'}
+                      value={billModel.getValue()}
+    />
+  }
+
+  /**
+   * 附件类型
+   * @param billModel
+   */
+  getEnclosureCom = (billModel) => {
+    return <div style={{
+      margin: '4px 10px 10px',
+    }}>
+      <Upload
+        action={this.props.baseUrl + '/mobile/uploadAttachs.action'}
+        listType={'picture'}
+        multiple={true}
+        fileList={billModel.fileList ? billModel.fileList : []}
+        showUploadList={true}
+        // withCredentials={false}
+        beforeUpload={this.beforeUpload}
+        onChange={this.handleChange}
+        // onPreview={this.handlePreview}
+        // onRemove={this.handleRemove}
+        // onDownload={() => {}}
+        data={''}
+        method={'post'}
+        className={'upload-list-inline'}
+      >
+        <div style={{ display: 'flex', alignItems: 'center' }}>
+          <div
+            className={'uploadBtn'}>
+            <Icon type="upload"
+                  style={{ color: 'white' }}/>
+            <span style={{ fontSize: '12px', marginLeft: '6px' }}>选择文件</span>
+          </div>
+          <span className='promptText'>(不能超过100MB)</span>
+        </div>
+      </Upload>
+    </div>
+  }
+
+  handleChange = ({ fileList }) => {
+    console.log('filelist', fileList)
+    if (uploadFail) {
+      return
+    }
+    const { count } = this.props
+    const { billModel } = this.state
+    if (isObjNull(count) || fileList.length <= count) {
+      /*if (fileList) {
+        fileList.forEach((value, index) => {
+          value.url = (value.response && value.response.data)
+            ? value.response.data.accessPath
+            : value.url
+          value.picUrl = value.url
+          value.relativeUrl = (value.response && value.response.data)
+            ? value.response.data.fullPath
+            : value.relativeUrl
+        })
+      }*/
+      billModel.fileList = fileList
+      console.log('raomeng', billModel)
+      this.setState({ billModel })
+      this.props.handleChange && this.props.handleChange(fileList)
+    }
+  }
+
+  handleRemove = (file) => {
+    if (this.props.handleRemove) {
+      return this.props.handleRemove(file)
+    }
+  }
+
+  beforeUpload = (file, fileList) => {
+    uploadFail = false
+    if (file.size && file.size > 100 * 1024 * 1024) {
+      uploadFail = false
+      Toast.fail('文件大小不能超过100M')
+      return false
+    }
+    const { count } = this.props
+    const { billModel } = this.state
+    if (count && billModel.fileList &&
+      ((billModel.fileList.length + fileList.length) > count)) {
+      Toast.fail(`上传失败,附件数量不能超过${count}`)
+      uploadFail = true
+      return false
+    } else {
+      return this.props.beforeUpload
+        ? this.props.beforeUpload(file, fileList)
+        : true
+    }
+  }
+
+  handlePreview = (file) => {
+
+  }
+
   onTextChange = value => {
     const { billModel } = this.state
     billModel.value = value
@@ -205,9 +384,10 @@ export default class FormInput extends Component {
 
   onModalSelect = index => {
     const { modalList } = this.state
+    console.log(modalList)
     if (!isObjEmpty(modalList) && modalList[index] && this.props.onTextChange) {
       this.props.onTextChange(this.props.groupIndex, this.props.childIndex,
-        modalList[index].value, modalList[index].display)
+        modalList[index].value + '', modalList[index].display + '')
     }
     this.setState({
       modalList: [],
@@ -246,8 +426,8 @@ export default class FormInput extends Component {
       case 'MF':
       case 'SF':
       case 'DF':
-        return false
-      // return true
+        // return false
+        return true
     }
     return false
   }

+ 23 - 0
uas-office-web/wxuasapproval/src/components/approvalAdd/formCommon.css

@@ -42,6 +42,7 @@
 
 .form-common-layout .am-list-line {
     padding-right: 8px;
+    border-bottom: none !important;
 }
 
 .form-common-layout .am-list-line::after {
@@ -100,6 +101,10 @@
     display: none;
 }
 
+.form-textarea-layout i.anticon.anticon-download {
+    display: none !important;
+}
+
 /************************************************************************************/
 
 .form-title-text {
@@ -151,3 +156,21 @@
     max-height: 76vh;
     overflow: auto;
 }
+
+.upload-list-inline .ant-upload-list-item {
+    /*附件列表格式*/
+}
+
+.uploadBtn {
+    background: #4197FC;
+    border-radius: 4px;
+    padding: 4px 8px;
+    color: white;
+}
+
+.uploadBtn-disable {
+    background: #e4e4e4;
+    border-radius: 3px;
+    padding: 4px;
+    color: gray;
+}

+ 530 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/ImageContainer.jsx

@@ -0,0 +1,530 @@
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+
+import raf from 'raf';
+import tween from './tween.js';
+import Loading from './Loading';
+import {isObjNull} from "../../utils/common";
+
+/**
+ *
+ * @param {number} value
+ * @param {number} min
+ * @param {number} max
+ */
+function setScope(value, min, max) {
+    if (value < min) {
+        return min;
+    }
+    if (value > max) {
+        return max;
+    }
+    return value;
+}
+
+
+function getDistanceBetweenTouches(e) {
+    if (e.touches.length < 2) return 1;
+    const x1 = e.touches[0].clientX;
+    const y1 = e.touches[0].clientY;
+    const x2 = e.touches[1].clientX;
+    const y2 = e.touches[1].clientY;
+    const distance = Math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2));
+    return distance;
+}
+
+// const msPerFrame = 1000 / 60;
+const maxAnimateTime = 1000;
+const minTapMoveValue = 5;
+const maxTapTimeValue = 300;
+
+/**
+ * 图片默认展示模式:宽度等于屏幕宽度,高度等比缩放;水平居中,垂直居中或者居顶(当高度大于屏幕高度时)
+ * 图片实际尺寸: actualWith, actualHeight
+ * 图片初始尺寸: originWidth, originHeight
+ * 坐标位置:left, top
+ * 放大倍数:zoom
+ * 最大放大倍数:maxZoomNum
+ * 坐标关系:-(maxZoomNum - 1) * originWidth / 2 < left < 0
+ *         -(maxZoomNum - 1) * originHeight / 2 < top < 0
+ * 尺寸关系:width = zoom * originWidth
+ *         heigth = zoom * originHeight
+ *
+ * 放大点位置关系:
+ * 初始点位置:oldPointLeft, oldPointTop
+ * 放大后位置:newPointLeft, newPointTop
+ * 对应关系: newPointLeft = zoom * oldPointLeft
+ *          newPointTop = zoom * oldPointTop
+ *
+ * 坐标位置:-1*left = -1*startLeft + (newPointLeft - oldPointLeft) =-1*startLeft (zoom - 1) * oldPointLeft
+ *         -1*top = -1*startTop + (newPointTop - oldPointTop) =-1*startLeft (zoom - 1) * oldPointTop
+ * =>
+ * left = startLeft + (1 - zoom) * oldPointLeft
+ * top = startTop + (1 - zoom) * oldPointTop
+ */
+
+class ImageContainer extends PureComponent {
+    static propTypes = {
+        maxZoomNum: PropTypes.number.isRequired,
+        handleStart: PropTypes.func.isRequired,
+        handleMove: PropTypes.func.isRequired,
+        handleEnd: PropTypes.func.isRequired,
+    }
+
+    static contextTypes = {
+        onClose: PropTypes.func,
+    };
+
+    constructor() {
+        super();
+        this.actualHeight = 0; // 图片实际高度
+        this.actualWith = 0; // 图片实际宽度
+
+        this.originHeight = 0; // 图片默认展示模式下高度
+        this.originWidth = 0; // 图片默认展示模式下宽度
+        this.originScale = 1; // 图片初始缩放比例
+
+        this.startLeft = 0; // 开始触摸操作时的 left 值
+        this.startTop = 0; // 开始触摸操作时的 top 值
+        this.startScale = 1; // 开始缩放操作时的 scale 值
+
+        this.onTouchStartTime = 0; // 单指触摸开始时间
+
+        this.isTwoFingerMode = false; // 是否为双指模式
+        this.oldPointLeft = 0;// 计算手指中间点在图片上的位置(坐标值)
+        this.oldPointTop = 0;// 计算手指中间点在图片上的位置(坐标值)
+        this._touchZoomDistanceStart = 0; // 用于记录双指距离
+        this.haveCallMoveFn = false;
+
+
+        this.diffX = 0;// 记录最后 move 事件 移动距离
+        this.diffY = 0;// 记录最后 move 事件 移动距离
+
+        this.animationID = 0;
+        this.animateStartTime = 0;
+        this.animateStartValue = {
+            x: 0,
+            y: 0,
+        };
+        this.animateFinalValue = {
+            x: 0,
+            y: 0,
+        };
+    }
+
+    state = {
+        width: 0,
+        height: 0,
+        scale: 1,
+        left: 0,
+        top: 0,
+        isLoaded: false,
+    }
+
+    componentWillMount() {
+        this.loadImg(this.props.src);
+    }
+
+    componentWillUnmount() {
+        this.unloadImg();
+        if (this.animationID) {
+            raf.cancel(this.animationID);
+        }
+    }
+
+    onLoad = () => {
+        if (isObjNull(this.img)) {
+            this.img = new Image();
+        }
+        this.actualWith = this.img.width;
+        this.actualHeight = this.img.height;
+
+        const {
+            screenHeight,
+            screenWidth,
+        } = this.props;
+
+        const left = 0;
+        let top = 0;
+
+        this.originWidth = screenWidth;
+        this.originHeight = (this.actualHeight / this.actualWith) * screenWidth;
+        this.originScale = 1;
+
+        if (this.actualHeight / this.actualWith < screenHeight / screenWidth) {
+            top = parseInt((screenHeight - this.originHeight) / 2, 10);
+        }
+        this.originTop = top;
+
+        this.setState({
+            width: this.originWidth,
+            height: this.originHeight,
+            scale: 1,
+            left,
+            top,
+            isLoaded: true,
+        });
+    }
+
+    onError = () => {
+        this.setState({
+            isLoaded: true,
+        });
+    }
+
+    loadImg = (url) => {
+        this.img = new Image();
+        this.img.src = url;
+        this.img.onload = this.onLoad;
+        this.img.onerror = this.onError;
+
+        this.setState({
+            isLoaded: false,
+        });
+    }
+
+    unloadImg = () => {
+        delete this.img.onerror;
+        delete this.img.onload;
+        delete this.img.src;
+        delete this.img;
+    }
+
+    handleTouchStart = (event) => {
+        event.preventDefault();
+        if (this.animationID) {
+            raf.cancel(this.animationID);
+        }
+        switch (event.touches.length) {
+            case 1: {
+                const targetEvent = event.touches[0];
+                this.startX = targetEvent.clientX;
+                this.startY = targetEvent.clientY;
+                this.diffX = 0;
+                this.diffY = 0;
+
+                this.startLeft = this.state.left;
+                this.startTop = this.state.top;
+
+                this.onTouchStartTime = (new Date()).getTime();
+                this.haveCallMoveFn = false;
+                break;
+            }
+            case 2: { // 两个手指
+                // 设置手双指模式
+                this.isTwoFingerMode = true;
+
+                // 计算两个手指中间点屏幕上的坐标
+                const middlePointClientLeft = Math.abs(Math.round((event.touches[0].clientX + event.touches[1].clientX) / 2));
+                const middlePointClientTop = Math.abs(Math.round((event.touches[0].clientY + event.touches[1].clientY) / 2));
+
+                // 保存图片初始位置和尺寸
+                this.startLeft = this.state.left;
+                this.startTop = this.state.top;
+                this.startScale = this.state.scale;
+
+                // 计算手指中间点在图片上的位置(坐标值)
+                this.oldPointLeft = middlePointClientLeft - this.startLeft;
+                this.oldPointTop = middlePointClientTop - this.startTop;
+
+                this._touchZoomDistanceStart = getDistanceBetweenTouches(event);
+                break;
+            }
+            default:
+                break;
+        }
+    }
+
+    handleTouchMove = (event) => {
+        event.preventDefault();
+
+        switch (event.touches.length) {
+            case 1: {
+                const targetEvent = event.touches[0];
+                const diffX = targetEvent.clientX - this.startX;
+                const diffY = targetEvent.clientY - this.startY;
+
+                this.diffX = diffX;
+                this.diffY = diffY;
+                // 判断是否为点击
+                if (Math.abs(diffX) < minTapMoveValue && Math.abs(diffY) < minTapMoveValue) {
+                    return;
+                }
+
+                const {scale, left} = this.state;
+                const width = scale * this.originWidth;
+                if (Math.abs(diffX) > Math.abs(diffY)) {
+                    // 水平移动
+                    if (this.state.scale === this.originScale && Math.abs(diffX) > minTapMoveValue) {
+                        this.haveCallMoveFn = true;
+                        this.callHandleMove(diffX);
+                        return;
+                    }
+
+                    if (diffX < 0 && this.startLeft <= this.originWidth - width) {
+                        this.haveCallMoveFn = true;
+                        this.callHandleMove(diffX);
+                        return;
+                    }
+
+                    if (diffX > 0 && this.startLeft >= 0) {
+                        this.haveCallMoveFn = true;
+                        this.callHandleMove(diffX);
+                        return;
+                    }
+                }
+
+                const {screenHeight} = this.props;
+                const height = scale * this.originHeight;
+                let newTop = (screenHeight - height) / 2;
+                const newLeft = this.startLeft + diffX;
+
+                if (height > screenHeight || this.state.scale === this.originScale) {
+                    newTop = this.startTop + diffY;
+                }
+                this.setState({
+                    left: newLeft,
+                    top: newTop,
+                });
+
+                break;
+            }
+            case 2: { // 两个手指
+                this._touchZoomDistanceEnd = getDistanceBetweenTouches(event);
+
+                const zoom = Math.sqrt(this._touchZoomDistanceEnd / this._touchZoomDistanceStart);
+                const scale = zoom * this.startScale;
+
+                this.setState(() => {
+                    const left = this.startLeft + ((1 - zoom) * this.oldPointLeft);
+                    const top = this.startTop + ((1 - zoom) * this.oldPointTop);
+
+                    return {
+                        left,
+                        top,
+                        scale,
+                    };
+                });
+                break;
+            }
+            default:
+                break;
+        }
+    }
+
+    handleTouchEnd = (event) => {
+        event.preventDefault();
+
+        if (this.isTwoFingerMode) { // 双指操作结束
+            const touchLen = event.touches.length;
+            this.isTwoFingerMode = false;
+
+            if (touchLen === 1) {
+                const targetEvent = event.touches[0];
+                this.startX = targetEvent.clientX;
+                this.startY = targetEvent.clientY;
+                this.diffX = 0;
+                this.diffY = 0;
+            }
+
+            this.setState((prevState, props) => {
+                const scale = setScope(prevState.scale, 1, props.maxZoomNum);
+                const width = scale * this.originWidth;
+                const height = scale * this.originHeight;
+                const zoom = scale / this.startScale;
+                const left = setScope(this.startLeft + ((1 - zoom) * this.oldPointLeft), this.originWidth - width, 0);
+
+                let top;
+                if (height > props.screenHeight) {
+                    top = setScope(this.startTop + ((1 - zoom) * this.oldPointTop), props.screenHeight - height, 0);
+                } else {
+                    top = (props.screenHeight - height) / 2;
+                }
+
+                if (touchLen === 1) {
+                    this.startLeft = left;
+                    this.startTop = top;
+                    this.startScale = scale;
+                }
+
+                return {
+                    left,
+                    top,
+                    scale,
+                };
+            });
+        } else { // 单指结束(ontouchend)
+            const diffTime = (new Date()).getTime() - this.onTouchStartTime;
+            const {diffX, diffY} = this;
+
+            // 判断为点击则关闭图片浏览组件
+            if (diffTime < maxTapTimeValue && Math.abs(diffX) < minTapMoveValue && Math.abs(diffY) < minTapMoveValue) {
+                this.context.onClose();
+                return;
+            }
+
+            // 水平移动
+            if (this.haveCallMoveFn) {
+                const isChangeImage = this.callHandleEnd(diffY < 30);
+                if (isChangeImage) { // 如果切换图片则重置当前图片状态
+                    setTimeout(() => {
+                        this.setState({
+                            scale: this.originScale,
+                            left: 0,
+                            top: this.originTop,
+                        });
+                    }, maxAnimateTime / 3);
+                    return;
+                }
+            }
+            // TODO 下拉移动距离超过屏幕高度的 1/3 则关闭
+            // console.info(Math.abs(diffY) > (this.props.screenHeight / 2), this.startTop, this.originTop);
+            // if (Math.abs(diffX) < Math.abs(diffY) && Math.abs(diffY) > (this.props.screenHeight / 3) && this.startTop === this.originTop) {
+            //   this.context.onClose();
+            //   return;
+            // }
+
+            let x;
+            let y;
+            const {scale} = this.state;
+            // const width = scale * this.originWidth;
+            const height = scale * this.originHeight;
+
+            // 使用相同速度算法
+            x = ((diffX * maxAnimateTime) / diffTime) + this.startLeft;
+            y = ((diffY * maxAnimateTime) / diffTime) + this.startTop;
+
+            if (this.state.scale === this.originScale) {
+                x = 0;
+                if (height > this.props.screenHeight) {
+                    y = setScope(y, this.props.screenHeight - height, 0);
+                } else {
+                    y = this.originTop;
+                }
+            }
+
+            // x = setScope(x, this.originWidth - width, 0);
+
+            // if (height > this.props.screenHeight) {
+            // y = setScope(y, this.props.screenHeight - height, 0);
+            // } else {
+            //   y = this.state.top;
+            // }
+
+            this.animateStartValue = {
+                x: this.state.left,
+                y: this.state.top,
+            };
+            this.animateFinalValue = {
+                x,
+                y,
+            };
+            this.animateStartTime = Date.now();
+            this.startAnimate();
+        }
+    }
+
+    startAnimate = () => {
+        this.animationID = raf(() => {
+            // calculate current time
+            const curTime = Date.now() - this.animateStartTime;
+            let left;
+            let top;
+
+            // animate complete
+            if (curTime > maxAnimateTime) {
+                this.setState((prevState, props) => {
+                    const width = prevState.scale * this.originWidth;
+                    const height = prevState.scale * this.originHeight;
+                    left = setScope(prevState.left, this.originWidth - width, 0);
+
+                    if (height > props.screenHeight) {
+                        top = setScope(prevState.top, props.screenHeight - height, 0);
+                    } else {
+                        top = (props.screenHeight - height) / 2;
+                    }
+                    return {
+                        left,
+                        top,
+                    };
+                });
+            } else {
+                left = tween.easeOutQuart(curTime, this.animateStartValue.x, this.animateFinalValue.x, maxAnimateTime);
+                top = tween.easeOutQuart(curTime, this.animateStartValue.y, this.animateFinalValue.y, maxAnimateTime);
+
+                this.setState({
+                    left,
+                    top,
+                });
+                this.startAnimate();
+            }
+        });
+    }
+
+    callHandleMove = (diffX) => {
+        if (!this.isCalledHandleStart) {
+            this.isCalledHandleStart = true;
+            if (this.props.handleStart) {
+                this.props.handleStart();
+            }
+        }
+        this.props.handleMove(diffX);
+    }
+
+    callHandleEnd = (isAllowChange) => {
+        if (this.isCalledHandleStart) {
+            this.isCalledHandleStart = false;
+            if (this.props.handleEnd) {
+                return this.props.handleEnd(isAllowChange);
+            }
+        }
+    }
+
+    render() {
+        const {
+            screenWidth,
+            screenHeight,
+            src,
+            left: divLeft,
+        } = this.props;
+
+        const {
+            isLoaded,
+            left,
+            top,
+            scale,
+            width,
+            height,
+        } = this.state;
+
+        const ImageStyle = {
+            width,
+            height,
+        };
+
+        const translate = `translate3d(${left}px, ${top}px, 0) scale(${scale})`;
+        ImageStyle.WebkitTransform = translate;
+        ImageStyle.transform = translate;
+
+        const defaultStyle = {
+            left: divLeft,
+            width: screenWidth,
+            height: screenHeight,
+        };
+        return (
+            <div
+                className="viewer-image-container"
+                onTouchStart={this.handleTouchStart}
+                onTouchMove={this.handleTouchMove}
+                onTouchEnd={this.handleTouchEnd}
+                style={defaultStyle}
+            >
+                {
+                    isLoaded ? <img src={src} style={ImageStyle} alt=""/> : <Loading/>
+                }
+            </div>
+        );
+    }
+}
+
+export default ImageContainer;

+ 110 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/ImageViewer.css

@@ -0,0 +1,110 @@
+
+.wx-image-viewer {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+}
+
+.wx-image-viewer .viewer-cover {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    background-color: #000000;
+}
+
+.wx-image-viewer .viewer-list-container {
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    -webkit-transition-property: -webkit-transform;
+    transition-property: -webkit-transform;
+    -o-transition-property: transform;
+    transition-property: transform;
+    transition-property: transform, -webkit-transform;
+}
+
+.wx-image-viewer .viewer-image-container {
+    position: absolute;
+    top: 0;
+    left: 0;
+    overflow: hidden;
+}
+
+.wx-image-viewer .viewer-image-container img {
+    position: absolute;
+    left: 0;
+    top: 0;
+    transform-origin: left top;
+    -webkit-transform-origin: left top;
+    -moz-transform-origin: left top;
+    -o-transform-origin: left top;
+    -webkit-transition-property: -webkit-transform;
+    transition-property: -webkit-transform;
+    -o-transition-property: transform;
+    transition-property: transform;
+    transition-property: transform, -webkit-transform;
+}
+
+.wx-image-viewer .viewer-image-pointer {
+    position: fixed;
+    bottom: 20px;
+    left: 0;
+    width: 100%;
+    text-align: center;
+}
+
+.wx-image-viewer .viewer-image-pointer .pointer {
+    display: inline-block;
+    width: 8px;
+    height: 8px;
+    margin: 0 5px;
+    border-radius: 100%;
+    background-color: #333;
+}
+
+.wx-image-viewer .viewer-image-pointer .pointer.on {
+    background-color: #fff;
+}
+
+.wx-image-viewer .viewer-image-loading {
+    position: absolute;
+    margin: auto;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    width: 32px;
+    height: 32px;
+    box-sizing: border-box;
+    border-radius: 100%;
+    border-width: 4px;
+    border-style: solid;
+    border-color: #333;
+    border-bottom-color: #FFF;
+    -webkit-animation: roll 1s linear infinite;
+    animation: roll 1s linear infinite;
+}
+
+@-webkit-keyframes roll {
+    from {
+        -webkit-transform: rotate(0deg);
+    }
+    to {
+        -webkit-transform: rotate(360deg);
+    }
+}
+
+@keyframes roll {
+    from {
+        transform: rotate(0deg);
+    }
+    to {
+        transform: rotate(360deg);
+    }
+}

+ 61 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/ImagesViewer.jsx

@@ -0,0 +1,61 @@
+import React, {Component} from 'react';
+import ReactDOM from 'react-dom';
+import PropTypes from 'prop-types';
+
+import WrapViewer from './WrapViewer';
+
+import './ImageViewer.css';
+
+class ImagesViewer extends Component {
+    static propTypes = {
+        maxZoomNum: PropTypes.number, // 最大放大倍数
+        zIndex: PropTypes.number, // 组件图层深度
+        index: PropTypes.number, // 当前显示图片的http链接
+        urls: PropTypes.array.isRequired, // 需要预览的图片http链接列表
+        gap: PropTypes.number, // 间隙
+        speed: PropTypes.number, // Duration of transition between slides (in ms)
+        onClose: PropTypes.func.isRequired, // 关闭组件回调
+        needPoint: PropTypes.bool
+    }
+
+    static childContextTypes = {
+        onClose: PropTypes.func,
+    };
+
+    static defaultProps = {
+        maxZoomNum: 4,
+        zIndex: 9999,
+        index: 0,
+        gap: 10,
+        speed: 300,
+        needPoint: true
+    }
+
+    constructor(props) {
+        super(props);
+        this.node = document.createElement('div');
+    }
+
+    getChildContext() {
+        return {onClose: this.props.onClose};
+    }
+
+    componentDidMount() {
+        document.body.appendChild(this.node);
+    }
+
+    componentWillUnmount() {
+        document.body.removeChild(this.node);
+    }
+
+    render() {
+        return ReactDOM.createPortal(
+            <WrapViewer
+                {...this.props}
+            />,
+            this.node,
+        );
+    }
+}
+
+export default ImagesViewer;

+ 183 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/ListContainer.jsx

@@ -0,0 +1,183 @@
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+
+import ImageContainer from './ImageContainer';
+
+// 快速拖动时间限制
+const DEFAULT_TIME_DIFF = 200;
+
+class ListContainer extends PureComponent {
+    static propTypes = {
+        maxZoomNum: PropTypes.number.isRequired,
+        changeIndex: PropTypes.func.isRequired,
+        gap: PropTypes.number.isRequired,
+        speed: PropTypes.number.isRequired, // Duration of transition between slides (in ms)
+    }
+
+    constructor() {
+        super();
+        this.isNeedSpring = false;
+    }
+
+    state = {
+        left: 0,
+    }
+
+    componentWillMount() {
+        const {
+            screenWidth,
+            urls,
+            index,
+            gap,
+        } = this.props;
+
+        this.length = urls.length;
+        this.perDistance = screenWidth + gap;
+        this.maxLeft = this.perDistance * (this.length - 1);
+        this.isNeedSpring = false;
+
+        this.setState({
+            left: -this.perDistance * index,
+        });
+    }
+
+    componentWillReceiveProps(nextProps) {
+        if (this.props.index !== nextProps.index) {
+            this.isNeedSpring = true;
+            this.setState({
+                left: -this.perDistance * nextProps.index,
+            });
+        }
+    }
+
+    /**
+     * 拖拽的缓动公式 - easeOutSine
+     * Link http://easings.net/zh-cn#
+     * t: current time(当前时间);
+     * b: beginning value(初始值);
+     * c: change in value(变化量);
+     * d: duration(持续时间)。
+     */
+    easing = (distance) => {
+        const t = distance;
+        const b = 0;
+        const d = this.props.screenWidth; // 允许拖拽的最大距离
+        const c = d / 2.5; // 提示标签最大有效拖拽距离
+
+        return (c * Math.sin((t / d) * (Math.PI / 2))) + b;
+    }
+
+    handleStart = () => {
+        // console.info("ListContainer handleStart")
+        this.startLeft = this.state.left;
+        this.startTime = (new Date()).getTime();
+        this.isNeedSpring = false;
+    }
+
+    handleMove = (diffX) => {
+        // console.info("ListContainer handleStart diffX = %s",diffX);
+        let nDiffx = diffX;
+        // 限制最大 diffx 值
+        if (Math.abs(nDiffx) > this.props.screenWidth) {
+            if (nDiffx < 0) {
+                nDiffx = -this.props.screenWidth;
+            }
+            if (nDiffx > 0) {
+                nDiffx = this.props.screenWidth;
+            }
+        }
+
+        if (this.state.left >= 0 && nDiffx > 0) {
+            nDiffx = this.easing(nDiffx);
+        } else if (this.state.left <= -this.maxLeft && nDiffx < 0) {
+            nDiffx = -this.easing(-nDiffx);
+        }
+
+        this.setState({
+            left: this.startLeft + nDiffx,
+        });
+    }
+
+    handleEnd = (isAllowChange) => {
+        let index;
+        const diffTime = (new Date()).getTime() - this.startTime;
+        console.info('handleEnd %s', isAllowChange, diffTime, this.state.left, this.startLeft, this.props.index);
+        // 快速拖动情况下切换图片
+        if (isAllowChange && diffTime < DEFAULT_TIME_DIFF) {
+            if (this.state.left < this.startLeft) {
+                index = this.props.index + 1;
+            } else {
+                index = this.props.index - 1;
+            }
+        } else {
+            index = Math.abs(Math.round(this.state.left / this.perDistance));
+        }
+
+        // 处理边界情况
+        if (index < 0) {
+            index = 0;
+        } else if (index > this.length - 1) {
+            index = this.length - 1;
+        }
+
+        this.setState({
+            left: -this.perDistance * index,
+        });
+        this.isNeedSpring = true;
+        if (index !== this.props.index) {
+            this.props.changeIndex(index);
+            return true;
+        }
+        return false;
+    }
+
+    render() {
+        const {
+            maxZoomNum,
+            screenWidth,
+            screenHeight,
+            urls,
+            speed,
+        } = this.props;
+
+        const {
+            left,
+        } = this.state;
+
+        const defaultStyle = {};
+
+        if (this.isNeedSpring) {
+            const duration = `${speed}ms`;
+            defaultStyle.WebkitTransitionDuration = duration;
+            defaultStyle.transitionDuration = duration;
+        }
+        const translate = `translate3d(${left}px, 0, 0)`;
+        defaultStyle.WebkitTransform = translate;
+        defaultStyle.transform = translate;
+
+        return (
+            <div
+                className="viewer-list-container"
+                style={defaultStyle}
+            >
+                {
+                    urls.map((item, i) => (
+                        <ImageContainer
+                            key={i} // eslint-disable-line
+                            src={item}
+                            maxZoomNum={maxZoomNum}
+                            handleStart={this.handleStart}
+                            handleMove={this.handleMove}
+                            handleEnd={this.handleEnd}
+                            left={this.perDistance * i}
+                            screenWidth={screenWidth}
+                            screenHeight={screenHeight}
+                        />
+                    ))
+                }
+            </div>
+        );
+    }
+}
+
+export default ListContainer;

+ 10 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/Loading.jsx

@@ -0,0 +1,10 @@
+import React from 'react';
+
+const Loading = (props) => {
+    return (
+        <div className="viewer-image-loading">
+        </div>
+    );
+};
+
+export default Loading;

+ 36 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/Pointer.jsx

@@ -0,0 +1,36 @@
+import React, {PureComponent} from 'react';
+import PropTypes from 'prop-types';
+import './ImageViewer.css'
+
+class Pointer extends PureComponent {
+    static propTypes = {
+        length: PropTypes.number.isRequired,
+        index: PropTypes.number.isRequired,
+        changeIndex: PropTypes.func
+    }
+
+    render() {
+        const {
+            length,
+            changeIndex,
+            index
+        } = this.props
+
+        let i = 0, items = [];
+        for (i; i < length; i++) {
+            if (i === index) {
+                items.push(<span onClick={changeIndex.bind(null, i)} key={i} className="pointer on"></span>);
+            } else {
+                items.push(<span onClick={changeIndex.bind(null, i)} key={i} className="pointer"></span>);
+            }
+        }
+
+        return (
+            <div className="viewer-image-pointer">
+                {items}
+            </div>
+        );
+    }
+}
+
+export default Pointer;

+ 79 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/WrapViewer.jsx

@@ -0,0 +1,79 @@
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+
+import ListContainer from './ListContainer';
+import Pointer from './Pointer';
+
+const screenWidth = typeof document !== 'undefined' && document.documentElement.clientWidth;
+const screenHeight = typeof document !== 'undefined' && document.documentElement.clientHeight;
+
+class WrapViewer extends Component {
+    static propTypes = {
+        index: PropTypes.number.isRequired, // 当前显示图片的http链接
+        urls: PropTypes.array.isRequired, // 需要预览的图片http链接列表
+        maxZoomNum: PropTypes.number.isRequired, // 最大放大倍数
+        zIndex: PropTypes.number.isRequired, // 组件图层深度
+        gap: PropTypes.number.isRequired, // 间隙
+        speed: PropTypes.number.isRequired, // Duration of transition between slides (in ms)
+        needPoint: PropTypes.bool
+    }
+
+    static defalutProps = {
+        needPoint: true
+    }
+
+    state = {
+        index: 0,
+    }
+
+    componentWillMount() {
+        const {
+            index,
+        } = this.props;
+
+        this.setState({
+            index,
+        });
+    }
+
+    changeIndex = (index) => {
+        console.info('changeIndex index = ', index);
+        this.setState({
+            index,
+        });
+    }
+
+    render() {
+        const {
+            zIndex,
+            urls,
+            maxZoomNum,
+            gap,
+            speed,
+            needPoint,
+        } = this.props;
+
+        const {
+            index,
+        } = this.state;
+
+        return (
+            <div className="wx-image-viewer" style={{zIndex}}>{/* root */}
+                <div className="viewer-cover"/>
+                <ListContainer
+                    screenWidth={screenWidth}
+                    screenHeight={screenHeight}
+                    changeIndex={this.changeIndex}
+                    urls={urls}
+                    maxZoomNum={maxZoomNum}
+                    gap={gap}
+                    speed={speed}
+                    index={index}
+                />
+                {needPoint ? <Pointer length={urls.length} index={index} changeIndex={this.changeIndex}/> : ''}
+            </div>
+        );
+    }
+}
+
+export default WrapViewer;

+ 3 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/index.js

@@ -0,0 +1,3 @@
+import ImagesViewer from './ImagesViewer'
+
+export default ImagesViewer

+ 255 - 0
uas-office-web/wxuasapproval/src/components/imagesVIewer/tween.js

@@ -0,0 +1,255 @@
+/* eslint-disable */
+'use strict';
+/**
+ * t: current time(当前时间);
+ * b: beginning value(初始值);
+ * c: change in value(变化量);
+ * _c: final value (最后值)
+ * d: duration(持续时间)。
+ */
+var tweenFunctions = {
+    linear: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * t / d + b;
+    },
+    easeInQuad: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * (t /= d) * t + b;
+    },
+    easeOutQuad: function (t, b, _c, d) {
+        var c = _c - b;
+        return -c * (t /= d) * (t - 2) + b;
+    },
+    easeInOutQuad: function (t, b, _c, d) {
+        var c = _c - b;
+        if ((t /= d / 2) < 1) {
+            return c / 2 * t * t + b;
+        } else {
+            return -c / 2 * ((--t) * (t - 2) - 1) + b;
+        }
+    },
+    easeInCubic: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * (t /= d) * t * t + b;
+    },
+    easeOutCubic: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * ((t = t / d - 1) * t * t + 1) + b;
+    },
+    easeInOutCubic: function (t, b, _c, d) {
+        var c = _c - b;
+        if ((t /= d / 2) < 1) {
+            return c / 2 * t * t * t + b;
+        } else {
+            return c / 2 * ((t -= 2) * t * t + 2) + b;
+        }
+    },
+    easeInQuart: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * (t /= d) * t * t * t + b;
+    },
+    easeOutQuart: function (t, b, _c, d) {
+        var c = _c - b;
+        return -c * ((t = t / d - 1) * t * t * t - 1) + b;
+    },
+    easeInOutQuart: function (t, b, _c, d) {
+        var c = _c - b;
+        if ((t /= d / 2) < 1) {
+            return c / 2 * t * t * t * t + b;
+        } else {
+            return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
+        }
+    },
+    easeInQuint: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * (t /= d) * t * t * t * t + b;
+    },
+    easeOutQuint: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
+    },
+    easeInOutQuint: function (t, b, _c, d) {
+        var c = _c - b;
+        if ((t /= d / 2) < 1) {
+            return c / 2 * t * t * t * t * t + b;
+        } else {
+            return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
+        }
+    },
+    easeInSine: function (t, b, _c, d) {
+        var c = _c - b;
+        return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
+    },
+    easeOutSine: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * Math.sin(t / d * (Math.PI / 2)) + b;
+    },
+    easeInOutSine: function (t, b, _c, d) {
+        var c = _c - b;
+        return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
+    },
+    easeInExpo: function (t, b, _c, d) {
+        var c = _c - b;
+        return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
+    },
+    easeOutExpo: function (t, b, _c, d) {
+        var c = _c - b;
+        return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
+    },
+    easeInOutExpo: function (t, b, _c, d) {
+        var c = _c - b;
+        if (t === 0) {
+            return b;
+        }
+        if (t === d) {
+            return b + c;
+        }
+        if ((t /= d / 2) < 1) {
+            return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
+        } else {
+            return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
+        }
+    },
+    easeInCirc: function (t, b, _c, d) {
+        var c = _c - b;
+        return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
+    },
+    easeOutCirc: function (t, b, _c, d) {
+        var c = _c - b;
+        return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
+    },
+    easeInOutCirc: function (t, b, _c, d) {
+        var c = _c - b;
+        if ((t /= d / 2) < 1) {
+            return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
+        } else {
+            return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
+        }
+    },
+    easeInElastic: function (t, b, _c, d) {
+        var c = _c - b;
+        var a, p, s;
+        s = 1.70158;
+        p = 0;
+        a = c;
+        if (t === 0) {
+            return b;
+        } else if ((t /= d) === 1) {
+            return b + c;
+        }
+        if (!p) {
+            p = d * 0.3;
+        }
+        if (a < Math.abs(c)) {
+            a = c;
+            s = p / 4;
+        } else {
+            s = p / (2 * Math.PI) * Math.asin(c / a);
+        }
+        return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
+    },
+    easeOutElastic: function (t, b, _c, d) {
+        var c = _c - b;
+        var a, p, s;
+        s = 1.70158;
+        p = 0;
+        a = c;
+        if (t === 0) {
+            return b;
+        } else if ((t /= d) === 1) {
+            return b + c;
+        }
+        if (!p) {
+            p = d * 0.3;
+        }
+        if (a < Math.abs(c)) {
+            a = c;
+            s = p / 4;
+        } else {
+            s = p / (2 * Math.PI) * Math.asin(c / a);
+        }
+        return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
+    },
+    easeInOutElastic: function (t, b, _c, d) {
+        var c = _c - b;
+        var a, p, s;
+        s = 1.70158;
+        p = 0;
+        a = c;
+        if (t === 0) {
+            return b;
+        } else if ((t /= d / 2) === 2) {
+            return b + c;
+        }
+        if (!p) {
+            p = d * (0.3 * 1.5);
+        }
+        if (a < Math.abs(c)) {
+            a = c;
+            s = p / 4;
+        } else {
+            s = p / (2 * Math.PI) * Math.asin(c / a);
+        }
+        if (t < 1) {
+            return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
+        } else {
+            return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
+        }
+    },
+    easeInBack: function (t, b, _c, d, s) {
+        var c = _c - b;
+        if (s === void 0) {
+            s = 1.70158;
+        }
+        return c * (t /= d) * t * ((s + 1) * t - s) + b;
+    },
+    easeOutBack: function (t, b, _c, d, s) {
+        var c = _c - b;
+        if (s === void 0) {
+            s = 1.70158;
+        }
+        return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
+    },
+    easeInOutBack: function (t, b, _c, d, s) {
+        var c = _c - b;
+        if (s === void 0) {
+            s = 1.70158;
+        }
+        if ((t /= d / 2) < 1) {
+            return c / 2 * (t * t * (((s *= 1.525) + 1) * t - s)) + b;
+        } else {
+            return c / 2 * ((t -= 2) * t * (((s *= 1.525) + 1) * t + s) + 2) + b;
+        }
+    },
+    easeInBounce: function (t, b, _c, d) {
+        var c = _c - b;
+        var v;
+        v = tweenFunctions.easeOutBounce(d - t, 0, c, d);
+        return c - v + b;
+    },
+    easeOutBounce: function (t, b, _c, d) {
+        var c = _c - b;
+        if ((t /= d) < 1 / 2.75) {
+            return c * (7.5625 * t * t) + b;
+        } else if (t < 2 / 2.75) {
+            return c * (7.5625 * (t -= 1.5 / 2.75) * t + 0.75) + b;
+        } else if (t < 2.5 / 2.75) {
+            return c * (7.5625 * (t -= 2.25 / 2.75) * t + 0.9375) + b;
+        } else {
+            return c * (7.5625 * (t -= 2.625 / 2.75) * t + 0.984375) + b;
+        }
+    },
+    easeInOutBounce: function (t, b, _c, d) {
+        var c = _c - b;
+        var v;
+        if (t < d / 2) {
+            v = tweenFunctions.easeInBounce(t * 2, 0, c, d);
+            return v * 0.5 + b;
+        } else {
+            v = tweenFunctions.easeOutBounce(t * 2 - d, 0, c, d);
+            return v * 0.5 + c * 0.5 + b;
+        }
+    }
+};
+
+module.exports = tweenFunctions;

+ 189 - 0
uas-office-web/wxuasapproval/src/components/uploadEnclosure/UploadEnclosure.jsx

@@ -0,0 +1,189 @@
+/**
+ * Created by RaoMeng on 2018/11/7
+ * Desc: 图片上传组件
+ */
+
+import React, { Component } from 'react'
+import { Upload, Icon } from 'antd'
+import { Toast } from 'antd-mobile'
+import { isObjEmpty, isObjNull } from '../../utils/common'
+import ImagesViewer from '../imagesVIewer'
+import PropTypes from 'prop-types'
+import 'upload.css'
+
+let uploadFail = false
+export default class UploadEnclosure extends Component {
+
+  static propTypes = {
+    action: PropTypes.string.isRequired,//上传地址
+    data: PropTypes.object,//上传所需参数
+    headers: PropTypes.object,//上传所需头部参数
+    accept: PropTypes.string,//接受上传的文件类型
+    listType: PropTypes.string,//附件列表格式,默认picture-card
+    count: PropTypes.number,//附件限制数量,默认为1
+    multiple: PropTypes.bool,//是非支持多选,默认为false
+    title: PropTypes.string,//title,默认为‘附件’
+    needPoint: PropTypes.bool,//是非需要下方的指示点,默认为true
+    beforeUpload: PropTypes.func,//上传附件前的回调花事件
+    handleChange: PropTypes.func,//附件选择后的回调
+    handlePreview: PropTypes.func,//附件预览(一般情况下不传)
+    limit: PropTypes.bool,//是否限制附件个数
+    handleRemove: PropTypes.func,//移除照片的回调
+  }
+
+  static defaultProps = {
+    listType: 'picture-card',
+    count: 1,
+    data: {
+      folderId: 0,
+    },
+    headers: {},
+    multiple: false,
+    title: '附件',
+    needPoint: true,
+    limit: true,
+    accept: 'image/*',
+  }
+
+  constructor () {
+    super()
+
+    this.state = {
+      previewVisible: false,
+      previewIndex: 0,
+      fileList: [],
+    }
+  }
+
+  componentDidMount () {
+    this.setState({
+      fileList: this.props.fileList,
+    })
+  }
+
+  componentWillReceiveProps (nextProps) {
+    this.setState({
+      fileList: nextProps.fileList,
+    })
+  }
+
+  render () {
+    const { fileList } = this.state
+    const {
+      action, listType, count, multiple,
+      title, needPoint, limit, accept, data, headers,
+    } = this.props
+
+    const imgs = []
+    if (!isObjEmpty(fileList) && fileList !== '[]') {
+      for (let i = 0; i < fileList.length; i++) {
+        imgs.push(fileList[i].url || fileList[i].thumbUrl)
+      }
+    }
+
+    let pointAble = needPoint
+    if (imgs.length > 9) {
+      pointAble = false
+    }
+    // const userInfo = store.getState().redUserInfo
+
+    const headerParams = {
+      // headers?...headers:{},
+      // 'Authorization': userInfo.token,
+      ...headers,
+    }
+
+    const uploadButton = (
+      <div>
+        <Icon type="plus"/>
+        <div className="ant-upload-text">Upload</div>
+      </div>
+    )
+
+    return (
+      <div style={{ width: '100%' }} className='upload-enclosure-root'>
+        <div className='chooseLayout'>
+          <span className='annexText'>{title}</span>
+          {limit ? <span
+            className='annexCount'>({fileList.length}/{count})张</span> : ''}
+        </div>
+        <div className='imagesLayout'>
+          <Upload
+            action={action}
+            accept={accept}
+            data={data}
+            listType={listType}
+            fileList={fileList}
+            multiple={multiple}
+            withCredentials={true}
+            headers={headerParams}
+            beforeUpload={this.beforeUpload}
+            onPreview={this.handlePreview}
+            onChange={this.handleChange}
+            onRemove={this.handleRemove}>
+            {(fileList.length >= count && limit) ? '' : uploadButton}
+          </Upload>
+          {this.state.previewVisible ?
+            <ImagesViewer onClose={this.handleCancel} urls={imgs}
+                          index={this.state.previewIndex}
+                          needPoint={pointAble}/> : ''}
+        </div>
+      </div>
+    )
+  }
+
+  beforeUpload = (file, fileList) => {
+    uploadFail = false
+    const { count, limit } = this.props
+    console.log('fileListb', fileList)
+    if (this.state.fileList.length + fileList.length > count && limit) {
+      Toast.fail(`上传失败,附件数量不能超过${count}张`)
+      uploadFail = true
+      return false
+    } else {
+      return this.props.beforeUpload(file, fileList)
+    }
+  }
+
+  handleCancel = () => this.setState({ previewVisible: false })
+
+  handlePreview = (file) => {
+    if (this.props.handlePreview) {
+      this.props.handlePreview(file)
+    } else {
+      this.setState({
+        previewVisible: true,
+        previewIndex: file.index || 0,
+      })
+    }
+  }
+
+  handleChange = ({ fileList }) => {
+    console.log('filelist', fileList)
+    if (uploadFail) {
+      return
+    }
+    if (fileList.length <= this.props.count || !this.props.limit) {
+      if (fileList) {
+        fileList.forEach((value, index) => {
+          value.url = (value.response && value.response.data)
+            ? value.response.data.accessPath
+            : value.url
+          value.picUrl = value.url
+          value.relativeUrl = (value.response && value.response.data)
+            ? value.response.data.fullPath
+            : value.relativeUrl
+        })
+      }
+
+      this.setState({ fileList })
+      this.props.handleChange(fileList)
+    }
+  }
+
+  handleRemove = (file) => {
+    if (this.props.handleRemove) {
+      return this.props.handleRemove(file)
+    }
+  }
+}

+ 11 - 0
uas-office-web/wxuasapproval/src/components/uploadEnclosure/upload.css

@@ -0,0 +1,11 @@
+.upload-enclosure-root {
+}
+
+.upload-enclosure-root .ant-upload-text {
+    display: none;
+}
+
+.upload-enclosure-root i.anticon.anticon-plus svg{
+    width: 3em;
+    height: 3em;
+}

+ 9 - 8
uas-office-web/wxuasapproval/src/pages/approval/Approval.jsx

@@ -1628,9 +1628,9 @@ class Approval extends Component {
           suffix = splits[splits.length - 1]
         }
         enclosure.caption = caption + '.' + suffix
-        let enclosureList = []
-        enclosureList.push(enclosure)
-        this.addEnclosure(enclosureList)
+        let enclosures = []
+        enclosures.push(enclosure)
+        this.addEnclosure(enclosures)
 
         continue
       }
@@ -1773,16 +1773,17 @@ class Approval extends Component {
 
   addEnclosure = (enclosures) => {
     if (!isObjNull(enclosures)) {
-      let { enclosureList } = this.state
-      if (enclosureList.length <= 0) {
+      if (mEnclosureList.length <= 0) {
         let tag = new ApprovalBean(ApprovalBean.TAG)
         tag.caption = '附件'
-        enclosureList.push(tag)
+        mEnclosureList.push(tag)
       }
 
-      enclosureList = enclosureList.concat(enclosures)
+      mEnclosureList = mEnclosureList.concat(enclosures)
 
-      this.setState({ enclosureList })
+      this.setState({
+        enclosureList: mEnclosureList,
+      })
     }
   }
 

+ 8 - 6
uas-office-web/wxuasapproval/src/pages/approval/ApprovalAdd.jsx

@@ -58,7 +58,7 @@ let mBaseUrl = window.location.origin
   + (process.env.REACT_APP_ROUTER_BASE_NAME || '/ERP')
   // && 'http://yrkj.usoftchina.com:9443/uas'
   // && 'http://erp.yitoa.com:8888/ERP'
-  // && 'http://29226oq576.qicp.vip/erp'
+// && 'http://29226oq576.qicp.vip/erp'
 
 export default class ApprovalAdd extends Component {
 
@@ -134,6 +134,7 @@ export default class ApprovalAdd extends Component {
                     formItems.push(
                       <FormInput billModel={billModel} groupIndex={g}
                                  childIndex={i}
+                                 baseUrl={mBaseUrl}
                                  onTextChange={this.onTextChange.bind(this)}
                                  onInputClick={this.onInputClick.bind(this)}/>,
                     )
@@ -285,6 +286,7 @@ export default class ApprovalAdd extends Component {
       />
       <ListView
         dataSource={modalDataSource}
+        initialListSize={30}
         renderRow={(rowData, sectionID, rowID) => {
           switch (selectType) {
             case SELECT_APPROVAL:
@@ -295,7 +297,7 @@ export default class ApprovalAdd extends Component {
                   rowData)}>
                 <EmployeeItem employee={rowData}/>
               </List.Item>
-            case 'MF':
+            /*case 'MF':
               return <CheckboxItem
                 key={rowID}
                 checked={rowData.isSelected == true}
@@ -305,7 +307,7 @@ export default class ApprovalAdd extends Component {
                   this.setState({
                     modalDataSource,
                   })
-                }}>{rowData.value}</CheckboxItem>
+                }}>{rowData.value}</CheckboxItem>*/
             default:
               return <List.Item
                 key={rowID}
@@ -319,9 +321,9 @@ export default class ApprovalAdd extends Component {
           height: '72vh',
           overflow: 'auto',
         }}
-        pageSize={10}
-        onScroll={() => {}}
-        scrollRenderAheadDistance={500}
+        pageSize={20}
+        // onScroll={() => {}}
+        // scrollRenderAheadDistance={800}
       />
     </Modal>
   }

+ 1 - 0
uas-office-web/wxuasapproval/src/pages/approval/approval.css

@@ -205,6 +205,7 @@
     color: #333333;
     letter-spacing: 1.6px;
     padding-top: 6px;
+    text-align: center;
 }
 
 .line {