项目介绍
一个机房监控项目,分为数据采集端和管理端。在机房内安装摄像头后,由各种模型分析产生各种类型报警(携带大件设备、动火监管、重要设备监管、堆放杂物和抽烟操作)。管理端能处理临时审批(由机房或者局站负责人审批是否能够进入机房),各种报警报表的展示,负责人管理等。
目录结构
\
Src
api
该目录存放项目的接口地址,每个模块对应一个接口文件,以下是审批模块的部分接口。
trial.js
import request from "@/utils/request";
import { CancelToken } from 'axios';
import cancels from "./cancels";
const trial = {
trialForm: (data) => request.post("/approval/approval", data), // 列表
getTrialRecord: (data) => {
cancels.getTrialRecord()
return request.get('/approval/approval', {
params: data,
cancelToken: new CancelToken(cancel => {
cancels.getTrialRecord = cancel;
})
})
}};export default trial;
cancelToken的作用是发生多次请求的情况下取消上次还没结束的请求
components
该目录存放公用组件
由于该项目滚动区域不是body,会导致使用antd组件时,菜单有滚动定位问题。所以针对此问题对antd组件做了一个简单的封装。
Antdd/BackTop.js
import { BackTop } from "antd";
function backTop(props) {
return <BackTop {...props} />;
}
backTop.propTypes = BackTop.propTypes;
backTop.defaultProps = {
...BackTop.defaultProps,
target: () => document.getElementById("BackTopMark"),
};
export default backTop;
FilterComponent.js\
import { useImperativeHandle, forwardRef } from "react";
import { Form, Row, Col, Button } from "antd";
import { Select } from "@/components/Antdd";
import SeverRoomChoose from "./SeverRoomChoose";
import { useSelector } from "react-redux";
import RangeTime from "@/components/RangeTime";
const FormItem = Form.Item;
const FilterComponent = (props, ref) => { const dict = useSelector(({ dict }) => dict); // 重置表单值
const locationIfm = useSelector(({ location }) => location);
const [form] = Form.useForm();
useImperativeHandle(ref, () => ({ formFields: form, }));
const initialValues = props.initialValues ? props.initialValues : null; return (
<Form layout="inline" onFinish={props.onFinish} form={form} initialValues={initialValues}>
<Row gutter={[20, 20]} style={{ width: "100%" }}>{props.hiddenPlacePick ? null :
(<Col span={24}>
<SeverRoomChoose hiddenRoom={props.hiddenRoom} form={form} locationIfm={locationIfm}/>
</Col>)} {props.hiddenStatus ? null : (<Col span={3}>
<FormItem name="approval_status">
<Select options={dict.approval_status} placeholder="审批状态"></Select> </FormItem>
</Col>)}
{props.extraFilters ? props.extraFilters.map((item) => item) : null}
<RangeTime form={form} />
<Col span={4}>
<FormItem>
<Button type="primary" htmlType="submit">查询</Button>
</FormItem>
</Col>
</Row>
</Form>);
};
const WrappedForm = forwardRef(FilterComponent);export default WrappedForm;
几乎所有的查询页面的顶部的筛选项都类似,由此封装了一个筛选组件,支持扩张其他筛选条件。只需要在父组件中传入extraFilters数组。该筛选组件中还包含一个地市,区域,局站,机房的级连组件(SeverRoomChoose)。可以发现这个筛选组件还向父组件暴露了实例formFields,必要的时候在父组件中可以通过绑定的ref获取该实例。
const getFormValue = useRef();
const { formFields } = getFormValue.current;
<FilterComponent ref={getFormValue}/>
layout
定义该项目的布局
LayoutMain.js
<Layout className="layout">
<Sider style={{overflow: "auto", height: "100vh", position: "fixed",left: 0,}}>
<SubMenu />
</Sider>
<Layout className="site-layout" style={{ marginLeft: 200 }}>
<Header />
<Layout.Content className="content-container">
<Content />
</Layout.Content>
</Layout>
</Layout>
由左边侧边栏,头部导航栏和右侧内容组成。
Content.js
<Layout.Content
style={{
margin: "30px 16px 0",
overflow: "auto",
background: "#fff",
position: "relative",
}}
id="BackTopMark"
>
<BackTop visibilityHeight={100} />
<Switch>
<Redirect exact from="/" to={routerConfig[0].path}></Redirect>
<Redirect exact path="/guide" to={routerConfig[0].path}></Redirect> {/* 增加视频路由 同时改变侧边栏 */}
<Redirect exact path="/videos" to="/404"></Redirect>
{renderRouteList(routerConfig)}
<Redirect to="/404"></Redirect>
</Switch>
</Layout.Content>
renderRouteList根据路由配置生成路由\
const renderRouteItem = (item) => (
<Route
key={item.path}
path={item.path}
component={item.component ? item.component : null}
></Route>
);
const renderRouteList = (list) =>
list.reduce((acc, item) => {
if (item.children === void 0) {
acc.push(renderRouteItem(item));
return acc;
}
item.children.forEach((subItem) => {
acc.push(renderRouteItem(subItem));
});
return acc;
}, []);
pages
这里的存放的都是每个模块,跟react-router对应的路由需要一一对应。每个模块内有多个页面, 每个页面都是一个文件夹,文件名就是页面名称,每个页面都要包含如下几个文件:
- index.jsx ---- 页面的入口文件
- index.module.scss ---- 页面所需要的样式 使用css module避免样式污染
routes
路由配置文件
import TemporaryTrial from "@/pages/Trial/TemporaryTrial/index";
import TrialPage from "@/pages/Trial/TrialPage/index";
const routes = [
{
path: "/guide/trial",
name: "临时审批",
children: [
{
path: "/guide/trial/temporary-trial",
name: "审批申请",
component: TemporaryTrial,
showStationListTitle: true,
},
]
}
];
export default routes;
可以在路由中配置showStationListTitle是否展示局站信息,在LayoutMain中可以遍历路由,在需要有特殊展示的路由中加入需要展示的组件。
Store
存放全局状态的文夹
我们知道每个页面都有自己的actions.js、actionTypes.js、reducer.js,但是这里是全局的,另外index.js会向外暴露store,然后在src根目录下的index.js中引入使用。
index.js
import { createStore, applyMiddleware, combineReducers } from "redux";
import thunkMiddleware from "redux-thunk";
import { persistStore, persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";/** * 这是一个 reducer,形式为 (state, action) => state 的纯函数。
* 描述了 action 如何把 state 转变成下一个 state。 * state 的形式取决于你,可以是基本类型、数组、对象、 * 惟一的要点是 当 state 变化时需要返回全新的对象,而不是修改传入的参数。 */
import loginReducer from "./login/reducer";
const rootReducer = combineReducers({ login: loginReducer,});
const persistConfig = { key: "root", storage,};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export default () => {
let store = createStore(persistedReducer, applyMiddleware(thunkMiddleware));
let persistor = persistStore(store);
return { store, persistor };
};
值得一提的是,针对PC端浏览器刷新后Store中的数据被清空,因此有必要在localStorage中保存Store。这里引入了redux-persist插件,在index.js中加入persistStore, persistReducer就能在浏览器刷新后仍能取到Store中的登录,地址信息等。
Style
存放一些全局样式\
utils
这里会存放一些自己的封装的js工具文件,比如我在项目基于axios封装了一个request.js,简化了axios的操作。\
request.js
import axios from "axios";
import { message } from "antd";
import config from "@/config";
// import qs from "qs";
const TIMEOUT = 5000;
const _axios = axios.create({
baseURL: config.url,
timeout: TIMEOUT,
headers: {
"Access-Control-Allow-Origin": "*",
},
});
_axios.interceptors.request.use(
(config) => {
// if (config.method === 'get') {
// config.paramsSerializer = function(params) {
// return qs.stringify(params, { arrayFormat: 'repeat' })
// }
// }
if (localStorage.getItem("token")) {
config.headers.Authorization = localStorage.getItem("token");
_axios.defaults.headers.Authorization = localStorage.getItem("token");
}
return config;
}, (err) => {
return Promise.reject(err);
});
_axios.interceptors.response.use(
(response) => {
if (response.status !== 200) {
message.error("接口出错了");
return Promise.reject(response.data);
}
return response.data;
}, (err) => {
if (err.message === `timeout of ${TIMEOUT}ms exceeded`) {
message.error("请求超时");
} else if (err.toString() === 'Cancel') {
} else {
message.error("接口出错了");
}
return Promise.reject(err);
}
);
export default _axios;
config.js
配置代理地址
index.js
webpack入口文件,主要一些全局js或者scss的导入,并执行react-dom下的render方法,代码如下:\
import React from "react";
import ReactDOM from "react-dom";import "@/style/index.css";
import "antd/dist/antd.css";import LayoutMain from "./layout";
import { history } from "@/routers";
import { Router } from "react-router-dom";
import { Provider } from "react-redux";
import storeConfig from "@/store";
import reportWebVitals from "./reportWebVitals";
import { PersistGate } from "redux-persist/integration/react";
import zhCN from "antd/es/locale/zh_CN";
import moment from "moment";
import "moment/locale/zh-cn";
import { ConfigProvider } from "antd";
const { persistor, store } = storeConfig();
moment.locale("zh-cn");
ReactDOM.render(
<ConfigProvider // 全局设置弹窗容器
getPopupContainer={(node) => {
// if (node) {
// return node
// }
return document.getElementById('BackTopMark');
}}
locale={zhCN}
>
<Router history={history}>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<LayoutMain />
</PersistGate>
</Provider>
</Router>
</ConfigProvider>,
document.getElementById("root"));
config-overrides.js
修改webpack配置文件
const path = require('path')
module.exports = {
webpack: (config) => {
config.output.library = 'server-room-guard'
config.output.libraryTarget = 'umd' // 添加别名
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, 'src'),
}
return config
},
devServer: (configFunction) => {
return function (proxy, allowHost) {
const config = configFunction(proxy, allowHost)
config.headers = {
'Access-Control-Allow-Origin': '*',
}
return config
}
},
总结
该项目使用react hooks开发所有组件,作为react的初学者,觉得hooks使用起来十分友好,上手快。开发过程中也发现有许多可以优化的地方, 并且都有做相关的组件封装(比如筛选,表格,级联选择,导出等等),在简化代码的同时极大的提升了开发效率。