├── src ├── styles │ ├── form_item.scss │ ├── entry.scss │ └── app.scss ├── templates │ ├── router.js.template │ ├── index.scss.template │ ├── bundleLoad.js.template │ ├── action.js.template │ ├── component.js.template │ ├── reducer.js.template │ └── container.js.template ├── assets │ └── img │ │ ├── common │ │ ├── bokeyuan.png │ │ ├── favicon.ico │ │ ├── git_code.png │ │ ├── loading.gif │ │ └── blog_icon.png │ │ └── javascriptErrorDetail │ │ ├── pc.png │ │ ├── click.png │ │ ├── browser.png │ │ ├── iPhone.png │ │ ├── pcDevice.png │ │ ├── url-bg.jpg │ │ ├── windows.png │ │ └── url-bg-active.jpg ├── components │ ├── loading │ │ ├── index.scss │ │ └── index.js │ └── header │ │ ├── index.scss │ │ └── index.js ├── modules │ ├── projectList │ │ ├── index.scss │ │ ├── index.js │ │ ├── reducer.js │ │ └── action.js │ ├── home │ │ ├── index.scss │ │ ├── action.js │ │ ├── reducer.js │ │ └── index.js │ ├── login │ │ ├── reducer.js │ │ ├── action.js │ │ ├── index.scss │ │ └── index.js │ ├── register │ │ ├── reducer.js │ │ ├── index.scss │ │ ├── action.js │ │ └── index.js │ ├── javascriptErrorDetail │ │ ├── reducer.js │ │ ├── action.js │ │ ├── index.scss │ │ └── index.js │ └── javascriptError │ │ ├── reducer.js │ │ ├── action.js │ │ ├── index.scss │ │ └── index.js ├── index.js ├── containers │ ├── home.js │ ├── login.js │ ├── register.js │ ├── projectList.js │ ├── javascriptError.js │ └── javascriptErrorDetail.js ├── router.js ├── lib │ ├── router.js │ ├── page │ │ ├── index.js │ │ └── index.scss │ ├── store.js │ ├── bundle.js │ └── entry.js ├── reducers.js ├── common │ ├── extension.js │ ├── env_config.js │ ├── http-api.js │ ├── utils.js │ └── http-util.js ├── bundle_load.js ├── chartConfig │ └── jsChartOption.js └── index.html ├── .gitignore ├── .babelrc ├── README.md ├── webpack.dev.js ├── api-server.js ├── package.json ├── webpack.pub.js ├── webpack.base.js ├── module.js └── .eslintrc /src/styles/form_item.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/templates/router.js.template: -------------------------------------------------------------------------------- 1 | { path: prePath + "/{1}", component: containers.{0} }, -------------------------------------------------------------------------------- /src/styles/entry.scss: -------------------------------------------------------------------------------- 1 | @import "./app"; 2 | @import "./form_item"; 3 | @import '~antd/dist/antd.css'; 4 | -------------------------------------------------------------------------------- /src/assets/img/common/bokeyuan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/common/bokeyuan.png -------------------------------------------------------------------------------- /src/assets/img/common/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/common/favicon.ico -------------------------------------------------------------------------------- /src/assets/img/common/git_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/common/git_code.png -------------------------------------------------------------------------------- /src/assets/img/common/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/common/loading.gif -------------------------------------------------------------------------------- /src/assets/img/common/blog_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/common/blog_icon.png -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/pc.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.* 3 | package-lock.json 4 | node_modules 5 | dist 6 | *.swp 7 | *.swo 8 | .idea/ 9 | .vscode/ 10 | .eslintrc -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/click.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/click.png -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/browser.png -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/iPhone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/iPhone.png -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/pcDevice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/pcDevice.png -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/url-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/url-bg.jpg -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/windows.png -------------------------------------------------------------------------------- /src/components/loading/index.scss: -------------------------------------------------------------------------------- 1 | .loading-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | min-height: 100%; 7 | } -------------------------------------------------------------------------------- /src/assets/img/javascriptErrorDetail/url-bg-active.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ringcrazy/webfunny_admin/HEAD/src/assets/img/javascriptErrorDetail/url-bg-active.jpg -------------------------------------------------------------------------------- /src/templates/index.scss.template: -------------------------------------------------------------------------------- 1 | .{1}-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | min-height: 100%; 7 | background: #f5f5f9; 8 | } -------------------------------------------------------------------------------- /src/modules/projectList/index.scss: -------------------------------------------------------------------------------- 1 | .projectList-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: #fff; 8 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { initApp } from "./lib/entry" 2 | import extraRoutes from "./router" 3 | import reducers from "./reducers" 4 | 5 | // 直接调用启动 6 | initApp("/webfunny", reducers, extraRoutes) 7 | 8 | -------------------------------------------------------------------------------- /src/modules/home/index.scss: -------------------------------------------------------------------------------- 1 | .home-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | padding-top: 150px; 6 | width: 100%; 7 | min-height: 100%; 8 | background: #f5f5f9; 9 | } -------------------------------------------------------------------------------- /src/templates/bundleLoad.js.template: -------------------------------------------------------------------------------- 1 | import {0}Container from "Containers/{1}" 2 | export const {0} = props => 3 | {Container => } 4 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Tahoma, Arial, "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", STXihei, "华文细黑", sans-serif; 3 | padding: 0; 4 | margin: 0; 5 | background-color: #FFF!important; 6 | } -------------------------------------------------------------------------------- /src/modules/home/action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | 3 | export const updateHomeState = createAction("updateHomeState", payload => payload) 4 | 5 | export const clearHomeState = createAction("clearHomeState") 6 | -------------------------------------------------------------------------------- /src/templates/action.js.template: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | // import HttpUtil from "Common/http-util" 3 | // import HttpApi from "Common/http-api" 4 | export const update{0}State = createAction("update{0}State", payload => payload) 5 | 6 | export const clear{0}State = createAction("clear{0}State") 7 | -------------------------------------------------------------------------------- /src/components/loading/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import { Spin } from "antd" 4 | export default class Loading extends Component { 5 | constructor(props) { 6 | super(props) 7 | } 8 | 9 | render() { 10 | return
11 | 12 |
13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/component.js.template: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | 4 | class {0} extends Component { 5 | constructor(props) { 6 | super(props) 7 | } 8 | 9 | componentDidMount() { 10 | } 11 | 12 | render() { 13 | return
{1}
14 | } 15 | } 16 | 17 | {0}.propTypes = { 18 | } 19 | 20 | export default {0} 21 | -------------------------------------------------------------------------------- /src/modules/home/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | isFetching: false 5 | } 6 | 7 | export default handleActions({ 8 | 9 | updateHomeState: (state = initialState, { payload }) => { 10 | return { 11 | ...state, 12 | ...payload 13 | } 14 | }, 15 | 16 | clearHomeState: () => { 17 | return { 18 | ...initialState 19 | } 20 | } 21 | }, initialState) 22 | -------------------------------------------------------------------------------- /src/templates/reducer.js.template: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | isFetching: false 5 | } 6 | 7 | export default handleActions({ 8 | 9 | update{0}State: (state = initialState, { payload }) => { 10 | return { 11 | ...state, 12 | ...payload 13 | } 14 | }, 15 | 16 | clear{0}State: () => { 17 | return { 18 | ...initialState 19 | } 20 | } 21 | }, initialState) 22 | -------------------------------------------------------------------------------- /src/modules/login/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | username: "", 5 | password: "" 6 | } 7 | 8 | export default handleActions({ 9 | 10 | updateLoginState: (state = initialState, { payload }) => { 11 | return { 12 | ...state, 13 | ...payload 14 | } 15 | }, 16 | 17 | clearLoginState: () => { 18 | return { 19 | ...initialState 20 | } 21 | } 22 | }, initialState) 23 | -------------------------------------------------------------------------------- /src/modules/projectList/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import Header from "Components/header" 4 | 5 | export default class ProjectList extends Component { 6 | constructor(props) { 7 | super(props) 8 | } 9 | 10 | componentDidMount() { 11 | } 12 | 13 | render() { 14 | return
15 |
16 |
17 |
18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/register/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | username: "", 5 | password: "" 6 | } 7 | 8 | export default handleActions({ 9 | 10 | updateRegisterState: (state = initialState, { payload }) => { 11 | return { 12 | ...state, 13 | ...payload 14 | } 15 | }, 16 | 17 | clearRegisterState: () => { 18 | return { 19 | ...initialState 20 | } 21 | } 22 | }, initialState) 23 | -------------------------------------------------------------------------------- /src/modules/projectList/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | username: "", 5 | password: "" 6 | } 7 | 8 | export default handleActions({ 9 | 10 | updateProjectListState: (state = initialState, { payload }) => { 11 | return { 12 | ...state, 13 | ...payload 14 | } 15 | }, 16 | 17 | clearProjectListState: () => { 18 | return { 19 | ...initialState 20 | } 21 | } 22 | }, initialState) 23 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | ["latest", {"modules": false}], 5 | "stage-3" 6 | ], 7 | // "plugins": [ 8 | // "react-hot-loader/babel" // Enables React code to work with HMR 9 | // ["import", { 10 | // "libraryName": "antd", 11 | // "style": true 12 | // }] 13 | // ], 14 | "plugins": [ 15 | "react-hot-loader/babel", 16 | ["import", { 17 | "libraryName": "antd", 18 | "style": "css" 19 | } 20 | ] 21 | ] 22 | } -------------------------------------------------------------------------------- /src/containers/home.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import Home from "Modules/home" 5 | import * as actions from "Modules/home/action" 6 | 7 | const HomeContainer = props => 8 | 9 | const mapStateToProps = state => { 10 | const { home } = state 11 | return { ...home } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)(HomeContainer) 17 | -------------------------------------------------------------------------------- /src/containers/login.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import Login from "Modules/login" 5 | import * as actions from "Modules/login/action" 6 | 7 | const LoginContainer = props => 8 | 9 | const mapStateToProps = state => { 10 | const { login } = state 11 | return { ...login } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer) 17 | -------------------------------------------------------------------------------- /src/modules/login/action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | import HttpUtil from "Common/http-util" 3 | import HttpApi from "Common/http-api" 4 | export const updateLoginState = createAction("updateLoginState", payload => payload) 5 | 6 | export const clearLoginState = createAction("clearLoginState") 7 | 8 | export const loginAction = (param, handleResult) => () => { 9 | return HttpUtil.post(HttpApi.login, param).then( response => { 10 | console.log(response) 11 | handleResult(response) 12 | }, () => { 13 | console.log("失败") 14 | } 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/templates/container.js.template: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import {0} from "Modules/{1}" 5 | import * as actions from "Modules/{1}/action" 6 | 7 | const {0}Container = props => <{0} {...props} /> 8 | 9 | const mapStateToProps = state => { 10 | const { {1} } = state 11 | return { ...{1} } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)({0}Container) 17 | -------------------------------------------------------------------------------- /src/modules/login/index.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | min-height: 100%; 7 | background: #fff; 8 | .box { 9 | width: 600px; 10 | height: 450px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | margin-top: 200px; 14 | .input-box { 15 | margin: 15px 0; 16 | } 17 | } 18 | .btn { 19 | margin: auto; 20 | display: block; 21 | float: left; 22 | width: 50%; 23 | margin-top: 10px; 24 | } 25 | } -------------------------------------------------------------------------------- /src/modules/projectList/action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | import HttpUtil from "Common/http-util" 3 | import HttpApi from "Common/http-api" 4 | export const updateProjectListState = createAction("updateProjectListState", payload => payload) 5 | 6 | export const clearProjectListState = createAction("clearProjectListState") 7 | 8 | export const getProjectListAction = (handleResult) => () => { 9 | return HttpUtil.get(HttpApi.projectList).then( response => { 10 | handleResult(response.data) 11 | }, () => { 12 | console.log("未能成功获取支持银行卡列表") 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/register/index.scss: -------------------------------------------------------------------------------- 1 | .register-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | min-height: 100%; 7 | background: #fff; 8 | .box { 9 | width: 600px; 10 | height: 450px; 11 | margin-left: auto; 12 | margin-right: auto; 13 | margin-top: 200px; 14 | .input-box { 15 | margin: 15px 0; 16 | } 17 | } 18 | .btn { 19 | margin: auto; 20 | display: block; 21 | float: left; 22 | width: 50%; 23 | margin-top: 10px; 24 | } 25 | } -------------------------------------------------------------------------------- /src/containers/register.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import Register from "Modules/register" 5 | import * as actions from "Modules/register/action" 6 | 7 | const RegisterContainer = props => 8 | 9 | const mapStateToProps = state => { 10 | const { register } = state 11 | return { ...register } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)(RegisterContainer) 17 | -------------------------------------------------------------------------------- /src/modules/javascriptErrorDetail/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | errorIndex: 0, 5 | errorAboutInfo: {}, 6 | errorDetail: {}, 7 | errorList: [], 8 | errorStackList: [], 9 | isIgnore: false 10 | } 11 | 12 | export default handleActions({ 13 | 14 | updateJavascriptErrorDetailState: (state = initialState, { payload }) => { 15 | return { 16 | ...state, 17 | ...payload 18 | } 19 | }, 20 | 21 | clearJavascriptErrorDetailState: () => { 22 | return { 23 | ...initialState 24 | } 25 | } 26 | }, initialState) 27 | -------------------------------------------------------------------------------- /src/containers/projectList.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import ProjectList from "Modules/projectList" 5 | import * as actions from "Modules/projectList/action" 6 | 7 | const ProjectListContainer = props => 8 | 9 | const mapStateToProps = state => { 10 | const { projectList } = state 11 | return { ...projectList } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)(ProjectListContainer) 17 | -------------------------------------------------------------------------------- /src/containers/javascriptError.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import JavascriptError from "Modules/javascriptError" 5 | import * as actions from "Modules/javascriptError/action" 6 | 7 | const JavascriptErrorContainer = props => 8 | 9 | const mapStateToProps = state => { 10 | const { javascriptError } = state 11 | return { ...javascriptError } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)(JavascriptErrorContainer) 17 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import * as containers from "./bundle_load" 2 | 3 | // 聚合路由 4 | const prePath = BUILD_ENV === "local" ? "/webfunny" : "" 5 | export default [ 6 | { path: prePath + "/", component: containers.Home, exact: true }, 7 | { path: prePath + "/home", component: containers.Home, exact: true }, 8 | { path: prePath + "/projectList", component: containers.ProjectList }, 9 | { path: prePath + "/register", component: containers.Register }, 10 | { path: prePath + "/login", component: containers.Login }, 11 | { path: prePath + "/javascriptError", component: containers.JavascriptError }, 12 | { path: prePath + "/javascriptErrorDetail", component: containers.JavascriptErrorDetail }, 13 | ] -------------------------------------------------------------------------------- /src/containers/javascriptErrorDetail.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { bindActionCreators } from "redux" 3 | import { connect } from "react-redux" 4 | import JavascriptErrorDetail from "Modules/javascriptErrorDetail" 5 | import * as actions from "Modules/javascriptErrorDetail/action" 6 | 7 | const JavascriptErrorDetailContainer = props => 8 | 9 | const mapStateToProps = state => { 10 | const { javascriptErrorDetail } = state 11 | return { ...javascriptErrorDetail } 12 | } 13 | const mapDispatchToProps = (dispatch) => { 14 | return bindActionCreators({ ...actions }, dispatch) 15 | } 16 | export default connect(mapStateToProps, mapDispatchToProps)(JavascriptErrorDetailContainer) 17 | -------------------------------------------------------------------------------- /src/lib/router.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react" 2 | import { Route } from "react-router-dom" 3 | import Page from "./page" 4 | 5 | class Routes extends Component { 6 | constructor(props) { 7 | super(props) 8 | } 9 | render() { 10 | return { 11 | return 12 | {this.props.extraRoutes.map((r, index) => { 13 | return 14 | })} 15 | 16 | }} /> 17 | } 18 | } 19 | 20 | Routes.PropTypes = { 21 | extraRoutes: PropTypes.object, 22 | } 23 | 24 | export default Routes 25 | -------------------------------------------------------------------------------- /src/modules/javascriptError/reducer.js: -------------------------------------------------------------------------------- 1 | import { handleActions } from "redux-actions" 2 | 3 | const initialState = { 4 | timeType: "month", 5 | jsErrorList: [], 6 | ignoreErrorList: [], 7 | jsErrorListByPage: [], 8 | pageErrorList: [], 9 | maxPageErrorCount: 0, 10 | totalPercent: "0", 11 | pcPercent: "0", 12 | iosPercent: "0", 13 | androidPercent: "0", 14 | activeKeyTop: "1", 15 | activeKeyDown: "1", 16 | project: null, 17 | simpleUrl: "" 18 | } 19 | 20 | export default handleActions({ 21 | 22 | updateJavascriptErrorState: (state = initialState, { payload }) => { 23 | return { 24 | ...state, 25 | ...payload 26 | } 27 | }, 28 | 29 | clearJavascriptErrorState: () => { 30 | return { 31 | ...initialState 32 | } 33 | } 34 | }, initialState) 35 | -------------------------------------------------------------------------------- /src/reducers.js: -------------------------------------------------------------------------------- 1 | import homeReducer from "Modules/home/reducer" 2 | import javascriptErrorReducer from "Modules/javascriptError/reducer" 3 | import javascriptErrorDetailReducer from "Modules/javascriptErrorDetail/reducer" 4 | import loginReducer from "Modules/login/reducer" 5 | import projectListReducer from "Modules/projectList/reducer" 6 | import registerReducer from "Modules/register/reducer" 7 | 8 | export default { 9 | home: {reducer: homeReducer, isCached: false}, 10 | javascriptError: {reducer: javascriptErrorReducer, isCached: false}, 11 | javascriptErrorDetail: {reducer: javascriptErrorDetailReducer, isCached: false}, 12 | login: {reducer: loginReducer, isCached: false}, 13 | projectList: {reducer: projectListReducer, isCached: false}, 14 | register: {reducer: registerReducer, isCached: false} 15 | } -------------------------------------------------------------------------------- /src/modules/register/action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | import HttpUtil from "Common/http-util" 3 | import HttpApi from "Common/http-api" 4 | export const updateRegisterState = createAction("updateRegisterState", payload => payload) 5 | 6 | export const clearRegisterState = createAction("clearRegisterState") 7 | 8 | export const registerAction = (param, handleResult) => () => { 9 | return HttpUtil.post(HttpApi.register, param).then( response => { 10 | console.log(response) 11 | handleResult(response) 12 | }, () => { 13 | console.log("失败") 14 | } 15 | ) 16 | } 17 | 18 | export const userListAction = (handleResult) => () => { 19 | return HttpUtil.get(HttpApi.userList).then( response => { 20 | console.log(response) 21 | handleResult(response) 22 | }, () => { 23 | console.log("失败") 24 | } 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/page/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from "react" 2 | import { CSSTransitionGroup } from "react-transition-group" 3 | import "./index.scss" 4 | class Page extends Component { 5 | constructor(props) { 6 | super(props) 7 | } 8 | render() { 9 | const { pageAnimate, ident, children } = this.props 10 | return ( 11 | 17 |
18 | {children} 19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | Page.defaultProps = { 26 | pageAnimate: "push" 27 | } 28 | 29 | Page.PropTypes = { 30 | pageAnimate: PropTypes.string, 31 | ident: PropTypes.string, 32 | children: PropTypes.list 33 | } 34 | export default Page 35 | -------------------------------------------------------------------------------- /src/lib/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers, applyMiddleware } from "redux" 2 | import thunkMiddleware from "redux-thunk" 3 | import { routerReducer, routerMiddleware } from "react-router-redux" 4 | import { composeWithDevTools } from "redux-devtools-extension" 5 | 6 | const getStore = (history, extraReducers) => { 7 | const historyMiddleware = routerMiddleware(history) 8 | 9 | const middleware = BUILD_ENV === "local" 10 | ? composeWithDevTools(applyMiddleware(historyMiddleware, thunkMiddleware)) 11 | : applyMiddleware(historyMiddleware, thunkMiddleware) 12 | 13 | // 以下是需要缓存,和不需要缓存的处理 14 | const reducers = {} 15 | for (const key in extraReducers) { 16 | if (!extraReducers.hasOwnProperty(key)) continue 17 | const obj = extraReducers[key] 18 | reducers[key] = obj.reducer 19 | } 20 | const store = createStore( 21 | combineReducers({ 22 | ...reducers, 23 | router: routerReducer 24 | }), 25 | middleware 26 | ) 27 | 28 | 29 | return store 30 | } 31 | 32 | export default getStore 33 | -------------------------------------------------------------------------------- /src/components/header/index.scss: -------------------------------------------------------------------------------- 1 | .header-container { 2 | width: 100%; 3 | height: 62px; 4 | position: fixed; 5 | top: 0; 6 | background: #fff; 7 | z-index: 9; 8 | .sub-header { 9 | display: flex; 10 | align-items: center; 11 | padding-left: 30px; 12 | padding-right: 30px; 13 | color: #493e54; 14 | border-bottom: 0; 15 | position: relative; 16 | margin: 0 0 10px; 17 | line-height: 1; 18 | font-size: 16px; 19 | min-height: 62px; 20 | box-shadow: 0 2px 0 rgba(37, 11, 54,.04); 21 | .project-select-box { 22 | width: 100%; 23 | .ant-dropdown-link { 24 | color: #2f2936; 25 | } 26 | } 27 | } 28 | .github-box { 29 | font-size: 26px; 30 | color: #727082; 31 | margin-right: 30px; 32 | &:hover { 33 | color: #1890ff; 34 | } 35 | } 36 | .bokeyuan-box { 37 | img { 38 | width: 26px; 39 | } 40 | &:hover { 41 | color: #1890ff; 42 | } 43 | } 44 | } 45 | 46 | .code-icon, .blog-icon { 47 | width: 16px!important; 48 | margin-right: 10px!important; 49 | } -------------------------------------------------------------------------------- /src/lib/page/index.scss: -------------------------------------------------------------------------------- 1 | // 进出场动画 2 | // 向左滑入 3 | .push-enter { 4 | transform: translate3D(100%, 0, 0); 5 | transition: transform .2s ease-in; 6 | 7 | &.push-enter-active { 8 | transform: translate3D(0, 0, 0); 9 | } 10 | } 11 | 12 | // 向左滑出 13 | .push-leave { 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | opacity: 1; 18 | transform: translate3D(0, 0, 0); 19 | transition: transform .15s ease-out; 20 | 21 | &.push-leave-active { 22 | opacity: 0.01; 23 | transform: translate3D(-100%, 0, 0); 24 | } 25 | } 26 | 27 | // 向右滑入 28 | .back-enter { 29 | transform: translate3D(-100%, 0, 0); 30 | transition: transform .2s ease-in; 31 | 32 | &.back-enter-active { 33 | transform: translate3D(0, 0, 0); 34 | } 35 | } 36 | 37 | // 向右滑出 38 | .back-leave { 39 | position: absolute; 40 | left: 0; 41 | top: 0; 42 | opacity: 1; 43 | transform: translate3D(0, 0, 0); 44 | transition: transform .15s ease-out; 45 | 46 | &.back-leave-active { 47 | opacity: 0.01; 48 | transform: translate3D(100%, 0, 0); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/home/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import Header from "Components/header" 4 | import { Row, Col, Card } from "antd" 5 | 6 | export default class Home extends Component { 7 | constructor(props) { 8 | super(props) 9 | } 10 | 11 | componentDidMount() { 12 | } 13 | 14 | render() { 15 | return
16 |
17 | 18 | 19 | 更多} bodyStyle={{ padding: 0 }}> 20 |
21 | example 22 |
23 |
24 |

