话题
今日中秋佳节,祝大家快乐 不止中秋 🎑~
前言
目前大部分公司项目都是实用前后端分离,前端使用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
1、string -> 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>
);
}
}
最后 上一下移动端的效果图
❤️ 感谢大家
1.如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我创作的动力。