入门react 【antd-mobile】 【电子签名】【腾讯定位】

792 阅读5分钟

话题

今日中秋佳节,祝大家快乐 不止中秋 🎑~

前言

目前大部分公司项目都是实用前后端分离,前端使用react技术的越来越广了。在此推荐antd组件,稳定性好,上手轻巧,使用体验如德芙版丝滑。博主这边对比了pc组件跟移动组件的用法,以及踩过的坑来述说,如有不妥之处,欢迎指点。次项目包含电子签名跟定位获取

特性优势

  • ui样式高度可配置,扩展性强,轻松适应各类产品风格
  • 基于react native的 ios/android/web多平台支持,组件丰富,能全面覆盖各类场景
  • 提供“组件按需加载”/“web页面高清显示”/“svg icon” 等优化方案,一体式开发
  • 使用typescript开发,提供类型定义文件,支持类型及属性智能提示,方便业务开发
  • 全面兼容react/preact

适用场景

  • 适合于大型产品应用
  • 适合于基于react/preact/react-native的多终端应用
  • 适合不同ui风格的高度定制需求的应用

快速上手

附官方API,特性+脚手架 文档说的很通透,可参考文档来 mobile.ant.design/docs/react/…

以form表单为例

1、PC 跟移动端创建表单的方式不一样,引入的属性不一样

  • PC端 import { Form } from 'antd';
import React, { PureComponent } from 'react';
import { Radio, Select ,Input,Checkbox,DatePicker,Switch} from 'antd';
import moment from 'moment';

const FormItem = Form.Item;
const RadioGroup = Radio.Group;
const CheckboxGroup = Checkbox.Group;
const { RangePicker } = DatePicker;
const { TextArea } = Input;

@Form.create()
export default class Demo extends PureComponent {

setBaseInfoValue(key,value)=>{
 
 }
 
 onDateChange = (dataIndex, value) => {
    this.setState({
      [dataIndex]: moment(value).format('YYYY-MM-DD'),
    });
  };
  
  render() {
  const{form:{getFieldDecorator}}=this.props;
   return (
      <>
       <Form>
            <Row>
             <Col span={12}>
               <FormItem label="姓名">
                {getFieldDecorator('name', {
                  initialValue: baseInfo.name,
                })(
                  <Input   onChange={value => this.setBaseInfoValue('name', e.target.value)}/>
                )}
             </Col>
             <Col span={12}>
               <FormItem label="性别">
                   <RadioGroup
                    onChange={e => {
                      this.setBaseInfoValue( 'sex', e.target.value );
                    }}
                  >
                    {sexList.map(item => {
                      return (
                        <Radio value={item.id} key={item.id}>
                          {item.name}
                        </Radio>
                      );
                    })}
                  </RadioGroup>
                  </FormItem>
             </Col>
            <Row>
            <Row>
            <Col span={12}>
              <FormItem label="注册时间" {...formItemLayout}>
                {getFieldDecorator('regTimeBegin', {
                  initialValue: regTimeBegin && moment(regTimeBegin),
                })(
                  <DatePicker
                    style={{ width: '49%' }}
                    placeholder=""
                    onChange={value => this.onDateChange('regTimeBegin', value)}
                  />
                )}
                -
                <span>
                  {getFieldDecorator('regTimeEnd', {
                    initialValue: regTimeEnd && moment(regTimeEnd),
                  })(
                    <DatePicker
                      style={{ width: '48%' }}
                      placeholder=""
                      onChange={value => this.onDateChange('regTimeEnd', value)}
                    />
                  )}
                </span>
              </FormItem>
            </Col>
             <Col span={12}>
                <FormItem {...formItemLayout} label="活动时间">
                    {getFieldDecorator('cerDate', {
                      initialValue:
                        (baseInfo.startDate &&
                          baseInfo.endDate && [
                            moment(baseInfo.startDate),
                            moment(baseInfo.endDate),
                          ]) ||
                        [],
                      rules: [
                        { 
                          required: true
                        },
                      ],
                    })(<RangePicker  disabled={readOnly}/>)}
                </FormItem>
               </Col>
            </Row>
            <Row>
            <Col span={12}>
              <FormItem label="是否二级承运">
                {getFieldDecorator('ifSecsupplier', {
                  initialValue: baseInfo.ifSecsupplier,
                  rules: [{ required: true }],
                })(
                  <Select
                    disabled={readOnly}
                    allowClear
                    showSearch
                    filterOption={(input, option) => option.props.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
                    }
                    children={getSelectedOptions(TMS_Y_N)}
                  />
                )}
              </FormItem>
              </Col>
              <Col span={12}>
                <FormItem label="备注">
                  {getFieldDecorator("remark", { initialValue: baseInfo.remark})(
                  <TextArea rows={15} 
                   onChange={e => {
                this.setBaseInfoValue('remark', e.target.value);
              }}/>)}
                </FormItem>
              </Col>
          </Row>
          <Row>
          <Col span={12}>
            <FormItem {...formItemLayout} label="是否营销活动">
             { getFieldDecorator('ifActivity')(
                <Switch
                disabled={readOnly}
                checkedChildren="是"
                unCheckedChildren="否"
                checked={baseInfo.ifActivity == '1'}
                onChange={value => this.setBaseInfoValue('ifActivity', value)}
                />  )}
            </FormItem>
            </Col>
          </Row>
       </Form>
      </>
   )
  }
}
  • 移动端:import { createForm } from 'rc-form';
