@ -0,0 +1,17 @@ |
|||
{ |
|||
"presets": [ |
|||
"@babel/preset-react", |
|||
"@babel/preset-env" |
|||
], |
|||
"plugins": [ |
|||
"@babel/plugin-proposal-class-properties", |
|||
"@babel/plugin-proposal-object-rest-spread", |
|||
// ["import", { |
|||
// // "libraryName": "antd", |
|||
// "libraryDirectory": "es" |
|||
// }] |
|||
], |
|||
"env": { |
|||
"development": {} |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
{ |
|||
"version": "0.2.0", |
|||
"configurations": [ |
|||
{ |
|||
"name": "Server", |
|||
"type": "node", |
|||
"request": "launch", |
|||
"program": "${workspaceRoot}/server.js", |
|||
"args": [ |
|||
"-u http://127.0.0.1:4200" |
|||
], |
|||
"outputCapture": "std", |
|||
"env": { |
|||
"NODE_ENV": "development" |
|||
} |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,12 @@ |
|||
|
|||
FROM repository.anxinyun.cn/base-images/nodejs12:20.10.12.2 |
|||
|
|||
COPY . /var/app |
|||
|
|||
WORKDIR /var/app |
|||
|
|||
EXPOSE 8080 |
|||
|
|||
CMD ["-u", "http://localhost:8088"] |
|||
|
|||
ENTRYPOINT [ "node", "server.js" ] |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 202 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 226 KiB |
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 538 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 549 B |
@ -0,0 +1,14 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> |
|||
<link rel="shortcut icon" href="/assets/images/favicon.ico"> |
|||
</head> |
|||
|
|||
<body> |
|||
<div id='App'></div> |
|||
</body> |
|||
|
|||
</html> |
@ -0,0 +1,45 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
|
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> |
|||
<link rel="shortcut icon" href="/assets/images/favicon.ico"> |
|||
</head> |
|||
|
|||
<body> |
|||
<div id='App'></div> |
|||
|
|||
<!-- Webpack --> |
|||
<script type="text/javascript" src="http://localhost:5201/client/build/app.js"></script> |
|||
<!-- Vite --> |
|||
<script type="module"> |
|||
import RefreshRuntime from "http://localhost:5202/@react-refresh" |
|||
RefreshRuntime.injectIntoGlobalHook(window) |
|||
window.$RefreshReg$ = () => { } |
|||
window.$RefreshSig$ = () => (type) => type |
|||
window.__vite_plugin_react_preamble_installed__ = true |
|||
const global = window |
|||
</script> |
|||
<script type="module" src="http://localhost:5202/src/index.jsx"></script> |
|||
<!-- Vite End --> |
|||
<script> |
|||
//过滤掉一些无用的警告、没有价值的报错 |
|||
//代理console.warn方法 |
|||
|
|||
// const _consoleWarn = (...rest) => { |
|||
// console.error(...rest) |
|||
// }; |
|||
// console.error = (...rest) => { |
|||
// if ( |
|||
// ![ |
|||
// 'Each child in a list should have a unique "key" prop', |
|||
// ].some((item) => rest[0].indexOf(item) !== -1) |
|||
// ) { |
|||
// _consoleWarn(...rest); |
|||
// } |
|||
// }; |
|||
</script> |
|||
</body> |
|||
|
|||
</html> |
@ -0,0 +1,20 @@ |
|||
/** |
|||
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
|||
* Date: 2016/2/22 |
|||
* Time: 15:29 |
|||
* |
|||
*/ |
|||
'use strict'; |
|||
|
|||
const views = require('koa-view'); |
|||
const path = require('path'); |
|||
|
|||
module.exports = { |
|||
entry: function (app, router, opt) { |
|||
app.use(views(__dirname)); |
|||
|
|||
router.get('(.*)', async function (ctx) { |
|||
await ctx.render(path.join(__dirname, './index')); |
|||
}); |
|||
} |
|||
}; |
@ -0,0 +1,22 @@ |
|||
'use strict'; |
|||
|
|||
import React, { useEffect } from 'react'; |
|||
import Layout from './layout'; |
|||
import Auth from './sections/auth'; |
|||
|
|||
const App = props => { |
|||
const { projectName } = props |
|||
|
|||
useEffect(() => { |
|||
document.title = projectName; |
|||
}, []) |
|||
|
|||
return ( |
|||
<Layout |
|||
title={projectName} |
|||
sections={[Auth]} |
|||
/> |
|||
) |
|||
} |
|||
|
|||
export default App; |
@ -0,0 +1,22 @@ |
|||
import React from "react"; |
|||
|
|||
const Coming = () => { |
|||
return ( |
|||
<div style={{ |
|||
height: 'calc(100% - 12px)', width: '100%', backgroundColor: '#fff', |
|||
display: 'flex', justifyContent: 'center', alignItems: 'center', |
|||
position: 'absolute', |
|||
}}> |
|||
<img |
|||
src='/assets/images/background/building.jpg' |
|||
style={{ |
|||
maxHeight: 228, |
|||
maxWidth: 645, |
|||
width: '80%' |
|||
}} |
|||
/> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default Coming |
@ -0,0 +1,6 @@ |
|||
'use strict'; |
|||
import Coming from './coming' |
|||
|
|||
export { |
|||
Coming |
|||
}; |
@ -0,0 +1,8 @@ |
|||
'use strict'; |
|||
|
|||
import React from 'react'; |
|||
import { render } from 'react-dom'; |
|||
import App from './app'; |
|||
import './index.less'; |
|||
|
|||
render((<App projectName="飞尚物联" />), document.getElementById('App')); |
@ -0,0 +1,38 @@ |
|||
// webpack (vite 用 alias 兼容了) |
|||
@import '~@douyinfe/semi-ui/dist/css/semi.min.css'; |
|||
@import '~perfect-scrollbar/css/perfect-scrollbar.css'; |
|||
@import '~nprogress/nprogress.css'; |
|||
|
|||
|
|||
*, |
|||
*::before, |
|||
*::after { |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
html, |
|||
body { |
|||
margin: 0; |
|||
height: 100%; |
|||
width: 100%; |
|||
|
|||
a:link { |
|||
text-decoration: none; |
|||
color: unset |
|||
} |
|||
|
|||
a:visited { |
|||
text-decoration: none; |
|||
color: unset |
|||
} |
|||
|
|||
a:hover { |
|||
text-decoration: none; |
|||
color: unset |
|||
} |
|||
|
|||
a:active { |
|||
text-decoration: none; |
|||
color: unset |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
'use strict'; |
|||
import { RouteRequest } from '@peace/utils'; |
|||
import { RouteTable } from '$utils' |
|||
|
|||
export const INIT_LAYOUT = 'INIT_LAYOUT'; |
|||
export function initLayout (title, copyright, sections, actions) { |
|||
return { |
|||
type: INIT_LAYOUT, |
|||
payload: { |
|||
title, |
|||
copyright, |
|||
sections, |
|||
actions |
|||
} |
|||
}; |
|||
} |
|||
|
|||
export const RESIZE = 'RESIZE'; |
|||
export function resize (clientHeight, clientWidth) { |
|||
const headerHeight = 60 |
|||
const footerHeight = 0 |
|||
return { |
|||
type: RESIZE, |
|||
payload: { |
|||
clientHeight: clientHeight - headerHeight - footerHeight, |
|||
clientWidth: clientWidth |
|||
} |
|||
} |
|||
} |
|||
|
|||
export const INIT_API_ROOT = 'INIT_API_ROOT'; |
|||
export function initApiRoot () { |
|||
return dispatch => { |
|||
RouteRequest.get(RouteTable.apiRoot).then(res => { |
|||
localStorage.setItem('apiRoot', res.root); |
|||
dispatch({ |
|||
type: INIT_API_ROOT, |
|||
payload: { |
|||
apiRoot: res.root |
|||
} |
|||
}) |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
'use strict'; |
|||
|
|||
import * as global from './global' |
|||
import * as socket from './webSocket'; |
|||
|
|||
export default { |
|||
...global, |
|||
...socket, |
|||
}; |
@ -0,0 +1,33 @@ |
|||
'use strict'; |
|||
import io from 'socket.io-client'; |
|||
|
|||
export const INIT_WEB_SOCKET = 'INIT_WEB_SOCKET' |
|||
export function initWebSocket ({ ioUrl, token }) { |
|||
if (!ioUrl) { |
|||
ioUrl = localStorage.getItem('apiRoot') |
|||
} |
|||
if (!token) { |
|||
const user = sessionStorage.getItem('user') |
|||
if (user) { |
|||
token = JSON.parse(user).token |
|||
} |
|||
} |
|||
if (!ioUrl || !token) { |
|||
return { |
|||
type: '', |
|||
} |
|||
} |
|||
return dispatch => { |
|||
const socket = io(ioUrl, { |
|||
query: { |
|||
token: token |
|||
}, |
|||
}); |
|||
dispatch({ |
|||
type: INIT_WEB_SOCKET, |
|||
payload: { |
|||
socket: socket |
|||
} |
|||
}) |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
'use strict'; |
|||
import React from 'react'; |
|||
import moment from 'moment' |
|||
|
|||
export default class Footer extends React.Component { |
|||
render () { |
|||
// const { } = this.props; |
|||
|
|||
return ( |
|||
<div style={{ textAlign: 'center', lineHeight: '32px' }}> |
|||
Copyright © {moment().year()} All Rights Reserved 版权所有· 江西飞尚科技有限公司 |
|||
</div> |
|||
); |
|||
} |
|||
}; |
@ -0,0 +1,85 @@ |
|||
"use strict"; |
|||
import React from "react"; |
|||
import { connect } from "react-redux"; |
|||
import { Nav, Avatar, Dropdown } from "@douyinfe/semi-ui"; |
|||
|
|||
const Header = (props) => { |
|||
const { dispatch, history, user, actions, socket } = props; |
|||
|
|||
return ( |
|||
<> |
|||
<Nav |
|||
mode={"horizontal"} |
|||
onClick={({ itemKey }) => { |
|||
if (itemKey == "logout") { |
|||
dispatch(actions.auth.logout()); |
|||
if (socket) { |
|||
socket.disconnect(); |
|||
} |
|||
history.push(`/signin`); |
|||
} |
|||
}} |
|||
style={{ |
|||
height: 60, |
|||
minWidth: 520, |
|||
background: "url(/assets/images/background/header.png)", |
|||
backgroundSize: "100% 100%", |
|||
color: "white", |
|||
}} |
|||
header={{ |
|||
logo: ( |
|||
<img |
|||
src="/assets/images/background/logo.png" |
|||
style={{ display: "inline-block", width: 280, height: 52}} |
|||
/> |
|||
), |
|||
text: "", |
|||
}} |
|||
footer={ |
|||
<Nav.Sub |
|||
itemKey={"user"} |
|||
text={ |
|||
<div |
|||
style={{ |
|||
marginLeft: 20, |
|||
display: "inline-block", |
|||
color: "white", |
|||
}} |
|||
> |
|||
<img |
|||
src="/assets/images/background/notice.png" |
|||
style={{ |
|||
display: "inline-block", |
|||
width: 18, |
|||
height: 18, |
|||
position: "relative", |
|||
top: 6, |
|||
left: -10, |
|||
}} |
|||
/> |
|||
|
|||
<Avatar size="small" color="light-blue" style={{ margin: 4 }}> |
|||
<img src="/assets/images/avatar/6.png" /> |
|||
</Avatar> |
|||
{user && user.namePresent} |
|||
</div> |
|||
} |
|||
> |
|||
<Nav.Item itemKey={"logout"} text={"退出"} /> |
|||
</Nav.Sub> |
|||
} |
|||
/> |
|||
</> |
|||
); |
|||
}; |
|||
|
|||
function mapStateToProps(state) { |
|||
const { global, auth, webSocket } = state; |
|||
return { |
|||
actions: global.actions, |
|||
user: auth.user, |
|||
socket: webSocket.socket, |
|||
}; |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(Header); |
@ -0,0 +1,76 @@ |
|||
import React, { useEffect, useState } from 'react'; |
|||
import PerfectScrollbar from 'perfect-scrollbar'; |
|||
import { connect } from 'react-redux'; |
|||
import { Nav } from '@douyinfe/semi-ui'; |
|||
import { push } from 'react-router-redux'; |
|||
|
|||
let scrollbar = null |
|||
const Sider = props => { |
|||
const { collapsed, clientHeight, dispatch } = props |
|||
const [items, setItems] = useState([]) |
|||
const [selectedKeys, setSelectedKeys] = useState([]) |
|||
const [openKeys, setOpenKeys] = useState([]) |
|||
|
|||
useEffect(() => { |
|||
const { sections, dispatch, user } = props; |
|||
let nextItems = [] |
|||
for (let c of sections) { |
|||
if (typeof c.getNavItem == 'function') { |
|||
let item = c.getNavItem(user, dispatch); |
|||
if (item) { |
|||
nextItems.push.apply(nextItems, item) |
|||
} |
|||
} |
|||
} |
|||
setItems(nextItems) |
|||
|
|||
const lastSelectedKeys = localStorage.getItem('fs_iot_auth_selected_sider') |
|||
if (lastSelectedKeys) { |
|||
setSelectedKeys(JSON.parse(lastSelectedKeys)) |
|||
} |
|||
const lastOpenKeys = localStorage.getItem('fs_iot_auth_open_sider') |
|||
if (lastOpenKeys) { |
|||
setOpenKeys(JSON.parse(lastOpenKeys)) |
|||
} |
|||
|
|||
scrollbar = new PerfectScrollbar('#page-slider', { suppressScrollX: true }); |
|||
}, []) |
|||
|
|||
useEffect(() => { |
|||
if (scrollbar) { |
|||
scrollbar.update(); |
|||
} |
|||
}) |
|||
|
|||
return ( |
|||
<div id={'page-slider'} style={{ height: clientHeight, position: 'relative' }}> |
|||
<Nav |
|||
style={{}} |
|||
selectedKeys={selectedKeys} |
|||
openKeys={openKeys} |
|||
onSelect={({ selectedItems, selectedKeys, }) => { |
|||
const selectItem = selectedItems[0] |
|||
if (selectItem.to) { |
|||
dispatch(push(selectItem.to)) |
|||
} |
|||
setSelectedKeys(selectedKeys) |
|||
localStorage.setItem('fs_iot_auth_selected_sider', JSON.stringify(selectedKeys)) |
|||
}} |
|||
onOpenChange={({ openKeys }) => { |
|||
setOpenKeys(openKeys) |
|||
localStorage.setItem('fs_iot_auth_open_sider', JSON.stringify(openKeys)) |
|||
}} |
|||
items={items} |
|||
/> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
function mapStateToProps (state) { |
|||
const { global } = state; |
|||
return { |
|||
clientHeight: global.clientHeight, |
|||
}; |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(Sider); |
@ -0,0 +1,6 @@ |
|||
'use strict'; |
|||
import Layout from './layout'; |
|||
import NoMatch from './no-match'; |
|||
|
|||
export { Layout }; |
|||
export { NoMatch }; |
@ -0,0 +1,145 @@ |
|||
'use strict'; |
|||
|
|||
import React, { useState, useEffect } from 'react'; |
|||
import { connect } from 'react-redux'; |
|||
import { Layout, Notification } from '@douyinfe/semi-ui'; |
|||
import Sider from '../../components/sider'; |
|||
import Header from '../../components/header'; |
|||
import Footer from '../../components/footer'; |
|||
import { resize } from '../../actions/global'; |
|||
import * as NProgress from 'nprogress'; |
|||
import PerfectScrollbar from 'perfect-scrollbar'; |
|||
|
|||
NProgress.configure({ |
|||
template: ` |
|||
<div class="bar" style="height:2px" role="bar"> |
|||
<div class="peg"></div> |
|||
</div> |
|||
<div class="spinner" role="spinner"> |
|||
<div class="spinner-icon"></div> |
|||
</div> |
|||
` |
|||
}); |
|||
|
|||
let scrollbar |
|||
|
|||
const LayoutContainer = props => { |
|||
console.log(props); |
|||
const { |
|||
dispatch, msg, user, copyright, children, sections, clientWidth, clientHeight, |
|||
location, match, routes, history |
|||
} = props |
|||
const [collapsed, setCollapsed] = useState(false) |
|||
|
|||
NProgress.start(); |
|||
|
|||
const resize_ = () => { |
|||
dispatch(resize( |
|||
document.body.clientHeight, |
|||
document.body.clientWidth - (collapsed ? 120 : 240) |
|||
)); |
|||
} |
|||
|
|||
useEffect(() => { |
|||
scrollbar = new PerfectScrollbar('#page-content', { suppressScrollX: true }); |
|||
|
|||
window.addEventListener('resize', resize_); |
|||
return () => { |
|||
window.removeEventListener('resize', resize_); |
|||
} |
|||
}, []) |
|||
|
|||
useEffect(() => { |
|||
NProgress.done(); |
|||
if ((!user || !user.authorized) && location.pathname != '/cross') { |
|||
history.push('/signin'); |
|||
} |
|||
if (msg) { |
|||
if (msg.done) { |
|||
Notification.success({ |
|||
// title: msg.done, |
|||
content: msg.done, |
|||
duration: 2, |
|||
}) |
|||
} |
|||
if (msg.error) { |
|||
Notification.error({ |
|||
// title: msg.error, |
|||
content: msg.error, |
|||
duration: 2, |
|||
}) |
|||
} |
|||
} |
|||
const dom = document.getElementById('page-content'); |
|||
if (dom) { |
|||
scrollbar.update(); |
|||
dom.scrollTop = 0; |
|||
} |
|||
}) |
|||
|
|||
return ( |
|||
<Layout id="layout"> |
|||
<Layout.Header> |
|||
<Header |
|||
user={user} |
|||
pathname={location.pathname} |
|||
toggleCollapsed={() => { |
|||
setCollapsed(!collapsed); |
|||
}} |
|||
collapsed={collapsed} |
|||
history={history} |
|||
/> |
|||
</Layout.Header> |
|||
<Layout> |
|||
<Layout.Sider> |
|||
<Sider |
|||
sections={sections} |
|||
dispatch={dispatch} |
|||
user={user} |
|||
pathname={location.pathname} |
|||
collapsed={collapsed} |
|||
/> |
|||
</Layout.Sider> |
|||
<Layout.Content> |
|||
<div style={{ |
|||
margin: '12px 12px 0px', |
|||
background: "#F6FAFF", |
|||
}}> |
|||
<div id="page-content" style={{ |
|||
height: clientHeight - 12, |
|||
minWidth: 520, |
|||
position: 'relative', |
|||
}}> |
|||
<div style={{ |
|||
minHeight: clientHeight - 32 - 12, |
|||
position: 'relative', |
|||
padding: '12px 8px', |
|||
}}> |
|||
{children} |
|||
</div> |
|||
<Layout.Footer> |
|||
<Footer /> |
|||
</Layout.Footer> |
|||
</div> |
|||
</div> |
|||
</Layout.Content> |
|||
</Layout> |
|||
</Layout > |
|||
) |
|||
} |
|||
|
|||
function mapStateToProps (state) { |
|||
const { global, auth, ajaxResponse } = state; |
|||
return { |
|||
title: global.title, |
|||
copyright: global.copyright, |
|||
sections: global.sections, |
|||
actions: global.actions, |
|||
clientWidth: global.clientWidth, |
|||
clientHeight: global.clientHeight, |
|||
msg: ajaxResponse.msg, |
|||
user: auth.user |
|||
}; |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(LayoutContainer); |
@ -0,0 +1,18 @@ |
|||
'use strict'; |
|||
|
|||
import React from 'react'; |
|||
import moment from 'moment' |
|||
|
|||
const NoMatch = props => { |
|||
return ( |
|||
<div style={{ textAlign: 'center', padding: 120 }}> |
|||
<p style={{ fontSize: 80, lineHeight: 1.5 }}>404</p> |
|||
<p style={{ fontSize: 32, lineHeight: 2 }}>PAGE NOT FOUND</p> |
|||
<p>很遗憾,您暂时无法访问该页面。</p> |
|||
<p>请检查您访问的链接地址是否正确。</p> |
|||
<p style={{ marginTop: 80 }}>Copyright © {moment().year()} 飞尚</p> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
export default NoMatch; |
@ -0,0 +1,178 @@ |
|||
'use strict'; |
|||
import React, { useEffect, useState } from 'react'; |
|||
import moment from 'moment'; |
|||
import configStore from './store'; |
|||
import { Provider } from 'react-redux'; |
|||
import { createBrowserHistory } from 'history'; |
|||
import { ConnectedRouter } from 'connected-react-router' |
|||
import { Layout, NoMatch } from './containers'; |
|||
import { Switch, Route } from "react-router-dom"; |
|||
import { ConfigProvider } from '@douyinfe/semi-ui'; |
|||
import layoutActions from './actions'; |
|||
import zhCN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN'; |
|||
import { basicReducer } from '@peace/utils'; |
|||
import 'moment/locale/zh-cn'; |
|||
|
|||
moment.locale('zh-cn'); |
|||
|
|||
const { initLayout, initApiRoot, resize, initWebSocket } = layoutActions; |
|||
|
|||
const Root = props => { |
|||
const { sections, title, copyright } = props; |
|||
const [history, setHistory] = useState(null) |
|||
const [store, setStore] = useState(null) |
|||
const [outerRoutes, setOuterRoutes] = useState([]) |
|||
const [combineRoutes, setCombineRoutes] = useState([]) |
|||
const [innnerRoutes, setInnerRoutes] = useState([]) |
|||
|
|||
const flatRoutes = (routes) => { |
|||
const combineRoutes = []; |
|||
|
|||
function flat (routes, parentRoute) { |
|||
routes.forEach((route, i) => { |
|||
let obj = { |
|||
path: route.path, |
|||
breadcrumb: route.breadcrumb, |
|||
component: route.component || null, |
|||
authCode: route.authCode || '', |
|||
key: route.key |
|||
} |
|||
if (!route.path.startsWith("/")) { |
|||
console.error('路由配置需以 "/" 开始:' + route.path); |
|||
} |
|||
if (route.path.length > 1 && route.path[route.path.length] == '/') { |
|||
console.error('除根路由路由配置不可以以 "/" 结束:' + route.path); |
|||
} |
|||
if (parentRoute && parentRoute != '/') { |
|||
obj.path = parentRoute + route.path; |
|||
} |
|||
if (route.exact) { |
|||
obj.exact = true |
|||
} |
|||
if (route.hasOwnProperty('childRoutes')) { |
|||
combineRoutes.push(obj); |
|||
flat(route.childRoutes, obj.path) |
|||
} else { |
|||
combineRoutes.push(obj) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
flat(routes); |
|||
return combineRoutes; |
|||
} |
|||
|
|||
const initReducer = (reducers, reducerName, action) => { |
|||
let reducerParams = {} |
|||
const { actionType, initReducer, reducer } = action()() |
|||
if (initReducer || reducer) { |
|||
if (reducer) { |
|||
if (reducer.name) { |
|||
reducerName = reducer.name |
|||
} |
|||
if (reducer.params) { |
|||
reducerParams = reducer.params |
|||
} |
|||
} else { |
|||
reducerName = `${reducerName}Rslt` |
|||
} |
|||
reducers[reducerName] = function (state, action) { |
|||
return basicReducer(state, action, Object.assign({ actionType: actionType }, reducerParams)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
useEffect(() => { |
|||
let innerRoutes = [] |
|||
let outerRoutes = [] |
|||
let reducers = {} |
|||
let actions = { |
|||
layout: layoutActions |
|||
} |
|||
|
|||
for (let s of sections) { |
|||
if (!s.key) console.warn('请给你的section添加一个key值,section name:' + s.name); |
|||
for (let r of s.routes) { |
|||
if (r.type == 'inner' || r.type == 'home') { |
|||
innerRoutes.push(r.route) |
|||
} else if (r.type == 'outer') { |
|||
outerRoutes.push(r.route) |
|||
} |
|||
} |
|||
if (s.reducers) { |
|||
reducers = { ...reducers, ...s.reducers } |
|||
} |
|||
if (s.actions) { |
|||
actions = { ...actions, [s.key]: s.actions } |
|||
if (s.key != 'auth') { |
|||
for (let ak in s.actions) { |
|||
let actions = s.actions[ak] |
|||
if (actions && typeof actions == 'object') { |
|||
for (let actionName in actions) { |
|||
initReducer(reducers, actionName, actions[actionName]) |
|||
} |
|||
} else if (typeof actions == 'function') { |
|||
initReducer(reducers, ak, actions) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
let history = createBrowserHistory(); |
|||
let store = configStore(reducers, history); |
|||
store.dispatch(initLayout(title, copyright, sections, actions)); |
|||
store.dispatch(resize(document.body.clientHeight, document.body.clientWidth)); |
|||
store.dispatch(actions.auth.initAuth()); |
|||
store.dispatch(initWebSocket({})) |
|||
store.dispatch(initApiRoot()) |
|||
|
|||
const combineRoutes = flatRoutes(innerRoutes); |
|||
|
|||
setInnerRoutes(combineRoutes) |
|||
setHistory(history) |
|||
setStore(store) |
|||
setOuterRoutes(outerRoutes.map(route => ( |
|||
<Route |
|||
key={route.key} |
|||
exact |
|||
path={route.path} |
|||
component={route.component} |
|||
/> |
|||
))) |
|||
setCombineRoutes(combineRoutes.map(route => ( |
|||
<Route |
|||
key={route.key} |
|||
exact={route.hasOwnProperty('exact') ? route.exact : true} |
|||
path={route.path} |
|||
component={route.component} |
|||
/> |
|||
))) |
|||
}, []) |
|||
|
|||
return ( |
|||
store ? |
|||
<ConfigProvider locale={zhCN}> |
|||
<Provider store={store}> |
|||
<ConnectedRouter history={history}> |
|||
<Switch> |
|||
{outerRoutes} |
|||
<Layout |
|||
history={history} |
|||
routes={innnerRoutes} |
|||
> |
|||
{combineRoutes} |
|||
</Layout> |
|||
<Route |
|||
path={'*'} |
|||
component={NoMatch} |
|||
/> |
|||
</Switch> |
|||
</ConnectedRouter> |
|||
</Provider> |
|||
</ConfigProvider> |
|||
: '' |
|||
) |
|||
} |
|||
|
|||
export default Root; |
@ -0,0 +1,28 @@ |
|||
/** |
|||
* Created by liu.xinyi |
|||
* on 2016/4/1. |
|||
*/ |
|||
'use strict'; |
|||
const initState = { |
|||
msg: null |
|||
}; |
|||
|
|||
import Immutable from 'immutable'; |
|||
|
|||
/** |
|||
* 全局ajax响应处理: |
|||
* 判断action中是否有done字段,如果有,则修改store中的msg.done |
|||
* 判断action中是否有error字段,如果有,则修改store中msg.error |
|||
* 在layout中根据msg的值,呈现提示信息。 |
|||
*/ |
|||
export default function ajaxResponse (state = initState, action) { |
|||
if (action.done) { |
|||
return Immutable.fromJS(state).set('msg', { done: action.done }).toJS(); |
|||
} |
|||
|
|||
if (action.error) { |
|||
return Immutable.fromJS(state).set('msg', { error: action.error }).toJS(); |
|||
} |
|||
|
|||
return { msg: null }; |
|||
}; |
@ -0,0 +1,39 @@ |
|||
'use strict'; |
|||
import Immutable from 'immutable'; |
|||
import { INIT_LAYOUT, RESIZE, INIT_API_ROOT } from '../actions/global'; |
|||
|
|||
function global (state = { |
|||
title: '', |
|||
copyright: '', |
|||
sections: [], |
|||
actions: {}, |
|||
plugins: {}, |
|||
clientHeight: 768, |
|||
clientWidth: 1024, |
|||
apiRoot: '', |
|||
}, action) { |
|||
const payload = action.payload; |
|||
switch (action.type) { |
|||
case RESIZE: |
|||
return Immutable.fromJS(state).merge({ |
|||
clientHeight: payload.clientHeight, |
|||
clientWidth: payload.clientWidth |
|||
}).toJS(); |
|||
case INIT_LAYOUT: |
|||
return Immutable.fromJS(state).merge({ |
|||
title: payload.title, |
|||
copyright: payload.copyright, |
|||
sections: payload.sections, |
|||
actions: payload.actions, |
|||
plugins: payload.plugins, |
|||
}).toJS(); |
|||
case INIT_API_ROOT: |
|||
return Immutable.fromJS(state).merge({ |
|||
apiRoot: payload.apiRoot, |
|||
}).toJS(); |
|||
default: |
|||
return state; |
|||
} |
|||
} |
|||
|
|||
export default global; |
@ -0,0 +1,17 @@ |
|||
/** |
|||
* User: liuxinyi/liu.xinyi@free-sun.com.cn |
|||
* Date: 2016/1/13 |
|||
* Time: 17:52 |
|||
* |
|||
*/ |
|||
'use strict'; |
|||
|
|||
import global from './global'; |
|||
import webSocket from './webSocket' |
|||
import ajaxResponse from './ajaxResponse'; |
|||
|
|||
export default { |
|||
global, |
|||
webSocket, |
|||
ajaxResponse, |
|||
}; |
@ -0,0 +1,21 @@ |
|||
'use strict'; |
|||
import * as actionTypes from '../actions/webSocket'; |
|||
import Immutable from 'immutable'; |
|||
|
|||
const initState = { |
|||
socket: null, |
|||
}; |
|||
|
|||
function webSocket (state = initState, action) { |
|||
const payload = action.payload; |
|||
switch (action.type) { |
|||
case actionTypes.INIT_WEB_SOCKET: |
|||
return Immutable.fromJS(state).merge({ |
|||
socket: payload.socket, |
|||
}).toJS(); |
|||
default: |
|||
return state; |
|||
} |
|||
} |
|||
|
|||
export default webSocket; |
@ -0,0 +1,13 @@ |
|||
'use strict'; |
|||
|
|||
import storeProd from './store.prod' |
|||
import storeDev from './store.dev' |
|||
|
|||
let store = null; |
|||
if (process.env.NODE_ENV == 'production') { |
|||
store = storeProd; |
|||
} else { |
|||
store = storeDev; |
|||
} |
|||
|
|||
export default store; |
@ -0,0 +1,30 @@ |
|||
/** |
|||
* Created by liu.xinyi |
|||
* on 2016/4/8. |
|||
*/ |
|||
'use strict'; |
|||
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'; |
|||
import reduxThunk from 'redux-thunk'; |
|||
import { connectRouter, routerMiddleware } from 'connected-react-router'; |
|||
import innerReducers from '../reducers'; |
|||
|
|||
function configStore(reducers, history) { |
|||
const reducer = Object.assign({}, innerReducers, reducers, { |
|||
router: connectRouter(history) |
|||
}); |
|||
|
|||
const composeEnhancers = |
|||
typeof window === 'object' && |
|||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? |
|||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ |
|||
// Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
|
|||
}) : compose; |
|||
|
|||
const enhancers = composeEnhancers( |
|||
applyMiddleware(routerMiddleware(history), reduxThunk) |
|||
); |
|||
|
|||
return createStore(combineReducers(reducer), {}, enhancers); |
|||
} |
|||
|
|||
export default configStore; |
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Created by liu.xinyi |
|||
* on 2016/4/8. |
|||
*/ |
|||
'use strict'; |
|||
|
|||
import { createStore, combineReducers, applyMiddleware } from 'redux'; |
|||
import reduxThunk from 'redux-thunk'; |
|||
import { connectRouter, routerMiddleware } from 'connected-react-router'; |
|||
import innerReducers from '../reducers'; |
|||
|
|||
function configStore(reducers, history){ |
|||
const reducer = Object.assign({}, innerReducers, reducers, { |
|||
router: connectRouter(history) |
|||
}); |
|||
|
|||
return createStore(combineReducers(reducer), {}, applyMiddleware(routerMiddleware(history), reduxThunk)); |
|||
} |
|||
|
|||
export default configStore; |
@ -0,0 +1,79 @@ |
|||
'use strict'; |
|||
|
|||
import { ApiTable } from '$utils' |
|||
import { Request } from '@peace/utils'; |
|||
|
|||
export const INIT_AUTH = 'INIT_AUTH'; |
|||
export function initAuth () { |
|||
const user = JSON.parse(sessionStorage.getItem('user')) || {}; |
|||
return { |
|||
type: INIT_AUTH, |
|||
payload: { |
|||
user: user |
|||
} |
|||
}; |
|||
} |
|||
|
|||
export const REQUEST_LOGIN = 'REQUEST_LOGIN'; |
|||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; |
|||
export const LOGIN_ERROR = 'LOGIN_ERROR'; |
|||
export function login (username, password) { |
|||
return dispatch => { |
|||
dispatch({ type: REQUEST_LOGIN }); |
|||
|
|||
if (!username || !password) { |
|||
dispatch({ |
|||
type: LOGIN_ERROR, |
|||
payload: { error: '请输入账号名和密码' } |
|||
}); |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
// return dispatch({
|
|||
// type: LOGIN_SUCCESS,
|
|||
// payload: {
|
|||
// user: {
|
|||
// authorized: true,
|
|||
// namePresent: 'TEST'
|
|||
// }
|
|||
// },
|
|||
// });
|
|||
|
|||
return Request.post(ApiTable.login, { username, password }) |
|||
.then(user => { |
|||
sessionStorage.setItem('user', JSON.stringify(user)); |
|||
localStorage.setItem('fs_iot_cross_user', JSON.stringify(user)); |
|||
return dispatch({ |
|||
type: LOGIN_SUCCESS, |
|||
payload: { user: user }, |
|||
}); |
|||
}, error => { |
|||
let { body } = error.response; |
|||
return dispatch({ |
|||
type: LOGIN_ERROR, |
|||
payload: { |
|||
error: body && body.message ? body.message : '登录失败' |
|||
} |
|||
}) |
|||
}); |
|||
} |
|||
} |
|||
|
|||
export const LOGOUT = 'LOGOUT'; |
|||
export function logout () { |
|||
const user = JSON.parse(sessionStorage.getItem('user')) |
|||
Request.put(ApiTable.logout, { |
|||
token: user.token |
|||
}); |
|||
sessionStorage.removeItem('user'); |
|||
localStorage.removeItem('fs_iot_cross_user'); |
|||
return { |
|||
type: LOGOUT |
|||
}; |
|||
} |
|||
|
|||
export default { |
|||
initAuth, |
|||
login, |
|||
logout |
|||
} |
@ -0,0 +1,7 @@ |
|||
'use strict'; |
|||
|
|||
import auth from './auth'; |
|||
|
|||
export default { |
|||
...auth |
|||
}; |
@ -0,0 +1,57 @@ |
|||
'use strict'; |
|||
import React, { useEffect, useRef } from 'react'; |
|||
import { connect } from "react-redux"; |
|||
|
|||
const Cross = ({ dispatch, actions }) => { |
|||
|
|||
useEffect(() => { |
|||
function messageListen (e) { |
|||
// 此处需做 域名 验证 |
|||
const { data } = e |
|||
if (data && data.action) { |
|||
if (data.action == 'logout') { |
|||
localStorage.removeItem('fs_iot_cross_user') |
|||
sessionStorage.removeItem('user') |
|||
} else if (data.action = 'login') { |
|||
localStorage.setItem('fs_iot_cross_user', JSON.stringify(data.user)) |
|||
} |
|||
} |
|||
} |
|||
function storageListen (e) { |
|||
if (e.key == 'fs_iot_cross_user') { |
|||
if (!e.newValue) { |
|||
// IOT AUTH 退出 |
|||
window.parent.postMessage({ action: 'logout' }, '*'); |
|||
} |
|||
} |
|||
} |
|||
if (window.parent) { |
|||
window.addEventListener('message', messageListen); |
|||
window.addEventListener("storage", storageListen); |
|||
const user = localStorage.getItem('fs_iot_cross_user') |
|||
if (user) { |
|||
window.parent.postMessage({ action: 'initUser', user: JSON.parse(user) }, '*'); |
|||
} else { |
|||
window.parent.postMessage({ action: 'initNotice' }, '*'); |
|||
} |
|||
} |
|||
return () => { |
|||
window.removeEventListener('message', messageListen); |
|||
window.removeEventListener('setItemEvent', messageListen); |
|||
} |
|||
}, []) |
|||
|
|||
return ( |
|||
<div>FS_IOT_AUTH_CROSS</div> |
|||
) |
|||
} |
|||
|
|||
function mapStateToProps (state) { |
|||
const { global, auth, webSocket } = state; |
|||
return { |
|||
actions: global.actions, |
|||
user: auth.user, |
|||
}; |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(Cross); |
@ -0,0 +1,5 @@ |
|||
'use strict'; |
|||
import Login from './login'; |
|||
import Cross from './cross'; |
|||
|
|||
export { Login, Cross }; |
@ -0,0 +1,97 @@ |
|||
'use strict'; |
|||
import React, { useEffect, useRef } from 'react'; |
|||
import { connect } from 'react-redux'; |
|||
import { push } from 'react-router-redux'; |
|||
import { Form, Button, Toast } from '@douyinfe/semi-ui'; |
|||
import { login, LOGIN_SUCCESS } from '../actions/auth'; |
|||
import { IconLock, IconUser } from '@douyinfe/semi-icons'; |
|||
import '../style.less' |
|||
|
|||
const Login = props => { |
|||
const { dispatch, user, error, actions, apiRoot, isRequesting } = props |
|||
const form = useRef(); |
|||
|
|||
useEffect(() => { |
|||
if (error) { |
|||
Toast.error(error); |
|||
form.current.setValue('password', '') |
|||
} |
|||
}, [error]) |
|||
|
|||
useEffect(() => { |
|||
if (user && user.authorized) { |
|||
dispatch(push('/')); |
|||
localStorage.setItem('fs_iot_auth_selected_sider', JSON.stringify([])) |
|||
localStorage.setItem('fs_iot_auth_open_sider', JSON.stringify([])) |
|||
} |
|||
}, [user]) |
|||
|
|||
return ( |
|||
<div style={{ |
|||
height: '100vh', |
|||
backgroundImage: "url('/assets/images/background/loginBackground.png')", |
|||
backgroundSize: 'cover', |
|||
backgroundRepeat: 'no-repeat', |
|||
position: 'relative', |
|||
}}> |
|||
<div style={{ |
|||
width: 446, |
|||
height: 348, |
|||
padding: '45px 60px', |
|||
backgroundImage: "url('/assets/images/background/loginbg.png')", |
|||
backgroundSize: '100% 100%', |
|||
backgroundRepeat: 'no-repeat', |
|||
position: 'absolute', |
|||
top: '33.89%', |
|||
right: '16.43%', |
|||
}}> |
|||
<div style={{ width: 113, height: 24, marginTop: 3, marginLeft: 5 }}> |
|||
<img src="/assets/images/background/user_login.png" alt="" style={{ width: '100%', height: '100%' }} /> |
|||
</div> |
|||
<Form |
|||
onSubmit={values => { |
|||
dispatch(login(values.username, values.password)).then(res => { |
|||
const data = res.payload.user |
|||
dispatch(actions.layout.initWebSocket({ ioUrl: apiRoot, token: data.token })) |
|||
}) |
|||
}} |
|||
getFormApi={formApi => form.current = formApi} |
|||
> |
|||
<Form.Input |
|||
className='inputbgc' |
|||
field='username' |
|||
noLabel={true} |
|||
label='用户名' |
|||
placeholder='请输入账号' |
|||
prefix={<IconUser style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />} |
|||
style={{ background: 'rgba(24, 89, 193, 0.08)', height: 40, marginTop: 26 }} |
|||
/> |
|||
<Form.Input |
|||
field='password' |
|||
noLabel={true} |
|||
mode="password" |
|||
autoComplete="" |
|||
placeholder='请输入密码' |
|||
label='密码' |
|||
prefix={<IconLock style={{ color: '#1859C1', marginRight: 14, marginLeft: 8 }} />} |
|||
style={{ background: 'rgba(24, 89, 193, 0.08)', height: 40 }} |
|||
/> |
|||
<Button htmlType='submit' block theme="solid" style={{ marginTop: 17, height: 40, backgroundColor: '#1859C1' }}>立即登录</Button> |
|||
</Form> |
|||
</div> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
function mapStateToProps (state) { |
|||
const { auth, global } = state; |
|||
return { |
|||
user: auth.user, |
|||
error: auth.error, |
|||
actions: global.actions, |
|||
apiRoot: global.apiRoot, |
|||
isRequesting: auth.isRequesting |
|||
} |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(Login); |
@ -0,0 +1,12 @@ |
|||
'use strict'; |
|||
|
|||
import routes from './routes'; |
|||
import reducers from './reducers'; |
|||
import actions from './actions'; |
|||
|
|||
export default { |
|||
key: 'auth', |
|||
reducers: reducers, |
|||
routes: routes, |
|||
actions: actions |
|||
}; |
@ -0,0 +1,40 @@ |
|||
'use strict'; |
|||
import * as actionTypes from '../actions/auth'; |
|||
import Immutable from 'immutable'; |
|||
|
|||
const initState = { |
|||
user: {}, |
|||
isRequesting: false, |
|||
error: null |
|||
}; |
|||
|
|||
function auth (state = initState, action) { |
|||
const payload = action.payload; |
|||
switch (action.type) { |
|||
case actionTypes.INIT_AUTH: |
|||
return Immutable.fromJS(state).set('user', payload.user).toJS(); |
|||
case actionTypes.REQUEST_LOGIN: |
|||
return Immutable.fromJS(state).merge({ |
|||
isRequesting: true, |
|||
error: null |
|||
}).toJS(); |
|||
case actionTypes.LOGIN_SUCCESS: |
|||
return Immutable.fromJS(state).merge({ |
|||
isRequesting: false, |
|||
user: payload.user |
|||
}).toJS(); |
|||
case actionTypes.LOGIN_ERROR: |
|||
return Immutable.fromJS(state).merge({ |
|||
isRequesting: false, |
|||
error: payload.error |
|||
}).toJS(); |
|||
case actionTypes.LOGOUT: |
|||
return Immutable.fromJS(state).merge({ |
|||
user: null |
|||
}).toJS(); |
|||
default: |
|||
return state; |
|||
} |
|||
} |
|||
|
|||
export default auth; |
@ -0,0 +1,6 @@ |
|||
'use strict'; |
|||
import auth from './auth' |
|||
|
|||
export default { |
|||
auth |
|||
}; |
@ -0,0 +1,22 @@ |
|||
'use strict'; |
|||
|
|||
import { Login, Cross } from './containers'; |
|||
|
|||
export default [ |
|||
{ |
|||
type: 'outer', |
|||
route: { |
|||
key: 'signin', |
|||
path: "/signin", |
|||
component: Login |
|||
} |
|||
}, |
|||
{ |
|||
type: 'outer', |
|||
route: { |
|||
key: 'cross', |
|||
path: "/cross", |
|||
component: Cross |
|||
} |
|||
} |
|||
]; |
@ -0,0 +1,7 @@ |
|||
input:-webkit-autofill{ |
|||
-webkit-text-fill-color:black !important; |
|||
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important; |
|||
box-shadow: 0 0 0px 1000px transparent inset !important; |
|||
background-color:transparent; |
|||
transition: background-color 50000s ease-in-out 0s; |
|||
} |
@ -0,0 +1,5 @@ |
|||
'use strict'; |
|||
|
|||
export const AuthorizationCode = { |
|||
|
|||
}; |
@ -0,0 +1,10 @@ |
|||
'use strict'; |
|||
|
|||
export const isAuthorized = (authcode) => { |
|||
if (JSON.parse(sessionStorage.getItem('user'))) { |
|||
const { resources } = JSON.parse(sessionStorage.getItem('user')); |
|||
return resources.includes(authcode); |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
'use strict'; |
|||
import { isAuthorized } from './func'; |
|||
import { AuthorizationCode } from './authCode'; |
|||
import { ApiTable, RouteTable } from './webapi' |
|||
|
|||
export { |
|||
isAuthorized, |
|||
|
|||
AuthorizationCode, |
|||
|
|||
ApiTable, |
|||
RouteTable, |
|||
} |
@ -0,0 +1,12 @@ |
|||
'use strict'; |
|||
|
|||
export const ApiTable = { |
|||
login: 'login', |
|||
logout: 'logout', |
|||
}; |
|||
|
|||
export const RouteTable = { |
|||
apiRoot: '/api/root', |
|||
fileUpload: '/_upload/new', |
|||
cleanUpUploadTrash: '/_upload/cleanup', |
|||
}; |
@ -0,0 +1,91 @@ |
|||
'use strict'; |
|||
/*jslint node:true*/ |
|||
const path = require('path'); |
|||
/*这种以CommonJS的同步形式去引入其它模块的方式代码更加简洁:获取组件*/ |
|||
const os = require('os'); |
|||
const moment = require('moment'); |
|||
const args = require('args'); |
|||
const dev = process.env.NODE_ENV == 'development' || process.env.NODE_ENV == 'developmentVite'; |
|||
const vite = process.env.NODE_ENV == 'developmentVite'; |
|||
|
|||
dev && console.log('\x1B[33m%s\x1b[0m', '请遵循并及时更新 readme.md,维护良好的开发环境,媛猿有责'); |
|||
// // 启动参数
|
|||
args.option(['p', 'port'], '启动端口'); |
|||
args.option(['u', 'api-url'], 'webapi的URL'); |
|||
|
|||
const flags = args.parse(process.argv); |
|||
|
|||
const API_URL = process.env.API_URL || flags.apiUrl; |
|||
|
|||
if (!API_URL) { |
|||
console.log('缺少启动参数,异常退出'); |
|||
args.showHelp(); |
|||
process.exit(-1); |
|||
} |
|||
|
|||
const product = { |
|||
port: flags.port || 8080, |
|||
staticDirs: [path.join(__dirname, './client')], |
|||
mws: [{ |
|||
entry: require('./middlewares/proxy').entry, |
|||
opts: { |
|||
host: API_URL, |
|||
match: /^\/_api\//, |
|||
} |
|||
}, { |
|||
entry: require('./routes').entry, |
|||
opts: { |
|||
apiUrl: API_URL, |
|||
staticRoot: './client', |
|||
} |
|||
}, { |
|||
entry: require('./client').entry,// 静态信息
|
|||
opts: {} |
|||
}], |
|||
logger: { |
|||
level: 'debug', |
|||
json: false, |
|||
filename: path.join(__dirname, 'log', 'runtime.txt'), |
|||
colorize: true, |
|||
maxsize: 1024 * 1024 * 5, |
|||
rotationFormat: false, |
|||
zippedArchive: true, |
|||
maxFiles: 10, |
|||
prettyPrint: true, |
|||
label: '', |
|||
timestamp: () => moment().format('YYYY-MM-DD HH:mm:ss.SSS'), |
|||
eol: os.EOL, |
|||
tailable: true, |
|||
depth: null, |
|||
showLevel: true, |
|||
maxRetries: 1 |
|||
} |
|||
}; |
|||
|
|||
let config; |
|||
if (dev) { |
|||
config = { |
|||
port: product.port, |
|||
staticDirs: product.staticDirs, |
|||
mws: product.mws |
|||
.concat([ |
|||
vite ? |
|||
{ |
|||
entry: require('./middlewares/vite-dev').entry, |
|||
opts: {} |
|||
} |
|||
: |
|||
{ |
|||
entry: require('./middlewares/webpack-dev').entry, |
|||
opts: {} |
|||
} |
|||
]) |
|||
, |
|||
logger: product.logger |
|||
} |
|||
config.logger.filename = path.join(__dirname, 'log', 'development.txt'); |
|||
} else { |
|||
config = product; |
|||
} |
|||
|
|||
module.exports = config;//区分开发和发布
|
@ -0,0 +1,15 @@ |
|||
{ |
|||
|
|||
"compilerOptions": { |
|||
"target": "es6", |
|||
"module": "commonjs", |
|||
"allowSyntheticDefaultImports": true |
|||
}, |
|||
"exclude": [ |
|||
"node_modules", |
|||
"bower_components", |
|||
"jspm_packages", |
|||
"tmp", |
|||
"temp" |
|||
] |
|||
} |
@ -0,0 +1,16 @@ |
|||
'use strict'; |
|||
|
|||
const proxy = require('koa-proxy'); |
|||
const convert = require('koa-convert'); |
|||
|
|||
module.exports = { |
|||
entry: function (app, router, opts) { |
|||
app.use(convert(proxy({ |
|||
host: opts.host, |
|||
match: opts.match, |
|||
map: function (path) { |
|||
return path.replace(opts.match, ''); |
|||
} |
|||
}))); |
|||
} |
|||
}; |
@ -0,0 +1,25 @@ |
|||
'use strict'; |
|||
|
|||
const express = require('express') |
|||
const { createServer: createViteServer } = require('vite') |
|||
|
|||
module.exports = { |
|||
entry: async function (app, router, opts) { |
|||
const server = express() |
|||
|
|||
// 以中间件模式创建 Vite 服务器
|
|||
// 竟然会自动读 /vite.config.js 的配置
|
|||
const vite = await createViteServer({}) |
|||
// 将 vite 的 connect 实例作中间件使用
|
|||
server.use(vite.middlewares) |
|||
|
|||
server.use('*', async (req, res) => { |
|||
// 如果 `middlewareMode` 是 `'ssr'`,应在此为 `index.html` 提供服务.
|
|||
// 如果 `middlewareMode` 是 `'html'`,则此处无需手动服务 `index.html`
|
|||
// 因为 Vite 自会接管
|
|||
}) |
|||
|
|||
server.listen(5102) |
|||
console.info('vite server.listen 5202'); |
|||
} |
|||
}; |
@ -0,0 +1,42 @@ |
|||
'use strict'; |
|||
const express = require('express') |
|||
const webpack = require('webpack'); |
|||
const devConfig = require('../webpack.config'); |
|||
const middleware = require('webpack-dev-middleware'); |
|||
const proxy = require('koa-better-http-proxy'); |
|||
const url = require('url'); |
|||
|
|||
const compiler = webpack(devConfig); |
|||
|
|||
module.exports = { |
|||
entry: function (app, router, opts) { |
|||
app.use(proxy('http://localhost:5201', { |
|||
filter: function (ctx) { |
|||
return /\/build/.test(url.parse(ctx.url).path); |
|||
}, |
|||
proxyReqPathResolver: function (ctx) { |
|||
return 'client' + url.parse(ctx.url).path; |
|||
} |
|||
})); |
|||
|
|||
app.use(proxy('http://localhost:5201', { |
|||
filter: function (ctx) { |
|||
return /\/$/.test(url.parse(ctx.url).path); |
|||
}, |
|||
proxyReqPathResolver: function (ctx) { |
|||
return 'client/build/index.html'; |
|||
} |
|||
})); |
|||
|
|||
const server = express(); |
|||
server.use(middleware(compiler)); |
|||
// server.use(require("webpack-hot-middleware")(compiler));
|
|||
server.listen('5201', function (err) { |
|||
if (err) { |
|||
console.error(err); |
|||
} else { |
|||
console.info(`webpack-dev listen 5201`); |
|||
} |
|||
}) |
|||
} |
|||
}; |
@ -0,0 +1,77 @@ |
|||
{ |
|||
"name": "fs-anxincloud-4.0", |
|||
"version": "1.0.0", |
|||
"description": "anxincloud-4.0", |
|||
"main": "server.js", |
|||
"scripts": { |
|||
"test": "mocha", |
|||
"start-vite": "cross-env NODE_ENV=developmentVite npm run start-params", |
|||
"start": "cross-env NODE_ENV=development npm run start-params", |
|||
"start-params": "node server -p 5200 -u http://10.8.30.82:4200", |
|||
"deploy": "export NODE_ENV=production&& npm run build && node server", |
|||
"build-dev": "cross-env NODE_ENV=development&&webpack --config webpack.config.js", |
|||
"build": "export NODE_ENV=production&&webpack --config webpack.config.prod.js" |
|||
}, |
|||
"keywords": [ |
|||
"app" |
|||
], |
|||
"author": "", |
|||
"license": "ISC", |
|||
"devDependencies": { |
|||
"@babel/core": "^7.14.6", |
|||
"@babel/plugin-proposal-class-properties": "^7.14.5", |
|||
"@babel/plugin-proposal-object-rest-spread": "^7.14.7", |
|||
"@babel/plugin-transform-runtime": "^7.14.5", |
|||
"@babel/polyfill": "^7.12.1", |
|||
"@babel/preset-env": "^7.14.7", |
|||
"@babel/preset-react": "^7.14.5", |
|||
"babel-loader": "^8.2.2", |
|||
"babel-plugin-import": "^1.13.3", |
|||
"connected-react-router": "^6.8.0", |
|||
"css-loader": "^3.5.0", |
|||
"express": "^4.17.1", |
|||
"file-loader": "^6.0.0", |
|||
"html-webpack-plugin": "^4.5.0", |
|||
"immutable": "^4.0.0-rc.12", |
|||
"less": "^3.12.2", |
|||
"less-loader": "^7.0.2", |
|||
"nprogress": "^0.2.0", |
|||
"react": "^17.0.0", |
|||
"react-dom": "^17.0.0", |
|||
"react-redux": "^7.2.1", |
|||
"react-router-dom": "^5.2.0", |
|||
"react-router-redux": "^4.0.8", |
|||
"redux": "^4.0.5", |
|||
"redux-thunk": "^2.3.0", |
|||
"style-loader": "^2.0.0", |
|||
"vite": "^2.9.5" |
|||
}, |
|||
"dependencies": { |
|||
"@douyinfe/semi-ui": "^2.8.0", |
|||
"@fs/attachment": "^1.0.0", |
|||
"@peace/components": "0.0.35", |
|||
"@peace/utils": "^0.0.48", |
|||
"@vitejs/plugin-react": "^1.3.1", |
|||
"@vitejs/plugin-react-refresh": "^1.3.6", |
|||
"args": "^5.0.1", |
|||
"babel-polyfill": "^6.26.0", |
|||
"copy-to-clipboard": "^3.3.1", |
|||
"cross-env": "^7.0.3", |
|||
"fs-web-server-scaffold": "^1.0.6", |
|||
"koa-better-http-proxy": "^0.2.5", |
|||
"koa-proxy": "^1.0.0-alpha.3", |
|||
"koa-view": "^2.1.4", |
|||
"moment": "^2.29.3", |
|||
"npm": "^7.20.6", |
|||
"perfect-scrollbar": "^1.5.5", |
|||
"socket.io-client": "^4.5.0", |
|||
"socket.io-parser": "^4.2.0", |
|||
"superagent": "^6.1.0", |
|||
"webpack": "^5.3.2", |
|||
"webpack-bundle-analyzer": "^4.1.0", |
|||
"webpack-cli": "^4.2.0", |
|||
"webpack-dev-middleware": "^4.0.2", |
|||
"webpack-dev-server": "^3.11.2", |
|||
"webpack-hot-middleware": "^2.25.0" |
|||
} |
|||
} |
@ -0,0 +1,214 @@ |
|||
创建时间:2021/08/19 |
|||
|
|||
## 1. 文档维护: |
|||
|
|||
- 文档相关内容若有更改,请及时更新文档,以备后来者查询; |
|||
|
|||
## 2. 项目开发: |
|||
|
|||
- 请遵循此文档约定的目录结构与约定 |
|||
|
|||
```js |
|||
|-- .babelrc |
|||
|-- config.js |
|||
|-- Dockerfile |
|||
|-- jsconfig.json |
|||
|-- package.json |
|||
|-- readme.md |
|||
|-- server.js |
|||
|-- webpack.config.js |
|||
|-- webpack.config.prod.js |
|||
|-- .vscode |
|||
| |-- launch.json |
|||
| |-- settings.json |
|||
|-- client |
|||
| |-- index.ejs |
|||
| |-- index.html // 当前 html 文件 |
|||
| |-- index.js |
|||
| |-- assets // 资源文件 |
|||
| | |-- images |
|||
| | |-- avatar |
|||
| |-- src // 项目代码 |
|||
| |-- app.js // 由此开始并加载模块 |
|||
| |-- index.js |
|||
| |-- components // 公用组件 |
|||
| | |-- index.js // 由此导出组件 |
|||
| | |-- Upload |
|||
| | |-- index.js |
|||
| |-- layout // 项目布局以及初始化等操作 |
|||
| | |-- index.js |
|||
| | |-- actions |
|||
| | | |-- global.js |
|||
| | |-- components |
|||
| | | |-- footer |
|||
| | | | |-- index.js |
|||
| | | |-- header |
|||
| | | | |-- index.js |
|||
| | | |-- sider |
|||
| | | |-- index.js |
|||
| | |-- containers |
|||
| | | |-- index.js |
|||
| | | |-- layout |
|||
| | | | |-- index.js |
|||
| | | | |-- index.less |
|||
| | | |-- no-match |
|||
| | | |-- index.js |
|||
| | |-- reducers |
|||
| | | |-- ajaxResponse.js |
|||
| | | |-- global.js // 全局数据,主要包含屏幕可视宽高、所有的 action 等 |
|||
| | | |-- index.js |
|||
| | |-- store |
|||
| | |-- index.js |
|||
| | |-- store.dev.js |
|||
| | |-- store.prod.js |
|||
| |-- sections // 各功能模块 |
|||
| | |-- auth // 比较特别的 Auth 模块,目前 action、reducer 依然采用原始写法;包含登录、忘记密码等项目基本功能页面 |
|||
| | | |-- index.js |
|||
| | | |-- routes.js |
|||
| | | |-- actions |
|||
| | | | |-- auth.js |
|||
| | | | |-- index.js |
|||
| | | |-- components |
|||
| | | |-- containers |
|||
| | | | |-- index.js |
|||
| | | | |-- login.js |
|||
| | | |-- reducers |
|||
| | | | |-- auth.js |
|||
| | | | |-- index.js |
|||
| | | |-- __tests__ |
|||
| | |-- example // 示例模块,一般的功能模块应遵循此结构 |
|||
| | |-- index.js // 由此导出该模块信息,应包括一个 key 值,actions 等 |
|||
| | |-- nav-item.js // 用于生成菜单项,此文件内可以进行权限判断 |
|||
| | |-- routes.js // 路由文件 |
|||
| | |-- style.less // 样式文件,若样式并不是非常多,每个模块一个样式文件即可 |
|||
| | |-- actions |
|||
| | | |-- example.js // 具体的 action 操作 |
|||
| | | |-- index.js // 由此导出该项目的 action |
|||
| | |-- components // 组件 |
|||
| | |-- containers // 容器,此文件夹内应只包括该模块第一层级的页面 |
|||
| | | |-- example.js |
|||
| | | |-- index.js |
|||
| | |-- reducers // 若采用封装后的 action 写法,则 reducer 可不写 |
|||
| | |-- index.js |
|||
| |-- utils // |
|||
| |-- authCode.js |
|||
| |-- func.js // 常用函数 |
|||
| |-- index.js |
|||
| |-- webapi.js // api 路由 |
|||
|-- log |
|||
|-- middlewares |
|||
| |-- proxy.js |
|||
| |-- webpack-dev.js |
|||
|-- routes |
|||
| |-- index.js |
|||
| |-- attachment |
|||
|-- typings |
|||
|-- node |
|||
| |-- node.d.ts |
|||
|-- react |
|||
|-- react.d.ts |
|||
``` |
|||
|
|||
- 封装后一般 action 写法: |
|||
|
|||
`@peace/utils 的 actionHelp 中有详细注释` |
|||
|
|||
``` js |
|||
'use strict'; |
|||
|
|||
import { basicAction } from '@peace/utils' |
|||
import { ApiTable } from '$utils' |
|||
|
|||
export function getMembers(orgId) { |
|||
return dispatch => basicAction({ |
|||
type: 'get', |
|||
dispatch: dispatch, |
|||
actionType: 'GET_MEMBERS', |
|||
url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, |
|||
msg: { error: '获取用户列表失败' }, |
|||
reducer: { name: 'members' } |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
1. 若 type=post,则可以使用 data 属性发送对象格式数据; |
|||
|
|||
2. reducer.name 会作为该 action 对应的 reducer 的名字,从 state 里可以解构此变量,获得该 action 异步或其他操作获得的数据; |
|||
|
|||
3. msg 可以发送 `{ option:'获取用户列表' }` ,则 actionHelp 会自动将其处理为失败和成功两种情况; |
|||
|
|||
若单独写 success 或 error 的 key,则只在成功或失败的时候进行提示; |
|||
|
|||
4. 后续可以优化:type=get 时候, |
|||
|
|||
使用 query 属性将数据传递,在 @peace/utils 的 actionHelp 中将其添加到路由后面;eg. `{ enterpriseId: orgId }` |
|||
|
|||
使用 replace 属性传递对象数据,对象数据中将被替换的值为key,替换的值为 value,然后再 actionHelp 中更改路由;eg. `{ "{enterpriseId}": orgId}` |
|||
|
|||
5. 最终取得的 reducer 中的数据格式一般为: |
|||
``` js |
|||
{ |
|||
data: xxx, // 接口返回的数据格式 |
|||
isRequesting: false, // 请求状态 |
|||
success: true, // 以此判断请求是否成功,不用再以 payload.type 判断 |
|||
} |
|||
``` |
|||
|
|||
- actions 的引用 |
|||
|
|||
从 reducer 的 state.global.actions 里引用具体 action |
|||
|
|||
```js |
|||
const Example = (props) => { |
|||
const { dispatch, actions, user, loading } = props |
|||
|
|||
useEffect(() => { |
|||
dispatch(actions.example.getMembers(user.orgId)) |
|||
}, []) |
|||
|
|||
return ( |
|||
<Spin tip="biubiubiu~" spinning={loading}> |
|||
example |
|||
</Spin> |
|||
) |
|||
} |
|||
|
|||
function mapStateToProps(state) { |
|||
const { auth, global, members } = state; |
|||
return { |
|||
loading: members.isRequesting, |
|||
user: auth.user, |
|||
actions: global.actions, |
|||
members: members.data |
|||
}; |
|||
} |
|||
|
|||
export default connect(mapStateToProps)(Example); |
|||
``` |
|||
|
|||
- 一般路由配置 |
|||
```js |
|||
'use strict'; |
|||
import { Example, } from './containers'; |
|||
|
|||
export default [{ |
|||
type: 'inner', // 是否在layout 内,如果为outer,则看不到 header、footer、sider等布局,比如登陆页面 |
|||
route: { |
|||
path: '/example', |
|||
key: 'example', |
|||
breadcrumb: '栗子', |
|||
// 不设置 component 则面包屑禁止跳转 |
|||
childRoutes: [{ |
|||
path: '/e1', // 自路由不必复写父路由内容,会自动拼接; 则此处组件的实际路由为 /example/e1 |
|||
key: 'e1', |
|||
component: Example, |
|||
breadcrumb: '棒子', |
|||
}] |
|||
} |
|||
}]; |
|||
``` |
|||
- cross-env 的使用限制 |
|||
|
|||
cross-env 可以统一不同操作系统下环境变量的导出方式,不用再在 windows 下写 set;linux 下写 export; 可以统一以 cross-env NODE_ENV=DEV 代替; |
|||
|
|||
但是这样的话就不能在同一条运行的命令中使用 && 切割,因为会把命令切割为两个环境,则最终拿不到我们设置的变量; |
@ -0,0 +1,92 @@ |
|||
'use strict'; |
|||
const request = require('superagent'); |
|||
const parse = require('async-busboy'); |
|||
const path = require('path') |
|||
const fs = require('fs'); |
|||
|
|||
const ext = { |
|||
project: [".txt", ".dwg", ".doc", ".docx", ".xls", ".xlsx", ".pdf", ".png", ".jpg", ".svg"], |
|||
report: [".doc", ".docx", ".xls", ".xlsx", ".pdf"], |
|||
data: [".txt", ".xls", ".xlsx"], |
|||
image: [".png", ".jpg", ".svg"], |
|||
three: [".js"], |
|||
video: [".mp4"], |
|||
bpmn: [".bpmn", ".bpmn20.xml", ".zip", ".bar"], |
|||
app: [".apk"] |
|||
} |
|||
|
|||
module.exports = { |
|||
entry: function (app, router, opts) { |
|||
|
|||
const getApiRoot = async function (ctx) { |
|||
const { apiUrl } = opts; |
|||
|
|||
ctx.status = 200; |
|||
ctx.body = { root: apiUrl }; |
|||
}; |
|||
|
|||
let upload = async function (ctx, next) { |
|||
try { |
|||
const { files } = await parse(ctx.req); |
|||
const file = files[0]; |
|||
const extname = path.extname(file.filename).toLowerCase(); |
|||
const fileType = ctx.query.type || "image"; |
|||
const fileFolder = ctx.query.fileFolder || 'common'; |
|||
if (ext[fileType].indexOf(extname) < 0) { |
|||
ctx.status = 400; |
|||
ctx.body = JSON.stringify({ name: 'UploadFailed', message: '文件格式无效' }); |
|||
return; |
|||
} |
|||
const date = new Date().toLocaleDateString(); |
|||
const time = new Date().getTime(); |
|||
let fileName = time + '_' + file.filename; |
|||
let saveFile = path.join(__dirname, '../../', `/client/assets/files/${fileFolder}`, fileName); |
|||
const pathUrl = `./client/assets/files/${fileFolder}`; |
|||
|
|||
const res1 = fs.existsSync(`./client/assets/files/${fileFolder}`); |
|||
!res1 && fs.mkdirSync(`./client/assets/files/${fileFolder}`); |
|||
const res = fs.existsSync(pathUrl); |
|||
!res && fs.mkdirSync(pathUrl); |
|||
let stream = fs.createWriteStream(saveFile); |
|||
fs.createReadStream(file.path).pipe(stream); |
|||
stream.on('error', function (err) { |
|||
app.fs.logger.log('error', '[Upload Heatmap]', err); |
|||
}); |
|||
ctx.status = 200; |
|||
ctx.body = { filename: path.join(`/assets/files/${fileFolder}`, fileName), name: 'UploadSuccess', message: '上传成功' }; |
|||
} catch (err) { |
|||
ctx.status = 500; |
|||
ctx.fs.logger.error(err); |
|||
ctx.body = { err: 'upload error.' }; |
|||
} |
|||
} |
|||
|
|||
let remove = async function (ctx, next) { |
|||
try { |
|||
const fkeys = ctx.request.body; |
|||
let removeUrl = path.join(__dirname, '../../', './client', fkeys.url); |
|||
const res = fs.existsSync(removeUrl); |
|||
if (!res) { |
|||
ctx.status = 400; |
|||
ctx.body = JSON.stringify({ name: 'DeleteFailed', message: '文件地址不存在' }); |
|||
return; |
|||
} |
|||
fs.unlink(removeUrl, function (error) { |
|||
if (error) { |
|||
console.log(error); |
|||
} |
|||
}) |
|||
ctx.status = 200; |
|||
ctx.body = { name: 'DeleteSuccess.', message: '删除成功' }; |
|||
} catch (err) { |
|||
ctx.status = 500; |
|||
ctx.fs.logger.error(err); |
|||
ctx.body = { err: 'upload cleanup error.' }; |
|||
} |
|||
} |
|||
|
|||
router.get('/api/root', getApiRoot); |
|||
router.post('/_upload/new', upload); |
|||
router.delete('/_upload/cleanup', remove); |
|||
} |
|||
}; |
@ -0,0 +1,20 @@ |
|||
/** |
|||
* Created by liu.xinyi |
|||
* on 2016/7/7. |
|||
*/ |
|||
'use strict'; |
|||
const path = require('path'); |
|||
const fs = require('fs'); |
|||
|
|||
module.exports = { |
|||
entry: function (app, router, opts) { |
|||
fs.readdirSync(__dirname).forEach(function (dir) { |
|||
if(fs.lstatSync(path.join(__dirname, dir)).isDirectory()){ |
|||
fs.readdirSync(path.join(__dirname, dir)).forEach(function (api) { |
|||
require(`./${dir}/${api}`).entry(app, router, opts); |
|||
app.fs.logger.log('info', '[Router]', 'Inject api:', dir + '/' + path.basename(api, '.js')); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
}; |
@ -0,0 +1,8 @@ |
|||
'use strict'; |
|||
/*jslint node:true*/ |
|||
//from koa
|
|||
|
|||
const scaffold = require('fs-web-server-scaffold'); |
|||
const config = require('./config.js'); |
|||
|
|||
module.exports = scaffold(config); |
@ -0,0 +1,33 @@ |
|||
import path from 'path'; |
|||
import { defineConfig } from 'vite' |
|||
import react from '@vitejs/plugin-react' |
|||
import reactRefresh from '@vitejs/plugin-react-refresh' |
|||
|
|||
// https://vitejs.dev/config/
|
|||
export default defineConfig({ |
|||
root: './client/', |
|||
plugins: [react({})], |
|||
// plugins: [reactRefresh({})],
|
|||
resolve: { |
|||
alias: [ |
|||
{ |
|||
find: '$utils', replacement: path.join('/src/utils'), |
|||
}, |
|||
{ |
|||
find: '$components', replacement: path.join('/src/components'), |
|||
}, |
|||
// 针对以 ~/[包名称]开头的,替换为 node_modules/@[包名称]
|
|||
{ |
|||
find: /^(~)(?!\/)(.+)/, replacement: path.join('node_modules/$2'), |
|||
}, |
|||
], |
|||
}, |
|||
cors: true, |
|||
server: { |
|||
hmr: { |
|||
protocol: 'ws', |
|||
host: 'localhost' |
|||
}, |
|||
middlewareMode: 'html', |
|||
} |
|||
}) |
@ -0,0 +1,71 @@ |
|||
const path = require('path'); |
|||
const webpack = require('webpack'); |
|||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; |
|||
|
|||
const PATHS = { |
|||
app: path.join(__dirname, 'client/src'), |
|||
build: path.join(__dirname, 'client/build') |
|||
}; |
|||
|
|||
module.exports = { |
|||
mode: "development", |
|||
devtool: 'source-map', |
|||
devServer: { |
|||
historyApiFallback: true, |
|||
}, |
|||
entry: { |
|||
app: ["@babel/polyfill", PATHS.app] |
|||
}, |
|||
output: { |
|||
publicPath: '/client/build/', |
|||
path: PATHS.build, |
|||
filename: '[name].js' |
|||
}, |
|||
resolve: { |
|||
modules: [path.resolve(__dirname, 'client/src'), path.resolve(__dirname, 'node_modules')], |
|||
extensions: ['.js', '.jsx'], |
|||
alias: { |
|||
crypto: false, |
|||
$utils: path.resolve(__dirname, 'client/src/utils/'), |
|||
$components: path.resolve(__dirname, 'client/src/components/'), |
|||
} |
|||
}, |
|||
plugins: [ |
|||
new webpack.HotModuleReplacementPlugin(), |
|||
new BundleAnalyzerPlugin({ |
|||
analyzerPort: 8200 |
|||
}), |
|||
], |
|||
module: { |
|||
rules: [{ |
|||
test: /\.css$/, |
|||
use: ['style-loader', { |
|||
loader: 'css-loader', |
|||
options: { |
|||
modules: true |
|||
} |
|||
}] |
|||
}, |
|||
{ |
|||
test: /\.less$/, |
|||
use: ['style-loader', 'css-loader', { |
|||
loader: 'less-loader', |
|||
options: { |
|||
lessOptions: { |
|||
javascriptEnabled: true |
|||
} |
|||
} |
|||
}] |
|||
}, |
|||
{ |
|||
test: /\.(js|jsx)$/, |
|||
use: 'babel-loader', |
|||
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], |
|||
}, |
|||
{ |
|||
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, |
|||
loader: "file-loader" |
|||
} |
|||
] |
|||
} |
|||
}; |
@ -0,0 +1,76 @@ |
|||
var path = require('path'); |
|||
var webpack = require('webpack'); |
|||
var HtmlWebpackPlugin = require('html-webpack-plugin'); |
|||
|
|||
const PATHS = { |
|||
app: path.join(__dirname, 'client/src'), |
|||
build: path.join(__dirname, 'client/build') |
|||
}; |
|||
|
|||
module.exports = { |
|||
mode: "production", |
|||
entry: { |
|||
app: ["babel-polyfill", PATHS.app] |
|||
}, |
|||
output: { |
|||
path: PATHS.build, |
|||
publicPath: '/build', |
|||
filename: '[name].[hash:5].js' |
|||
}, |
|||
resolve: { |
|||
modules: [path.resolve(__dirname, 'client/src'), path.resolve(__dirname, 'node_modules')], |
|||
extensions: ['.js', '.jsx'], |
|||
alias: { |
|||
crypto: false, |
|||
$utils: path.resolve(__dirname, 'client/src/utils/'), |
|||
$components: path.resolve(__dirname, 'client/src/components/'), |
|||
} |
|||
}, |
|||
plugins: [ |
|||
new HtmlWebpackPlugin({ |
|||
filename: '../index.html', |
|||
template: './client/index.ejs' |
|||
}) |
|||
], |
|||
optimization: { |
|||
splitChunks: { |
|||
cacheGroups: { |
|||
commons: { |
|||
test: /[\\/]node_modules[\\/]/, |
|||
name: "vendors", |
|||
chunks: "all" |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
module: { |
|||
rules: [{ |
|||
test: /\.css$/, |
|||
use: ['style-loader', { |
|||
loader: 'css-loader', |
|||
options: { |
|||
modules: true |
|||
} |
|||
}] |
|||
}, |
|||
{ |
|||
test: /\.less$/, |
|||
use: ['style-loader', 'css-loader', |
|||
{ |
|||
loader: 'less-loader', options: { |
|||
lessOptions: { |
|||
javascriptEnabled: true |
|||
} |
|||
} |
|||
}] |
|||
}, |
|||
{ |
|||
test: /\.jsx?$/, |
|||
use: 'babel-loader', |
|||
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], |
|||
},{ |
|||
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, |
|||
loader: "file-loader" |
|||
}] |
|||
} |
|||
}; |