前言
本人因所在地四月疫情原因,导致无法在京的实习因为延期无法到达而遗憾收场。在学校度过了两个月,最后在暑假在家附近找了一个公司来前端实习,距今已经有将近四个月了,在这里工作氛围很好,leader也待人和善,但是在这里呆了四个月,我最终决定结束实习,展望未来,迎接春招。
公司主要技术栈
- 框架使用的是react16.8以后的函数式组件开发。
- 使用了umi框架,进行了一些权限设置、路由设置、和接口请求封装。
- 组件库使用的是antd,4x的版本。
离开原因
- 公司业务较为重复,不利于未来发展。 我所在的公司,业务一般都是面向后台管理系统 + 首页门户的模式,大多数都是表单的格式,来进行后台数据的管理,没有自己的产品。
- 同学喜迎offer,心中略有不甘,我有一些同学线上加线下实习后,在秋招面试中获得了喜人的成绩,而我因实习时间不太长,还在学习一些业务上的知识,导致没有准备错过秋招。
- 公司排期紧,首先导致加班严重,我没有时间自学其他方面知识,其次其他同事也因为排期很忙,导致有一些问题会一知半解或者随便找以前的业务代码来学习,往往在解决完问题后仍然一知半解,因为排期紧而又没有时间自己弄懂,不利于未来发展。
收获总结
- 熟悉了业务,懂得了前后端联调,和基本的接口请求等。
- 熟悉了antd方面关于表单后台的一些组件的使用。
- 学会了自学查看属性及其文档,
学习总结:
封装组件
在我原来的文章中要恋爱好歹开个小鹏吧中,就是运用react-router@6 版本来配置路由,用自己的代码没用组件来实现一个手机效果的分页切换路由的效果。而在实习期间,因为使用umi4,Umi4 使用 react-router@6 作为路由组件,并使用antd组件实现类似的效果。
配置前台门户路由
{
path: '/portal',
name: '门户',
layout: false,
routes: [
{
path: '/portal/home',
name: 'test1111',
icon: '',
component: './LzjcPages/Portal/Home',
// access: 'canNormalUser',
},
{
path: '/portal/organization',
name: 'test2222',
icon: '',
component: './LzjcPages/Portal/Organization',
// access: 'canNormalUser',
},
{
path: '/portal/learnCenter',
name: 'test33333',
icon: '',
component: './LzjcPages/Portal/LearnCenter',
// access: 'canNormalUser',
},
{
path: '/portal/activeCenter',
name: 'test44444',
icon: '',
component: './LzjcPages/Portal/ActiveCenter',
// access: 'canNormalUser',
},
{
path: '/portal',
redirect: './LzjcPages/Portal/Home',
},
],
},
配置基本上同react-router@6,并且因为增加了权限控制access能够根据用户身份,来控制用户是否能够进入改页面。并且最后一个路由会一定匹配,因为与父路由一样,当其他子路由都没有匹配到的时候,直接跳转到首页。
封装公用组件HomeLayout
HomeLayout 是门户路由通用的组件,用来进行门户模块的切换时候的导航栏,在这里可以用来写一些通用的方法,例如登出、查询个人信息等,
const PortalLayout = (props) => {
const { tabKey, children } = props;
const [curTab, setCurTab] = useState('home');
const [tabMenus, setTabMenus] = useState([ //
{ title: 'test1', key: 'home' },
{ title: 'test2', key: 'organization' },
{ title: 'test3', key: 'learnCenter' },
{ title: 'test4', key: 'activeCenter' },
{ title: 'test5', key: 'checkCenter' },
]);
useEffect(() => {
setCurTab(tabKey);
}, [tabKey]);
const switchTab = (key) => {
if (key != 'courseDetailCenter' || checkLogin()) {
setCurTab(key);
history.push(`/portal/${key}`);
}
};
const loginOut = async (currentUser) => {
await outLogin();
Cookies.remove('accessToken'); //删除存储
const { origin, href } = window.location;
let url = `${origin}${ORIGIN_BASE}/#${loginPath}?redirect=${href}&appCode=${APP_CODE}`;
redirectLoginPath(url);
};
return (
<div className={styles.proContainer}>
<img className={styles.topImg} src={appImages.bgbanner} alt="" />
<div className={styles.topToolBar}>
<span className={styles.welcomeLine}>
欢迎来到党建工作{' '}
{formatTime({ type: 'yyyy-mm-dd', isweek: true })}
</span>
<span className={styles.enterBlock}>
<span onClick={() => { history.push('/index') }}>后台入口</span>
{/* <span>{currentUser && currentUser.user.userName}</span> */}
<span onClick={() => { loginOut() }}> 退出</span>
</span>
</div>
<div className={styles.tabMenus}>
<ul className={styles.nvaMenu}>
{tabMenus.map((d) => (
<li
className={`${styles.tabMenu} ${curTab == d.key && styles.tabMenuSelected}`}
key={d.key}
onClick={() => {
switchTab(d.key);
}}
>
<span>{d.title}</span>
</li>
))}
</ul>
</div>
<div className={styles.header}>
{children} // 渲染子组件
</div>
</div>
);
}
在上面代码中,封装了通用组件,来作为门户路由的通用组件,所有门户组件如首页都要引入改组件,作为导航栏,并且使用postion:stick 来使得导航栏一直出现在门户页面上。
<PortalLayout tabKey="home">
<div style={{display:"flex",justifyContent:"center",}}>
<div style={{fontWeight:"bold",fontSize:"40px"}}>test111111</div>
</div>
</PortalLayout>
StepsForm - 分步表单
实习目标效果如下:这里业务是一个复杂的流程表单,业务目标是在每个小流程中的下一步的时候,都要请求post 接口,保存当前小流程,并且当保存下一步后,不能退回进行修改了。当进入下一流程需要进行判断:如果以前进提交过大流程,只需要进入下一流程就可以。如果以前没有提交过大流程,则需要请求接口将整个大流程进行post接口提交。
定义主要变量:
current:表示当前处在哪个大流程
current1,current2,current3,current4,current5 分布控制处在大流程下的组件中所在小流程状况。
stepsKey 用来表示当前角色最高已经走到哪个大流程,因为我们能够返回上一流程,此时我们并不知道我们的大流程表单是否需要提交,若stepKey==current 当前流程,则表示当前走的流程是我们还未提交的,需要调用接口发送数据,如果stepKey > current,表示以前已经走过了,此时不需要调用接口,直接将current = current +1 ,通过current + 用户id + current1/2/3/4/5 来请求接口,拿到对应的数据。
代码实现:
<StepsForm
current={current}
formMapRef={formRef}
submitter={{
render: (props) => {
if (props.step > 0 && props.step <= 3) {
return [
<Button key="pre" onClick={() => {
setCurrent(current - 1)
}}>
上一流程
</Button>,
// {(current==1&¤t1==2)}
((current == 0 && current1 >= 2) || (current == 1 && current2 >= 4) || (current == 2 && current3 >= 5) || (current == 3 && current4 >= 7) || current == 4 && (current5 >= 7)) && <Button type="primary" key="goToTree" onClick={() => {
formStep.validateFields().then((errors, values) => {
props.onSubmit?.()
}).catch((error) => { });
}}>
下一流程
</Button>,
];
}
if (props.step <= 3) {
return [
((current == 0 && current1 == 2) || (current == 1 && current2 == 4) || (current == 2 && current3 == 5) || (current == 3 && current4 >= 7) || current == 4 && (current5 >= 7)) && <Button type="primary" key="goToTree" onClick={() => {
formStep.validateFields().then((errors, values) => {
props.onSubmit?.()
}).catch((error) => { });
}}>
下一流程
</Button>,
]
}
if (props.step == 4) {
return [
<Button key="pre" onClick={() => {
setCurrent(current - 1)
}}>
上一流程
</Button>,
(current == 4 && (current5 >= 7)) && <Button type="primary" key="goToTree" onClick={() => {
formStep.validateFields().then((errors, values) => {
props.onSubmit?.()
}).catch((error) => { });
}}>
完成
</Button>,
];
}
},
}}
>
<StepsForm.StepForm name="step1" title="大流程1" className={styles.stepsleft} onFinish={(value) => {
if (current1 == 2) {
if (stepsKey == 0) {
handOneFinish()
} else {
setCurrent(current + 1)
setStepsKey(current + 1)
}
}
}}>
{current == 0 &&
<PartyApply
dict={dict}
formRef={formStep}
activeKeyForm={activeKeyForm}
stepsKey={current}
initialValues={initialValues}
current={current1}
setCurrent={setCurrent1}
file={file}
setFile={setFile}
fileList={fileList}
setFileList={setFileList}
conversation={conversation}
setConversation={setConversation}
conversationList={conversationList}
setConversationList={setConversationList}
id={id}
setId={setId}
/>
}
</StepsForm.StepForm>
<StepsForm.StepForm name="step2" title='大流程2' className={styles.stepsleft} onFinish={(value) => {
if (current2 == 4) {
if (stepsKey == 1) {
handTwoFinish()
} else {
setCurrent(current + 1)
setStepsKey(current + 1)
}
}
}}>
{current == 1 &&
<PartyMember
dict={dict}
formRef={formStep}
activeKeyForm={activeKeyForm}
stepsKey={current}
initialValues={initialValues}
current={current2}
setCurrent={setCurrent2}
file={file}
setFile={setFile}
fileList={fileList}
setFileList={setFileList}
conversation={conversation}
setConversation={setConversation}
conversationList={conversationList}
setConversationList={setConversationList}
keep={keep}
keepList={keepList}
setKeep={setKeep}
setKeepList={setKeepList}
register={register}
registerList={registerList}
setRegister={setRegister}
setRegisterList={setRegisterList}
id={id}
setId={setId}
/>
}
</StepsForm.StepForm>
<StepsForm.StepForm name="step3" title='大流程3' className={styles.stepsleft} onFinish={(value) => {
if (current3 == 5) {
if (stepsKey == 2) {
handThreeFinish()
} else {
setCurrent(current + 1)
setStepsKey(current + 1)
}
}
}}>
{
current == 2 &&
<GrowTarget
formRef={formStep}
activeKeyForm={activeKeyForm}
dict={dict}
initialValues={initialValues}
current={current3}
setCurrent={setCurrent3}
stepsKey={current}
file={file}
setFile={setFile}
fileList={fileList}
setFileList={setFileList}
conversation={conversation}
setConversation={setConversation}
conversationList={conversationList}
setConversationList={setConversationList}
keep={keep}
keepList={keepList}
setKeep={setKeep}
setKeepList={setKeepList}
register={register}
registerList={registerList}
setRegister={setRegister}
setRegisterList={setRegisterList}
id={id}
setId={setId}
/>
}
</StepsForm.StepForm>
<StepsForm.StepForm name="step4" title='大流程4' className={styles.stepsleft} onFinish={(value) => {
if (current4 == 7) {
if (stepsKey == 3) {
handFourFinish()
} else {
setCurrent(current + 1)
setStepsKey(current + 1)
}
}
}}>
{
current == 3 &&
<PartyReceive
formRef={formStep}
activeKeyForm={activeKeyForm}
dict={dict}
initialValues={initialValues}
current={current4}
setCurrent={setCurrent4}
stepsKey={current}
file={file}
setFile={setFile}
fileList={fileList}
setFileList={setFileList}
conversation={conversation}
setConversation={setConversation}
conversationList={conversationList}
setConversationList={setConversationList}
keep={keep}
keepList={keepList}
setKeep={setKeep}
setKeepList={setKeepList}
register={register}
registerList={registerList}
setRegister={setRegister}
setRegisterList={setRegisterList}
wishBook={wishBook}
setWishBook={setWishBook}
wishBookList={wishBookList}
setWishBooksList={setWishBooksList}
opinion={opinion}
setOpinion={setOpinion}
opinionList={opinionList}
setOpinionList={setOpinionList}
id={id}
setId={setId}
/>
}
</StepsForm.StepForm>
<StepsForm.StepForm name="step5" title='大流程5' className={styles.stepsleft} onFinish={(value) => {
if (current5 == 7) {
if (stepsKey == 4) {
handFiveFinish()
} else {
setVisible(false)
actionRef.current.reload();
}
}
}}>
{
current == 4 &&
<PartyWorker
formRef={formStep}
activeKeyForm={activeKeyForm}
dict={dict}
initialValues={initialValues}
current={current5}
setCurrent={setCurrent5}
stepsKey={current}
file={file}
setFile={setFile}
fileList={fileList}
setFileList={setFileList}
keep={keep}
keepList={keepList}
setKeep={setKeep}
setKeepList={setKeepList}
register={register}
registerList={registerList}
setRegister={setRegister}
setRegisterList={setRegisterList}
wishBook={wishBook}
setWishBook={setWishBook}
wishBookList={wishBookList}
setWishBooksList={setWishBooksList}
opinion={opinion}
setOpinion={setOpinion}
opinionList={opinionList}
setOpinionList={setOpinionList}
id={id}
setId={setId}
/>
}
</StepsForm.StepForm>
</StepsForm>
代码详情:
首先拆分结构忽略功能, 我们最基本的代码结构是如下:下面代码就能够将大流程的流程图显示出来,通过current={current}来自动判断当前在哪个流程。
<StepsForm
current={current}
submitter={...}
>
<StepsForm.StepForm name="step5" title='大流程1' onFinish={()=>{}}>
</StepsForm.StepForm>
<StepsForm.StepForm name="step5" title='大流程2' onFinish={()=>{}}>
</StepsForm.StepForm>
<StepsForm.StepForm name="step5" title='大流程3' onFinish={()=>{}}>
</StepsForm.StepForm>
<StepsForm.StepForm name="step5" title='大流程4' onFinish={()=>{}}>
</StepsForm.StepForm>
<StepsForm.StepForm name="step5" title='大流程5' onFinish={()=>{}}>
</StepsForm.StepForm>
</StepsForm>
==============================================
其次通过current 当前流程,来调用该流程对应的组件,该组件控制的是小流程的显示,通过传入currentn, 进行小流程控制
{ current == n && <SomeComponent/>}
// 下面写一个大流程1对应的组件
const StepsProces = (props) => {
const { formRef, stepsKey, dict, activeKeyForm, initialValues, current, setCurrent, file, setFile, fileList, setFileList, conversation, setConversation, conversationList, setConversationList, id, setId, ...propsI } = props;
const actionRef = useRef();
const [validator, setValidator] = useState([]);
const validatorData = validator ? validatorObj(validator) : {};
const accessToken = Cookies.get('accessToken') || ''; //token
//form回显
useEffect(() => {
.... // 表单数据回显
}, [])
const stepsForm1 = (<div>
<Form form={formRef} action={actionRef} layout="vertical" >
<Row gutter={24}>
<Col span={8}>
<Form.Item name="sqrdzz" label="吃饭" rules={formItemValidator(validatorData.sqrdzz)}>
<TreeSelect
treeData={handleData(
dict?.treeData,
{ nodeName: 'name', nodeId: 'id', children: 'children' },
'treeSelect',
)}
showCheckedStrategy={TreeSelect.SHOW_ALL}
placeholder="吃饭"
treeDefaultExpandAll
disabled={current == 2 || activeKeyForm > 1}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="sqrdsj" label="睡觉" rules={formItemValidator(validatorData.sqrdsj)}>
<DatePicker placeholder='睡觉' disabled={current == 2 || activeKeyForm > 1} />
</Form.Item>
</Col>
</Row>
</Form>
</div >)
const stepsForm2 = current > 1 && (<div>
<Form form={formRef} layout="vertical">
<Row gutter={24}>
<Col span={8}>
<Form.Item name="thr" label="多读书" rules={formItemValidator(validatorData.thr)}>
<Select placeholder="多读书" disabled={activeKeyForm > 1} showSearch filterOption={(input, option) =>
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0
}>
{dict.dyryxx &&
dict.dyryxx.map((item, index) => (
<Option value={item.id} key={index}>
{item.xm}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="thsj" label="多看报" rules={formItemValidator(validatorData.thsj)}>
<DatePicker placeholder='多看报' disabled={activeKeyForm > 1} />
</Form.Item>
</Col>
</Row>
</Form>
</div>)
// 请求下一步的接口,对当前小流程进行保存
const handleNext = () => {
.......
}
return (
stepsKey == 0 &&
<div className={styles.stepsInfo}>
<Steps progressDot direction="vertical" current={current} initial={1} className={styles.stepsInfoHeader}>
<Step key={1} title={"小流程1111"} description={stepsForm1} />
<Step key={2} title={"小流程2222"} description={current > 1 && stepsForm2} />
</Steps>
<div className="steps-action">
{current < 2 && (
<Button type="primary" onClick={() => { handleNext() }}>
下一步
</Button>
)}
</div>
</div>
);
};
代码解释:
小流程需要用到antd的Steps的步骤条,也跟StepsForm 类似,有总Steps 和 分 Step, Step 的每一个description 都表示一个Form表单,表达的就是小流程,有完整的表单事件。然后当点击下一步,会将当前处在的流程进行上传接口,进入下一个小流程。
Echars 图
Echars 图在实现前我也接触过,在实习中也用到了,复习了一遍还是很不错的,其中主要难点都在配置项的一些繁琐配置中,在开发的过程中,经常都是去Examples 例子中,找到相似的在此基础上进行修改,常常会遗漏某些配置项的属性的意义,在总结中也给我了复习的机会,重新全面了解这些配置项。
例如:以玫瑰图为例子,列举其详细属性
// 玫瑰图设置
const Echars2 = (props) => {
const { id } = { ...props } // 通过透传获取id,该id用于请求接口,获取某个id下的数据项
let option = { // 配置项
legend: { // 图例组件。 图例组件展现了不同系列的标记(symbol),颜色和名字。可以通过点击图例控制哪些系列不显示
top: '15%', // 图例组件离容器上侧的距离,可以是百分比,也可以是像素,也可以是单词如:middle,top,bottom
width: "20%", // 图例的宽度,当orient设置为vertical,就是每一行一个图例,设置horizontal只有一行排满才会换行
right: "5%", // 距离右边的距离 top,width,right,left 不给值默认都是auto
orient:"vertical", // 设置排列为竖向排列,默认设置为"horizontal",横向排列
// type :'plain':普通图例。缺省就是普通图例。 / 'scroll':可滚动翻页的图例。当图例数量较多时可以使用。
// tooltip 在legend 中也有tooltip,表示的是鼠标悬浮图例显示提醒,也有formatter,返回标签显示效果
// formatter: function (name) {
// return echarts.format.truncateText(name, 40, '14px Microsoft Yahei', '…'); //name:数据, 40宽度, 14px.. 字体
// },
},
tooltip: {
trigger: 'item',
formatter: function (params) {
return `<span>${params.name}</span></br><span style="color:#5470C6">${params.data.value}(${params.percent}%)</span>`
},
borderColor: "#f2f3f5",
},
toolbox: {
show: true,
},
series: [
{
name: '暂无',
type: 'pie',
radius: [20, 80],
center: ['35%', '50%'],
roseType: 'area',
// itemStyle: {
// borderRadius: 5
// },
label: {
show: false
},
data: []
}
]
};
const initChart = () => {
let element = document.getElementById('chart2');
let myChart = echarts.init(element);
myChart.clear()
myChart.setOption(option);
}
const ageSwitch = (num) => {
switch (parseInt(num)) {
case 1:
return '28岁以下'
case 2:
return '29-35岁'
case 3:
return '36-50岁'
case 4:
return '51-60岁'
case 5:
return '61岁以上'
}
}
useEffect(async () => {
const resp = await getNlfbTj({ unitId: id })
if (resp.code == 200) {
let list = []
resp.result.map(v => {
list.push({ value: v.sl, name: ageSwitch(v.zd) })
})
console.log(list)
option.series[0].data = list
initChart()
} else {
message.error(resp.message)
}
}, [id])
return (
<>
<div style={{ display: "flex", justifyContent: "flex-start", height: "15%" }}>
<div className={styles.echars1}>
<div className={styles.image}></div>
<div className={styles.text}>党员年龄分布</div>
</div>
</div>
{
<div id='chart2' style={{ width: '100%', margin: '14px auto 0px', height: '85%', marginTop: 6 }}>
</div>
// <div className={styles.brithEmpty}><img src={appImages.empty} /></div>
}
</>
)
}
export default Echars2;
各种配置项介绍
option:设置配置项
legend:
图例组件。 图例组件展现了不同系列的标记(symbol),颜色和名字。可以通过点击图例控制哪些系列不显示
legend.top 图例组件离容器上侧的距离,可以是百分比,也可以是像素,还可以是单词:middle,top,bottom
legend.bottom 图例组件离容器下侧的距离,可以是百分比,也可以是像素
legend.left/ legend.right 图例组件离容器左/右侧的距离,可以是百分比,也可以是像素 。
上面四个属性代表图例在容器中的位置,默认值是auto
legend.orient 图例组件的排列方式,是水平排列还是竖直排列,默认值为'horizontal'水平排列。'vertical'垂直排列需手动设置
legend.width/ height
图例组件的宽高,可以是百分比,也可以是像素 。 当图例组件设置水平排列,当width不够放下一个图例的Item的时候,就会在下一行重新进行水平排列,同理垂直排列类似,当高度不够下一个图例的Item的时候,改变width,移动继续垂直排列。
legend.type
'plain' : 普通图例,当width和height 都不够的时候,会发生缺省,就是普通实例
'scroll': 当宽和高都不够,也就是图例数量较多时,可滚动翻页的图例,需要详细配置
legend.tooltip 设置悬浮于某个图例的弹窗提醒,跟option.tooltip类似,在后面详细介绍
legend.formatter 制定悬浮框的某些样式,提示框浮层内容格式器,支持字符串模板和回调函数两种形式。
剩下的一些属性比较杂,不常用,多是Item图例的详细设置,就不多介绍了。
tooltip:
提示框组件的通用介绍:
提示框组件可以设置在多种地方:
- 可以设置在全局,即 tooltip
- 可以设置在坐标系中,即 grid.tooltip、polar.tooltip、single.tooltip
- 可以设置在系列中,即 series.tooltip
- 可以设置在系列的每个数据项中,即 series.data.tooltip
- 可以设置在图例中,即legend.tooltip
tooltip.trigger
触发类型,默认为 tooltip.trigger= 'item'
'item' : 数据项图形触发,主要在散点图,饼图等无类目轴的图表中使用
'axis' : 坐标轴触发,主要在柱状图,折线图等会使用类目轴的图表中使用。
'none': 什么都不触发。
tooltip.formatter
提示框浮层内容格式器,支持字符串模板和回调函数两种形式:
支持字符串模板
模板变量有
{a},{b},{c},{d},{e},分别表示系列名,数据名,数据值等。 在tigger为'axis'的时候,会有多个系列的数据,此时可以通过{a0},{a1},{a2}这种后面加索引的方式表示系列的索引。 不同图表类型下的{a},{b},{c},{d}含义不一样。 其中变量{a},{b},{c},{d}在不同图表类型下代表数据含义为:
- 折线(区域)图、柱状(条形)图、K线图 :
{a}(系列名称),{b}(类目值),{c}(数值),{d}(无)- 散点图(气泡)图 :
{a}(系列名称),{b}(数据名称),{c}(数值数组),{d}(无)- 地图 :
{a}(系列名称),{b}(区域名称),{c}(合并数值),{d}(无)- 饼图、仪表盘、漏斗图:
{a}(系列名称),{b}(数据项名称),{c}(数值),{d}(百分比)回调函数
formatter: function (params) { return `<span>${params.name}</span></br><span style="color:#5470C6">${params.data.value} (${params.percent}%)</span>` },其中 params.name 代替的是饼图的 {a}, params.data.value 代表 {c}, params.percent 代表{d},这些只需要打印出来就可以了。
回调函数比字符串模板相比,有一些优势,比如更加灵活,比如出现了在一个弹窗中的字符串中,得出现俩个不同颜色的,如上面的例子,这里使用模板字符串,使用统一的 tooltip.textStyle.color 的话会全部修改其颜色,因此最好的方法是通过回调函数,返回Html字符串,通过Html中的style 来设置特定某些文字的颜色。
tooltip.borderColor 提示框浮层的边框颜色。
tooltip.textStyle 提示框浮层的文本样式。拥有包括color,fontStyle,fontWeight,fontFamily,fontSize 等等属性。