import React, { PureComponent } from 'react';
import { List, InputItem, DatePicker, Switch, Checkbox, Accordion, ImagePicker, TextareaItem, Toast, Radio,DatePicker,TextareaItem,Switch } from 'antd-mobile';
import { createForm } from 'rc-form';
import zhCn from 'antd-mobile/lib/date-picker/locale/zh_CN';

const Item=List.Item;
const RadioItem = Radio.RadioItem;
const CheckboxItem = Checkbox.CheckboxItem;

class Demo extends PureComponent {

 setBaseInfoValue(key,value)=>{
 
 }
 
 render() {
  const { form:{getFieldProps}}=this.props;
  return (
      <>
       <form>
         <List>
         <InputItem
            {...getFieldProps('name', {
              initialValue: baseInfo.name
            })}
            placeholder="请输入姓名"
            clear
            onChange={value => this.setBaseInfoValue('姓名', e.target.value)}
          >姓名
          </InputItem>
          <Item extra={baseInfo.name}>性别</Item>
          <Item
            extra={
              <Switch
                    {...getFieldProps('isExe', {
                      initialValue: operateInfo.isExe == 'Y',
                      valuePropName: 'checked',
                      onChange: value => this.setBaseInfoValue('isExe', value)
                    })}
                    onClick={(checked) => {
                      // set new value
                      this.props.form.setFieldsValue({
                            isExe: checked,
                      });
                    }}
                    color='#1890FF'
              />}
            >是否{label}异常
        </Item>
          <Accordion accordion openAnimation={{}} className="my-accordion" onChange={this.onChange} defaultActiveKey="0">
            <Accordion.Panel header="超时原因">
              <List>
                {timeOutList.map(i => {
                  let _checked = i.id == csyy;
                  return (
                    <RadioItem key={i.id} onChange={() => this.setBaseInfoValue('csyy', i.id, '')} checked={_checked}>
                      <label className={styles.checkCss}>{i.name}</label>
                    </RadioItem>
                  )
                })}
              </List>
            </Accordion.Panel>
          </Accordion>
          <Accordion accordion openAnimation={{}} className="my-accordion" onChange={this.onChange} defaultActiveKey="0">
            <Accordion.Panel header="异常原因" >
              <List>
                {excList.map(i => {
                  let checked = excRecord.find(s => s.excType == i.id) != null;
                  return (
                    <CheckboxItem key={i.id} checked={checked} onClick={() => this.setBaseInfoValue('excRecord', i, checked)}>
                      <label className={styles.checkCss}>{i.name}</label>
                    </CheckboxItem>
                  )
                })}
              </List>
            </Accordion.Panel>
          </Accordion>
           <DatePicker
            {...getFieldProps('actTime', {
              initialValue: new Date(operateInfo.actTime),
            })}
            minDate={new Date(operateInfo.requireTime)}
            locale={zhCn}
            onChange={value => this.setBaseInfoValue('actTime', value)}
          >
            <Item arrow="horizontal">实际{label}时间</Item>
          </DatePicker>
          <TextareaItem
            {...getFieldProps('remark', {
              initialValue: operateInfo.remark
            })}
            clear
            placeholder="请输入备注"
            rows={3}
            onChange={value => this.setBaseInfoValue('remark', value)}
          />
         </List>
       </form>
      </>
   )
 }
}
export createForm()(Demo);

表单基本组件