用户行为分析

25 |

记录用户在页面上的各种行为

26 |
27 |
28 | 29 | 30 | 跳转到JsERROR 31 | 32 |
33 | 34 |
35 | } 36 | turnToJsError() { 37 | this.props.history.push("projectList") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/common/extension.js: -------------------------------------------------------------------------------- 1 | if (!String.prototype.format) { 2 | String.prototype.format = function() { 3 | const fmt = this 4 | const params = Array.prototype.slice.call(arguments) 5 | return fmt.replace(/(\{(\d+)\})/g, function(match, firstCap, index) { 6 | return params[index] === undefined ? match : params[index] 7 | }) 8 | } 9 | } 10 | if (!String.prototype.trim) { 11 | const rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g 12 | String.prototype.trim = function() { 13 | return this.replace(rtrim, "") 14 | } 15 | } 16 | if (!Date.prototype.Format) { 17 | Date.prototype.Format = function(format) { // author: meizz 18 | let fmt = format 19 | const o = { 20 | "M+": this.getMonth() + 1, // 月份 21 | "d+": this.getDate(), // 日 22 | "h+": this.getHours(), // 小时 23 | "m+": this.getMinutes(), // 分 24 | "s+": this.getSeconds(), // 秒 25 | "q+": Math.floor((this.getMonth() + 3) / 3), // 季度 26 | "S": this.getMilliseconds() // 毫秒 27 | } 28 | if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length)) 29 | for (const k in o) {if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length))} 30 | return fmt 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modules/login/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import { Card, Button, Input } from "antd" 4 | class Login extends Component { 5 | constructor(props) { 6 | super(props) 7 | } 8 | 9 | componentDidMount() { 10 | } 11 | 12 | render() { 13 | const {username, password} = this.props 14 | return
15 | 16 | { 21 | this.props.updateLoginState({username: e.target.value}) 22 | }} 23 | /> 24 | { 29 | this.props.updateLoginState({password: e.target.value}) 30 | }} 31 | /> 32 | 33 | 34 | 35 |
36 | } 37 | login() { 38 | const { username, password } = this.props 39 | this.props.loginAction({ username, password }, (res) => { 40 | console.log(res) 41 | }) 42 | } 43 | } 44 | 45 | Login.propTypes = { 46 | } 47 | 48 | export default Login 49 | -------------------------------------------------------------------------------- /src/lib/bundle.js: -------------------------------------------------------------------------------- 1 | import { Component, PropTypes } from "react" 2 | class Bundle extends Component { 3 | constructor(props) { 4 | super(props) 5 | this.state = { 6 | container: null 7 | } 8 | } 9 | componentWillMount() { 10 | this.load(this.props) 11 | } 12 | componentDidMount() { 13 | // 提前加载一页需要加载的组件 14 | this.preload(this.props) 15 | } 16 | componentWillReceiveProps(nextProps) { 17 | if (nextProps.loadContainer !== this.props.loadContainer) { 18 | this.load(nextProps) 19 | this.preload(nextProps) 20 | } 21 | } 22 | 23 | 24 | render() { 25 | const { container } = this.state 26 | 27 | return container ? this.props.children(container) : null 28 | } 29 | 30 | load(props) { 31 | document.title = props.title 32 | this.setState({ container: null }) 33 | props.loadContainer( container => { 34 | this.setState({ container: container.default ? container.default : container }) 35 | }) 36 | } 37 | preload(props) { 38 | // bundle next属性传递下一个需要加载的页面的container(可以是数组,多个),可以提前加载,使得下个页面加载更快 39 | if (props.next) { 40 | const nextPages = Array.isArray(props.next) ? props.next : [props.next] 41 | 42 | nextPages.forEach((pageComponent) => { 43 | pageComponent(() => {}) 44 | }) 45 | } 46 | } 47 | } 48 | 49 | Bundle.propTypes = { 50 | loadContainer: PropTypes.func, 51 | children: PropTypes.func, 52 | title: PropTypes.string, 53 | entry: PropTypes.bool 54 | } 55 | 56 | export default Bundle 57 | -------------------------------------------------------------------------------- /src/bundle_load.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Bundle from "./lib/bundle" // 调用基础库的方法 3 | 4 | // 加载模块 5 | import HomeContainer from "Containers/home" 6 | import ProjectListContainer from "Containers/projectList" 7 | 8 | 9 | export const Home = props => 10 | {Container => } 11 | 12 | 13 | 14 | export const ProjectList = props => 15 | {Container => } 16 | 17 | 18 | import RegisterContainer from "Containers/register" 19 | export const Register = props => 20 | {Container => } 21 | 22 | 23 | import LoginContainer from "Containers/login" 24 | export const Login = props => 25 | {Container => } 26 | 27 | 28 | import JavascriptErrorContainer from "Containers/javascriptError" 29 | export const JavascriptError = props => 30 | {Container => } 31 | 32 | 33 | import JavascriptErrorDetailContainer from "Containers/javascriptErrorDetail" 34 | export const JavascriptErrorDetail = props => 35 | {Container => } 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 简介: 2 | 3 | 这是一个一react技术栈为基础的单页应用 4 | 5 | 技术组成: es6 + webpack + react + react-redux + react-router + node.js v6.4.0 6 | 7 | 这是一个完整的应用级项目,能够适用于很多的业务场景 8 | 9 | 这里边包含了完整的组件化管理,数据流管理,接口请求业务逻辑处理 10 | 11 | 使用脚本创建模块,能够进行快速的开发和迭代 12 | 13 | 这是一个运用于前端监控系统的项目,我在知乎上对其作用做了介绍。 14 | 15 | 线上Demo: 16 | https://www.webfunny.cn/webfunny/javascriptError 17 | 18 | 博客讲解: 19 | https://www.cnblogs.com/warm-stranger/p/9417084.html 20 | 21 | 如何运行: 22 | 23 | Node版本: v6.4.0 24 | 25 | 安装依赖包: npm install 26 | 27 | 本地运行: npm run start 28 | 29 | 运行完成后访问: http://localhost:9010/webfunny/javascriptError 目前以这个页面为开始 30 | 31 | 32 | 如何打包: 33 | 34 | 打包QA环境: npm run qa 35 | 36 | 打包staging环境:npm run staging 37 | 38 | 打包生产环境: npm run prod 39 | 40 | 41 | 42 | 一、如何增加业务线模块 43 | 44 | node module.js moduleName 45 | 46 | 即可快速创建一系列的模块代码 47 | modules, action, reducer, container, scss, router 48 | 49 | 二、如何部署打包的代码 50 | 51 | 需要安装Nginx服务器, 配置如下 52 | server { 53 | listen 8010; 54 | server_name localhost; 55 | root /Users/jiangyw/WebstormProjects/webfunny/dist; 56 | index /webfunny/index.html; 57 | location /webfunny/ { 58 | try_files $uri /webfunny/index.html; 59 | } 60 | } 61 | 重启Nginx后 62 | 63 | 访问 http://localhost:8010/webfunny/javascriptError 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/common/env_config.js: -------------------------------------------------------------------------------- 1 | const envUrls = { 2 | local: { 3 | apiServerUrl: "http://localhost:8010", 4 | nodeApiServerUrl: "http://localhost:9010", 5 | uri: "http://localhost" 6 | }, 7 | dev: { 8 | apiServerUrl: "", 9 | nodeApiServerUrl: "", 10 | uri: "" 11 | }, 12 | qa: { 13 | apiServerUrl: "https://www.webfunny.cn", 14 | assetsUrl: "https://www.webfunny.cn", 15 | nodeApiServerUrl: "https://www.webfunny.cn", 16 | uri: "https://www.webfunny.cn" 17 | }, 18 | staging: { 19 | apiServerUrl: "https://www.webfunny.cn", 20 | assetsUrl: "https://www.webfunny.cn", 21 | nodeApiServerUrl: "https://www.webfunny.cn", 22 | uri: "https://www.webfunny.cn" 23 | }, 24 | prod: { 25 | apiServerUrl: "https://www.webfunny.cn", 26 | assetsUrl: "https://www.webfunny.cn", 27 | nodeApiServerUrl: "https://www.webfunny.cn", 28 | uri: "https://www.webfunny.cn" 29 | } 30 | } 31 | 32 | const getApiHost = () => { 33 | return envUrls[BUILD_ENV].apiServerUrl 34 | } 35 | 36 | const getNodeApiHost = () => { 37 | return envUrls[BUILD_ENV].nodeApiServerUrl 38 | } 39 | // relativePath eg: "/ltvfe/cl/" 40 | const getAssetsUrl = (env = BUILD_ENV, relativePath) => { 41 | const assetsUrl = envUrls[env].assetsUrl || "" 42 | const suffix = env === "local" ? "/webfunny/" : relativePath 43 | return assetsUrl + suffix 44 | } 45 | 46 | const getUri = () => { 47 | return envUrls[BUILD_ENV].uri 48 | } 49 | 50 | module.exports = { 51 | getApiHost, 52 | getNodeApiHost, 53 | getAssetsUrl, 54 | getUri 55 | } 56 | -------------------------------------------------------------------------------- /src/modules/javascriptError/action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | import HttpUtil from "Common/http-util" 3 | import HttpApi from "Common/http-api" 4 | export const updateJavascriptErrorState = createAction("updateJavascriptErrorState", payload => payload) 5 | 6 | export const clearJavascriptErrorState = createAction("clearJavascriptErrorState") 7 | 8 | export const getJsErrorCountByDayAction = (param, handleResult) => () => { 9 | return HttpUtil.get(HttpApi.getJsErrorCountByDay, param).then( response => { 10 | handleResult(response) 11 | }) 12 | } 13 | export const getJsErrorCountByHourAction = (handleResult) => () => { 14 | return HttpUtil.get(HttpApi.getJsErrorCountByHour).then( response => { 15 | handleResult(response) 16 | }) 17 | } 18 | export const getJsErrorCountByPageAction = (param, handleResult) => () => { 19 | return HttpUtil.get(HttpApi.getJavascriptErrorListByPage, param).then( response => { 20 | handleResult(response.data) 21 | }) 22 | } 23 | 24 | export const getJsErrorSortAction = (param, handleResult) => () => { 25 | return HttpUtil.post(HttpApi.getJsErrorSort, param).then( response => { 26 | handleResult(response) 27 | }) 28 | } 29 | 30 | export const getJavascriptErrorCountByOsAction = (param, handleResult) => () => { 31 | return HttpUtil.get(HttpApi.getJavascriptErrorCountByOs, param).then( response => { 32 | handleResult(response.data) 33 | }) 34 | } 35 | 36 | export const getIgnoreJavascriptErrorListAction = (handleResult) => () => { 37 | return HttpUtil.get(HttpApi.ignoreErrorByApplication).then( response => { 38 | handleResult(response.data) 39 | }) 40 | } -------------------------------------------------------------------------------- /src/modules/register/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import { Card, Button, Input } from "antd" 4 | class Register extends Component { 5 | constructor(props) { 6 | super(props) 7 | } 8 | 9 | componentDidMount() { 10 | } 11 | 12 | render() { 13 | const { username, password } = this.props 14 | return
15 | 16 | { 21 | this.props.updateRegisterState({username: e.target.value}) 22 | }} 23 | /> 24 | { 29 | this.props.updateRegisterState({password: e.target.value}) 30 | }} 31 | /> 32 | 33 | 34 | 35 | 36 |
37 | } 38 | register() { 39 | const { username, password } = this.props 40 | this.props.registerAction({ username, password }, (res) => { 41 | console.log(res) 42 | }) 43 | } 44 | goLogin() { 45 | fetch("http://localhost:3000/api/v1/user/list", (res) => { 46 | console.log(res) 47 | }) 48 | } 49 | } 50 | 51 | Register.propTypes = { 52 | } 53 | 54 | export default Register 55 | -------------------------------------------------------------------------------- /src/common/http-api.js: -------------------------------------------------------------------------------- 1 | import envConfig from "./env_config" 2 | 3 | const apiHost = envConfig.getApiHost() 4 | const api = { 5 | // 登录 6 | "login": apiHost + "/api/v1/user/login", 7 | // 注册 8 | "register": apiHost + "/api/v1/user", 9 | // 查询所有用户 10 | "userList": apiHost + "/api/v1/user/list", 11 | // 查询所有项目列表 12 | "projectList": apiHost + "/api/v1/project/list", 13 | 14 | 15 | // 根据时间查询每天JS的错误量 16 | "getJsErrorCountByDay": apiHost + "/api/v1/getJavascriptErrorInfoListByDay", 17 | // 根据时间查询一天内js错误总量和最近几小时的错误量 18 | "getJsErrorCountByHour": apiHost + "/api/v1/getJavascriptErrorInfoListByHour", 19 | // 根据JS错误量进行排序 20 | "getJsErrorSort": apiHost + "/api/v1/getJavascriptErrorSort", 21 | // 根据平台获取JS错误数量 22 | "getJavascriptErrorCountByOs": apiHost + "/api/v1/getJavascriptErrorCountByOs", 23 | // errorMsg 获取js错误列表 24 | "getJavascriptErrorListByMsg": apiHost + "/api/v1/getJavascriptErrorListByMsg", 25 | // 获取js错误相关信息 26 | "getJavascriptErrorAboutInfo": apiHost + "/api/v1/getJavascriptErrorAboutInfo", 27 | // 获取js错误详情 28 | "getJavascriptErrorDetail": id => apiHost + "/api/v1/javascriptErrorInfo/" + id, 29 | // 获取js错误对应的code 30 | "getJavascriptErrorStackCode": apiHost + "/api/v1/getJavascriptErrorStackCode", 31 | // 根据页面每天JS的错误量 32 | "getJavascriptErrorListByPage": apiHost + "/api/v1/getJavascriptErrorListByPage", 33 | // 设置需要忽略的js错误 34 | "setIgnoreJavascriptError": apiHost + "/api/v1/ignoreError", 35 | // 获取忽略的js错误列表 36 | "ignoreErrorByApplication": apiHost + "/api/v1/ignoreErrorByApplication", 37 | 38 | } 39 | 40 | const nodeApi = { 41 | } 42 | 43 | export default { 44 | ...api, 45 | ...nodeApi 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/modules/javascriptErrorDetail/action.js: -------------------------------------------------------------------------------- 1 | import { createAction } from "redux-actions" 2 | import HttpUtil from "Common/http-util" 3 | import HttpApi from "Common/http-api" 4 | export const updateJavascriptErrorDetailState = createAction("updateJavascriptErrorDetailState", payload => payload) 5 | 6 | export const clearJavascriptErrorDetailState = createAction("clearJavascriptErrorDetailState") 7 | 8 | export const getJavascriptErrorDetailAction = (param, handleResult) => () => { 9 | return HttpUtil.get(HttpApi.getJavascriptErrorDetail(param.id), param).then( response => { 10 | handleResult(response) 11 | }) 12 | } 13 | export const getJavascriptErrorListByMsgAction = (param, handleResult) => () => { 14 | return HttpUtil.post(HttpApi.getJavascriptErrorListByMsg, param).then( response => { 15 | handleResult(response.data) 16 | }) 17 | } 18 | 19 | export const getJavascriptErrorStackCodeAction = (param, handleResult) => () => { 20 | return HttpUtil.post(HttpApi.getJavascriptErrorStackCode, param).then( response => { 21 | handleResult(response.data) 22 | }) 23 | } 24 | 25 | export const getJavascriptErrorAboutInfoAction = (param, handleResult) => () => { 26 | return HttpUtil.post(HttpApi.getJavascriptErrorAboutInfo, param).then( response => { 27 | handleResult(response.data) 28 | }) 29 | } 30 | 31 | export const setIgnoreJavascriptErrorAction = (param, handleResult) => () => { 32 | return HttpUtil.post(HttpApi.setIgnoreJavascriptError, param).then( response => { 33 | handleResult(response.data) 34 | }) 35 | } 36 | 37 | export const getIgnoreJavascriptErrorListAction = (handleResult) => () => { 38 | return HttpUtil.get(HttpApi.ignoreErrorByApplication).then( response => { 39 | handleResult(response.data) 40 | }) 41 | } -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const Merge = require('webpack-merge'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const autoprefixer = require('autoprefixer'); 6 | // const pxtorem = require('postcss-pxtorem'); 7 | const baseConfig = require('./webpack.base.js'); 8 | const OpenBrowserPlugin = require('open-browser-webpack-plugin'); 9 | 10 | const port = 9010; 11 | module.exports = env => { 12 | return Merge(baseConfig, { 13 | entry: [ 14 | 'babel-polyfill', 15 | 'react-hot-loader/patch', 16 | path.resolve(__dirname, 'src/index.js'), 17 | ], 18 | output: { 19 | filename: "app.[hash:8].js", 20 | chunkFilename: '[name].[chunkhash:8].chunk.js', 21 | path: path.resolve(__dirname, 'dist'), 22 | publicPath: '/' 23 | }, 24 | devServer: { 25 | contentBase: path.resolve(__dirname), 26 | compress: true, 27 | historyApiFallback: true, 28 | hot: true, 29 | inline: true, 30 | port: port, 31 | host: '0.0.0.0', 32 | disableHostCheck: true, 33 | proxy: [ 34 | { 35 | context: ['/webfunny/**'], 36 | target: 'https://webfunny.com/', 37 | secure: false 38 | } 39 | ] 40 | }, 41 | devtool: "source-map", 42 | plugins: [ 43 | new webpack.HotModuleReplacementPlugin(), // Enable HMR 44 | new webpack.DefinePlugin({ 45 | 'process.env.NODE_ENV': JSON.stringify('development'), 46 | 'BUILD_ENV': JSON.stringify(env) 47 | }), 48 | new HtmlWebpackPlugin({ 49 | template: './src/index.html', 50 | htmlWebpackPlugin: { 51 | 'files': { 52 | 'js': ['index.js'] 53 | } 54 | } 55 | }), 56 | new webpack.ProvidePlugin({ 57 | $: 'zepto-webpack' 58 | }), 59 | new OpenBrowserPlugin({ url: 'http://localhost:'+port}) 60 | ] 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /api-server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var express = require('express'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var bodyParser = require('body-parser'); 7 | var cors = require('cors'); 8 | var app = express(); 9 | 10 | var jsonFileBase = './mock'; 11 | function isFunction(obj) { 12 | return Object.prototype.toString.call(obj) == '[object Function]'; 13 | } 14 | function route(fileName) { 15 | return function(req, res) { 16 | console.log(req.body); 17 | var fn = fileName; 18 | var args = process.argv; 19 | if (isFunction(fn)) fn = fn(req); 20 | setTimeout(function() { 21 | jsonFromFile(res, fn); 22 | }, 500); 23 | }; 24 | } 25 | 26 | function jsonFromFile(res, fileName) { 27 | fs.readFile(fileName, {encoding:'utf8'}, function(err, data) { 28 | if (err) { 29 | console.log(err); 30 | throw err; 31 | } 32 | res.json(JSON.parse(data)); 33 | }); 34 | } 35 | 36 | function gets(router, routes) { 37 | routes.forEach(function(r) { 38 | var p = path.join(jsonFileBase, r.replace(/\/:[^\/]+(?=[\/$])/g, '')); 39 | if (p[p.length - 1] == '/') p = p.substr(0, p.length - 1); 40 | p += '.json'; 41 | router.use(r, route(p)); 42 | }); 43 | } 44 | 45 | app.use(function(req, res, next) { 46 | console.log(req.originalUrl); 47 | next(); 48 | }); 49 | app.use(cors({ 50 | origin: '*', 51 | exposedHeaders: 'access-token' 52 | })); 53 | app.use(bodyParser.json()); 54 | app.use(bodyParser.urlencoded({extended: true})); 55 | 56 | var router = express.Router(); 57 | gets(router, 58 | [ 59 | '' 60 | ]); 61 | 62 | 63 | 64 | router.post('', function(req, res) { 65 | console.log(req.body); 66 | res.json({ 67 | "status": 0, 68 | "msg": "失败", 69 | "data": { 70 | "success":true, 71 | "reachMaxCount":false, 72 | "msg":"银行卡信息校验失败" 73 | } 74 | }); 75 | }); 76 | 77 | 78 | app.use('/', router); 79 | 80 | var server = app.listen(9011, function() { 81 | var address = server.address(); 82 | console.log('api server is running at:' + address.port); 83 | }); 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | export default class Utils { 2 | static isArray(object) { 3 | return Object.prototype.toString.call(object) === "[object Array]" 4 | } 5 | static isObject(object) { 6 | return Object.prototype.toString.call(object) === "[object Object]" 7 | } 8 | static qs(object, cache) { 9 | const arr = [] 10 | function inner(innerObj, prefix) { 11 | for (const prop in innerObj) { 12 | if (!innerObj.hasOwnProperty(prop)) return 13 | const textValue = innerObj[prop] 14 | if (!Utils.isArray(textValue)) { 15 | if (Utils.isObject(textValue)) inner(textValue, prefix ? prefix + "." + prop : prop) 16 | else arr.push(encodeURIComponent((prefix ? prefix + "." : "") + prop) + "=" + encodeURIComponent(textValue || "")) 17 | } else { 18 | textValue.forEach((val) => { 19 | arr.push(encodeURIComponent((prefix ? prefix + "." : "") + prop + "[]") + "=" + encodeURIComponent(val || "")) 20 | }) 21 | } 22 | } 23 | } 24 | inner(object, "") 25 | if (cache && !object._) { 26 | arr.push("_=" + encodeURIComponent(BUILD_NO)) 27 | } 28 | return arr.length ? "?" + arr.join("&") : "" 29 | } 30 | 31 | static parseQs() { 32 | const s = window.location.search 33 | const index = s.indexOf("?") 34 | const result = {} 35 | if (index === -1) return result 36 | const arr = s.substr(index + 1).split("&") 37 | arr.forEach(function(item) { 38 | const equals = item.split("=") 39 | let key = decodeURIComponent(equals[0]) 40 | const val = decodeURIComponent(equals[1] || "") 41 | let i = 0 42 | const splitting = key.split(".") 43 | const len = splitting.length 44 | key = splitting[len - 1] 45 | let temp = result 46 | if (len > 1) { 47 | for (; i < len - 1; i++) { 48 | if (!temp[splitting[i]] || !CommonTool.isObject(temp[splitting[i]])) temp[splitting[i]] = {} 49 | temp = temp[splitting[i]] 50 | } 51 | } 52 | if (key.substr(-2) !== "[]") { 53 | temp[key] = val 54 | } else { 55 | key = key.substr(0, key.length - 2) 56 | if (!temp[key]) temp[key] = [val] 57 | else temp[key].push(val) 58 | } 59 | }) 60 | return result 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/entry.js: -------------------------------------------------------------------------------- 1 | import "babel-polyfill" 2 | import React from "react" 3 | import ReactDOM from "react-dom" 4 | import { Provider } from "react-redux" 5 | import createHistory from "history/createBrowserHistory" 6 | import { ConnectedRouter } from "react-router-redux" 7 | import { AppContainer } from "react-hot-loader" 8 | import FastClick from "fastclick" 9 | import getStore from "./store" 10 | import Routes from "./router" 11 | 12 | import "../styles/entry" 13 | 14 | // const fundebug = require("fundebug-javascript") 15 | // fundebug.apikey = "b023d631e1bd44b6badcca3eb028229f2f9652401c14e939cd99bfbe21f94130" 16 | // fundebug.silentVideo = false 17 | // class ErrorBoundary extends React.Component { 18 | // constructor(props) { 19 | // super(props) 20 | // this.state = { hasError: false } 21 | // } 22 | // 23 | // componentDidCatch(error, info) { 24 | // this.setState({ hasError: true }) 25 | // // 将component中的报错发送到Fundebug 26 | // fundebug.notifyError(error, { 27 | // metaData: { 28 | // info: info 29 | // } 30 | // }) 31 | // } 32 | // 33 | // render() { 34 | // if (this.state.hasError) { 35 | // return null 36 | // // Note: 也可以在出错的component处展示出错信息,返回自定义的结果。 37 | // } 38 | // return this.props.children 39 | // } 40 | // } 41 | 42 | function init(reducers, extraRoutes, history) { 43 | history.listen((location, action) => { 44 | if (action === "PUSH") { 45 | window.pageAnimate = "push" 46 | } 47 | if (action === "POP") { 48 | window.pageAnimate = "back" 49 | } 50 | }) 51 | const store = getStore(history, reducers || {}) 52 | ReactDOM.render( 53 | 54 | 55 | 56 | 57 | 58 | 59 | , 60 | document.getElementById("app") 61 | ) 62 | } 63 | 64 | if (module.hot) { 65 | module.hot.accept() 66 | } 67 | 68 | export function initApp(business, reducers, extraRoutes) { 69 | window.appState = {} 70 | window.baseUrl = BUILD_ENV === "local" ? "" : business 71 | const history = createHistory({ 72 | basename: window.baseUrl 73 | }) 74 | if ("addEventListener" in document) { 75 | document.addEventListener("DOMContentLoaded", function() { 76 | FastClick.attach(document.body) 77 | }, false) 78 | } 79 | 80 | init(reducers, extraRoutes, history) 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webfunny", 3 | "version": "1.0.0", 4 | "description": "test fe lib", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "webpack-dev-server --env local --config webpack.dev.js --devtool source-map --progress --colors", 9 | "build": "webpack --config webpack.pub.js -p", 10 | "dev": "npm run build -- --env dev ", 11 | "qa": "npm run build -- --env qa ", 12 | "staging": "npm run build -- --env staging", 13 | "prod": "npm run build -- --env prod", 14 | "eslint": "eslint src/", 15 | "fix": "eslint src/ --fix" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "ssh://git@github.com:a597873885/webfunny_admin.git" 20 | }, 21 | "author": "jiangyw", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "autoprefixer": "^7.1.2", 25 | "babel": "^6.23.0", 26 | "babel-cli": "^6.26.0", 27 | "babel-core": "^6.24.1", 28 | "babel-eslint": "^8.2.6", 29 | "babel-loader": "^7.0.0", 30 | "babel-plugin-import": "^1.2.1", 31 | "babel-polyfill": "^6.23.0", 32 | "babel-preset-latest": "^6.24.1", 33 | "babel-preset-react": "^6.24.1", 34 | "babel-preset-stage-3": "^6.24.1", 35 | "body-parser": "^1.17.2", 36 | "bundle-loader": "^0.5.5", 37 | "classnames": "^2.2.5", 38 | "clean-webpack-plugin": "^0.1.16", 39 | "cors": "^2.8.4", 40 | "css-loader": "^0.28.1", 41 | "eslint": "^4.11.0", 42 | "eslint-loader": "^1.9.0", 43 | "eslint-plugin-react": "^7.4.0", 44 | "extract-text-webpack-plugin": "^2.1.2", 45 | "file-loader": "^0.11.1", 46 | "html-webpack-plugin": "^2.29.0", 47 | "image-webpack-loader": "^3.3.1", 48 | "less": "^2.7.2", 49 | "less-loader": "^4.0.5", 50 | "lodash-webpack-plugin": "^0.11.5", 51 | "moment": "^2.22.2", 52 | "node-sass": "^4.5.3", 53 | "open-browser-webpack-plugin": "0.0.5", 54 | "postcss-loader": "^2.0.6", 55 | "postcss-pxtorem": "^3.3.1", 56 | "rimraf": "^2.6.2", 57 | "sass-loader": "^6.0.6", 58 | "style-loader": "^0.17.0", 59 | "svg-sprite-loader": "^0.3.1", 60 | "transform-runtime": "0.0.0", 61 | "url-loader": "^0.5.9", 62 | "webpack": "^2.6.1", 63 | "webpack-dev-server": "^2.5.1", 64 | "webpack-manifest-plugin": "^1.2.1", 65 | "webpack-merge": "^4.1.0" 66 | }, 67 | "dependencies": { 68 | "antd": "^3.6.6", 69 | "antd-mobile": "^1.4.2", 70 | "babel-plugin-import": "^1.8.0", 71 | "echarts": "^4.1.0", 72 | "fastclick": "^1.0.6", 73 | "fundebug-javascript": "^1.2.2", 74 | "history": "^4.6.3", 75 | "iscroll": "^5.2.0", 76 | "moment": "^2.18.1", 77 | "prop-types": "^15.5.10", 78 | "react": "^15.6.1", 79 | "react-dom": "^15.6.1", 80 | "react-hot-loader": "^3.0.0-beta.7", 81 | "react-pull-to-refresh": "^1.1.1", 82 | "react-redux": "^5.0.5", 83 | "react-router-dom": "^4.1.1", 84 | "react-router-redux": "^5.0.0-alpha.6", 85 | "react-time": "^4.3.0", 86 | "react-transition-group": "^1.2.0", 87 | "redux": "^3.7.1", 88 | "redux-actions": "^2.2.1", 89 | "redux-devtools-extension": "^2.13.2", 90 | "redux-logger": "^3.0.6", 91 | "redux-thunk": "^2.2.0", 92 | "whatwg-fetch": "^2.0.3", 93 | "zepto-webpack": "^1.2.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /webpack.pub.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const Merge = require('webpack-merge'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); 7 | const CleanPlugin = require("clean-webpack-plugin"); 8 | const autoprefixer = require('autoprefixer'); 9 | const pxtorem = require('postcss-pxtorem'); 10 | const baseConfig = require('./webpack.base.js'); 11 | const envConfig = require("./src/common/env_config.js"); 12 | const ManifestPlugin = require('webpack-manifest-plugin'); 13 | 14 | module.exports = env => { 15 | const curEnv = env || "local" 16 | const assetsUrl = envConfig.getAssetsUrl(curEnv, "/webfunny/") 17 | return Merge(baseConfig, { 18 | entry: { 19 | app: path.resolve(__dirname, 'src/index.js'), 20 | vendor: [ 21 | 'react-redux', 22 | // 'react' 23 | // , 'react-dom', 'react-redux', 'react-router-dom', 'react-router-redux' 24 | // ,'react-transition-group', 'redux', 'redux-actions','redux-logger', 'redux-thunk' 25 | // ,'antd/lib/card', 'antd/lib/dropdown', 'antd/lib/row', 'antd/lib/col', 'antd/lib/tabs', 'antd/lib/menu' 26 | // ,'antd/lib/icon', 'antd/lib/pagination' 27 | ], 28 | // common: [ 29 | // 'prius','zepto-webpack', 30 | // 'JS/lib/common-tool', 'JS/lib/underscore', 'JS/lib/cache', 'JS/lib/consts', 31 | // 'JS/lib/http-api', 'JS/lib/http-util' 32 | // ], 33 | }, 34 | output: { 35 | filename: "[name].[chunkhash:8].js", 36 | chunkFilename: '[name].[chunkhash:8].chunk.js', 37 | path: path.resolve(__dirname, 'dist/webfunny'), 38 | publicPath: assetsUrl 39 | }, 40 | plugins: [ 41 | new LodashModuleReplacementPlugin, 42 | new CleanPlugin(['dist']), 43 | new webpack.DefinePlugin({ 44 | 'process.env.NODE_ENV': JSON.stringify('production'), 45 | 'BUILD_ENV': JSON.stringify(curEnv) 46 | }), 47 | new webpack.optimize.UglifyJsPlugin({ 48 | beautify: false, // 最紧凑的输出 49 | comments: false, 50 | compress: { 51 | warnings: false, // 在UglifyJs删除没有用到的代码时不输出警告 52 | drop_console: (curEnv === 'staging' || curEnv === 'prod'), 53 | collapse_vars: true, // 内嵌定义了但是只用到一次的变量 54 | reduce_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 55 | } 56 | }), 57 | new ExtractTextPlugin({ 58 | filename: "app.[contenthash:8].css", 59 | allChunks: true, 60 | }), 61 | new webpack.optimize.CommonsChunkPlugin({ 62 | name: 'common', 63 | minChunks: function (module) { 64 | return module.context && module.context.indexOf('node_modules') !== -1; // this assumes your vendor imports exist in the node_modules directory 65 | } 66 | }), 67 | new HtmlWebpackPlugin({ 68 | template: './src/index.html', 69 | htmlWebpackPlugin: { 70 | 'files': { 71 | 'css': ['app.css'], 72 | 'js': ['index.js', 'common.js'] 73 | } 74 | }, 75 | minify: { 76 | removeComments: true, 77 | collapseWhitespace: true, 78 | removeAttributeQuotes: true, 79 | minifyJS: true 80 | }, 81 | chunksSortMode: function (chunk1, chunk2) { 82 | var orders = ['common', 'vendor', 'debug', 'app']; 83 | var order1 = orders.indexOf(chunk1.names[0]); 84 | var order2 = orders.indexOf(chunk2.names[0]); 85 | if (order1 > order2) { 86 | return 1; 87 | } else if (order1 < order2) { 88 | return -1; 89 | } else { 90 | return 0; 91 | } 92 | } 93 | }), 94 | new ManifestPlugin({ 95 | publicPath: assetsUrl 96 | }) 97 | ] 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /webpack.base.js: -------------------------------------------------------------------------------- 1 | require("babel-polyfill"); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const autoprefixer = require('autoprefixer'); 5 | const pxtorem = require('postcss-pxtorem'); 6 | const postcssConfig = { 7 | loader: 'postcss-loader', 8 | options: { 9 | plugins: () => [ 10 | autoprefixer({browsers: ['> 1%', 'last 4 versions']}), 11 | pxtorem({ 12 | rootValue: 100, 13 | propWhiteList: [], 14 | }) 15 | ] 16 | } 17 | }; 18 | module.exports = { 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js?$/, 23 | include: path.resolve(__dirname, 'src/containers'), 24 | use: [ 25 | { 26 | loader: 'bundle-loader', 27 | options: { 28 | lazy: true, 29 | name: '[name]' 30 | } 31 | }, 32 | "babel-loader" 33 | ] 34 | }, 35 | { 36 | test: /\.js?$/, 37 | exclude: /node_modules/, 38 | loader: "babel-loader" 39 | }, 40 | { 41 | test: /\.js?$/, 42 | exclude: /node_modules/, 43 | loader: "eslint-loader" 44 | }, 45 | { 46 | test: /\.less$/, 47 | use: [ 48 | "style-loader", 49 | "css-loader", 50 | { 51 | loader: 'postcss-loader', 52 | options: { 53 | plugins: () => [ 54 | autoprefixer({browsers: ['> 1%', 'last 4 versions']}), 55 | pxtorem({ 56 | rootValue: 100, 57 | propWhiteList: [], 58 | }) 59 | ] 60 | } 61 | }, 62 | "less-loader" 63 | ], 64 | }, 65 | { 66 | test: /\.css$/, 67 | use: [ 68 | 'style-loader', 69 | { 70 | loader: 'css-loader', 71 | options: { 72 | importLoaders: 1, 73 | modules: true, 74 | sourceMap: true 75 | } 76 | }, 77 | postcssConfig, 78 | ] 79 | }, 80 | { 81 | test: /\.scss$/, 82 | use: [ 83 | 'style-loader', 84 | 'css-loader', 85 | postcssConfig, 86 | { 87 | loader: 'sass-loader', 88 | options: { 89 | sassLoader: { 90 | includePaths: [ 91 | path.resolve(__dirname, "src/styles"), 92 | path.resolve(__dirname, "src/components") 93 | ] 94 | } 95 | } 96 | } 97 | ], 98 | }, 99 | { 100 | test: /\.svg$/, 101 | include: [ 102 | /node_modules/, 103 | require.resolve('antd').replace(/warn\.js$/, ''), // antd-mobile 内置svg 104 | path.resolve(__dirname, 'src/assets/img'), // 业务代码本地私有 svg 存放目录 105 | path.resolve(__dirname, 'src/components') // 本地通用组件 svg 存放目录 106 | ], 107 | loader: "svg-sprite-loader", 108 | }, 109 | { 110 | test: /\.(png|jpg|jpeg|gif)$/, 111 | use: [ 112 | { 113 | loader: 'url-loader', 114 | options: { 115 | name:'[path][name].[ext]', 116 | limit: 25000 117 | } 118 | } 119 | ] 120 | } 121 | ] 122 | }, 123 | resolve: { 124 | alias: { 125 | Libs: path.resolve(__dirname, 'src/lib/'), 126 | Components: path.resolve(__dirname, 'src/components/'), 127 | Containers: path.resolve(__dirname, 'src/containers/'), 128 | Modules: path.resolve(__dirname, 'src/modules/'), 129 | Common: path.resolve(__dirname, 'src/common/'), 130 | Images: path.resolve(__dirname, 'src/assets/img/'), 131 | ChartConfig: path.resolve(__dirname, 'src/chartConfig/') 132 | }, 133 | mainFiles: ["index.web", "index"], 134 | modules: [path.resolve(__dirname, "src"), "node_modules"], 135 | extensions: ['.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.ts', '.tsx', '.js', '.jsx', '.json', '.scss'], 136 | mainFields: [ 137 | 'browser', 138 | 'jsnext:main', 139 | 'main', 140 | ], 141 | }, 142 | } 143 | -------------------------------------------------------------------------------- /src/components/header/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import { Menu, Dropdown, Icon } from "antd" 4 | import HttpUtil from "Common/http-util" 5 | import HttpApi from "Common/http-api" 6 | export default class Header extends Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { 10 | projectList: [], 11 | chooseProject: { 12 | webMonitorId: "", 13 | projectName: "" 14 | } 15 | } 16 | this.choseProject = this.choseProject.bind(this) 17 | } 18 | 19 | componentDidMount() { 20 | const chooseWebMonitorId = window.localStorage.chooseWebMonitorId 21 | HttpUtil.get(HttpApi.projectList).then( res => { 22 | const projectList = res.data.rows 23 | let chooseProject = res.data.rows[0] 24 | for (let i = 0; i < projectList.length; i ++) { 25 | if (chooseWebMonitorId === projectList[i].webMonitorId) { 26 | chooseProject = projectList[i] 27 | break 28 | } 29 | } 30 | this.setState({projectList: res.data.rows, chooseProject}) 31 | window.localStorage.chooseWebMonitorId = chooseProject.webMonitorId 32 | if (typeof this.props.loadedProjects === "function") this.props.loadedProjects(chooseProject) 33 | }, () => { 34 | console.log("未能成功获取应用列表") 35 | }) 36 | } 37 | 38 | render() { 39 | const { projectList, chooseProject } = this.state 40 | const menu = 41 | 42 | { 43 | projectList.map((project, index) => { 44 | return 45 | {project.projectName} 46 | 47 | }) 48 | } 49 | 50 | const gitMenu = 51 | 52 | 页面探针代码 53 | 54 | 55 | 分析后台代码 56 | 57 | 58 | 展示平台代码 59 | 60 | 61 | const blogMenu = 62 | 63 | 搭建前端监控系统(一)阿里云服务器搭建篇 64 | 65 | 66 | 搭建前端监控系统(二)JS错误监控篇 67 | 68 | 69 | 搭建前端监控系统(三)NodeJs服务器部署篇 70 | 71 | 72 | return
73 |
74 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 |
93 | } 94 | 95 | choseProject(project) { 96 | this.setState({chooseProject: project}) 97 | window.localStorage.chooseWebMonitorId = project.webMonitorId 98 | this.props.chooseProject(project) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/chartConfig/jsChartOption.js: -------------------------------------------------------------------------------- 1 | export const jsErrorOption = (result) => { 2 | return { 3 | color: [ "#5d5cb6" ], 4 | tooltip: { 5 | trigger: "axis", 6 | // axisPointer: { 7 | // type: 'cross', 8 | // crossStyle: { 9 | // color: '#666' 10 | // } 11 | // }, 12 | confine: true, 13 | position: ["50%", "50%"], 14 | alwaysShowContent: false, 15 | hideDelay: 100 16 | }, 17 | grid: { 18 | top: "15%", 19 | left: "5%", 20 | right: "4%", 21 | bottom: "1%", 22 | containLabel: true 23 | }, 24 | xAxis: [ 25 | { 26 | type: "category", 27 | data: result[0], 28 | axisPointer: { 29 | type: "shadow" 30 | }, 31 | axisLine: { 32 | show: true, 33 | lineStyle: { 34 | color: "#666", 35 | type: "dashed" 36 | } 37 | }, 38 | axisTick: { 39 | show: false 40 | } 41 | } 42 | ], 43 | yAxis: [ 44 | { 45 | type: "value", 46 | name: "次数", 47 | min: 0, 48 | max: "dataMax", 49 | axisLabel: { 50 | formatter: "{value}" 51 | }, 52 | splitLine: { 53 | show: false 54 | }, 55 | axisLine: { 56 | show: true, 57 | lineStyle: { 58 | color: "#666", 59 | type: "dashed" 60 | } 61 | }, 62 | axisTick: { 63 | show: false 64 | } 65 | 66 | } 67 | ], 68 | series: [ 69 | { 70 | name: "Error发生次数:", 71 | type: "line", 72 | smooth: true, 73 | data: result[1], 74 | areaStyle: {} 75 | } 76 | ] 77 | } 78 | } 79 | 80 | // 一天内,js错误分布 81 | export const jsErrorOptionByHour = (result) => { 82 | return { 83 | color: [ "#5d5cb6" ], 84 | tooltip: { 85 | trigger: "axis", 86 | // axisPointer: { 87 | // type: 'cross', 88 | // crossStyle: { 89 | // color: '#666' 90 | // } 91 | // }, 92 | confine: true, 93 | position: ["50%", "50%"], 94 | alwaysShowContent: false, 95 | hideDelay: 100 96 | }, 97 | grid: { 98 | top: "15%", 99 | left: "5%", 100 | right: "4%", 101 | bottom: "1%", 102 | containLabel: true 103 | }, 104 | xAxis: [ 105 | { 106 | type: "category", 107 | data: result[0], 108 | axisPointer: { 109 | type: "shadow" 110 | }, 111 | axisLine: { 112 | show: true, 113 | lineStyle: { 114 | color: "#666", 115 | type: "dashed" 116 | } 117 | }, 118 | axisTick: { 119 | show: false 120 | } 121 | } 122 | ], 123 | yAxis: [ 124 | { 125 | type: "value", 126 | name: "次数", 127 | min: 0, 128 | max: "dataMax", 129 | axisLabel: { 130 | formatter: "{value}" 131 | }, 132 | splitLine: { 133 | show: false 134 | }, 135 | axisLine: { 136 | show: true, 137 | lineStyle: { 138 | color: "#666", 139 | type: "dashed" 140 | } 141 | }, 142 | axisTick: { 143 | show: false 144 | } 145 | 146 | } 147 | ], 148 | series: [ 149 | { 150 | name: "Error发生次数:", 151 | type: "bar", 152 | data: result[1], 153 | } 154 | ] 155 | } 156 | } 157 | // js错误排序报排序表 158 | export const jsErrorSortOption = (resArray) => { 159 | const categoryArray = [] 160 | const barArray = [] 161 | let percentArray = [] 162 | let total = 0 163 | const loopCount = resArray.length > 20 ? 20 : resArray.length 164 | for (let i = 0; i < resArray.length; i ++) { 165 | total += resArray[i].count 166 | } 167 | for (let i = 0; i < loopCount; i ++) { 168 | categoryArray.push(resArray[i].errorMessage) 169 | barArray.push(resArray[i].count) 170 | percentArray.push(parseFloat(resArray[i].count * 100 / total)) 171 | } 172 | percentArray = percentArray.reverse() 173 | return { 174 | tooltip: { 175 | trigger: "axis", 176 | alwaysShowContent: false, 177 | axisPointer: { // 坐标轴指示器,坐标轴触发有效 178 | type: "shadow" // 默认为直线,可选为:'line' | 'shadow' 179 | } 180 | }, 181 | grid: { 182 | top: "0%", 183 | left: "3%", 184 | right: "15%", 185 | bottom: "1%", 186 | containLabel: true 187 | }, 188 | xAxis: { 189 | type: "value", 190 | position: "top", 191 | boundaryGap: [0, 0.01], 192 | axisTick: { 193 | show: false, 194 | }, 195 | axisLine: { 196 | show: false 197 | }, 198 | splitLine: { 199 | lineStyle: { 200 | color: "#eaeaea", 201 | type: "dashed" 202 | } 203 | }, 204 | }, 205 | yAxis: { 206 | type: "category", 207 | data: categoryArray.reverse(), 208 | axisTick: { 209 | show: false 210 | }, 211 | axisLine: { 212 | show: true, 213 | lineStyle: { 214 | color: "#666", 215 | type: "dashed" 216 | } 217 | }, 218 | }, 219 | series: [ 220 | { 221 | name: "js报错数量", 222 | type: "bar", 223 | label: { 224 | normal: { 225 | position: "right", 226 | show: true, 227 | color: "#111", 228 | formatter: (res) => { 229 | return parseFloat(percentArray[res.dataIndex]).toFixed(2) + "%" 230 | } 231 | } 232 | }, 233 | areaStyle: {normal: {}}, 234 | data: barArray.reverse(), 235 | itemStyle: { 236 | normal: { 237 | color: "#f38376" 238 | } 239 | }, 240 | } 241 | ] 242 | } 243 | } -------------------------------------------------------------------------------- /src/modules/javascriptErrorDetail/index.scss: -------------------------------------------------------------------------------- 1 | .javascriptErrorDetail-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | min-height: 100%; 7 | padding-top: 62px; 8 | background: #fff; 9 | .detail-container { 10 | span { 11 | float: left; 12 | } 13 | .error-type { 14 | height: 50px; 15 | line-height: 50px; 16 | font-size: 28px; 17 | font-weight: bold; 18 | } 19 | .error-url { 20 | height: 50px; 21 | line-height: 55px; 22 | font-size: 20px; 23 | padding-left: 20px; 24 | color: #727082; 25 | } 26 | .error-msg { 27 | width: 100%; 28 | label { 29 | float: left; 30 | } 31 | label:first-child { 32 | width: 18px; 33 | height: 18px; 34 | border-radius: 9px; 35 | background: #e67482; 36 | margin-top: 5px; 37 | } 38 | label:nth-child(2) { 39 | font-size: 18px; 40 | padding-left: 10px; 41 | } 42 | label:nth-child(3) { 43 | margin-left: 20px; 44 | margin-top: 4px; 45 | color: #949090; 46 | } 47 | } 48 | .error-page-link { 49 | margin-top: 8px; 50 | .anticon { 51 | font-size: 17px; 52 | font-weight: bold; 53 | color: #5d74b9; 54 | } 55 | a { 56 | margin-left: 10px; 57 | } 58 | } 59 | .info-box { 60 | width: 100px; 61 | height: 74px; 62 | margin-left: 40px; 63 | float: left; 64 | &:first-child { 65 | margin-left: 0; 66 | } 67 | span { 68 | display: block; 69 | width: 100%; 70 | text-align: center; 71 | } 72 | span:first-child { 73 | font-size: 16px; 74 | margin-top: 10px; 75 | font-weight: bold; 76 | color: #c1bdbd; 77 | } 78 | span:last-child { 79 | font-size: 26px; 80 | color: #556eba; 81 | } 82 | } 83 | .operation-container { 84 | margin-top: 10px; 85 | .ant-btn { 86 | font-size: 12px; 87 | height: 24px; 88 | } 89 | .ant-btn span { 90 | float: right; 91 | } 92 | .ant-btn i { 93 | margin-left: 0; 94 | margin-right: 8px; 95 | font-weight: bold; 96 | } 97 | .ant-btn:nth-child(2), .ant-btn:nth-child(3), .ant-btn:nth-child(5) { 98 | margin-left: 15px; 99 | } 100 | .ant-btn:nth-child(4) { 101 | margin-left: 25px; 102 | } 103 | } 104 | } 105 | .ant-row.device-container { 106 | padding-top: 30px; 107 | padding-bottom: 30px; 108 | color: #727082; 109 | .anticon { 110 | font-size: 50px; 111 | float: left; 112 | } 113 | .ip-address-icon { 114 | color: #5d74b9; 115 | } 116 | .device-info-box { 117 | float: left; 118 | padding: 5px 10px; 119 | span { 120 | display: block; 121 | } 122 | span:first-child { 123 | font-weight: bold; 124 | height: 25px; 125 | .customer-key { 126 | width: 80px; 127 | display: block; 128 | //white-space: nowrap; 129 | //overflow: hidden; 130 | //text-overflow: ellipsis; 131 | float: left; 132 | } 133 | .copy-key { 134 | font-size: 18px; 135 | color: #7a7979; 136 | } 137 | } 138 | } 139 | .browser-icon { 140 | width: 50px; 141 | float: left; 142 | } 143 | } 144 | .ant-row { 145 | padding: 10px 20px 20px 20px; 146 | border-bottom: 1px solid #eaeaea; 147 | } 148 | .table-container { 149 | padding: 20px; 150 | h4 { 151 | color: #727082; 152 | } 153 | .ant-table-thead { 154 | font-size: 12px; 155 | color: #727082; 156 | } 157 | .ant-table-thead > tr > th { 158 | background: #f9fafd; 159 | } 160 | .ant-table-thead > tr > th, .ant-table-tbody > tr > td { 161 | font-size: 12px; 162 | color: #727082; 163 | } 164 | } 165 | .stack-container { 166 | padding: 20px; 167 | h4 { 168 | color: #727082; 169 | } 170 | .error-msg { 171 | display: block; 172 | margin: 10px 0; 173 | } 174 | .ant-collapse-header { 175 | background: #f9fafd; 176 | font-size: 12px; 177 | font-weight: bold; 178 | } 179 | .ant-collapse-content { 180 | border-top: 0; 181 | font-size: 12px; 182 | } 183 | .ant-collapse { 184 | border: 0; 185 | } 186 | .ant-collapse > .ant-collapse-item { 187 | border-bottom: 0; 188 | } 189 | } 190 | .footprint-container { 191 | padding-left: 5px; 192 | padding-bottom: 10px; 193 | .ant-timeline-item { 194 | font-size: 12px; 195 | padding-bottom: 0; 196 | } 197 | .ant-timeline-item-content { 198 | padding: 5px 0; 199 | } 200 | .ant-collapse-item { 201 | border-bottom: 0; 202 | } 203 | .ant-collapse-header { 204 | color: #727082; 205 | font-weight: bold; 206 | } 207 | } 208 | } -------------------------------------------------------------------------------- /src/modules/javascriptError/index.scss: -------------------------------------------------------------------------------- 1 | .javascriptError-container { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | min-height: 100%; 7 | padding-top: 62px; 8 | background: #fff; 9 | .ant-tabs-nav .ant-tabs-tab { 10 | .anticon { 11 | font-weight: bold; 12 | } 13 | } 14 | .ant-tabs-nav .ant-tabs-tab-active { 15 | color: #5d5cb6; 16 | } 17 | .ant-tabs-ink-bar { 18 | background-color: #5d5cb6; 19 | } 20 | .ant-row { 21 | padding: 0 20px; 22 | margin: 20px 0; 23 | .info-box { 24 | width: 50%; 25 | min-height: 100px; 26 | text-align: center; 27 | float: left; 28 | span:first-child { 29 | display: block; 30 | font-size: 40px; 31 | } 32 | label { 33 | font-size: 12px; 34 | display: block; 35 | } 36 | .anticon { 37 | color: #727082; 38 | font-weight: bold; 39 | } 40 | } 41 | } 42 | .click-export { 43 | font-size: 16px; 44 | margin-left: 10px; 45 | font-weight: bold; 46 | color: #aaa; 47 | } 48 | .ant-pagination { 49 | text-align: right; 50 | padding-right: 30px; 51 | padding-bottom: 30px; 52 | } 53 | .chart-box { 54 | width: 100%; 55 | height: 200px; 56 | } 57 | .main-info-container { 58 | padding-bottom: 20px; 59 | } 60 | .error-list-container { 61 | p { 62 | height: 50px; 63 | line-height: 50px; 64 | border-bottom: 1px solid #e8e8e8; 65 | margin: 0; 66 | padding: 0 20px; 67 | font-size: 12px; 68 | cursor: pointer; 69 | &:last-child { 70 | border-bottom: 0; 71 | } 72 | } 73 | span:nth-child(2) { 74 | margin-left: 20px; 75 | font-size: 16px; 76 | color: #1890ff; 77 | } 78 | span:nth-child(3) { 79 | margin-left: 20px; 80 | color: #666; 81 | } 82 | span:nth-child(4) { 83 | margin-left: 20px; 84 | color: #333; 85 | } 86 | span:last-child { 87 | float: right; 88 | font-size: 12px; 89 | margin-right: 15px; 90 | font-weight: bold; 91 | } 92 | .ignore-state { 93 | width: 50px; 94 | line-height: 26px; 95 | text-align: center; 96 | position: absolute; 97 | color: #ddd; 98 | margin-top: 10px; 99 | margin-left: 30px; 100 | border: 1px solid #ddd; 101 | border-radius: 4px; 102 | transform:rotate(-14deg); 103 | -ms-transform:rotate(-14deg); /* IE 9 */ 104 | -moz-transform:rotate(-14deg); /* Firefox */ 105 | -webkit-transform:rotate(-14deg); /* Safari 和 Chrome */ 106 | -o-transform:rotate(-14deg); /* Opera */ 107 | } 108 | .resolve-state { 109 | width: 50px; 110 | line-height: 26px; 111 | text-align: center; 112 | position: absolute; 113 | color: #06bf06; 114 | margin-top: 10px; 115 | margin-left: 30px; 116 | border: 1px solid #06bf06; 117 | border-radius: 4px; 118 | box-shadow: 3px 4px 12px 0px; 119 | transform:rotate(-14deg); 120 | -ms-transform:rotate(-14deg); /* IE 9 */ 121 | -moz-transform:rotate(-14deg); /* Firefox */ 122 | -webkit-transform:rotate(-14deg); /* Safari 和 Chrome */ 123 | -o-transform:rotate(-14deg); /* Opera */ 124 | } 125 | .loading-box { 126 | display: block; 127 | width: 100%; 128 | text-align: center; 129 | } 130 | .loading-icon { 131 | font-size: 40px; 132 | color: #b2b1f5; 133 | margin: 40px auto; 134 | } 135 | .right-icon { 136 | float: right; 137 | font-size: 14px; 138 | font-weight: bold; 139 | } 140 | .not-today { 141 | color: #aaa; 142 | } 143 | .status-icon { 144 | display: inline-block; 145 | width: 12px; 146 | height: 12px; 147 | border-radius: 50%; 148 | background: #e67482; 149 | } 150 | .status-icon-ignore { 151 | display: inline-block; 152 | width: 12px; 153 | height: 12px; 154 | border-radius: 50%; 155 | background: #ddd; 156 | } 157 | .status-icon-resolve { 158 | display: inline-block; 159 | width: 12px; 160 | height: 12px; 161 | border-radius: 50%; 162 | background: #06bf06; 163 | } 164 | } 165 | .ant-card-body { 166 | padding: 0; 167 | overflow-x: auto; 168 | } 169 | .page-container { 170 | padding-top: 5px; 171 | padding-right: 10px; 172 | .url-box { 173 | margin-bottom: 0; 174 | height: 40px; 175 | line-height: 40px; 176 | border-radius: 5px; 177 | margin-top: 15px; 178 | font-weight: bold; 179 | background: url("../../assets/img/javascriptErrorDetail/url-bg.jpg"); 180 | background-repeat: no-repeat; 181 | background-size: 100% 100%; 182 | width: 90%; 183 | margin-left: 5%; 184 | cursor: pointer; 185 | span { 186 | float: left; 187 | } 188 | span:first-child { 189 | width: 60%; 190 | display: block; 191 | white-space: nowrap; 192 | overflow: hidden; 193 | text-overflow: ellipsis; 194 | } 195 | } 196 | .url-box-active { 197 | background: url("../../assets/img/javascriptErrorDetail/url-bg-active.jpg"); 198 | background-repeat: no-repeat; 199 | background-size: 100% 100%; 200 | } 201 | } 202 | .page-error-container { 203 | padding-top: 5px; 204 | padding-left: 10px; 205 | } 206 | } -------------------------------------------------------------------------------- /src/common/http-util.js: -------------------------------------------------------------------------------- 1 | import Utils from "Common/utils" 2 | import "whatwg-fetch" 3 | import "./extension" 4 | const timeout = 30000 5 | export default class HttpUtil { 6 | /** 7 | * get 请求 8 | * @param url 9 | * @param params 10 | * @param isHandleError 11 | * @param httpCustomerOpertion 使用者传递过来的参数, 用于以后的扩展用户自定义的行为 12 | * { 13 | * isHandleResult: boolen //是否需要处理错误结果 true 需要/false 不需要 14 | * isShowLoading: boolen //是否需要显示loading动画 15 | * customHead: object // 自定义的请求头 16 | * timeout: int //自定义接口超时的时间 17 | * } 18 | * @returns {Promise} 19 | */ 20 | static get(url, params = {}, httpCustomerOpertion = { isHandleResult: true, isShowLoading: true }) { 21 | const method = "GET" 22 | params.webMonitorId = window.localStorage.chooseWebMonitorId 23 | const fetchUrl = url + Utils.qs(params) 24 | const fetchParams = Object.assign({}, { method }, this.getHeaders()) 25 | return HttpUtil.handleFetchData(fetchUrl, fetchParams, httpCustomerOpertion) 26 | } 27 | 28 | /** 29 | * post 请求 30 | * @param url 31 | * @param params 32 | * @param isHandleError 33 | * @param httpCustomerOpertion 使用者传递过来的参数, 用于以后的扩展用户自定义的行为 34 | * @returns {Promise} 35 | */ 36 | static post(url, params = {}, httpCustomerOpertion = { isHandleResult: true, isShowLoading: true }) { 37 | const method = "POST" 38 | params.webMonitorId = window.localStorage.chooseWebMonitorId 39 | const body = JSON.stringify(params) 40 | const fetchParams = Object.assign({}, { method, body }, this.getHeaders()) 41 | return HttpUtil.handleFetchData(url, fetchParams, httpCustomerOpertion) 42 | } 43 | 44 | /** 45 | * put 请求 46 | * @param url 47 | * @param params 48 | * @param isHandleError 49 | * @param httpCustomerOpertion 使用者传递过来的参数, 用于以后的扩展用户自定义的行为 50 | * @returns {Promise} 51 | */ 52 | static put(url, params = {}, httpCustomerOpertion = { isHandleResult: true, isShowLoading: true }) { 53 | const method = "PUT" 54 | params.webMonitorId = window.localStorage.chooseWebMonitorId 55 | const body = JSON.stringify(params) 56 | const fetchParams = Object.assign({}, { method, body }, this.getHeaders()) 57 | return HttpUtil.handleFetchData(url, fetchParams, httpCustomerOpertion) 58 | } 59 | 60 | /** 61 | * 发送fetch请求 62 | * @param fetchUrl 63 | * @param fetchParams 64 | * @returns {Promise} 65 | */ 66 | static handleFetchData(fetchUrl, fetchParams, httpCustomerOpertion) { 67 | // 如果是照片的base64数据,ios系统会卡死 68 | // TODO: debugPanel不使用react 69 | const logParams = { ...fetchParams } 70 | if (logParams.body && logParams.body.length > 1024) { 71 | logParams.body = logParams.body.substr(0, 1024) + "..." 72 | } 73 | const { isShowLoading } = httpCustomerOpertion 74 | if (isShowLoading) { 75 | HttpUtil.showLoading() 76 | } 77 | httpCustomerOpertion.isFetched = false 78 | httpCustomerOpertion.isAbort = false 79 | // 处理自定义的请求头 80 | if (httpCustomerOpertion.hasOwnProperty("customHead")) { 81 | const { customHead } = httpCustomerOpertion 82 | fetchParams.headers = Object.assign({}, fetchParams.headers, customHead) 83 | } 84 | const fetchPromise = new Promise((resolve, reject) => { 85 | fetch(fetchUrl, fetchParams).then( 86 | response => { 87 | if (httpCustomerOpertion.isAbort) { 88 | // 请求超时后,放弃迟到的响应 89 | return 90 | } 91 | if (isShowLoading) { 92 | HttpUtil.hideLoading() 93 | } 94 | httpCustomerOpertion.isFetched = true 95 | response.json().then(jsonBody => { 96 | if (response.ok) { 97 | if (jsonBody.status) { 98 | // 业务逻辑报错 99 | reject(HttpUtil.handleResult(jsonBody, httpCustomerOpertion)) 100 | } else { 101 | resolve(HttpUtil.handleResult(jsonBody, httpCustomerOpertion)) 102 | } 103 | } else { 104 | // http status header <200 || >299 105 | let msg = "当前服务繁忙,请稍后再试" 106 | if (response.status === 404) { 107 | msg = "您访问的内容走丢了…" 108 | } 109 | console.log(msg, 2) 110 | reject(HttpUtil.handleResult({ fetchStatus: "error", netStatus: response.status }, httpCustomerOpertion)) 111 | } 112 | }).catch(e => { 113 | const errMsg = e.name + " " + e.message 114 | reject(HttpUtil.handleResult({ fetchStatus: "error", error: errMsg, netStatus: response.status }, httpCustomerOpertion)) 115 | }) 116 | } 117 | ).catch(e => { 118 | const errMsg = e.name + " " + e.message 119 | if (httpCustomerOpertion.isAbort) { 120 | // 请求超时后,放弃迟到的响应 121 | return 122 | } 123 | if (isShowLoading) { 124 | HttpUtil.hideLoading() 125 | } 126 | httpCustomerOpertion.isFetched = true 127 | if (httpCustomerOpertion.isHandleResult === true) { 128 | console.log("网络开小差了,稍后再试吧", 2) 129 | } 130 | reject(HttpUtil.handleResult({ fetchStatus: "error", error: errMsg }, httpCustomerOpertion)) 131 | }) 132 | }) 133 | return Promise.race([fetchPromise, HttpUtil.fetchTimeout(httpCustomerOpertion)]) 134 | } 135 | 136 | /** 137 | * 统一处理后台返回的错误结果 138 | * @param result 139 | * ps: 通过 this.isHandleError 来判断是否需要有fetch方法来统一处理错误信息 140 | */ 141 | static handleResult(result, httpCustomerOpertion) { 142 | if (result.status && httpCustomerOpertion.isHandleResult === true) { 143 | const errMsg = result.msg || result.message || "服务器开小差了,稍后再试吧" 144 | console.log(`${errMsg}(${result.status})`, 2) 145 | } 146 | return result 147 | } 148 | /** 149 | * 控制Fetch请求是否超时 150 | * @returns {Promise} 151 | */ 152 | static fetchTimeout(httpCustomerOpertion) { 153 | const { isShowLoading } = httpCustomerOpertion 154 | return new Promise((resolve, reject) => { 155 | setTimeout(() => { 156 | if (!httpCustomerOpertion.isFetched) { 157 | // 还未收到响应,则开始超时逻辑,并标记fetch需要放弃 158 | httpCustomerOpertion.isAbort = true 159 | if (isShowLoading) { 160 | HttpUtil.hideLoading() 161 | } 162 | console.log("网络开小差了,稍后再试吧", 2) 163 | reject({ fetchStatus: "timeout" }) 164 | } 165 | }, httpCustomerOpertion.timeout || timeout) 166 | }) 167 | } 168 | 169 | /** 170 | * 获取请求头信息 171 | * @returns {{app-info: string, access-token: string}} 172 | */ 173 | static getHeaders() { 174 | // 需要通过app来获取 175 | const fetchCommonParams = { 176 | // "mode": "cors", 177 | // "credentials": "same-origin" 178 | } 179 | const headers = { 180 | // "Accept": "*/*", 181 | // "Content-Type": "application/json;charset=utf-8", 182 | } 183 | return Object.assign({}, fetchCommonParams, { headers }) 184 | } 185 | 186 | /** 187 | * 添加loadding状态 188 | */ 189 | static showLoading() { 190 | } 191 | 192 | /** 193 | * 取消loadding状态 194 | */ 195 | static hideLoading() { 196 | } 197 | } 198 | 199 | 200 | -------------------------------------------------------------------------------- /module.js: -------------------------------------------------------------------------------- 1 | /* 2 | * modukle.js 3 | * 4 | * 创建新模块 5 | */ 6 | 7 | "use strict"; 8 | 9 | var path = require('path'); 10 | var fs = require('fs'); 11 | 12 | 13 | //正则+replace 实现首字母大写 14 | function titleCase(s) { 15 | return s.replace(/\b([\w|']+)\b/g, function (word) { 16 | return word.replace(word.charAt(0), word.charAt(0).toUpperCase()); 17 | }); 18 | } 19 | 20 | /** 21 | * 创建目录 22 | * @param {string} url 目录路径 23 | * @param {function} callBack 回调函数 24 | */ 25 | function createDir(url, callBack) { 26 | fs.mkdir(__dirname + url, function (err) { 27 | if (err) { 28 | throw err; 29 | } else { 30 | callBack && callBack(); 31 | console.log('创建目录成功'); 32 | } 33 | }); 34 | } 35 | /** 36 | * 创建文件 37 | * @param {string} fileName 文件路径 38 | * @param {string} data 文件内容 39 | */ 40 | function createFile(fileName, data) { 41 | var ws = fs.createWriteStream(__dirname + fileName, { start: 0 }); 42 | var temp = data.replace(/\{0\}/g, CompName).replace(/\{1\}/g, modlueName); 43 | var buffer = new Buffer(temp); 44 | ws.write(buffer, 'utf8', function (err, buffer) { 45 | if (err) { 46 | throw err; 47 | } else { 48 | console.log('创建文件成功'); 49 | } 50 | }); 51 | } 52 | /** 53 | * 创建具体模块 54 | * @param {string} name 模块名称 55 | * @param {function} callBack 回调函数 56 | */ 57 | function createModule(name, callBack) { 58 | var componentsUrl = "/src/modules/" + name; 59 | var containerUrl = "/src/containers/"; 60 | //创建模块 components 61 | createDir(componentsUrl, function () { 62 | //创建 container index.js 63 | var containerTemp = fs.readFileSync(__dirname + '/src/templates/container.js.template', 'utf8'); 64 | createFile(containerUrl + "/"+name+".js", containerTemp); 65 | //创建 component index.js 66 | var compTemp = fs.readFileSync(__dirname + '/src/templates/component.js.template', 'utf8'); 67 | createFile(componentsUrl + "/index.js", compTemp); 68 | //创建 component index.scss 69 | var scssTemp = fs.readFileSync(__dirname + '/src/templates/index.scss.template', 'utf8'); 70 | createFile(componentsUrl + "/index.scss", scssTemp); 71 | //创建 action.js 72 | var actionTemp = fs.readFileSync(__dirname + '/src/templates/action.js.template', 'utf8'); 73 | createFile(componentsUrl + "/action.js", actionTemp); 74 | //创建 reducer.js 75 | var reducerTemp = fs.readFileSync(__dirname + '/src/templates/reducer.js.template', 'utf8'); 76 | createFile(componentsUrl + "/reducer.js", reducerTemp); 77 | //成功回调 78 | callBack && callBack(); 79 | }); 80 | } 81 | 82 | 83 | /** 84 | * 更新reducers聚合文件 85 | * @param {string} content 需要更新的内容 86 | * *@param {function} callBack 成功回调 87 | */ 88 | function updateReducers(content, callBack) { 89 | fs.open(__dirname + '/src/reducers.js', 'w', function (err, fd) { 90 | if (err) { 91 | console.error('打开reducers聚合文件失败',err); 92 | fs.close(fd); 93 | return; 94 | } else { 95 | var buffer = new Buffer(content); 96 | //写入,写入总长,从起始位置写入 97 | fs.write(fd, buffer,0, buffer.length, 0, function (err, written, buffer) { 98 | fs.close(fd); 99 | if (err) { 100 | console.log('更新reducers聚合文件失败', err); 101 | return; 102 | } else { 103 | console.log("更新reducers聚合文件 OK"); 104 | callBack && callBack(); 105 | } 106 | }); 107 | } 108 | }); 109 | } 110 | /** 111 | * 在bandle load追加内容 112 | * @param {function} callBack 成功回调函数 113 | */ 114 | function appendBundleLoad(callBack){ 115 | var bundleLoadTemp = fs.readFileSync(__dirname + '/src/templates/bundleLoad.js.template', 'utf8'); 116 | var data = bundleLoadTemp.replace(/\{0\}/g, CompName).replace(/\{1\}/g, modlueName); 117 | data = '\n' + data + '\n'; 118 | var buffer = new Buffer(data); 119 | fs.appendFile(__dirname + '/src/bundle_load.js', buffer, function (err) { 120 | if (err) { 121 | console.error("追加 bandler load 失败",err); 122 | return; 123 | } else { 124 | callBack && callBack(); 125 | console.log("追加 bandler load OK"); 126 | } 127 | }); 128 | } 129 | 130 | /** 131 | * 更新路由 132 | * @param {function} callBack 成功回调函数 133 | */ 134 | function updateRouter(callBack) { 135 | var data = fs.readFileSync(__dirname + '/src/router.js', 'utf8'); 136 | data = data.substr(0, data.lastIndexOf("]")); 137 | var routerTemp = fs.readFileSync(__dirname + '/src/templates/router.js.template', 'utf8'); 138 | var router = routerTemp.replace(/\{0\}/g, CompName).replace(/\{1\}/g, modlueName); 139 | var buf = data + " " + router + "\n]" 140 | var buffer = new Buffer(buf); 141 | fs.writeFile(__dirname + '/src/router.js', buffer, function (err) { 142 | if (err) { 143 | console.error("追加 router 失败",err); 144 | return; 145 | } else { 146 | callBack && callBack(); 147 | console.log("追加 router OK"); 148 | } 149 | }); 150 | } 151 | /** 152 | * 读取modules 模块目录, 153 | * @param {string} url 需要读取的目录路径 154 | * @param {function} callBack 回调函数 155 | */ 156 | function readDir(url, callBack) { 157 | fs.readdir(__dirname + url, function (err, files) { 158 | if (err) { 159 | console.log('读取目录失败', err); 160 | throw err; 161 | } else { 162 | console.log('读取目录成功'); 163 | callBack && callBack(files); 164 | } 165 | }) 166 | } 167 | /** 168 | * 拼接reducers内容 demo3: {reducer: demo3Reducer, isCached: false}, 169 | * @param {array} files modules目录下的文件夹数组 170 | */ 171 | function jointReducersContent(files){ 172 | console.log(files); 173 | var updateReducer = ""; 174 | var reducerPath = ""; 175 | var reducerContent = "export default {\n"; 176 | var store = ""; 177 | for (var i = 0; i < files.length; i++) { 178 | var file = files[i]; 179 | if(file === '.DS_Store'){ 180 | continue; 181 | } 182 | reducerPath += 'import ' + file + 'Reducer from "Modules/' + file + '/reducer"\n'; 183 | store += " " + file + ': {reducer: ' + file + 'Reducer, isCached: false},\n'; 184 | } 185 | store = store.substr(0, store.lastIndexOf(',')); 186 | updateReducer = reducerPath + '\n' + reducerContent + store + "\n}"; 187 | return updateReducer; 188 | } 189 | 190 | var index = 0; 191 | //获取参数 192 | var args = process.argv.splice(2); 193 | var modlueName = args[index]; //模块名称 194 | var CompName = titleCase(modlueName); //组件名称 模块名称的首字母大写 195 | /** 196 | * 递归循环创建 module 197 | */ 198 | function loopCreate () { 199 | if(index >= args.length) return; 200 | modlueName = args[index]; //模块名称 201 | CompName = titleCase(modlueName); //组件名称 模块名称的首字母大写 202 | //创建模块 具体实现 203 | createModule(modlueName, function() { 204 | console.log('创建成功'); 205 | //读取目录 206 | readDir("/src/modules/", function (files) { 207 | //更新reducers聚合 208 | updateReducers(jointReducersContent(files), function() { 209 | //追加bandle load 210 | appendBundleLoad(function() { 211 | //更新路由 212 | updateRouter(function(){ 213 | console.log("更新路由成功"); 214 | index++; 215 | loopCreate(); 216 | }); 217 | }); 218 | }); 219 | }); 220 | }); 221 | } 222 | 223 | /** 224 | * 递归创建 225 | */ 226 | loopCreate(); 227 | 228 | -------------------------------------------------------------------------------- /src/modules/javascriptError/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import Header from "Components/header" 4 | import { Row, Col, Tabs, Card, Icon, Tooltip } from "antd" 5 | import { jsErrorOption, jsErrorOptionByHour } from "ChartConfig/jsChartOption" 6 | const TabPane = Tabs.TabPane 7 | const echarts = require("echarts") 8 | class JavascriptError extends Component { 9 | constructor(props) { 10 | super(props) 11 | this.state = { 12 | jsErrorCountByDayChart: null, 13 | activePageIndex: 0 14 | } 15 | this.initData = this.initData.bind(this) 16 | this.loadInitData = this.loadInitData.bind(this) 17 | } 18 | 19 | componentDidMount() { 20 | // try { 21 | // throw new Error("获取通讯录失败") 22 | // } catch (e) { 23 | // console.error(e) 24 | // } 25 | } 26 | 27 | componentWillUnmount() { 28 | this.props.clearJavascriptErrorState() 29 | } 30 | render() { 31 | const { jsErrorList, ignoreErrorList, jsErrorListByPage, pageErrorList, 32 | maxPageErrorCount, totalPercent, pcPercent, 33 | iosPercent, androidPercent, activeKeyTop, 34 | activeKeyDown } = this.props 35 | return
36 |
40 | 41 | 42 | 43 | 44 | 月统计} key="1"> 45 |
46 | 47 | 实时统计} key="2"> 48 |
49 | 50 | 51 | 52 | 53 | 54 | 一周统计} key="1"> 55 |
56 | 57 | {totalPercent}% 58 |
59 |
60 | 61 | {pcPercent}% 62 |
63 |
64 | 65 | {iosPercent}% 66 |
67 |
68 | 69 | {androidPercent}% 70 |
71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 错误列表} key="1"> 80 | 81 | { 82 | jsErrorList.length <= 0 && 83 | } 84 | { 85 | jsErrorList.map((error, index) => { 86 | const ignoreStatus = ignoreErrorList.filter(data => data.ignoreErrorMessage === error.errorMessage && data.type === "ignore").length > 0 87 | const resolveStatus = ignoreErrorList.filter(data => data.ignoreErrorMessage === error.errorMessage && data.type === "resolve").length > 0 88 | const msgArr = error.errorMessage.split(": ") 89 | const len = msgArr.length 90 | const nowTime = new Date().getTime() 91 | const latestTime = parseInt(error.happenTime, 10) 92 | const timeStatus = nowTime - latestTime > 24 * 60 * 60 * 1000 93 | return

94 | {msgArr[0] || "空"} 95 | {msgArr[len - 1] || "..."} 96 | { 97 | error.osInfo.map((obj) => { 98 | let osType = "" 99 | if (obj.os === "ios") { 100 | osType = "apple" 101 | } else if (obj.os === "and") { 102 | osType = "android" 103 | } else { 104 | osType = "windows" 105 | } 106 | return 107 | 108 | 109 | }) 110 | } 111 | { 112 | ignoreStatus && || 113 | resolveStatus && 114 | } 115 | 116 | {timeStatus ? "最近:" : "24小时内:"}{new Date(latestTime).Format("yyyy-MM-dd hh:mm:ss")} 117 |

118 | }) 119 | } 120 |
121 |
122 | 错误页面} key="2"> 123 | 124 | 125 | { 126 | pageErrorList.map((page, index) => { 127 | const percent = page.count * 100 / maxPageErrorCount + "%" 128 | return 129 |

130 | {page.simpleUrl}({page.count}次) 131 |

132 |
133 | }) 134 | } 135 |
136 | 137 | 138 | 139 | { 140 | jsErrorListByPage.map((error, index) => { 141 | const msgArr = error.errorMessage.split(": ") 142 | const len = msgArr.length 143 | return

144 | {msgArr[0] || "空"} 145 | {msgArr[len - 1] || "..."} 146 | { 147 | error.osInfo.map((obj) => { 148 | let osType = "" 149 | if (obj.os === "ios") { 150 | osType = "apple" 151 | } else if (obj.os === "and") { 152 | osType = "android" 153 | } else { 154 | osType = "windows" 155 | } 156 | return 157 | 158 | 159 | }) 160 | } 161 | 162 | 最近:{new Date(parseInt(error.happenTime, 10)).Format("yyyy-MM-dd hh:mm:ss")} 163 |

164 | }) 165 | } 166 |
167 | 168 |
169 |
170 | 171 |
172 |
173 | } 174 | onStatistic(key) { 175 | let timeType = "month" 176 | if (key === "2") { 177 | timeType = "day" 178 | this.props.getJsErrorCountByHourAction((res) => { 179 | // 基于准备好的dom,初始化echarts实例 180 | const jsErrorChartByHour = echarts.init(document.getElementById("jsErrorCountByHour")) 181 | const data = res.data 182 | const dateArray = [], jsErrorArray = [] 183 | for (let i = 0; i < data.length; i ++) { 184 | dateArray.push(data[i].day) 185 | jsErrorArray.push(data[i].count) 186 | } 187 | jsErrorChartByHour.setOption(jsErrorOptionByHour([dateArray, jsErrorArray])) 188 | }) 189 | this.loadInitData("day") 190 | } else if (key === "1") { 191 | timeType = "month" 192 | this.loadInitData("month") 193 | } 194 | this.props.updateJavascriptErrorState({activeKeyTop: key, activeKeyDown: "1", timeType}) 195 | } 196 | onPageError(key) { 197 | const { timeType } = this.props 198 | this.props.updateJavascriptErrorState({activeKeyDown: key}) 199 | if (key === "2") { 200 | this.props.getJsErrorCountByPageAction({ timeType }, (res) => { 201 | if (res.length) { 202 | this.props.getJsErrorSortAction({simpleUrl: res[0].simpleUrl, timeType}, (result) => { 203 | const maxPageErrorCount = parseInt(res[0].count, 10) 204 | this.props.updateJavascriptErrorState({jsErrorListByPage: result.data, maxPageErrorCount, pageErrorList: res}) 205 | }) 206 | } else { 207 | this.props.updateJavascriptErrorState({pageErrorList: []}) 208 | } 209 | }) 210 | } 211 | } 212 | getJsErrorListByPage(simpleUrl, index) { 213 | const { timeType } = this.props 214 | this.props.getJsErrorSortAction({simpleUrl, timeType}, (result) => { 215 | this.props.updateJavascriptErrorState({jsErrorListByPage: result.data}) 216 | this.setState({activePageIndex: index}) 217 | }) 218 | } 219 | turnToDetail(error) { 220 | this.props.history.push("javascriptErrorDetail?errorMsg=" + error.errorMessage) 221 | } 222 | 223 | initData() { 224 | this.loadInitData() 225 | 226 | // 根据平台获取并计算错误率 227 | this.props.getJavascriptErrorCountByOsAction({day: 7}, (result) => { 228 | const pcError = parseInt(result.pcError.count, 10) 229 | const iosError = parseInt(result.iosError.count, 10) 230 | const androidError = parseInt(result.androidError.count, 10) 231 | const pcPv = parseInt(result.pcPv.count, 10) 232 | const iosPv = parseInt(result.iosPv.count, 10) 233 | const androidPv = parseInt(result.androidPv.count, 10) 234 | 235 | const errorTotal = pcError + iosError + androidError 236 | const pvTotal = pcPv + iosPv + androidPv 237 | 238 | const totalPercent = (errorTotal * 100 / pvTotal).toFixed(2) 239 | const pcPercent = (pcError * 100 / pcPv).toFixed(2) 240 | const iosPercent = (iosError * 100 / iosPv).toFixed(2) 241 | const androidPercent = (androidError * 100 / androidPv).toFixed(2) 242 | this.props.updateJavascriptErrorState({totalPercent, pcPercent, iosPercent, androidPercent}) 243 | }) 244 | } 245 | 246 | // 加载错误图表数据 247 | async loadInitData(newTimeType) { 248 | const timeType = newTimeType ? newTimeType : this.props.timeType 249 | // 基于准备好的dom,初始化echarts实例 250 | this.state.jsErrorCountByDayChart = echarts.init(document.getElementById("jsErrorCountByDay")) 251 | // 绘制图表 252 | this.props.getJsErrorCountByDayAction({ timeType }, (result) => { 253 | const data = result.data 254 | const dateArray = [], jsErrorArray = [] 255 | for (let i = 0; i < 30; i ++) { 256 | dateArray.push(data[i].day) 257 | jsErrorArray.push(data[i].count) 258 | } 259 | this.state.jsErrorCountByDayChart.setOption(jsErrorOption([dateArray, jsErrorArray])) 260 | }) 261 | // 获取忽略js错误列表 262 | await this.props.getIgnoreJavascriptErrorListAction((result) => { 263 | this.props.updateJavascriptErrorState({ignoreErrorList: result}) 264 | }) 265 | // 获取js错误列表 266 | await this.props.getJsErrorSortAction({ timeType }, (result) => { 267 | this.props.updateJavascriptErrorState({jsErrorList: result.data}) 268 | }) 269 | } 270 | choseProject() { 271 | this.props.clearJavascriptErrorState() 272 | this.initData() 273 | } 274 | loadedProjects() { 275 | this.initData() 276 | } 277 | } 278 | 279 | JavascriptError.propTypes = { 280 | } 281 | 282 | export default JavascriptError 283 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // http://eslint.cn/docs/rules/ 2 | { 3 | "parser": "babel-eslint", // https://github.com/babel/babel-eslint 4 | "plugins": [ 5 | "react" // https://github.com/yannickcr/eslint-plugin-react 6 | ], 7 | "env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments 8 | "browser": true, // browser global variables 9 | "node": true // Node.js global variables and Node.js-specific rules 10 | }, 11 | "rules": { 12 | /** 13 | * Strict mode 14 | */ 15 | // babel inserts "use strict"; for us 16 | "strict": [2, "never"], // http://eslint.org/docs/rules/strict 不允许使用严格模式 bable为我们导入了use strict 17 | /** 18 | * ES6 19 | */ 20 | "no-var": 2, // http://eslint.org/docs/rules/no-var 不允许var 定义变量 21 | "prefer-const": 2, // http://eslint.org/docs/rules/prefer-const 对于在声明之后从未重新分配的变量,需要const声明 22 | /** 23 | * Variables 24 | */ 25 | "no-shadow": 2, // http://eslint.org/docs/rules/no-shadow 不允许申明外部作用域已经申明过的变量 26 | "no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names 不允许定义受限的词, 或者关键字 27 | "no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars 不允许未使用的变量 28 | "vars": "local", 29 | "args": "after-used" 30 | }], 31 | "no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define 不允许使用未定义的变量 32 | /** 33 | * Possible errors 34 | */ 35 | // "comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle 不允许结尾有逗号 36 | "no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign 不允许在条件表达式中赋值? 37 | "no-console": 1, // http://eslint.org/docs/rules/no-console 警告代码中有console.log() 38 | "no-debugger": 1, // http://eslint.org/docs/rules/no-debugger 警告使用调试器 39 | "no-alert": 1, // http://eslint.org/docs/rules/no-alert 警告使用alert 40 | "no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition 警告常量表达式 41 | "no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys 对象中不允许重复的键 42 | "no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case switch 表达式不允许重复的case 43 | "no-empty": 2, // http://eslint.org/docs/rules/no-empty 禁止空的代码块 44 | "no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign 不允许在catch给exception 赋值 45 | "no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast 不必要的布尔转换(不判断) 46 | "no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi 不允许不必要的封号 47 | "no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign 不允许重新赋值给一个已经申明的函数 48 | "no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations 不允许在一个代码块中申明一个函数 49 | "no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp 不允许无效的正则表达式 50 | "no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace 禁止字符串和注释之外的不规则空格 51 | "no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls 不允许将全局对象的属性用作一个函数 52 | "no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays 不允许稀疏数组 53 | "no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable 不允许无法访问的代码 54 | "use-isnan": 2, // http://eslint.org/docs/rules/use-isnan 用isNaN来判断NaN 而不是 == 55 | "block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var 在其定义的范围内强制使用变量 在代码块中定义变量,在代码块之外使用变量 56 | /** 57 | * Best practices 58 | */ 59 | "consistent-return": 2, // http://eslint.org/docs/rules/consistent-return 代码中如果有多处返回值, 要么都有返回值, 要么都没有返回值 60 | "curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly 所有的控制语句都要加大括号 61 | "default-case": 2, // http://eslint.org/docs/rules/default-case switch语句需要default 62 | "dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation 强制执行圆点符号 63 | "allowKeywords": true 64 | }], 65 | "eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq 使用强等号 66 | "guard-for-in": 1, // http://eslint.org/docs/rules/guard-for-in 使用for-in 循环需要判断是否属于本对象的属性,否则会遍历原型链上的属性 67 | "no-caller": 2, // http://eslint.org/docs/rules/no-caller 禁止使用arguments.caller and arguments.callee ,已经被遗弃 68 | "no-else-return": 2, // http://eslint.org/docs/rules/no-else-return 如果if块包含return语句,则else块变得不必要。 其内容可以放在块外。 69 | "no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null null 值比较用强等 70 | "no-eval": 1, // http://eslint.org/docs/rules/no-eval 不允许用eval 71 | // "no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native 不允许原生类型的定义 Object.prototype.a = "a"; 72 | "no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind ???不懂 73 | "no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough switch 两个case之间需要break 74 | "no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal 不允许数字文字中的前导或尾随小数点 75 | "no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval 不允许使用eval() - 类似的方法 76 | "no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks 禁止不必要的嵌套块 77 | "no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func 不允许函数声明和循环语句内的表达式 78 | "no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str 禁止多行字符串 79 | "no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign 禁止给原生的对象赋值 80 | "no-new": 2, // http://eslint.org/docs/rules/no-new 禁止直接new, 需要有赋值语句 81 | "no-new-func": 2, // http://eslint.org/docs/rules/no-new-func 禁止 var x = new Function("a", "b", "return a + b"); 82 | "no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers 错误:var stringObject = new String("Hello world"); 正确:var text = String(someValue); 83 | "no-octal": 2, // http://eslint.org/docs/rules/no-octal 不允许八进制文字 84 | "no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape 不允许字符串中的八进制转义序列 85 | "no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign 不允许重新分配函数参数 (给参数赋值) 86 | "no-proto": 2, // http://eslint.org/docs/rules/no-proto 不允许使用__proto__属性 使用 var a = Object.getPrototypeOf(obj); 87 | "no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare 不允许变量重新声明 88 | "no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign 不允许返回语句中的赋值运算符 89 | "no-script-url": 2, // http://eslint.org/docs/rules/no-script-url 不允许url写javascript location.href = "javascript:void(0)"; 90 | "no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare 不允许比较同一个变量或者相同的值 91 | "no-sequences": 2, // http://eslint.org/docs/rules/no-sequences 不允许逗号运算符 92 | "no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal 不允许将文字作为异常抛出 93 | "no-with": 2, // http://eslint.org/docs/rules/no-with 不允许width 语句 94 | "radix": 2, // http://eslint.org/docs/rules/radix 在使用parseInt()时强制执行radix参数的一致使用,不确定是否需要 95 | "vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top var声明放置在它们包含作用域的顶部 96 | "wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife 需要在立即函数调用周围使用圆括号 97 | "yoda": 2, // http://eslint.org/docs/rules/yoda ???不知道 98 | /** 99 | * Style 100 | */ 101 | // "indent": ["error", 2], // http://eslint.org/docs/rules/indent 一致的缩进 102 | "brace-style": [2, // http://eslint.org/docs/rules/brace-style 对块执行一致的大括号风格 103 | "1tbs", { 104 | "allowSingleLine": true 105 | }], 106 | "quotes": [ 107 | 1, "double", { 108 | "avoidEscape": true 109 | } // http://eslint.org/docs/rules/quotes 双引号引号 110 | ], 111 | "camelcase": [2, { // http://eslint.org/docs/rules/camelcase 驼峰命名 112 | "properties": "never" 113 | }], 114 | "comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing 在逗号前后使用一致的间距 115 | "before": false, 116 | "after": true 117 | }], 118 | "comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style 强制执行一致的逗号风格 119 | //"eol-last": 2, // http://eslint.org/docs/rules/eol-last 在文件末尾需要或禁止换行 120 | //"func-names": 1, // http://eslint.org/docs/rules/func-names 禁止匿名函数 121 | "key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing 在对象文字属性中的键和值之间强制使用一致的间距 122 | "beforeColon": false, 123 | "afterColon": true 124 | }], 125 | "new-cap": [1, { // http://eslint.org/docs/rules/new-cap 要求构造函数名以大写字母开头 126 | "newIsCap": true 127 | }], 128 | "no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines 不允许有多行空行 129 | "max": 2 130 | }], 131 | "no-nested-ternary": 2, // http://eslint.org/docs/rules/no-nested-ternary 不允许嵌套的三元表达式 132 | "no-new-object": 2, // http://eslint.org/docs/rules/no-new-object 禁止Object构造函数 133 | "no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func 方法名和括号之间不允许空格 134 | "no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces 不允许在行尾结束空格 135 | "no-extra-parens": 1, // http://eslint.org/docs/rules/no-extra-parens 不需要多余的括号 136 | "no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle 不允许在标识符中悬挂下划线 137 | //"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var 强制变量在函数中一起或单独声明 138 | "padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks 需要或不允许块内填充 自己看,不好描述 139 | "semi": [2, "never"], // http://eslint.org/docs/rules/semi ??? 不懂 140 | "semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing 141 | "before": false, 142 | "after": true 143 | }], 144 | "keyword-spacing": 2, // http://eslint.org/docs/rules/space-after-keywords 在关键字之前和之后强制一致的间隔 145 | "space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks 在块之前强制一致的间距 146 | "space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren 在函数定义开始括号之前强制使用一致的间距 147 | "space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops 需要在中缀运算符周围留出间距 148 | "spaced-comment": 2, // http://eslint.org/docs/rules/spaced-comment 在注释中的//或/ *后面强制一致的间距 149 | /** 150 | * JSX style 151 | */ 152 | "react/display-name": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/display-name.md 防止在React组件定义中缺少displayName 153 | "react/jsx-boolean-value": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-boolean-value.md 在JSX中强制执行布尔属性符号 154 | "react/jsx-no-undef": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-undef.md 在JSX中禁止未声明的变量 155 | "react/jsx-sort-props": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-props.md 执行道具字母顺序排序 156 | "react/jsx-sort-prop-types": 0, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-sort-prop-types.md 157 | "react/jsx-uses-react": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-react.md 防止React被错误地标记为未使用 158 | "react/jsx-uses-vars": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-uses-vars.md 防止在JSX中使用的变量被错误地标记为未使用 159 | "react/no-did-update-set-state": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-did-update-set-state.md 防止在componentDidUpdate中使用setState 160 | // "react/no-multi-comp": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-multi-comp.md 防止每个文件的多个组件定义 161 | "react/no-unknown-property": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unknown-property.md 防止使用未知的DOM属性 162 | // "react/prop-types": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/prop-types.md 在React组件定义中防止缺少props验证 163 | "react/react-in-jsx-scope": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/react-in-jsx-scope.md 在使用JSX时防止丢失React 164 | "react/self-closing-comp": 1, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/self-closing-comp.md 防止没有子项的组件的额外结束标记 165 | //"react/jsx-wrap-multilines": 2, // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/wrap-multilines.md 跟no-extra-parens 冲突了 166 | "react/sort-comp": [1, { // https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md 强制组件方法顺序 167 | "order": [ 168 | "constructor", 169 | "displayName", 170 | "propTypes", 171 | "contextTypes", 172 | "childContextTypes", 173 | "mixins", 174 | "statics", 175 | "defaultProps", 176 | "getDefaultProps", 177 | "getInitialState", 178 | "getChildContext", 179 | "componentWillMount", 180 | "componentDidMount", 181 | "componentWillReceiveProps", 182 | "shouldComponentUpdate", 183 | "componentWillUpdate", 184 | "componentDidUpdate", 185 | "componentWillUnmount", 186 | "/^on.+$/", 187 | "/^get.+$/", 188 | "/^render.+$/", 189 | "render" 190 | ] 191 | }] 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 23 | 74 | 75 | 76 |
77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/modules/javascriptErrorDetail/index.js: -------------------------------------------------------------------------------- 1 | import "./index.scss" 2 | import React, { Component } from "react" 3 | import { Row, Col, Button, Icon, Table, Collapse, Timeline, Modal } from "antd" 4 | import Header from "Components/header" 5 | import Utils from "Common/utils" 6 | const Panel = Collapse.Panel 7 | 8 | class JavascriptErrorDetail extends Component { 9 | constructor(props) { 10 | super(props) 11 | this.analysisError = this.analysisError.bind(this) 12 | } 13 | 14 | async componentDidMount() { 15 | const { errorMsg } = Utils.parseQs() 16 | let errorDetail = [] 17 | await this.props.getJavascriptErrorListByMsgAction({errorMsg: encodeURIComponent(errorMsg)}, (data) => { 18 | const { errorIndex } = this.props 19 | const errorList = data 20 | errorDetail = this.analysisError(errorList[errorIndex]) 21 | this.getTheLocationOfError(errorDetail.jsPathArray) 22 | this.props.updateJavascriptErrorDetailState({errorList, errorDetail}) 23 | }) 24 | 25 | this.props.getJavascriptErrorAboutInfoAction({errorMsg: encodeURIComponent(errorMsg), customerKey: errorDetail.customerKey}, (res) => { 26 | this.props.updateJavascriptErrorDetailState({errorAboutInfo: res}) 27 | }) 28 | 29 | this.props.getIgnoreJavascriptErrorListAction((res) => { 30 | const result = res.filter(data => data.ignoreErrorMessage === errorMsg) 31 | if (result.length > 0) { 32 | this.props.updateJavascriptErrorDetailState({isIgnore: true}) 33 | } 34 | }) 35 | } 36 | componentWillUnmount() { 37 | this.props.clearJavascriptErrorDetailState() 38 | } 39 | render() { 40 | const { errorDetail, errorList, errorStackList, errorAboutInfo, isIgnore } = this.props 41 | const columns = [ 42 | { title: "错误信息", dataIndex: "errorMessage", key: "errorMessage"}, 43 | { title: "页面", dataIndex: "simpleUrl", key: "simpleUrl" }, 44 | { title: "设备", dataIndex: "deviceName", key: "deviceName" }, 45 | { title: "客户IP地址", dataIndex: "monitorIp", key: "monitorIp" }, 46 | { title: "省份", dataIndex: "province", key: "province" }, 47 | { title: "城市", dataIndex: "city", key: "city" }, 48 | { title: "浏览器信息", dataIndex: "browserInfo", key: "browserInfo" }, 49 | { title: , width: 200, dataIndex: "happenTime", key: "happenTime", fixed: "right"}, 50 | { 51 | title: "操作", 52 | key: "operation", 53 | fixed: "right", 54 | width: 100, 55 | render: (text, detail, index) => { 56 | return 查看详情 57 | }, 58 | }, 59 | ] 60 | const ipIcon = 61 | const browserIcon = 62 | let osIcon = null 63 | let deviceIcon = 64 | if (errorDetail.os === "android") { 65 | osIcon = 66 | } else if (errorDetail.os === "ios" || errorDetail.os === "Mac") { 67 | osIcon = 68 | } else if (errorDetail.os === "Windows") { 69 | osIcon = 70 | } 71 | if (errorDetail.deviceName && errorDetail.deviceName.indexOf("iphone") !== -1) { 72 | deviceIcon = 73 | } else if (errorDetail.deviceName && errorDetail.deviceName.indexOf("PC") !== -1) { 74 | deviceIcon = 75 | } 76 | 77 | const data = [] 78 | const len = errorList.length > 100 ? 100 : errorList.length 79 | for (let i = 0; i < len; i ++) { 80 | const error = this.analysisError(errorList[i]) 81 | const result = Object.assign({}, {key: Math.random()}, error) 82 | data.push(result) 83 | } 84 | 85 | return
86 |
87 | 88 | 89 | {errorDetail.errorType}{errorDetail.titleDetail || "..."} 90 | 91 | {errorDetail.simpleUrl || "..."} 92 | 93 | 94 |
95 | 发生次数 96 | {errorList.length >= 200 ? "200+" : errorList.length} 97 |
98 |
99 | 影响用户 100 | {errorAboutInfo.customerCount} 101 |
102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 |
111 | 112 | 113 | { ipIcon } 114 |
115 | 116 | 发生{errorAboutInfo.occurCount || 0}次 117 |
118 | 119 | 120 | { browserIcon } 121 |
122 | {errorDetail.browserName || "..."} 123 | 版本: {errorDetail.browserVersion || "..."} 124 |
125 | 126 | 127 | { osIcon } 128 |
129 | {errorDetail.os} 130 | 版本: {errorDetail.osVersion} 131 |
132 | 133 | 134 | { deviceIcon } 135 |
136 | {errorDetail.deviceName} 137 |   138 |
139 | 140 |
141 | { 142 | 143 | 144 | 145 | 146 | 进入页面 /omega/home 147 | 点击了按钮 下一步 148 | 149 |

发生了一个错误 Toast is not defined

150 |
151 | 152 |

进入页面 /omega/openAccount

153 |
154 |
155 |
156 |
157 |
158 | } 159 | 160 |

Js错误堆栈

161 | { errorDetail.errorMessage } 162 | 163 | { 164 | errorStackList.map((stack, index) => { 165 | return 166 |

{decodeURIComponent(stack.code)}

167 |
168 | }) 169 | } 170 | 171 |

{ errorDetail.errorStack }

172 |
173 |
174 |
175 | 176 | 177 | 178 | 179 | } 180 | callback(key) { 181 | console.log(key) 182 | } 183 | resolveError() { 184 | const { errorMsg } = Utils.parseQs() 185 | Modal.confirm({ 186 | title: "提示", 187 | content: "是否已经修复了这个问题?", 188 | okText: "是", 189 | cancelText: "否", 190 | iconType: "warning", 191 | onOk: () => { 192 | this.props.setIgnoreJavascriptErrorAction({ignoreErrorMessage: errorMsg, type: "resolve"}, () => { 193 | this.props.updateJavascriptErrorDetailState({isIgnore: true}) 194 | }) 195 | } 196 | }) 197 | } 198 | ignoreError() { 199 | const { errorMsg } = Utils.parseQs() 200 | Modal.confirm({ 201 | title: "提示", 202 | content: "此操作将会忽略所有该类型的报错,以后也不会再继续上传了,是否忽略?", 203 | okText: "确定忽略", 204 | cancelText: "取消", 205 | iconType: "warning", 206 | onOk: () => { 207 | this.props.setIgnoreJavascriptErrorAction({ignoreErrorMessage: errorMsg, type: "ignore"}, () => { 208 | this.props.updateJavascriptErrorDetailState({isIgnore: true}) 209 | }) 210 | } 211 | }) 212 | } 213 | deleteError() { 214 | Modal.confirm({ 215 | title: "提示", 216 | content: "抱歉,删除功能暂时不对线上开放!", 217 | okText: "确定", 218 | cancelText: "取消", 219 | iconType: "warning", 220 | onOk: () => {} 221 | }) 222 | } 223 | turnToPrev() { 224 | const { errorList, errorIndex } = this.props 225 | const tempIndex = errorIndex > 0 ? errorIndex - 1 : 0 226 | const errorDetail = this.analysisError(errorList[tempIndex]) 227 | this.props.updateJavascriptErrorDetailState({errorDetail, errorIndex: tempIndex}) 228 | } 229 | turnToNext() { 230 | const { errorList, errorIndex } = this.props 231 | if (errorList.length >= errorIndex + 1) { 232 | const errorDetail = this.analysisError(errorList[errorIndex + 1]) 233 | this.props.updateJavascriptErrorDetailState({errorDetail, errorIndex: errorIndex + 1}) 234 | this.props.getJavascriptErrorAboutInfoAction({errorMsg: encodeURIComponent(errorDetail.errorMessage), customerKey: errorDetail.customerKey}, (res) => { 235 | this.props.updateJavascriptErrorDetailState({errorAboutInfo: res}) 236 | }) 237 | } 238 | } 239 | 240 | analysisError(error) { 241 | if (!error) return {} 242 | const errMsgArr = error.errorMessage.split(": ") 243 | const errorType = errMsgArr[0] 244 | const errorMessage = errMsgArr[errMsgArr.length - 1] 245 | const errorStack = error.errorStack 246 | const happenTime = new Date(parseInt(error.happenTime, 10)).Format("yyyy-MM-dd hh:mm:ss") 247 | const simpleUrl = error.simpleUrl 248 | const monitorIp = error.monitorIp 249 | let browserName = error.browserName 250 | let browserVersion = error.browserVersion 251 | const browserInfo = error.browserInfo 252 | let os = error.os.split(" ")[0] 253 | let osVersion = error.os.split(" ")[1] 254 | const deviceName = error.deviceName 255 | const jsPathArray = error.errorStack.match(/\([(http)?:]?[\S]*\d+\)/g) 256 | const tempArr = jsPathArray ? jsPathArray[0].split("/") : [] 257 | const titleDetail = tempArr.length ? tempArr[tempArr.length - 1] : "" 258 | const customerKey = error.customerKey 259 | const province = error.province 260 | const city = error.city 261 | 262 | let browserArr = [], osVersionArr = [] 263 | if (os === "web") { 264 | if (/Mac OS/i.test(browserInfo)) { 265 | os = "Mac" 266 | osVersionArr = browserInfo.match(/Mac OS X [0-9_]+/g) 267 | osVersionArr = osVersionArr[0].split(" ") 268 | osVersion = osVersionArr[osVersionArr.length - 1] 269 | } else if (/Windows/i.test(browserInfo)) { 270 | os = "Windows" 271 | osVersionArr = browserInfo.match(/Windows NT [0-9.]+/g) 272 | osVersionArr = osVersionArr[0].split(" ") 273 | osVersion = osVersionArr[osVersionArr.length - 1] 274 | } 275 | } else { 276 | if (/MicroMessenger/i.test(browserInfo)) { 277 | browserName = "MicroMessenger(微信)" 278 | browserArr = browserInfo.match(/MicroMessenger\/[0-9\.]+/g) 279 | browserVersion = browserArr.length ? browserArr[0].split("/")[1] : "..." 280 | } else if (/MQQBrowser/i.test(browserInfo)) { 281 | browserName = "MQQBrowser" 282 | browserArr = browserInfo.match(/MQQBrowser\/[0-9\.]+/g) 283 | browserVersion = browserArr.length ? browserArr[0].split("/")[1] : "..." 284 | } else if (/UCBrowser/i.test(browserInfo)) { 285 | browserName = "UCBrowser" 286 | browserArr = browserInfo.match(/UCBrowser\/[0-9\.]+/g) 287 | browserVersion = browserArr.length ? browserArr[0].split("/")[1] : "..." 288 | } else if (/QihooBrowser/i.test(browserInfo)) { 289 | browserName = "QihooBrowser" 290 | browserArr = browserInfo.match(/QihooBrowser\/[0-9\.]+/g) 291 | browserVersion = browserArr.length ? browserArr[0].split("/")[1] : "..." 292 | } else if (/CriOS/i.test(browserInfo)) { 293 | browserName = "CriOS(谷歌)" 294 | browserArr = browserInfo.match(/CriOS\/[0-9\.]+/g) 295 | browserVersion = browserArr.length ? browserArr[0].split("/")[1] : "..." 296 | } else if (/DingTalk/i.test(browserInfo)) { 297 | browserName = "DingTalk" 298 | browserArr = browserInfo.match(/DingTalk\/[0-9\.]+/g) 299 | browserVersion = browserArr.length ? browserArr[0].split("/")[1] : "..." 300 | } else { 301 | browserName = "Mobile UI WebView" 302 | } 303 | } 304 | 305 | return { 306 | errorType, 307 | errorMessage, 308 | errorStack, 309 | happenTime, 310 | simpleUrl, 311 | monitorIp, 312 | browserName, 313 | browserVersion, 314 | browserInfo, 315 | os, 316 | osVersion, 317 | deviceName, 318 | jsPathArray, 319 | titleDetail, 320 | customerKey, 321 | province, 322 | city 323 | } 324 | } 325 | getTheLocationOfError(tempJsPathArray) { 326 | let jsPathArray = tempJsPathArray 327 | if (!jsPathArray || !jsPathArray.length) { 328 | jsPathArray = [] 329 | } 330 | const stackList = [] 331 | for (let i = 0; i < jsPathArray.length; i ++) { 332 | const jsPathStr = jsPathArray[i].replace(/[()]/g, "") 333 | const strArr = jsPathStr.split(":") 334 | const jsPath = jsPathStr.match(/https?:\/\/\S*.js/g)[0] 335 | const locationX = strArr[strArr.length - 2] 336 | const locationY = strArr[strArr.length - 1] 337 | stackList.push({ 338 | jsPathStr, 339 | jsPath, 340 | locationX, 341 | locationY 342 | }) 343 | } 344 | this.props.getJavascriptErrorStackCodeAction({stackList}, (data) => { 345 | this.props.updateJavascriptErrorDetailState({errorStackList: data}) 346 | }) 347 | } 348 | showErrorDetail(text, detail, index) { 349 | const errorIndex = index 350 | const errorDetail = detail 351 | document.documentElement.scrollTop = 0 352 | this.props.updateJavascriptErrorDetailState({text, errorIndex, errorDetail}) 353 | } 354 | } 355 | 356 | JavascriptErrorDetail.propTypes = { 357 | } 358 | 359 | export default JavascriptErrorDetail 360 | --------------------------------------------------------------------------------