1.加速播放语音
// A文件:
import React, { PureComponent, Fragment } from 'react';
import { Button } from 'antd';
import ReactAudioPlayer from 'react-audio-player';
export default class AudioPlayer extends PureComponent {
handleSpeed=(val) => {
this.rap.audioEl.playbackRate = val || 1;
this.rap.audioEl.play();
}
render() {
const { src } = this.props;
return (
<Fragment>
<ReactAudioPlayer ref={(element) => { this.rap = element; }} src={src} controls />
<div style={{ display: 'inline-block', position: 'relative' }}>
<Button style={{ marginLeft: 5 }} size="small" onClick={() => this.handleSpeed(1)}>1X</Button>
<Button style={{ marginLeft: 5 }} size="small" onClick={() => this.handleSpeed(1.2)}>1.2X</Button>
<Button style={{ marginLeft: 5 }} size="small" onClick={() => this.handleSpeed(1.5)}>1.5X</Button>
<Button style={{ marginLeft: 5 }} size="small" onClick={() => this.handleSpeed(2)}>2X</Button>
</div>
</Fragment>
);
}
}
B文件:
import AudioPlayer from 'components/Audio/AudioPlayer';
<Row style={{ marginBottom: 10, paddingLeft: 50 }}>
<AudioPlayer src={model.recordUrl} />
</Row>
2.权限组件
// 组件:
import { PureComponent } from 'react';
import { connect } from 'dva';
@connect(({ user }) => ({
permission: user.permission,
isAdmin: user.currentUser.superAdmin,
}))
export default class Auth extends PureComponent {
render() {
const { id, permission, isAdmin } = this.props;
const hasPermission = isAdmin || !!permission[id];
if (process.env.NODE_ENV === 'development') {
return typeof this.props.children === 'function' ? this.props.children(true) : this.props.children;
}
// support render props
// https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce
if (typeof this.props.children === 'function') {
return this.props.children(hasPermission);
}
return hasPermission ? this.props.children : null;
}
}
// rademe.md
jsx
<Auth id="authname">
<Button>xxx</Button>
</Auth>
或
<Auth id="authname">
{
(hasAuth) => {
return hasAuth ? <Button>xxx</Button> : <span>没有权限</span>
}
}
</Auth>
// A文件:
import Auth from "components/Auth";
<Auth id="后端返回的权限码">
<a onClick={() => listSelect(record)}>跟进</a>
</Auth>
3.table列表-columns
const bizTypeOptions = [{ value: '1', text: 'C1' }, { value: '2', text: 'C2' },
{ value: '6', text: '客服' }];
// 时间格式转化
transTime = (value) => {
const date = new Date(value);
const Y = date.getFullYear();
let m = date.getMonth() + 1;
let d = date.getDate();
let H = date.getHours();
let i = date.getMinutes();
let s = date.getSeconds();
if (m < 10) {
m = `0${m}`;
}
if (d < 10) {
d = `0${d}`;
}
if (H < 10) {
H = `0${H}`;
}
if (i < 10) {
i = `0${i}`;
}
if (s < 10) {
s = `0${s}`;
}
const t = `${Y}-${m}-${d} ${H}:${i}:${s}`;
return t;
};
[
{
title: "序号",
key: "id",
// eslint-disable-next-line no-mixed-operators
render: (text, record, index) => (
<span>{index + 1 + 10 * (pageIndex - 1)}</span>
),
},
{
title: '接通率',
key: 'callSuccessRatio',
dataIndex: 'callSuccessRatio',
align: 'center',
width: 100,
render: val => (`${val}%`),
},
{
title: '排班开始日期',
dataIndex: 'startDate',
},
{ title: '价格',
width: 200,
render: (row) => {
return (
<div>
<p>底价{row.floorPrice}售价{row.salePrice}<br />展板价{row.exhibitionPriceMin}-{row.exhibitionPriceMax}<br />金融{row.financialPriceMin}-{row.financialPriceMax}</p>
</div>
);
},
},
{
title: "绑定时间",
dataIndex: "operateTime",
key: "operateTime",
render: (text, record) => (
<span>
{record.operateTime ? this.transTime(record.operateTime) : ""}
</span>
),
},
{
title: '是否外呼',
dataIndex: 'isDial',
render: (text, item) => (
<span>{item.isDial === true ? '是' : '否'}</span>
),
},
{
title: "是否启用",
dataIndex: "enabled",
render: (h) => {
return <span>{h ? "是" : "否"}</span>;
},
},
{
title: '业务类型',
dataIndex: 'bizType',
render: val => <span>{bizTypeOptions.find(x => x.value === val) ? bizTypeOptions.find(x => x.value === val).text : ''}</span>,
},
{
title: '当前状态',
dataIndex: 'enabled',
render: enabled => (enabled === 1 ? '已启用' : '已关闭'),
},
{
title: '车源编号',
render: (record) => {
return (
<span>
<a onClick={() => this.listSelect(record)}>{record.carCode}</a>
</span>
);
},
},
{
title: '操作',
fixed: 'right',
align: 'center',
width: 120,
render: (item) => {
return (
<span>
<a onClick={this.handleEdit.bind(this, item)}>编辑</a>
<a onClick={this.cancelItem.bind(this, item)} style={{ marginLeft: 10 }}>删除</a>
</span>
);
},
}
]
<Col md={8} sm={8}>
<FormItem label="业务线">
{getFieldDecorator('bizType')(
<Select allowClear placeholder="请选择" style={{ width: '100%' }}>
{bizTypeOptions.map(x => <Option key={x.value}>{x.text}</Option>)}
</Select>
)}
</FormItem>
</Col>
<Col {...ColLayout}>
<FormItem label="是否本地" {...FormItemLayout}>
{getFieldDecorator('bizSubType')(
<Select placeholder="请选择" allowClear>
<Option value={1} key={1}>异地购</Option>
<Option value={2} key={2}>本地购</Option>
</Select>
)}
</FormItem>
</Col>
4.utils小方法
手机号中间四位加星
/**
* 手机号中间四位加星
* @param {string} tel
*/
export function getRemainPhone(tel) {
if (!tel || typeof tel !== 'string') {
return '';
}
return tel.replace(/-/g, '').replace(/(\d{3})\d*(\d{4})/, '$1****$2');
}
A文件:
<span>电话:{getRemainPhone(dealerMobile)} </span>
字符串拆成数组
// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
function urlToList(url) {
const urllist = url.split('/').filter(i => i);
return urllist.map((urlItem, index) => {
return `/${urllist.slice(0, index + 1).join('/')}`;
});
}
// 示例:
function urlToList(url) {
const urllist = url.split('/').filter(i => i);
return urllist.map((urlItem, index) => {
return `/${urllist.slice(0, index + 1).join('/')}`;
});
}
urlToList('/userinfo/2144/id')
(3) ['/userinfo', '/userinfo/2144', '/userinfo/2144/id']
是否是电话号
IsTelNumber(number) {
return /^(0\d{2}\d{8}(-\d{1,4})?)|(0\d{3}\d{7,8}(-\d{1,4})?)|1[3|4|5|6|7|8|9][0-9]{9}$/.test(number);
},
import { isTelNumber } from "utils/check";
<Input
placeholder="请填写服务号码"
style={{ width: 250 }}
onBlur={this.handleCheckExist}
/>
handleCheckExist = () => {
const number = getFieldValue("number");
if (isTelNumber(number)) {.....}
}
手机号验证
/*
*手机验证
*/
IsPhoneNumber(number) {
const phoneRexp = new RegExp(/^1[3|4|5|6|7|8|9][0-9]{9}$/);
if (number === '') {
return '';
} else {
return phoneRexp.test(number);
}
},
# A文件:
import { isPhoneNumber } from 'utils/check';
<Col md={8} sm={24}>
<FormItem {...formItemLayout} label={fieldLabels.TelePhone}>
{getFieldDecorator("telephone", {
rules: [
{
required: true,
message: "请输入正确的手机号",
validator: (rule, value, callback) => {
if (isPhoneNumber(value)) {
callback();
} else {
callback(rule.message);
}
},
},
],
valuePropName: "text",
})(<PhoneEye />)}
</FormItem>
</Col>
或者在一些弹窗蒙层中校验后再做ajax操作
confirm({
title: '修改手机号',
content: Content,
onOk: (close) => {
if (!isPhoneNumber(mobilePhone)) {
Modal.error({
title: '错误',
content: '请您输入正确的11位手机号码',
});
return false;
}
this.props.dispatch({
type: 'xxxxxx,
payload: {
......
},
}).then((response) => {
});
return false;
},
});
姓名验证
/*
*姓名验证
*/
IsName(number) {
const nameRexp = new RegExp(/^[\u4e00-\u9fa5]{2,8}$/);// 2-8位汉字
if (number === '') {
return '';
} else {
return nameRexp.test(number);
}
},
登录密码验证
/*
*登录密码验证
*正则表达式验证(数字+字母)或者(数字+特殊字符)或者(字母+特殊字符),不能是纯数字、纯字母、纯特殊字符,即只要符合这3个组合其中之一都为true
*/
IsPassword(number) {
const passwordRexp = new RegExp(/^((?=.*[a-zA-Z])(?=.*\d)|(?=[a-zA-Z])(?=.*[#@!~%^&*])|(?=.*\d)(?=.*[#@!~%^&*]))[a-zA-Z\d#@!~%^&*]{6,20}$/);
if (number === '') {
return '';
} else {
return passwordRexp.test(number);
}
},
年龄是否满足成年人
/**
*年龄是否满足成年人
* @param {number} number
*/
IsAdult(number) {
if (number === '') {
return '';
} else if (number >= 18 && number <= 100) {
return true;
} else {
return false;
}
},
};
元转换万元
// 元转换万元
export const wanYuan = (value) => !isNaN(Number(value) / 10000) && `${(Number(value) / 10000).toFixed(2)}万元`;
# A文件使用:
import { wanYuan } from "../../utils";
<Col span={8} style={colStyle}>
新车指导价: {details && details.riskApply && details.riskApply.newVehiclePrice && wanYuan(details.riskApply.newVehiclePrice)}
</Col>
获取cookie中value
export function getCookie(name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}
# A文件:
import {getCookie, getQueryString} from '../utils';
const phone = getCookie('phone');
获取浏览器地址参数 问号后面
// 获取浏览器地址参数 问号后面
export function getQueryStringQuestion(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.href.split('?')[1].match(reg)
if (r != null) return decodeURI(r[2]);
return null;
}
A文件:
import { getQueryStringQuestion } from "../../utils";
instanceId: getQueryStringQuestion("id") || "",
获取选项值
//获取选项值
export function getValue(name, key, value) {
let result = undefined;
if (name) {
for (let i in name) {
if (value) {
result = name[key][value];
} else {
result = name[key];
}
}
}
return result;
}
import { getValue } from '../utils';
金融来源:<span>{getValue(formdata.approval, 'padFundedFrom')}</span>
示例:
var aaa={'name':'zhangsan',data:'1'}
getValue(aaa,'name') // 张三
5.踩坑手记
踩坑手记
- antd组件默认属性(例如initialValue、defaultValue、defaultFileList)只会被初始化一次),直接修改不会重新渲染,如果需要重新渲染则需要修改另外的属性值(例如setFieldsValue、value、fileList)
- setState直接修改数组或者对象的某个值是不会导致渲染的(深复制,浅复制的问题)代码入下:
```javascript
//❌
//获取父组件的数据
var arr = this.state.tags;
//子组件中传来的数据数组
for(var i=0; i<rows.length; i++){
//isAddData方法是判断数据存不存在
if(this.isAddData(arr, rows[i])){
arr.unshift(rows[i]);
}
}
this.setState({ tags: arr });
//✅
var arr = this.state.tags;
//子组件中传来的数据数组
for(var i=0; i<rows.length; i++){
//isAddData方法是判断数据存不存在
if(this.isAddData(arr, rows[i])){
arr.unshift(rows[i]);
}
}
this.setState({ tags: [...arr] });
-
同一个页面多个子组件调用同一个接口,数据可能会导致出错问题
解决方案: 用 state 定义一个 name: 设置 true/false,等待第一个接口加载完毕,再进行 第二次请求(建议设计组件的时候数据可以传入)
-
同一个接口需要点击多次,以最后一次请求为准,因为没有用 loading 才会 出现的数据回来了但是界面没有刷新数据
解决方案:fetch 添加一个 type:’takeLatest’,(takeEvery(默认),takeLatest,throttle,watcher)代码如下:
effects: {
fetch: [function* ({payload}, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(calendarMonth,payload);
if(response){
yield put({
type: 'save',
payload: response,
});
}
}, {type: 'takeLatest'}]
}
注意: watcher是指传入的任务函数就是一个watcher直接fork就好 throttle还要传入一个ms配置,这个ms代表着在多少毫秒内只触发一次同一类型saga任务 而takeEvery是不会限制同一类型执行次数 takeLatest只能执行一个同一类型任务,有执行中的再次执行就会取消
-
一个页面由多个组件组成 如何清空表单 如果通过emit分别触发各个组件,是可以实现的,但感觉代价太大
解决方案:不需要emit,this.props.dispatch(),dispatch 是根据你里面设置的type内容 然后转发到指定的model的,所以你这边 要设置正确以后,在model那边才能接收到你发送的这条action,model中reducers 处理数据(同步),effects接收数据(异步逻辑都要在effects里),subscriptions 监听数据,代码如下
this.props.dispatch({
type: 'global/clearNotices',
payload: type,
});
//models
export default {
namespace: 'global',
reducers: {
clearNotices(state, action) {
return {
...state,
list: action.payload,
};
},
},
};
- 单页面应用不要使用location.href这种方式会导致整页刷新,写类似a的跳转可以使用Link组建,如果js控制跳转,代码如下:
import { routerRedux } from 'dva/router';
this.props.dispatch(routerRedux.push({
pathname: '/user/register-result',
state: {
account,
},
}));
-
一个页面由多个组件组成,跨组件校验问题现在是通过Modal弹窗自己实现的 ,当组件保存按钮在别的组件里,需要带标识实现,实现特别复杂费劲(任务跟进是这样实现的)
-
TABLE翻页 selectRow一定要清空
-
页面级的数据尽量不放store中,放在本文件state中,这种类似于session的 会带来bug
-
变量在不通层级中尽量不要重复,析构时候会指向不明
-
this问题 箭头函数this是成员this,普通函数this指向函数本身,可以在钩子函数中赋值给自定义的_this
-
route.js定义的组件中不要遗忘model的配置,否则dispatch的type的model没有配置则无法生效 代码如下:
//dynamicWrapper(app,[model],() =>import('router路径'))
const routerConfig = {
'/': {
component: dynamicWrapper(app, ['user', 'login'], () => import('../layouts/BasicLayout')),
},
-
render中改state容易死循环
解决方案:建议只有在componentWillMount,componentDidMount,componentWillReceiveProps方法中可以修改state值,在componentWillUpdate和componentDidUpdate中修改也可能导致无限循环调用
开发建议
- 文件拆分和建文件夹归类 有的文件好几千行代码
- 文件组件化,现在很多文件都是为了实现自己的功能
- 根据是否需要高度的复用性,把组件划分为 Dumb 和 Smart 组件,约定俗成地把它们分别放到 components 和 containers 目录下。