2.1、布局
  • pc端 一般用row,col-24分割
  • 移动 端用List ,List.Item
2.2、输入框
  • pc Input onChange事件总结
  • 移动端 InputItem
//1、pc:
//input/RadioGroup /TextArea        
   :value=>e.target.value()

//inputNumber/Select/Switch/DatePicker
:value=>value()
<FormItem label="性别">
    {getFieldDecorator('数量')
    (<InputNumber onChange={value => this.setBaseInfoValue('quality', value)} />)}
</FormItem>
//2、移动端
//InputItem/Select/Switch: value=>value()
2.1、显示
  • pc端 Input
  • 移动端 List.Item,extra属性用来放内容
踩坑移动端不能用InputItem 做左右对齐的样式,只能文字左边,内容剧中(有老铁其他想法的可以Cue我)。由于移动端一般都是标签左边,内容右边显示,推荐List.Item,全局更改样式
    
 :global {
  .am-list-item .am-list-line .am-list-extra{
    -webkit-flex-basis:60% !important;
    flex-basis: 60% !important;
    color:black !important;
  }
  ::-webkit-scrollbar {
      width: 2px !important;
      height:  none !important;
  }
}
2.3、单选
  • pc Radio Select
  • 移动端 Radio
2.4、复选
  • pc Checkbox
  • 移动端 Checkbox
2.5、时间控件
  • pc DatePicker(RangePicker范围,)
  • 移动端 DatePicker
DatePicker 存放的都是时间对象 new Date
时间处理控间 moment
1string -> date
   直接 new Date("2021-09-23"),moment("2021-09-23")
   比较 date1,date2 
    if (new Date("2021-09-22") > new Date("2021-09-23") ) {
        console.log('yes');
    }
2、date -> string
   moment(regTimeEnd).format('YYYY-MM-DD')
   
3、移动端踩坑,时间控件需要加上中文包,否则ios识别为英文。
   ios显示NAN问题处理,后端不能返回格式形如“YYYY-MM-DD”,不能识别。需要转成“YYYY/MM/DD”,在此提供一个转换方式replace(/-/g, '/') ;
   let actTime=actTime: actPickupTime ? actPickupTime.replace(/-/g, '/') : now(),

2.6、文本框
  • pc Input->TextArea
  • 移动端 TextareaItem
2.7、开关
- pc  Switch
- 移动端 Switch

地图定位

  • pc 暂无
  • 移动端 引入腾讯地图
    • 1、项目全局document.ejs引入腾讯js ,key用自己注册的
 <script src="https://map.qq.com/api/js?v=2.exp&key=xxx"></script>
  <script src="https://apis.map.qq.com/tools/geolocation/min?key=xx&referer=myapp"></script>
    • 2、初始化地图一步加载
import React, { PureComponent } from 'react';
import dynamic from 'umi/dynamic';
const TMap = dynamic({
  loader: () => import('react-tmap'),
  loading() {
    return <div>加载中...</div>;
  },
  render(loaded, props) {
    let { QMap, Marker } = props;
    return (
      <QMap center={loaded.center} style={{ height: 200 }} zoom={14}>
        <Marker position={loaded.center} />
      </QMap>
    );
  },
});

class Operate extends PureComponent {
    state = {
        center: {
          lat: 39.92,
          lng: 116.46,
        },
      }
      
    componentDidMount() {
        this.getCurrentPosition();
    }
    
    getCurrentPosition = () => {
        var geolocation = new qq.maps.Geolocation();
        console.log('geolocation', geolocation);
        geolocation.getLocation(res => {
          // alert(JSON.stringify(res));
          let { addr, lat, lng } = res;
          this.setState({
            _gpsAddress: addr,
            center: {
              lat,
              lng,
            },
          })
        }, error => {
          // alert(JSON.stringify(error));
          console.log('error', error);
        }, options);
  }
  
  render() {
      return (
          <div className={styles.qmap}>
            <TMap center={center ? center : this.state.center} />
          </div>
      )
  }
}

电子签名

  • pc 暂无
  • 移动端 引入 react-signature-canvas
    • 提供上传,清除按钮
import React, { PureComponent } from 'react';
import { NavBar, Icon, Toast } from 'antd-mobile';
import { connect } from 'dva';
import router from 'umi/router';
import SignatureCanvas from 'react-signature-canvas';
import styles from '../index.less';
import { setCache } from 'utils/authority';

@connect(({ app, mbHome, common }) => ({ app, mbHome, common }))
export default class index extends PureComponent {

  state = {
    disabled: false
  }

  componentDidMount() {

  }

  componentWillUnmount() {

  }

  handleBack = params => {
    const { location: { query }, mbHome: { operateInfo }, dispatch } = this.props;
    const { billNo, type, page, orderType } = query;
    let _operateInfo = { ...operateInfo, isBack: false };
    if (params == 0) {
      setCache('operateInfo', _operateInfo);
      dispatch({
        type: 'mbHome/updateState',
        payload: { operateInfo: _operateInfo }
      });
    }
    let path = '';
    //返回主页
    switch (page) {
      case 'exppickup':
        path = '/mobile/pickup';
        break;
      case 'handover':
        path = '/mobile/handover';
        break;
      case 'signup':
        path = '/mobile/signup';
        break;
    }
    router.push({
      pathname: path,
      query: { billNo, type, isBack: true, orderType }
    })
  }

  async signBtn(type) {
    //得到画布
    let canvas = this.signCanvas.current._canvas;
    if (canvas.getContext) {
      let ctx = canvas.getContext('2d');
      ctx.fillStyle = "#fff";//添加颜色
      ctx.fillRect(0, 0, canvas.width, canvas.height);
    }
  }

  resetSignBtn = () => {
    if (this.signCanvas.isEmpty()) {
      this.tipMsg('没有可清除的签名.');
      return;
    }
    this.signCanvas.clear();
  }

  tipMsg = msg => {
    Toast.info(msg, 2);
  }

  dataURLtoBlob = (dataurl) => {
    var arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1],
      bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8arr], { type: mime });
  }

  onUpload = () => {
    const { dispatch, location: { query }, mbHome: { operateInfo } } = this.props;
    const { billNo, page, type } = query;
    let { disabled } = this.state;
    console.log('disabled', disabled);
    if (disabled) {
      return;
    }
    let dataurl = this.signCanvas.toDataURL("image/png");
    let blob = this.dataURLtoBlob(dataurl);
    // console.log('signCanvas', this.signCanvas);
    // console.log('signCanvas', dataurl);
    // console.log('blob', blob);
    let orderNo = (type == '3' ? operateInfo.orderNo : operateInfo.appointmentNo);
    setTimeout(() => {
      this.setState({ disabled: true });
    }, 200);
    console.log('disabled', this.state.disabled);
    let formData = new FormData();
    formData.append('file', blob);
    formData.append('fileType', 'image');
    formData.append('type', page);
    formData.append('orderNo', orderNo);
    dispatch({
      type: 'common/mbUploadImg',
      payload: { formData }
    }).then(res => {
      if (res.status) {
        this.setState({ disabled: false });
        const { fileName, fileUrl } = res.body;
        let _signOjb = {
          fileName,
          enclosureType: '2',
          url: fileUrl
        };
        setTimeout(() => {
          let _operateInfo = { ...operateInfo, signObj: _signOjb };
          setCache('operateInfo', _operateInfo);
          dispatch({
            type: 'mbHome/updateState',
            payload: { operateInfo: _operateInfo }
          });
          this.tipMsg('签名上传成功');
          this.handleBack(1);
        }, 1500);
      } else {
        this.setState({ disabled: false });
        if (res.message) {
          this.tipMsg(res.message);
        }
      }
    })
  }

  render() {
    const { mbHome: { type } } = this.props;
    const { disabled } = this.state;
    let height = window.innerHeight - 104;
    console.log('height', height);
    console.log('disabled111', disabled);
    return (
      <div>
        <NavBar mode="dark" icon={<Icon type="left" />}
          onLeftClick={() => this.handleBack(0)} className={styles.title}>电子签名</NavBar>
        <div className={styles.signDiv}>
          <SignatureCanvas
            ref={(ref) => { this.signCanvas = ref }}
            penColor='#000'
            canvasProps={{ width: 900, height, className: 'sigCanvas' }}
          />
        </div>
        <div className={styles.btnDiv}>
          <div className={styles.btnSignClear} onClick={this.resetSignBtn}>清除</div>
          <div className={disabled ? styles.btnSignUploadDis : styles.btnSignUpload} onClick={this.onUpload}>上传</div>
        </div>
      </div>
    );
  }
}

最后 上一下移动端的效果图

image.png

image.png

image.png

image.png

❤️ 感谢大家

1.如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。