;
+ const state = { a: 1 };
+ const newState = reducer(state, '');
+ expect(newState).toBe(state);
+ });
+});
diff --git a/src/pages/OnlinInquiry/src/redux/configureStore.ts b/src/pages/OnlinInquiry/src/redux/configureStore.ts
new file mode 100644
index 0000000..f578a45
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/redux/configureStore.ts
@@ -0,0 +1,24 @@
+import { configureStore } from '@reduxjs/toolkit';
+import injectReducerEnhancer from '@/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/enhancer';
+// import rejectedErrorHandlerMiddleware from '../utils/@reduxjs/rejectedErrorHandlerMiddleware';
+import { createReducer } from './reducers';
+
+export function configureAppStore() {
+ const enhancers = [injectReducerEnhancer(createReducer)];
+
+ const store = configureStore({
+ reducer: createReducer(),
+ // middleware: getDefaultMiddleware =>
+ // getDefaultMiddleware({
+ // serializableCheck: false,
+ // // immutableCheck: false,
+ // }).prepend(rejectedErrorHandlerMiddleware.middleware),
+ // devTools:
+ // /* istanbul ignore next line */
+ // process.env.NODE_ENV !== 'production' ||
+ // process.env.PUBLIC_URL.length > 0,
+ enhancers,
+ });
+
+ return store;
+}
diff --git a/src/pages/OnlinInquiry/src/redux/reducers.ts b/src/pages/OnlinInquiry/src/redux/reducers.ts
new file mode 100644
index 0000000..fdb3209
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/redux/reducers.ts
@@ -0,0 +1,18 @@
+/**
+ * Combine all reducers in this file and export the combined reducers.
+ */
+import { combineReducers } from '@reduxjs/toolkit'
+import type { InjectedReducersType } from '@/pages/OnlinInquiry/src/utils/types/injector-typings'
+
+/**
+ * Merges the main reducer with the router state and dynamically injected reducers
+ */
+export function createReducer(injectedReducers: InjectedReducersType = {}) {
+ // Initially we don't have any injectedReducers, so returning identity function to avoid the error
+ if (Object.keys(injectedReducers).length === 0) {
+ return (state: any) => state
+ }
+ return combineReducers({
+ ...injectedReducers
+ })
+}
diff --git a/src/pages/OnlinInquiry/src/styles/StyleConstants.ts b/src/pages/OnlinInquiry/src/styles/StyleConstants.ts
new file mode 100644
index 0000000..79d7a89
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/StyleConstants.ts
@@ -0,0 +1,114 @@
+/**
+ * 字体、颜色样式
+ */
+// spacing
+export const SPACE_UNIT = 4;
+export const SPACE_TIMES = (multiple?: number) =>
+ `${SPACE_UNIT * (multiple || 1)}px`;
+export const SPACE = SPACE_TIMES(1); // 4
+export const SPACE_XS = SPACE_TIMES(2); // 8
+export const SPACE_SM = SPACE_TIMES(3); // 12
+export const SPACE_MD = SPACE_TIMES(4); // 16
+export const SPACE_LG = SPACE_TIMES(5); // 20
+export const SPACE_XL = SPACE_TIMES(9); // 36 = 16 + 20
+export const SPACE_XXL = SPACE_TIMES(14); // 56 = 20 + 36
+
+// z-index
+export const MINUS_LEVEL_1 = -1;
+export const LEVEL_1 = 1;
+export const LEVEL_5 = 5;
+export const LEVEL_10 = 10;
+export const LEVEL_20 = 20;
+export const LEVEL_50 = 50;
+export const LEVEL_100 = 100;
+export const LEVEL_1000 = 1000;
+
+export const LEVEL_DASHBOARD_EDIT_OVERLAY = LEVEL_50;
+
+// base color
+export const BLUE = '#1B9AEE';
+export const GREEN = '#15AD31';
+export const ORANGE = '#FA8C15';
+export const YELLOW = '#FAD414';
+export const RED = '#E62412';
+
+/* gray
+ *
+ * as reference
+ * G10 - body background
+ * G20 - split line light
+ * G30 - split line dark
+ * G40 - border
+ * G50 - disabled font
+ * G60 - light font
+ * G70 - secondary font
+ * G80 - font
+ */
+export const WHITE = '#FFFFFF';
+export const G10 = '#F5F8FA';
+export const G20 = '#EFF2F5';
+export const G30 = '#E4E6EF';
+export const G40 = '#B5B5C3';
+export const G50 = '#A1A5B7';
+export const G60 = '#7E8299';
+export const G70 = '#5E6278';
+export const G80 = '#3F4254';
+export const G90 = '#181C32';
+export const BLACK = '#000000';
+
+export const DG10 = '#1b1b29';
+export const DG20 = '#2B2B40';
+export const DG30 = '#323248';
+export const DG40 = '#474761';
+export const DG50 = '#565674';
+export const DG60 = '#6D6D80';
+export const DG70 = '#92929F';
+export const DG80 = '#CDCDDE';
+export const DG90 = '#FFFFFF';
+
+// theme color
+export const PRIMARY = BLUE;
+export const INFO = PRIMARY;
+export const SUCCESS = GREEN;
+export const PROCESSING = BLUE;
+export const ERROR = RED;
+export const HIGHLIGHT = RED;
+export const WARNING = ORANGE;
+export const NORMAL = G40;
+
+// font
+export const FONT_SIZE_BASE = 16;
+export const FONT_SIZE_LABEL = `${FONT_SIZE_BASE * 0.75}px`;
+export const FONT_SIZE_SUBTITLE = `${FONT_SIZE_BASE * 0.8125}px`;
+export const FONT_SIZE_BODY = `${FONT_SIZE_BASE * 0.875}px`;
+export const FONT_SIZE_SUBHEADING = `${FONT_SIZE_BASE * 0.9375}px`;
+export const FONT_SIZE_TITLE = `${FONT_SIZE_BASE}px`;
+export const FONT_SIZE_HEADING = `${FONT_SIZE_BASE * 1.125}px`;
+export const FONT_SIZE_ICON_SM = `${FONT_SIZE_BASE * 1.25}px`;
+export const FONT_SIZE_ICON_MD = `${FONT_SIZE_BASE * 1.5}px`;
+export const FONT_SIZE_ICON_LG = `${FONT_SIZE_BASE * 1.75}px`;
+export const FONT_SIZE_ICON_XL = `${FONT_SIZE_BASE * 2}px`;
+export const FONT_SIZE_ICON_XXL = `${FONT_SIZE_BASE * 2.25}px`;
+
+export const LINE_HEIGHT_LABEL = `${FONT_SIZE_BASE * 1.25}px`;
+export const LINE_HEIGHT_BODY = `${FONT_SIZE_BASE * 1.375}px`;
+export const LINE_HEIGHT_TITLE = `${FONT_SIZE_BASE * 1.5}px`;
+export const LINE_HEIGHT_HEADING = `${FONT_SIZE_BASE * 1.625}px`;
+export const LINE_HEIGHT_ICON_SM = `${FONT_SIZE_BASE * 1.75}px`;
+export const LINE_HEIGHT_ICON_MD = `${FONT_SIZE_BASE * 2}px`;
+export const LINE_HEIGHT_ICON_LG = `${FONT_SIZE_BASE * 2.25}px`;
+export const LINE_HEIGHT_ICON_XL = `${FONT_SIZE_BASE * 2.5}px`;
+export const LINE_HEIGHT_ICON_XXL = `${FONT_SIZE_BASE * 2.75}px`;
+
+export const FONT_FAMILY =
+ '-apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
+export const CODE_FAMILY =
+ '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace';
+export const FONT_WEIGHT_LIGHT = 300;
+export const FONT_WEIGHT_REGULAR = 400;
+export const FONT_WEIGHT_MEDIUM = 500;
+export const FONT_WEIGHT_BOLD = 600;
+
+// border
+
+export const BORDER_RADIUS = '4px';
diff --git a/src/pages/OnlinInquiry/src/styles/antd/variables.less b/src/pages/OnlinInquiry/src/styles/antd/variables.less
new file mode 100644
index 0000000..9a722a9
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/antd/variables.less
@@ -0,0 +1,2 @@
+@import '~antd/lib/style/core/index.less';
+@import '~antd/lib/style/themes/index.less';
diff --git a/src/pages/OnlinInquiry/src/styles/globalStyles/base.ts b/src/pages/OnlinInquiry/src/styles/globalStyles/base.ts
new file mode 100644
index 0000000..7815a89
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/globalStyles/base.ts
@@ -0,0 +1,63 @@
+/**
+ * Datart
+ *
+ * Copyright 2021
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createGlobalStyle } from 'styled-components';
+import { FONT_FAMILY, FONT_SIZE_BODY } from '@/pages/OnlinInquiry/src/styles/StyleConstants';
+
+/* istanbul ignore next */
+export const Base = createGlobalStyle`
+ body {
+ font-family: ${FONT_FAMILY};
+ font-size: ${FONT_SIZE_BODY};
+ background-color: ${(p: { theme: { bodyBackground: any; }; }) => p.theme.bodyBackground};
+ height:100%;
+ with:100%;
+
+ h1,h2,h3,h4,h5,h6 {
+ margin: 0;
+ font-weight: inherit;
+ color: inherit;
+ }
+
+ p, figure {
+ margin: 0;
+ }
+
+ ul {
+ padding: 0;
+ margin: 0;
+ }
+
+ li {
+ list-style-type: none;
+ }
+
+ input {
+ padding: 0;
+ }
+
+ table th {
+ padding: 0;
+ text-align: center;
+ }
+
+ * {
+ -webkit-overflow-scrolling: touch;
+ }
+ }
+`;
diff --git a/src/pages/OnlinInquiry/src/styles/globalStyles/index.tsx b/src/pages/OnlinInquiry/src/styles/globalStyles/index.tsx
new file mode 100644
index 0000000..7860469
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/globalStyles/index.tsx
@@ -0,0 +1,18 @@
+import { Base } from './base';
+// import { Form } from './overwritten/form';
+import { GlobalOverlays } from './overwritten/globalOverlays';
+import { Hardcoded } from './overwritten/hardcoded';
+import { Viz } from './overwritten/viz';
+import { ReactSplit } from './reactSplit';
+
+export function GlobalStyles() {
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/globalOverlays.ts b/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/globalOverlays.ts
new file mode 100644
index 0000000..5845ebd
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/globalOverlays.ts
@@ -0,0 +1,94 @@
+/**
+ * Datart
+ *
+ * Copyright 2021
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createGlobalStyle } from 'styled-components';
+import {
+ LEVEL_1000,
+ SPACE_SM,
+ SPACE_TIMES,
+ SPACE_XS,
+} from '@/pages/OnlinInquiry/src/styles/StyleConstants';
+
+export const GlobalOverlays = createGlobalStyle`
+ /* app/components/Popup */
+ .datart-popup {
+ z-index: ${LEVEL_1000 - 1};
+
+ &.on-modal {
+ z-index: ${LEVEL_1000 + 30};
+ }
+
+ .ant-popover-arrow {
+ display: none;
+ }
+ .ant-popover-inner-content {
+ padding: 0;
+ }
+ .ant-dropdown-menu {
+ box-shadow: none;
+ }
+ &.ant-popover-placement-bottom,
+ &.ant-popover-placement-bottomLeft,
+ &.ant-popover-placement-bottomRight {
+ padding-top: 0;
+ }
+ }
+
+
+ /* schema table header action dropdown menu */
+ .datart-schema-table-header-menu {
+ min-width: ${SPACE_TIMES(40)};
+
+ .ant-dropdown-menu-submenu-selected {
+ .ant-dropdown-menu-submenu-title {
+ color:blue;
+ }
+ }
+ }
+
+ /* config panel */
+ .datart-config-panel {
+ &.ant-collapse >
+ .ant-collapse-item >
+ .ant-collapse-header {
+ padding: ${SPACE_XS} 0;
+ color: blue;
+
+ .ant-collapse-arrow {
+ margin-right: ${SPACE_XS};
+ }
+ }
+
+ .ant-collapse-content >
+ .ant-collapse-content-box {
+ padding: ${SPACE_XS} 0 ${SPACE_SM} !important;
+ }
+ }
+
+ /* data config section dropdown */
+ .datart-data-section-dropdown {
+ z-index: ${LEVEL_1000 - 1};
+ }
+
+ /* color popover */
+ .datart-aggregation-colorpopover{
+ .ant-popover-arrow{
+ display:none;
+ }
+ }
+`;
diff --git a/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/hardcoded.ts b/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/hardcoded.ts
new file mode 100644
index 0000000..8225e6c
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/hardcoded.ts
@@ -0,0 +1,74 @@
+/**
+ * Datart
+ *
+ * Copyright 2021
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createGlobalStyle } from 'styled-components';
+import { FONT_SIZE_LABEL, SPACE_SM } from '@/pages/OnlinInquiry/src/styles/StyleConstants';
+
+export const Hardcoded = createGlobalStyle`
+ body {
+ .ant-form-item-label > label {
+ color: ${(p: { theme: { textColorLight: any; }; }) => p.theme.textColorLight};
+ }
+
+ .ant-form-item-label-left > label {
+ padding-left: ${SPACE_SM};
+
+ &:before {
+ position: absolute;
+ left: 0;
+ }
+ }
+
+ .ant-popover-inner {
+ box-shadow: ${(p: { theme: { shadow3: any; }; }) => p.theme.shadow3};
+ }
+ .ant-popover.ant-popconfirm {
+ z-index: 1060;
+ }
+
+ /* fix antd bugs #32919 */
+ .ant-tabs-dropdown-menu-item {
+ display: flex;
+ align-items: center;
+
+ > span {
+ flex: 1;
+ white-space: nowrap;
+ }
+
+ &-remove {
+ flex: none;
+ margin-left: ${SPACE_SM};
+ font-size: ${FONT_SIZE_LABEL};
+ color: ${(p: { theme: { textColorLight: any; }; }) => p.theme.textColorLight};
+ cursor: pointer;
+ background: 0 0;
+ border: 0;
+
+ &:hover {
+ color: ${(p: { theme: { primary: any; }; }) => p.theme.primary};
+ }
+ }
+ }
+ }
+
+ /* react-grid-layout */
+ .react-grid-item.react-grid-placeholder {
+ background-color: ${(p: { theme: { textColorDisabled: any; }; }) => p.theme.textColorDisabled} !important;
+ }
+`;
diff --git a/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/viz.ts b/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/viz.ts
new file mode 100644
index 0000000..9b13a14
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/globalStyles/overwritten/viz.ts
@@ -0,0 +1,29 @@
+/**
+ * Datart
+ *
+ * Copyright 2021
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createGlobalStyle } from 'styled-components';
+
+export const Viz = createGlobalStyle`
+ /* 覆盖antd 默认样式 */
+ @media (max-width: 575px) {
+ .datart-viz .ant-form .ant-form-item .ant-form-item-label,
+ .datart-viz .ant-form .ant-form-item .ant-form-item-control {
+ flex: 1;
+ }
+ }
+`;
diff --git a/src/pages/OnlinInquiry/src/styles/globalStyles/reactSplit.ts b/src/pages/OnlinInquiry/src/styles/globalStyles/reactSplit.ts
new file mode 100644
index 0000000..bd4dd0f
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/globalStyles/reactSplit.ts
@@ -0,0 +1,78 @@
+/**
+ * Datart
+ *
+ * Copyright 2021
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createGlobalStyle } from 'styled-components';
+import { LEVEL_10, SPACE_TIMES } from '@/pages/OnlinInquiry/src/styles/StyleConstants';
+
+export const ReactSplit = createGlobalStyle`
+ /* react-split */
+ .datart-split {
+ min-width: 0;
+ min-height: 0;
+
+ .gutter-horizontal {
+ &:before {
+ width: 2px;
+ height: 100%;
+ transform: translate(-50%, 0);
+ }
+ &:after {
+ width: ${SPACE_TIMES(2)};
+ height: 100%;
+ cursor: ew-resize;
+ transform: translate(-50%, 0);
+ }
+ }
+
+ .gutter-vertical {
+ &:before {
+ width: 100%;
+ height: 2px;
+ transform: translate(0, -50%);
+ }
+ &:after {
+ width: 100%;
+ height: ${SPACE_TIMES(2)};
+ cursor: ns-resize;
+ transform: translate(0, -50%);
+ }
+ }
+
+ .gutter-horizontal,
+ .gutter-vertical{
+ position: relative;
+
+ &:before {
+ position: absolute;
+ z-index: ${LEVEL_10};
+ content: '';
+ }
+ &:after {
+ position: absolute;
+ z-index: ${LEVEL_10};
+ content: '';
+ }
+ &:hover,
+ &:active {
+ &:before {
+ background-color: ${(p: { theme: { primary: any; }; }) => p.theme.primary};
+ }
+ }
+ }
+ }
+`;
diff --git a/src/pages/OnlinInquiry/src/styles/media.ts b/src/pages/OnlinInquiry/src/styles/media.ts
new file mode 100644
index 0000000..dfd9e9c
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/media.ts
@@ -0,0 +1,56 @@
+/*
+ * Media queries utility
+ */
+
+import type {
+ CSSObject,
+ DefaultTheme,
+ FlattenInterpolation,
+ Interpolation,
+ InterpolationFunction,
+ ThemedStyledProps
+} from 'styled-components/macro'
+import { css } from 'styled-components'
+
+/*
+ * Taken from https://github.com/DefinitelyTyped/DefinitelyTyped/issues/32914
+ */
+
+// Update your breakpoints if you want
+export const sizes = {
+ small: 600,
+ medium: 1024,
+ large: 1440,
+ xlarge: 1920
+}
+
+// Iterate through the sizes and create a media template
+export const media = (Object.keys(sizes) as (keyof typeof sizes)[]).reduce((acc, label) => {
+ acc[label] = (first: any, ...interpolations: any[]) => css`
+ @media (min-width: ${sizes[label]}px) {
+ ${css(first, ...interpolations)}
+ }
+ `
+
+ return acc
+}, {} as { [key in keyof typeof sizes]: MediaFunction })
+
+/*
+ * @types/styled-component is not working properly as explained in the github issue referenced above.
+ * We must overcome this with custom typings, however, this might not work in time as the styled-components update.
+ * Be carefull and keep an eye on the issue and the possible improvements
+ */
+type MediaFunction = (
+ first: TemplateStringsArray | CSSObject | InterpolationFunction>,
+ ...interpolations: Interpolation>[]
+) => FlattenInterpolation>
+
+/* Example
+const SomeDiv = styled.div`
+ display: flex;
+ ....
+ ${media.medium`
+ display: block
+ `}
+`;
+*/
diff --git a/src/pages/OnlinInquiry/src/styles/theme/ThemeProvider.tsx b/src/pages/OnlinInquiry/src/styles/theme/ThemeProvider.tsx
new file mode 100644
index 0000000..37bd68a
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/ThemeProvider.tsx
@@ -0,0 +1,17 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import React, { useLayoutEffect } from 'react'
+import { useSelector } from 'react-redux'
+import { ThemeProvider as OriginalThemeProvider } from 'styled-components'
+import { useThemeSlice } from './slice'
+// import { selectThemeKey } from './slice/selectors'
+import { themes } from './themes'
+
+
+export const ThemeProvider = (props: { children: React.ReactChild }) => {
+ useThemeSlice()
+
+ const theme = themes.light
+
+
+ return {React.Children.only(props.children)}
+}
diff --git a/src/pages/OnlinInquiry/src/styles/theme/slice/index.ts b/src/pages/OnlinInquiry/src/styles/theme/slice/index.ts
new file mode 100644
index 0000000..5a5b2d2
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/slice/index.ts
@@ -0,0 +1,28 @@
+import type { PayloadAction } from '@reduxjs/toolkit'
+import { useInjectReducer } from '../../../utils/@reduxjs/injectReducer'
+import { createSlice } from '../../../utils/@reduxjs/toolkit'
+import { getThemeFromStorage } from '../utils'
+import type { ThemeKeyType, ThemeState } from './types'
+
+export const initialState: ThemeState = {
+ selected: getThemeFromStorage()
+}
+
+const themeSlice = createSlice({
+ name: 'theme',
+ initialState,
+ reducers: {
+ changeTheme(state, action: PayloadAction) {
+ state.selected = action.payload
+ }
+ }
+})
+
+export default themeSlice
+
+export const { actions: themeActions, reducer } = themeSlice
+
+export const useThemeSlice = () => {
+ useInjectReducer({ key: themeSlice.name, reducer: themeSlice.reducer })
+ return { actions: themeSlice.actions }
+}
diff --git a/src/pages/OnlinInquiry/src/styles/theme/slice/selectors.ts b/src/pages/OnlinInquiry/src/styles/theme/slice/selectors.ts
new file mode 100644
index 0000000..7d2f19a
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/slice/selectors.ts
@@ -0,0 +1,7 @@
+// import { createSelector } from '@reduxjs/toolkit'
+// import { initialState } from '.'
+// import type { RootState } from '../../../types'
+// import { themes } from '../themes'
+
+// export const selectThemeKey = createSelector([(state: RootState) => state.theme], () => {
+// })
diff --git a/src/pages/OnlinInquiry/src/styles/theme/slice/types.ts b/src/pages/OnlinInquiry/src/styles/theme/slice/types.ts
new file mode 100644
index 0000000..655e86a
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/slice/types.ts
@@ -0,0 +1,7 @@
+import type { themes } from '../themes'
+
+export type ThemeKeyType = keyof typeof themes
+
+export interface ThemeState {
+ selected: ThemeKeyType
+}
diff --git a/src/pages/OnlinInquiry/src/styles/theme/styled.d.ts b/src/pages/OnlinInquiry/src/styles/theme/styled.d.ts
new file mode 100644
index 0000000..0b16cde
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/styled.d.ts
@@ -0,0 +1,7 @@
+import 'styled-components'
+import type { Theme } from './themes'
+
+/* This is the suggested way of declaring theme types */
+declare module 'styled-components' {
+ export interface DefaultTheme extends Theme {}
+}
diff --git a/src/pages/OnlinInquiry/src/styles/theme/themes.ts b/src/pages/OnlinInquiry/src/styles/theme/themes.ts
new file mode 100644
index 0000000..ae1c870
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/themes.ts
@@ -0,0 +1,103 @@
+import { lighten, rgba } from 'polished'
+import {
+ BLACK,
+ BLUE,
+ // DG10,
+ // DG20,
+ // DG30,
+ // DG40,
+ // DG50,
+ // DG60,
+ // DG70,
+ // DG80,
+ // DG90,
+ ERROR,
+ G10,
+ G20,
+ G30,
+ G40,
+ G50,
+ G60,
+ G70,
+ G80,
+ G90,
+ GREEN,
+ HIGHLIGHT,
+ INFO,
+ NORMAL,
+ ORANGE,
+ PRIMARY,
+ PROCESSING,
+ RED,
+ SUCCESS,
+ WARNING,
+ WHITE,
+ YELLOW
+} from '../StyleConstants'
+
+const common = {
+ primary: PRIMARY,
+ info: INFO,
+ success: SUCCESS,
+ processing: PROCESSING,
+ error: ERROR,
+ highlight: HIGHLIGHT,
+ warning: WARNING,
+ normal: NORMAL,
+
+ blue: BLUE,
+ green: GREEN,
+ orange: ORANGE,
+ yellow: YELLOW,
+ red: RED,
+ white: WHITE,
+ black: BLACK
+}
+
+const lightTheme = {
+ bodyBackground: G10,
+ componentBackground: WHITE,
+ emphasisBackground: G20,
+ highlightBackground: G30,
+ textColor: G90,
+ textColorSnd: G80,
+ textColorLight: G60,
+ textColorDisabled: G50,
+ iconColorHover: rgba(BLACK, 0.75),
+ borderColorBase: G40,
+ borderColorEmphasis: G30,
+ borderColorSplit: G20,
+ shadow1: `0 1px 3px 0 ${rgba(lighten(0.15, BLACK), 0.1)}`,
+ shadow2: `0 4px 16px 0 ${rgba(lighten(0.15, BLACK), 0.12)}`,
+ shadow3: `0 12px 32px 0 ${rgba(lighten(0.15, BLACK), 0.16)}`,
+ shadowSider: `0px 0 32px 0px ${rgba(G70, 0.075)}`,
+ shadowBlock: `0px 0 32px 0px ${rgba(G70, 0.025)}`,
+ ...common
+}
+
+// const darkTheme: Theme = {
+// bodyBackground: BLACK,
+// componentBackground: DG10,
+// emphasisBackground: DG20,
+// highlightBackground: DG30,
+// textColor: DG90,
+// textColorSnd: DG80,
+// textColorLight: DG60,
+// textColorDisabled: DG50,
+// iconColorHover: DG70,
+// borderColorBase: DG40,
+// borderColorEmphasis: DG30,
+// borderColorSplit: DG20,
+// shadow1: `0 1px 5px 0 ${rgba(BLACK, 0.1)}`,
+// shadow2: `0 6px 18px 0 ${rgba(BLACK, 0.4)}`,
+// shadow3: `0 10px 32px 0 ${rgba(BLACK, 0.54)}`,
+// shadowSider: 'none',
+// shadowBlock: 'none',
+// ...common,
+// };
+
+export type Theme = typeof lightTheme
+
+export const themes = {
+ light: lightTheme
+}
diff --git a/src/pages/OnlinInquiry/src/styles/theme/utils.ts b/src/pages/OnlinInquiry/src/styles/theme/utils.ts
new file mode 100644
index 0000000..6a264fe
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/styles/theme/utils.ts
@@ -0,0 +1,59 @@
+import darkTheme from 'antd/dist/dark-theme'
+import lightTheme from 'antd/dist/default-theme'
+// import { StorageKeys } from '../../globalConstants'
+import type { ThemeKeyType } from './slice/types'
+import { themes } from './themes'
+
+/* istanbul ignore next line */
+export const isSystemDark = window?.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)')?.matches : undefined
+
+// export function saveTheme(theme: ThemeKeyType) {
+// window.localStorage && localStorage.setItem(StorageKeys.Theme, theme)
+// }
+
+/* istanbul ignore next line */
+export function getThemeFromStorage(): ThemeKeyType {
+ const theme = 'light' as ThemeKeyType
+ // try {
+ // const storedTheme = window.localStorage && localStorage.getItem(StorageKeys.Theme)
+ // if (storedTheme) {
+ // theme = storedTheme as ThemeKeyType
+ // }
+ // } catch (error) {
+ // throw error
+ // }
+ return theme
+}
+
+export function getTokenVariableMapping(themeKey: string) {
+ const currentTheme = themes[themeKey]
+ return {
+ '@primary-color': currentTheme.primary,
+ '@success-color': currentTheme.success,
+ '@processing-color': currentTheme.processing,
+ '@error-color': currentTheme.error,
+ '@highlight-color': currentTheme.highlight,
+ '@warning-color': currentTheme.warning,
+ '@body-background': currentTheme.bodyBackground,
+ '@text-color': currentTheme.textColor,
+ '@text-color-secondary': currentTheme.textColorLight,
+ '@heading-color': currentTheme.textColor,
+ '@disabled-color': currentTheme.textColorDisabled
+ }
+}
+
+export function getVarsToBeModified(themeKey: string) {
+ const tokenVariableMapping = getTokenVariableMapping(themeKey)
+ return {
+ ...(themeKey === 'light' ? lightTheme : darkTheme),
+ ...tokenVariableMapping
+ }
+}
+
+// export async function changeAntdTheme(themeKey: string) {
+// try {
+// await (window as any).less.modifyVars(getVarsToBeModified(themeKey))
+// } catch (error) {
+// console.log(error)
+// }
+// }
diff --git a/src/pages/OnlinInquiry/src/types.ts b/src/pages/OnlinInquiry/src/types.ts
new file mode 100644
index 0000000..41d6ade
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/types.ts
@@ -0,0 +1,81 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// import type { WorkbenchState } from '@/OnlinInquiry/src/app/pages/ChartWorkbenchPage/slice/types'
+// import { BoardState } from 'app/pages/DashBoardPage/pages/Board/slice/types';
+// import { EditBoardState } from 'app/pages/DashBoardPage/pages/BoardEditor/slice/types';
+// import { MemberState } from 'app/pages/MainPage/pages/MemberPage/slice/types';
+// import { PermissionState } from 'app/pages/MainPage/pages/PermissionPage/slice/types';
+// // import { ScheduleState } from 'app/pages/MainPage/pages/SchedulePage/slice/types';
+// import type { SourceState } from '@/OnlinInquiry/src/app/pages/MainPage/pages/SourcePage/slice/types'
+// import { VariableState } from 'app/pages/MainPage/pages/VariablePage/slice/types';
+import type { ViewState } from '@/pages/OnlinInquiry/src/app/pages/MainPage/pages/ViewPage/slice/types'
+// import { VizState } from 'app/pages/MainPage/pages/VizPage/slice/types';
+import type { MainState } from '@/pages/OnlinInquiry/src/app/pages/MainPage/pages/slice/types';
+// import { SharePageState } from 'app/pages/SharePage/slice/types';
+// import { StoryBoardState } from 'app/pages/StoryBoardPage/slice/types';
+// import { AppState } from 'app/slice/types';
+import type { CSSProp } from 'styled-components/index'
+import type { ThemeState } from './styles/theme/slice/types'
+
+/**
+ * 公共参数类型
+ */
+declare module 'react' {
+ interface DOMAttributes {
+ css?: CSSProp
+ }
+}
+
+export interface RootState {
+ theme?: ThemeState
+ // app?: AppState
+ main?: MainState
+ // member?: MemberState
+ // permission?: PermissionState
+ // variable?: VariableState
+ // source?: SourceState
+ // schedule?: ScheduleState;
+ view?: ViewState
+ // viz?: VizState
+ // board?: BoardState
+ // storyBoard?: StoryBoardState
+ // editBoard?: EditBoardState
+ // workbench?: WorkbenchState
+ // share?: SharePageState
+}
+// 响应
+export interface APIResponse {
+ success: boolean
+ errCode: number
+ message: string
+ exception: string
+ data: T
+ warnings: string[]
+}
+
+// dinero.js
+export declare type Currency = {
+ /**
+ * The unique code of the currency.
+ */
+ readonly code: string
+ /**
+ * The base, or radix of the currency.
+ */
+ readonly base: TAmount
+ /**
+ * The exponent of the currency.
+ */
+ readonly exponent: TAmount
+}
+
+export type ValueOf = T[keyof T]
+
+export type Nullable = T | null | undefined
+
+export interface IFontDefault {
+ fontFamily: string
+ fontSize: number | string
+ fontWeight: number | string
+ fontStyle: string
+ color: string
+}
diff --git a/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/checkStore.ts b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/checkStore.ts
new file mode 100644
index 0000000..2fd48d4
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/checkStore.ts
@@ -0,0 +1,22 @@
+import invariant from 'invariant';
+import conformsTo from 'lodash/conformsTo';
+import isFunction from 'lodash/isFunction';
+import isObject from 'lodash/isObject';
+
+/**
+ * Validates the redux store is set up properly to work with this library.
+ */
+export default function checkStore(store: any) {
+ const shape = {
+ dispatch: isFunction,
+ subscribe: isFunction,
+ getState: isFunction,
+ replaceReducer: isFunction,
+ createReducer: isFunction,
+ injectedReducers: isObject,
+ };
+ invariant(
+ conformsTo(store, shape),
+ '(redux-injectors...) checkStore: Expected a redux store that has been configured for use with redux-injectors.',
+ );
+}
diff --git a/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/enhancer.ts b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/enhancer.ts
new file mode 100644
index 0000000..a5e0618
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/enhancer.ts
@@ -0,0 +1,16 @@
+import type { StoreEnhancer } from '@reduxjs/toolkit'
+
+const injectReducerEnhancer = (createReducer: any): StoreEnhancer => {
+ return createStore =>
+ (...args) => {
+ const store = createStore(...args)
+
+ return {
+ ...store,
+ createReducer,
+ injectedReducers: {}
+ }
+ }
+}
+
+export default injectReducerEnhancer
diff --git a/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/index.tsx b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/index.tsx
new file mode 100644
index 0000000..64f2083
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/index.tsx
@@ -0,0 +1,86 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { Reducer } from '@reduxjs/toolkit'
+import hoistNonReactStatics from 'hoist-non-react-statics'
+import React from 'react'
+import { ReactReduxContext, useStore } from 'react-redux'
+import type { RootState } from '../../../types'
+import getInjectors from './reducerInjectors'
+
+interface InjectReducerParams {
+ key: keyof RootState
+ reducer: Reducer
+}
+
+/**
+ * A higher-order component that dynamically injects a reducer when the
+ * component is instantiated
+ *
+ * @param {Object} params
+ * @param {string} params.key The key to inject the reducer under
+ * @param {function} params.reducer The reducer that will be injected
+ *
+ * @example
+ *
+ * class BooksManager extends React.PureComponent {
+ * render() {
+ * return null;
+ * }
+ * }
+ *
+ * export default injectReducer({ key: "books", reducer: booksReducer })(BooksManager)
+ *
+ * @public
+ */
+// const injectReducer =
+// ({ key, reducer }) =>
+// ( WrappedComponent: JSX.IntrinsicAttributes) => {
+// class ReducerInjector extends React.Component {
+// static WrappedComponent = WrappedComponent
+
+// static contextType = ReactReduxContext
+
+// static displayName = `withReducer(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`
+
+// constructor(props: {}, context: { store: any }) {
+// super(props, context)
+
+// getInjectors(context.store).injectReducer(key, reducer)
+// }
+
+// render() {
+// return
+// }
+// }
+
+// return hoistNonReactStatics(ReducerInjector, WrappedComponent)
+// }
+
+// export default injectReducer
+
+/**
+ * A react hook that dynamically injects a reducer when the hook is run
+ *
+ * @param {Object} params
+ * @param {string} params.key The key to inject the reducer under
+ * @param {function} params.reducer The reducer that will be injected
+ *
+ * @example
+ *
+ * function BooksManager() {
+ * useInjectReducer({ key: "books", reducer: booksReducer })
+ *
+ * return null;
+ * }
+ *
+ * @public
+ */
+export const useInjectReducer = ({ key, reducer }: InjectReducerParams) => {
+ const store = useStore()
+
+ const isInjected = React.useRef(false)
+
+ if (!isInjected.current) {
+ getInjectors(store).injectReducer(key, reducer)
+ isInjected.current = true
+ }
+}
diff --git a/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/reducerInjectors.ts b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/reducerInjectors.ts
new file mode 100644
index 0000000..4a8fed6
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/@reduxjs/injectReducer/reducerInjectors.ts
@@ -0,0 +1,34 @@
+import invariant from 'invariant'
+import isEmpty from 'lodash/isEmpty'
+import isFunction from 'lodash/isFunction'
+import isString from 'lodash/isString'
+import type { AnyAction, Store } from 'redux'
+import checkStore from './checkStore'
+
+export function injectReducerFactory(
+ store: { injectedReducers: object; replaceReducer: (arg0: any) => void; createReducer: (arg0: any) => any },
+ isValid?: boolean
+) {
+ return function injectReducer(key: PropertyKey, reducer: any) {
+ if (!isValid) checkStore(store)
+
+ invariant(
+ isString(key) && !isEmpty(key) && isFunction(reducer),
+ '(redux-injectors...) injectReducer: Expected `reducer` to be a reducer function'
+ )
+
+ // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
+ if (Reflect.has(store.injectedReducers, key) && store.injectedReducers[key] === reducer) return
+
+ store.injectedReducers[key] = reducer // eslint-disable-line no-param-reassign
+ store.replaceReducer(store.createReducer(store.injectedReducers))
+ }
+}
+
+export default function getInjectors(store: Store) {
+ checkStore(store)
+
+ return {
+ injectReducer: injectReducerFactory(store, true)
+ }
+}
diff --git a/src/pages/OnlinInquiry/src/utils/@reduxjs/rejectedErrorHandlerMiddleware.ts b/src/pages/OnlinInquiry/src/utils/@reduxjs/rejectedErrorHandlerMiddleware.ts
new file mode 100644
index 0000000..4924f1f
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/@reduxjs/rejectedErrorHandlerMiddleware.ts
@@ -0,0 +1,39 @@
+/**
+ * Datart
+ *
+ * Copyright 2021
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createListenerMiddleware, isRejected } from '@reduxjs/toolkit';
+import message from 'antd/lib/message';
+
+const rejectedErrorHandlerMiddleware = createListenerMiddleware();
+
+rejectedErrorHandlerMiddleware.startListening({
+ predicate: isRejected,
+ effect: async (action: any, listenerApi) => {
+ listenerApi.cancelActiveListeners();
+ await listenerApi.delay(100);
+
+ if (action?.payload?.message) {
+ message.error(action?.payload?.message);
+ } else if (action?.error) {
+ message.error((action as any)?.error?.message);
+ }
+ console.error(`Redux Rejection Error | `, action);
+ },
+});
+
+export default rejectedErrorHandlerMiddleware;
diff --git a/src/pages/OnlinInquiry/src/utils/@reduxjs/toolkit.tsx b/src/pages/OnlinInquiry/src/utils/@reduxjs/toolkit.tsx
new file mode 100644
index 0000000..74d13b5
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/@reduxjs/toolkit.tsx
@@ -0,0 +1,18 @@
+import type { CreateSliceOptions, SliceCaseReducers } from '@reduxjs/toolkit'
+import { createSlice as createSliceOriginal, isRejected } from '@reduxjs/toolkit'
+import type { RootStateKeyType } from '../types/injector-typings'
+
+/* Wrap createSlice with stricter Name options */
+
+/* istanbul ignore next */
+export const createSlice = , Name extends RootStateKeyType>(
+ options: CreateSliceOptions
+) => createSliceOriginal(options)
+
+export function isMySliceAction(action: { type: string }, targetSliceName: string) {
+ return action?.type?.startsWith(targetSliceName)
+}
+
+export function isRejectedScopedSlice(sliceNames: string[]) {
+ return (action: any) => isRejected(action) && sliceNames.some(sliceName => isMySliceAction(action, sliceName))
+}
diff --git a/src/pages/OnlinInquiry/src/utils/object.ts b/src/pages/OnlinInquiry/src/utils/object.ts
new file mode 100644
index 0000000..9a03bd8
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/object.ts
@@ -0,0 +1,254 @@
+// import { ChartStyleConfig } from 'app/types/ChartConfig';
+import type { List } from 'lodash'
+import camelCase from 'lodash/camelCase'
+import cloneDeep from 'lodash/cloneDeep'
+import isEqual from 'lodash/isEqual'
+import isFunction from 'lodash/isFunction'
+import isObject from 'lodash/isObject'
+import lowerCase from 'lodash/lowerCase'
+import mean from 'lodash/mean'
+import omit from 'lodash/omit'
+import pick from 'lodash/pick'
+import uniq from 'lodash/uniq'
+import uniqWith from 'lodash/uniqWith'
+
+type PipeFunction = (accumulator?: TA, options?: TO) => TA | undefined
+
+export function pipe(...fns: PipeFunction[]) {
+ return (v: any, o?: T2 | undefined) => (fns || []).reduce((y, f) => f(y, o), v)
+}
+
+export function curry(fn: { length: number; apply: (arg0: any, arg1: any[]) => any }) {
+ let _args: any[] = []
+ const collector: any = (...args: any[]) => {
+ _args = _args.concat(args || [])
+ if (_args.length < fn.length) {
+ return collector.bind(Object.create(null))
+ }
+ return fn.apply(Object.create(null), _args)
+ }
+ return collector
+}
+
+// export function cond(...predicates: any[][]) {
+// return (value: any, defaultValue?: any) => {
+// for (let i = 0; i < predicates.length; i++) {
+// if (typeof predicates[i] === 'function' && predicates[i](value)) {
+// return predicates[i](value)
+// }
+// if (
+// isPairArray(predicates[i]) &&
+// typeof predicates[i]?.[0] === 'function' &&
+// predicates[i][0].call(Object.create(null), value)
+// ) {
+// if (typeof predicates[i]?.[1] === 'function') {
+// return predicates[i][1].call(Object.create(null), value)
+// }
+// return predicates[i][1]
+// }
+// }
+// return defaultValue
+// }
+// }
+
+export function isPairArray(arr?: any[]) {
+ return Array.isArray(arr) && arr?.length === 2
+}
+
+export function isNumerical(n?: number | string) {
+ return !isEmpty(n) && !isNaN(+n!)
+}
+
+export function isNumericEqual(a?: number | string, b?: number | string) {
+ // eslint-disable-next-line eqeqeq
+ return a == b
+}
+
+export function isInPairArrayRange(count: number | string, pairArray: number[]) {
+ if (!isPairArray(pairArray)) {
+ return false
+ }
+ return pairArray[0] <= +count && +count <= pairArray[1]
+}
+
+export function PatchUpdate(target: T1, collectionKey: string, unitMatcher: (c: T2) => boolean, item: T2): T1 {
+ if (!target || !target[collectionKey] || !Array.isArray(target[collectionKey])) {
+ return target
+ }
+
+ PatchUpdateCollection(target[collectionKey], unitMatcher, item)
+ return target
+}
+
+export function PatchUpdateCollection(
+ target: T2[] = [],
+ unitMatcher: (c: T2) => boolean,
+ item: T2,
+ updateByKey?: string
+): T2[] {
+ const collection = target.find(unitMatcher)
+ if (!collection) return target
+
+ if (updateByKey) {
+ collection[updateByKey] = item[updateByKey]
+ } else {
+ Object.assign(collection, item)
+ }
+
+ return target
+}
+
+export function shadowCopyCollection(collection: any) {
+ return [...(collection || [])]
+}
+
+export function ArrayToObject(arr: any[]) {
+ if (!arr) return {}
+
+ return arr?.reduce((acc: any, cur: any) => {
+ return Object.assign(cur, acc)
+ }, {})
+}
+
+export function ObjectToArray(o: ArrayLike | Record) {
+ if (!o) return []
+
+ return Object.values(o)
+}
+
+export function UniqArray(arr: T[]) {
+ return uniq(arr)
+}
+
+export function UniqWith(arr: T[], compareFn: (a: T, b: T) => boolean) {
+ return uniqWith(arr, compareFn)
+}
+
+export function Omit(object: any, keys: string[]) {
+ return omit(object, keys)
+}
+
+export function ToCamelCase(str: string | undefined) {
+ return camelCase(str)
+}
+
+export function ToLowerCase(str: string | undefined) {
+ return lowerCase(str)
+}
+
+export function AssignDeep(target: T, ...source: any[]) {
+ return Object.assign({}, target, ...source)
+}
+
+export function mergeOptions(
+ oldOptions?: ChartStyleRowOption,
+ newOptions?: ChartStyleRowOption
+): ChartStyleRowOption {
+ return Object.assign(oldOptions || {}, newOptions)
+}
+
+export function CloneValueDeep(value: T): T {
+ return cloneDeep(value)
+}
+
+export function isUndefined(o: undefined): boolean {
+ return o === undefined
+}
+
+export function isEmpty(o?: null | any): boolean {
+ return o === null || isUndefined(o)
+}
+
+export function isEmptyString(o?: null | any): boolean {
+ return isEmpty(o) || o === ''
+}
+
+export function isFunc(f: any) {
+ return isFunction(f)
+}
+
+export function meanValue(value: List | null | undefined) {
+ return mean(value)
+}
+
+export function isConfigRow(value: any) {
+ return isObject(value) && 'comType' in value
+}
+
+export function pickValues(o: any, values: string[]) {
+ return pick(o, values)
+}
+
+export function StringInsert(source: string, value: string, index: number) {
+ return source.slice(0, index) + value + source.slice(index)
+}
+
+export function IsKeyIn(o: T, key: K): boolean {
+ return !!o && typeof o === 'object' && key in o
+}
+
+// export function mergeDefaultToValue(
+// configs?: ChartStyleConfig[],
+// ): ChartStyleConfig[] {
+// return (configs || []).map(c => {
+// if (c.comType !== 'group') {
+// if (isEmpty(c.value) && !isEmpty(c.default)) {
+// c.value = c.default;
+// }
+// // update(c, 'value', isEmpty(c.value) ? c.default : c.value);
+// }
+// // const newRows = initChartConfigValueByDefaultValue(c.rows);
+// // update(c, 'rows', newRows);
+// if (!!c?.rows?.length) {
+// c.rows = mergeDefaultToValue(c.rows);
+// }
+// return c;
+// });
+// }
+
+// export function cleanChartConfigValueByDefaultValue(
+// configs?: ChartStyleConfig[],
+// ): ChartStyleConfig[] {
+// return (configs || []).filter(Boolean).map(c => {
+// if (c.comType !== 'group') {
+// c.value = c.default;
+// }
+// c.rows = cleanChartConfigValueByDefaultValue(c.rows);
+// return c;
+// });
+// }
+
+// export function resetValue(config: ChartStyleConfig): ChartStyleConfig {
+// config.value = config.default;
+// config.rows = config?.rows?.map(r => {
+// return resetValue(r);
+// });
+// return config;
+// }
+
+export function isTreeModel(data: any[]) {
+ if (!Array.isArray(data)) {
+ return false
+ }
+ return data.some(d => d?.children?.length > 0)
+}
+
+export function isEmptyArray(value?: string | any[]) {
+ if (isEmpty(value)) {
+ return true
+ }
+
+ return Array.isArray(value) && !value?.length
+}
+
+export function isPromise(obj?: Promise) {
+ if (isEmpty(obj)) {
+ return false
+ }
+
+ return (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
+}
+
+export function isEqualObject(obj: string[] | undefined, cObj: string[] | undefined) {
+ return isEqual(obj, cObj)
+}
diff --git a/src/pages/OnlinInquiry/src/utils/types/injector-typings.ts b/src/pages/OnlinInquiry/src/utils/types/injector-typings.ts
new file mode 100644
index 0000000..6c788ee
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/types/injector-typings.ts
@@ -0,0 +1,14 @@
+import type { AnyAction, Reducer } from '@reduxjs/toolkit'
+import type { RootState } from '../../types'
+
+type RequiredRootState = Required
+
+export type RootStateKeyType = keyof RootState
+
+export type InjectedReducersType = {
+ [P in RootStateKeyType]?: Reducer
+}
+export interface InjectReducerParams {
+ key: Key
+ reducer: Reducer
+}
diff --git a/src/pages/OnlinInquiry/src/utils/utils.ts b/src/pages/OnlinInquiry/src/utils/utils.ts
new file mode 100644
index 0000000..fd28e4a
--- /dev/null
+++ b/src/pages/OnlinInquiry/src/utils/utils.ts
@@ -0,0 +1,430 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import type { TreeDataNode, TreeNodeProps } from 'antd'
+import { message } from 'antd'
+// import { message } from 'antd'
+// import { ColumnRole } from 'app/pages/MainPage/pages/ViewPage/slice/types';
+import type { AxiosError } from 'axios'
+import classnames from 'classnames'
+import type { ReactElement } from 'react'
+import { FONT_FAMILY, FONT_SIZE_BODY, FONT_WEIGHT_REGULAR } from '../styles/StyleConstants'
+// import { FONT_FAMILY, FONT_SIZE_BODY, FONT_WEIGHT_REGULAR } from 'styles/StyleConstants'
+// import type { APIResponse } from '../types'
+// import { SaveFormModel } from '../app/pages/MainPage/pages/VizPage/SaveFormContext';
+// import { removeToken } from './auth'
+
+export { v4 as uuidv4 } from 'uuid'
+
+// export function errorHandle(error: { response: any; message: any }) {
+// if (error?.response) {
+// // AxiosError
+// const { response } = error as AxiosError
+// switch (response?.status) {
+// case 401:
+// message.error({ key: '401', content: i18next.t('global.401') })
+// removeToken()
+// break
+// default:
+// message.error(response?.data.message || error.message)
+// break
+// }
+// } else if (error?.message) {
+// // Error
+// message.error(error.message)
+// } else {
+// message.error(error)
+// }
+// return error
+// }
+
+export function getErrorMessage(error: any) {
+ if (error?.response) {
+ const { response } = error as AxiosError
+ switch (response?.status) {
+ // case 401:
+ // removeToken()
+ // return i18next.t('global.401')
+ default:
+ return error.message
+ }
+ }
+ return error?.message
+}
+
+// export function reduxActionErrorHandler(errorAction) {
+// if (errorAction?.payload) {
+// message.error(errorAction?.payload)
+// } else if (errorAction?.error) {
+// message.error(errorAction?.error.message)
+// }
+// }
+
+// export function rejectHandle(error, rejectWithValue) {
+// if (error?.response?.status === 401) {
+// removeToken()
+// }
+// if ((error as AxiosError).response) {
+// return rejectWithValue(((error as AxiosError).response as AxiosResponse>).data.message)
+// } else {
+// return rejectWithValue(error.message)
+// }
+// }
+
+export const mergeClassNames = (origin: any, added: string) => classnames({ [origin]: !!origin, [added]: true })
+
+// export function stopPPG(e) {
+// e.stopPropagation()
+// }
+
+export function listToTree<
+ T extends {
+ id: string
+ name: string
+ parentId: string | null
+ isFolder: boolean
+ index: number | null
+ }
+>(
+ list: undefined | T[],
+ parentId: null | string = null,
+ parentPath: string[] = [],
+ options?: {
+ getIcon?: (o: T) => ReactElement | ((props: TreeNodeProps) => ReactElement)
+ getDisabled?: (o: T, path: string[]) => boolean
+ getSelectable?: (o: T) => boolean
+ filter?: (path: string[], o: T) => boolean
+ }
+): undefined | any[] {
+ if (!list) {
+ return list
+ }
+
+ const treeNodes: any[] = []
+ const childrenList: T[] = []
+
+ list.forEach(o => {
+ const path = parentPath.concat(o.id)
+ if (options?.filter && !options.filter(path, o)) {
+ return false
+ }
+ if (o.parentId === parentId) {
+ treeNodes.push({
+ ...o,
+ key: o.id,
+ title: o.name,
+ value: o.id,
+ path,
+ ...(options?.getIcon && { icon: options.getIcon(o) }),
+ ...(options?.getDisabled && { disabled: options.getDisabled(o, path) }),
+ ...(options?.getSelectable && { selectable: options.getSelectable(o) })
+ })
+ } else {
+ childrenList.push(o)
+ }
+ })
+
+ treeNodes.sort((a, b) => Number(a.index) - Number(b.index))
+
+ return treeNodes.map(node => {
+ const children = listToTree(childrenList, node.key, node.path, options)
+ return children?.length ? { ...node, children } : { ...node, isLeaf: true }
+ })
+}
+
+export function findTreeNode<
+ T extends {
+ key: string | number
+ children?: T[]
+ }
+>(path: string[], nodes: T[] | undefined): T | undefined {
+ if (path.length > 0) {
+ const currentNode = nodes?.find(({ key }) => key === path[0])
+ return path.length > 1 ? findTreeNode(path.slice(1), currentNode?.children) : currentNode
+ }
+}
+
+// export const loopTree = (data, key: string, keyname: string, callback) => {
+// for (let i = 0; i < data.length; i++) {
+// if (data[i].key === key) {
+// return callback(data[i], i, data)
+// }
+// if (data[i].children) {
+// loopTree(data[i].children, key, keyname, callback)
+// }
+// }
+// }
+
+// export const onDropTreeFn = ({ info, treeData, callback }) => {
+// const dropKey = info.node.key //落下的key
+// const dragKey = info.dragNode.key //拖动的key
+// const dropPos = info.node.pos.split('-')
+// const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1])
+// const data = treeData || []
+// let dragObj,
+// dropArr,
+// dropIndex,
+// index = 0
+
+// loopTree(data, dragKey, 'key', item => {
+// dragObj = item
+// })
+
+// loopTree(data, dropKey, 'key', (item, idx, arr) => {
+// dropArr = arr
+// dropIndex = idx
+// })
+// if (!info.dropToGap && !dropArr[dropIndex].isFolder) {
+// //判断不能移动到非目录下面
+// return false
+// }
+
+// if (
+// dropArr[dropIndex].parentId === dragObj.id ||
+// (dropArr[dropIndex].isFolder && dropArr[dropIndex].id === dragObj.id)
+// ) {
+// return false
+// }
+
+// if (!info.dropToGap) {
+// //如果移动到二级目录里面的第一个,获取到该目录children中[0]元素的index-1
+// index = dropArr[dropIndex].children ? dropArr[dropIndex].children[0]?.index - 1 : 0
+// } else if (dropPosition === -1) {
+// // 移动到第一个
+// index = dropArr[dropIndex] ? dropArr[dropIndex].index - 1 : 0
+// } else if (dropIndex === dropArr.length - 1) {
+// // 移动到最后一个
+// index = dropArr[dropArr.length - 1].index + 1
+// } else {
+// //中间
+// if (!dropArr[dropIndex].index && !dropArr[dropIndex + 1].index) {
+// index = dropArr[dropArr.length - 1].index + 1
+// } else {
+// index = (dropArr[dropIndex].index + dropArr[dropIndex + 1].index) / 2
+// }
+// }
+// let { id } = dragObj,
+// parentId = !info.dropToGap ? dropArr[dropIndex].id : dropArr[dropIndex].parentId || null
+// //如果移动到二级目录里面的第一个,就用当前目录的id,如果不是就用文件的parentId
+// callback(id, parentId, index)
+// }
+
+export const getInsertedNodeIndex = (
+ // AddData: Omit & { config?: object | string },
+ viewData: any
+) => {
+ const index: number = 0
+ /* eslint-disable */
+ // if (viewData?.length) {
+ // let IndexArr = viewData.filter((v: any) => v.parentId == AddData.parentId).map(val => Number(val.index) || 0)
+ // index = IndexArr?.length ? Math.max(...IndexArr) + 1 : 0
+ // }
+ /* eslint-disable */
+ return index
+}
+
+// export function getPath(
+// list: T[],
+// item: T,
+// rootId: string,
+// path: string[] = []
+// ) {
+// if (!item?.parentId) {
+// if (item) {
+// return [rootId].concat(item.id).concat(path)
+// }
+// return [rootId].concat(path)
+// } else {
+// const parent = list.find(({ id }) => id === item.parentId)!
+// return getPath(list, parent, rootId, [item.id].concat(path))
+// }
+// }
+
+export function filterListOrTree(
+ dataSource: T[],
+ keywords: string,
+ filterFunc: (keywords: string, data: T) => boolean,
+ filterLeaf?: boolean // 是否展示所有叶子节点
+) {
+ return keywords
+ ? dataSource.reduce((filtered, d) => {
+ const isMatch = filterFunc(keywords, d)
+ let isChildrenMatch: T[] | undefined
+ if (filterLeaf && d.children?.every(c => (c as any).isLeaf)) {
+ isChildrenMatch = isMatch || d.children.some(c => filterFunc(keywords, c)) ? d.children : void 0
+ } else {
+ isChildrenMatch = d.children && filterListOrTree(d.children, keywords, filterFunc, filterLeaf)
+ }
+ if (isMatch || (isChildrenMatch && isChildrenMatch.length > 0)) {
+ filtered.push({ ...d, children: isChildrenMatch })
+ }
+ return filtered
+ }, [])
+ : dataSource
+}
+
+export function getExpandedKeys(nodes: T[]): any {
+ return nodes.reduce((keys, { key, children }) => {
+ if (Array.isArray(children) && children.length) {
+ return keys.concat(key as string).concat(children ? getExpandedKeys(children) : [])
+ }
+ return keys
+ }, [])
+}
+
+let utilCanvas: null | HTMLCanvasElement = null
+
+export const getTextWidth = (
+ text: string,
+ fontWeight: string = `${FONT_WEIGHT_REGULAR}`,
+ fontSize: string = FONT_SIZE_BODY,
+ fontFamily: string = FONT_FAMILY
+): number => {
+ const canvas = utilCanvas || (utilCanvas = document.createElement('canvas'))
+ const context = canvas.getContext('2d')
+ if (context) {
+ context.font = `${fontWeight} ${fontSize} ${fontFamily}`
+ const metrics = context.measureText(text)
+ return Math.ceil(metrics.width)
+ }
+ return 0
+}
+
+export function getDiffParams(
+ origin: T[],
+ changed: T[],
+ matchFunc: (originElement: T, changedElement: T) => boolean,
+ compareFunc: (originElement: T, changedElement: T) => boolean,
+ continueFunc?: (originElement: T) => boolean
+) {
+ let reserved: T[] = []
+ let created: T[] = []
+ let updated: T[] = []
+ let deleted: T[] = []
+
+ for (let i = 0; i < origin.length; i += 1) {
+ /**
+ * 由于 fastDeleteArrayElement 会改变数组元素位置,因此代码中使用 origin[i]、
+ * changed[j] 即时获取对应下标元素,而非使用变量暂存
+ */
+ if (continueFunc && continueFunc(origin[i])) {
+ reserved.push(origin[i])
+ fastDeleteArrayElement(origin, i)
+ i -= 1
+ continue
+ }
+
+ for (let j = 0; j < changed.length; j += 1) {
+ if (matchFunc(origin[i], changed[j])) {
+ const updatedElement = { ...changed[j], id: origin[i].id }
+ if (compareFunc(origin[i], changed[j])) {
+ updated.push(updatedElement)
+ }
+ reserved.push(updatedElement)
+ fastDeleteArrayElement(origin, i)
+ fastDeleteArrayElement(changed, j)
+ i -= 1
+ break
+ }
+ }
+ }
+
+ created = [...changed]
+ deleted = [...origin]
+
+ return {
+ created,
+ deleted,
+ updated,
+ reserved
+ }
+}
+
+export function fastDeleteArrayElement(arr: any[], index: number) {
+ arr[index] = arr[arr.length - 1]
+ arr.pop()
+}
+
+// export function newIssueUrl({ type, ...options }) {
+// const repoUrl = `https://${type}.com/running-elephant/datart`
+// let issuesUrl = ''
+
+// if (repoUrl) {
+// issuesUrl = repoUrl
+// } else {
+// throw new Error('You need to specify either the `repoUrl` option or both the `user` and `repo` options')
+// }
+
+// const url = new URL(`${issuesUrl}/issues/new`)
+
+// const types =
+// type === 'gitee'
+// ? ['description', 'title']
+// : ['body', 'title', 'labels', 'template', 'milestone', 'assignee', 'projects']
+
+// for (const type of types) {
+// let value = options[type]
+
+// if (value === undefined) {
+// continue
+// }
+
+// if (type === 'labels' || type === 'projects') {
+// if (!Array.isArray(value)) {
+// throw new TypeError(`The \`${type}\` option should be an array`)
+// }
+
+// value = value.join(',')
+// }
+
+// url.searchParams.set(type, value)
+// }
+
+// return url.toString()
+// }
+
+// export function modelListFormsTreeByTableName(model, type) {
+// const tableNameList: string[] = []
+// const columnNameObj: { [key: string]: any } = {}
+// const columnTreeData: any = []
+
+// model?.forEach(v => {
+// const path = v.path
+// const tableName = path.slice(0, path.length - 1).join('.')
+// if (!tableNameList.includes(tableName)) {
+// tableNameList.push(tableName)
+// }
+// })
+
+// model?.forEach(v => {
+// const path = v.path
+// const tableName = path.slice(0, path.length - 1).join('.')
+// const fieldName = path[path.length - 1]
+// if (tableNameList.includes(tableName)) {
+// const columnNameArr = columnNameObj[tableName] || []
+// columnNameObj[tableName] = columnNameArr.concat([{ ...v, displayName: fieldName }])
+// }
+// })
+
+// tableNameList.sort((a, b) => a.localeCompare(b))
+
+// tableNameList.forEach((v, i) => {
+// const treeData = {
+// name: v,
+// category: 'hierarchy',
+// role: ColumnRole.Table,
+// subType: undefined,
+// type: 'STRING',
+// children: columnNameObj[v]
+// } as any
+
+// if (type === 'analysisPage') {
+// treeData.id = v
+// }
+// if (type === 'viewPage') {
+// treeData.index = i
+// }
+
+// columnTreeData.push(treeData)
+// })
+
+// return columnTreeData
+// }
diff --git a/src/pages/User/login/index.less b/src/pages/User/login/index.less
new file mode 100644
index 0000000..48839ba
--- /dev/null
+++ b/src/pages/User/login/index.less
@@ -0,0 +1,50 @@
+@import '~antd/es/style/themes/index';
+
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ overflow: auto;
+ background: @layout-body-background;
+}
+
+.lang {
+ width: 100%;
+ height: 40px;
+ line-height: 44px;
+ text-align: right;
+ :global(.ant-dropdown-trigger) {
+ margin-right: 24px;
+ }
+}
+
+.content {
+ flex: 1;
+ padding: 32px 0;
+}
+
+@media (min-width: @screen-md-min) {
+ .container {
+ background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
+ background-repeat: no-repeat;
+ background-position: center 110px;
+ background-size: 100%;
+ }
+
+ .content {
+ padding: 32px 0 24px;
+ }
+}
+
+.icon {
+ margin-left: 8px;
+ color: rgba(0, 0, 0, 0.2);
+ font-size: 24px;
+ vertical-align: middle;
+ cursor: pointer;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+}
diff --git a/src/pages/User/login/index.tsx b/src/pages/User/login/index.tsx
new file mode 100644
index 0000000..547a66e
--- /dev/null
+++ b/src/pages/User/login/index.tsx
@@ -0,0 +1,338 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+import {
+ // AlipayCircleOutlined,
+ LockOutlined,
+ MobileOutlined,
+ // TaobaoCircleOutlined,
+ UserOutlined,
+ // WeiboCircleOutlined,
+} from '@ant-design/icons';
+import { Alert, Col, message, Row, Tabs, Image } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { ProFormCaptcha, ProFormCheckbox, ProFormText, LoginForm } from '@ant-design/pro-form';
+import { useIntl, history, FormattedMessage, SelectLang, useModel } from 'umi';
+// import Footer from '@/components/Footer';
+import { getCaptchaImage, getFakeCaptcha, login } from '@/services/login';
+
+import styles from './index.less';
+import { clearSessionToken, setSessionToken } from '@/access';
+
+const LoginMessage: React.FC<{
+ content: string;
+}> = ({ content }) => (
+
+);
+
+const Login: React.FC = () => {
+ const [userLoginState, setUserLoginState] = useState({});
+ const [type, setType] = useState('account');
+ const { initialState, setInitialState } = useModel('@@initialState');
+
+ const [captchaCode, setCaptchaCode] = useState('');
+ const [uuid, setUuid] = useState('');
+
+ const intl = useIntl();
+
+ const fetchUserInfo = async () => {
+ const userInfo = await initialState?.fetchUserInfo?.();
+ if (userInfo) {
+ await setInitialState((s) => ({
+ ...s,
+ currentUser: userInfo,
+ }));
+ }
+ };
+ const getCaptchaCode = async () => {
+ const response = await getCaptchaImage()
+ const imgdata = `data:image/png;base64,${response.img}`;
+ setCaptchaCode(imgdata);
+ setUuid(response.uuid);
+ };
+
+ const handleSubmit = async (values: API.LoginParams) => {
+ try {
+ // 登录
+ const response = await login({ ...values, uuid });
+ if (response.code === 200) {
+ const defaultLoginSuccessMessage = intl.formatMessage({
+ id: 'pages.login.success',
+ defaultMessage: '登录成功!',
+ });
+ const current = new Date();
+ const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
+ setSessionToken(response.token, response.token, expireTime);
+ message.success(defaultLoginSuccessMessage);
+
+ await fetchUserInfo();
+ /** 此方法会跳转到 redirect 参数所在的位置 */
+ if (!history)
+ return;
+
+ const { query } = history.location;
+ const { redirect } = query as { redirect: string };
+ history.push(redirect || '/');
+ return;
+ } else{
+ clearSessionToken();
+ // 如果失败去设置用户错误信息
+ setUserLoginState({status: 'error', type: 'account', massage: response.msg});
+ message.error(response.msg);
+ getCaptchaCode();
+ }
+ } catch (error) {
+ clearSessionToken();
+ const defaultLoginFailureMessage = intl.formatMessage({
+ id: 'pages.login.failure',
+ defaultMessage: '登录失败,请重试!',
+ });
+ message.error(defaultLoginFailureMessage);
+ getCaptchaCode();
+ }
+ };
+ const { status, type: loginType, massage } = userLoginState;
+
+ useEffect(() => {
+ getCaptchaCode();
+ }, []);
+
+ return (
+
+
+ {SelectLang && }
+
+
+
}
+ title="Salpa"
+ // subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
+ initialValues={{
+ autoLogin: true,
+ }}
+ onFinish={async (values) => {
+ await handleSubmit(values as API.LoginParams);
+ }}
+ >
+
+
+ {/* */}
+
+
+ {status === 'error' && loginType === 'account' && (
+
+ )}
+ {type === 'account' && (
+ <>
+
,
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.username.placeholder',
+ defaultMessage: '用户名: admin',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+
+ ),
+ },
+ ]}
+ />
+
,
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.password.placeholder',
+ defaultMessage: '密码: admin123',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+
+ ),
+ },
+ ]}
+ />
+ {/*
+
+
+ ),
+ },
+ ]}
+ />
+
+
+ getCaptchaCode()}
+ />
+
+
*/}
+ >
+ )}
+
+ {/* {status === 'error' && loginType === 'mobile' &&
}
+ {type === 'mobile' && (
+ <>
+
,
+ }}
+ name="mobile"
+ placeholder={intl.formatMessage({
+ id: 'pages.login.phoneNumber.placeholder',
+ defaultMessage: '手机号',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+
+ ),
+ },
+ {
+ pattern: /^1\d{10}$/,
+ message: (
+
+ ),
+ },
+ ]}
+ />
+
,
+ }}
+ captchaProps={{
+ size: 'large',
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.captcha.placeholder',
+ defaultMessage: '请输入验证码',
+ })}
+ captchaTextRender={(timing, count) => {
+ if (timing) {
+ return `${count} ${intl.formatMessage({
+ id: 'pages.getCaptchaSecondText',
+ defaultMessage: '获取验证码',
+ })}`;
+ }
+ return intl.formatMessage({
+ id: 'pages.login.phoneLogin.getVerificationCode',
+ defaultMessage: '获取验证码',
+ });
+ }}
+ name="captcha"
+ rules={[
+ {
+ required: true,
+ message: (
+
+ ),
+ },
+ ]}
+ onGetCaptcha={async (phone) => {
+ const result = await getFakeCaptcha({
+ phone,
+ });
+ if (result === false) {
+ return;
+ }
+ message.success('获取验证码成功!验证码为:1234');
+ }}
+ />
+ >
+ )} */}
+ {/*
*/}
+
+
+ {/*
*/}
+
+ );
+};
+
+export default Login;
diff --git a/src/pages/User/register-result/index.tsx b/src/pages/User/register-result/index.tsx
new file mode 100644
index 0000000..8b4fac5
--- /dev/null
+++ b/src/pages/User/register-result/index.tsx
@@ -0,0 +1,42 @@
+import { Button, Result } from 'antd';
+import { Link } from 'umi';
+import React from 'react';
+import type { RouteChildrenProps } from 'react-router';
+
+import styles from './style.less';
+
+const actions = (
+
+);
+
+export type LocationState = Record;
+
+const RegisterResult: React.FC = ({ location }) => {
+ const email = location.state
+ ? (location.state as LocationState).account
+ : 'AntDesign@example.com';
+ return (
+
+ 你的账户:{email} 注册成功
+
+ }
+ subTitle="激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。"
+ extra={actions}
+ />
+ );
+};
+
+export default RegisterResult;
diff --git a/src/pages/User/register-result/style.less b/src/pages/User/register-result/style.less
new file mode 100644
index 0000000..dc1b890
--- /dev/null
+++ b/src/pages/User/register-result/style.less
@@ -0,0 +1,23 @@
+.registerResult {
+ width: 800px;
+ min-height: 400px;
+ margin: auto;
+ padding: 80px;
+ background: none;
+ :global {
+ .anticon {
+ font-size: 64px;
+ }
+ }
+ .title {
+ margin-top: 32px;
+ font-size: 20px;
+ line-height: 28px;
+ }
+ .actions {
+ margin-top: 40px;
+ a + a {
+ margin-left: 8px;
+ }
+ }
+}
diff --git a/src/pages/User/register/_mock.ts b/src/pages/User/register/_mock.ts
new file mode 100644
index 0000000..aa7ac78
--- /dev/null
+++ b/src/pages/User/register/_mock.ts
@@ -0,0 +1,10 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import type { Request, Response } from 'express';
+
+export default {
+ 'POST /api/register': (_: Request, res: Response) => {
+ res.send({
+ data: { status: 'ok', currentAuthority: 'user' },
+ });
+ },
+};
diff --git a/src/pages/User/register/index.tsx b/src/pages/User/register/index.tsx
new file mode 100644
index 0000000..47c9e84
--- /dev/null
+++ b/src/pages/User/register/index.tsx
@@ -0,0 +1,285 @@
+import type { FC } from 'react';
+import { useState, useEffect } from 'react';
+import { Form, Button, Col, Input, Popover, Progress, Row, Select, message } from 'antd';
+import type { Store } from 'antd/es/form/interface';
+import { Link, useRequest, history } from 'umi';
+import type { StateType } from './service';
+import { fakeRegister } from './service';
+
+import styles from './style.less';
+
+const FormItem = Form.Item;
+const { Option } = Select;
+const InputGroup = Input.Group;
+
+const passwordStatusMap = {
+ ok: (
+
+ 强度:强
+
+ ),
+ pass: (
+
+ 强度:中
+
+ ),
+ poor: (
+
+ 强度:太短
+
+ ),
+};
+
+const passwordProgressMap: {
+ ok: 'success';
+ pass: 'normal';
+ poor: 'exception';
+} = {
+ ok: 'success',
+ pass: 'normal',
+ poor: 'exception',
+};
+
+const Register: FC = () => {
+ const [count, setCount]: [number, any] = useState(0);
+ const [visible, setVisible]: [boolean, any] = useState(false);
+ const [prefix, setPrefix]: [string, any] = useState('86');
+ const [popover, setPopover]: [boolean, any] = useState(false);
+ const confirmDirty = false;
+ let interval: number | undefined;
+ const [form] = Form.useForm();
+
+ useEffect(
+ () => () => {
+ clearInterval(interval);
+ },
+ [interval],
+ );
+
+ const onGetCaptcha = () => {
+ let counts = 59;
+ setCount(counts);
+ interval = window.setInterval(() => {
+ counts -= 1;
+ setCount(counts);
+ if (counts === 0) {
+ clearInterval(interval);
+ }
+ }, 1000);
+ };
+
+ const getPasswordStatus = () => {
+ const value = form.getFieldValue('password');
+ if (value && value.length > 9) {
+ return 'ok';
+ }
+ if (value && value.length > 5) {
+ return 'pass';
+ }
+ return 'poor';
+ };
+
+ const { loading: submitting, run: register } = useRequest<{ data: StateType }>(fakeRegister, {
+ manual: true,
+ onSuccess: (data, params) => {
+ if (data.status === 'ok') {
+ message.success('注册成功!');
+ history.push({
+ pathname: '/user/register-result',
+ state: {
+ account: params.email,
+ },
+ });
+ }
+ },
+ });
+ const onFinish = (values: Store) => {
+ register(values);
+ };
+
+ const checkConfirm = (_: any, value: string) => {
+ const promise = Promise;
+ if (value && value !== form.getFieldValue('password')) {
+ return promise.reject('两次输入的密码不匹配!');
+ }
+ return promise.resolve();
+ };
+
+ const checkPassword = (_: any, value: string) => {
+ const promise = Promise;
+ // 没有值的情况
+ if (!value) {
+ setVisible(!!value);
+ return promise.reject('请输入密码!');
+ }
+ // 有值的情况
+ if (!visible) {
+ setVisible(!!value);
+ }
+ setPopover(!popover);
+ if (value.length < 6) {
+ return promise.reject('');
+ }
+ if (value && confirmDirty) {
+ form.validateFields(['confirm']);
+ }
+ return promise.resolve();
+ };
+
+ const changePrefix = (value: string) => {
+ setPrefix(value);
+ };
+
+ const renderPasswordProgress = () => {
+ const value = form.getFieldValue('password');
+ const passwordStatus = getPasswordStatus();
+ return value && value.length ? (
+
+
+ ) : null;
+ };
+
+ return (
+
+ );
+};
+export default Register;
diff --git a/src/pages/User/register/service.ts b/src/pages/User/register/service.ts
new file mode 100644
index 0000000..8380184
--- /dev/null
+++ b/src/pages/User/register/service.ts
@@ -0,0 +1,22 @@
+import { request } from 'umi';
+
+export interface StateType {
+ status?: 'ok' | 'error';
+ currentAuthority?: 'user' | 'guest' | 'admin';
+}
+
+export interface UserRegisterParams {
+ mail: string;
+ password: string;
+ confirm: string;
+ mobile: string;
+ captcha: string;
+ prefix: string;
+}
+
+export async function fakeRegister(params: UserRegisterParams) {
+ return request('/api/register', {
+ method: 'POST',
+ data: params,
+ });
+}
diff --git a/src/pages/User/register/style.less b/src/pages/User/register/style.less
new file mode 100644
index 0000000..e13868f
--- /dev/null
+++ b/src/pages/User/register/style.less
@@ -0,0 +1,60 @@
+@import '~antd/es/style/themes/default.less';
+
+.main {
+ width: 368px;
+ margin: 0 auto;
+
+ h3 {
+ margin-bottom: 20px;
+ font-size: 16px;
+ }
+
+ .password {
+ margin-bottom: 24px;
+ :global {
+ .ant-form-item-explain {
+ display: none;
+ }
+ }
+ }
+
+ .getCaptcha {
+ display: block;
+ width: 100%;
+ }
+
+ .submit {
+ width: 50%;
+ }
+
+ .login {
+ float: right;
+ line-height: @btn-height-lg;
+ }
+}
+
+.success,
+.warning,
+.error {
+ transition: color 0.3s;
+}
+
+.success {
+ color: @success-color;
+}
+
+.warning {
+ color: @warning-color;
+}
+
+.error {
+ color: @error-color;
+}
+
+.progress-pass > .progress {
+ :global {
+ .ant-progress-bg {
+ background-color: @warning-color;
+ }
+ }
+}
diff --git a/src/pages/Welcome.less b/src/pages/Welcome.less
new file mode 100644
index 0000000..914c40d
--- /dev/null
+++ b/src/pages/Welcome.less
@@ -0,0 +1,8 @@
+@import '~antd/lib/style/themes/default.less';
+
+.pre {
+ margin: 12px 0;
+ padding: 12px 20px;
+ background: @input-bg;
+ box-shadow: @card-shadow;
+}
diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx
new file mode 100644
index 0000000..12bf6d2
--- /dev/null
+++ b/src/pages/Welcome.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Alert, Typography } from 'antd';
+import { useIntl, FormattedMessage } from 'umi';
+import styles from './Welcome.less';
+
+const CodePreview: React.FC = ({ children }) => (
+
+
+ {children}
+
+
+);
+
+export default (): React.ReactNode => {
+ const intl = useIntl();
+ return (
+
+
+
+
+ {' '}
+
+
+
+
+ yarn add @ant-design/pro-table
+
+ {' '}
+
+
+
+
+ yarn add @ant-design/pro-layout
+
+
+ );
+};
diff --git a/src/pages/account/center/Center.less b/src/pages/account/center/Center.less
new file mode 100644
index 0000000..ec790fe
--- /dev/null
+++ b/src/pages/account/center/Center.less
@@ -0,0 +1,62 @@
+@import '~antd/es/style/themes/default.less';
+
+.avatarHolder {
+ margin-bottom: 16px;
+ text-align: center;
+ position: relative;
+ display: inline-block;
+ height: 120px;
+
+ & > img {
+ width: 120px;
+ height: 120px;
+ margin-bottom: 20px;
+ border-radius: 50%;
+ }
+ &:hover:after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ color: #eee;
+ font-size: 24px;
+ font-style: normal;
+ line-height: 110px;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ cursor: pointer;
+ content: '+';
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+}
+
+.teamTitle {
+ margin-bottom: 12px;
+ color: @heading-color;
+ font-weight: 500;
+}
+
+.team {
+ :global {
+ .ant-avatar {
+ margin-right: 12px;
+ }
+ }
+
+ a {
+ display: block;
+ margin-bottom: 24px;
+ overflow: hidden;
+ color: @text-color;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+}
diff --git a/src/pages/account/center/_mock.ts b/src/pages/account/center/_mock.ts
new file mode 100644
index 0000000..67acab0
--- /dev/null
+++ b/src/pages/account/center/_mock.ts
@@ -0,0 +1,243 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import type { Request, Response } from 'express';
+import type { ListItemDataType } from './data.d';
+
+const titles = [
+ 'Alipay',
+ 'Angular',
+ 'Ant Design',
+ 'Ant Design Pro',
+ 'Bootstrap',
+ 'React',
+ 'Vue',
+ 'Webpack',
+];
+const avatars = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
+ 'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
+ 'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
+ 'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
+ 'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
+ 'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
+ 'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
+ 'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
+];
+
+const covers = [
+ 'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
+ 'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
+];
+const desc = [
+ '那是一种内在的东西, 他们到达不了,也无法触及的',
+ '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ '生命就像一盒巧克力,结果往往出人意料',
+ '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ '那时候我只会想自己想要什么,从不想自己拥有什么',
+];
+
+const user = [
+ '付小小',
+ '曲丽丽',
+ '林东东',
+ '周星星',
+ '吴加好',
+ '朱偏右',
+ '鱼酱',
+ '乐哥',
+ '谭小仪',
+ '仲尼',
+];
+
+// 当前用户信息
+const currentUseDetail = {
+ name: 'Serati Ma',
+ avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
+ userid: '00000001',
+ email: 'antdesign@alipay.com',
+ signature: '海纳百川,有容乃大',
+ title: '交互专家',
+ group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+ tags: [
+ {
+ key: '0',
+ label: '很有想法的',
+ },
+ {
+ key: '1',
+ label: '专注设计',
+ },
+ {
+ key: '2',
+ label: '辣~',
+ },
+ {
+ key: '3',
+ label: '大长腿',
+ },
+ {
+ key: '4',
+ label: '川妹子',
+ },
+ {
+ key: '5',
+ label: '海纳百川',
+ },
+ ],
+ notice: [
+ {
+ id: 'xxx1',
+ title: titles[0],
+ logo: avatars[0],
+ description: '那是一种内在的东西,他们到达不了,也无法触及的',
+ updatedAt: new Date(),
+ member: '科学搬砖组',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx2',
+ title: titles[1],
+ logo: avatars[1],
+ description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
+ updatedAt: new Date('2017-07-24'),
+ member: '全组都是吴彦祖',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx3',
+ title: titles[2],
+ logo: avatars[2],
+ description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
+ updatedAt: new Date(),
+ member: '中二少女团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx4',
+ title: titles[3],
+ logo: avatars[3],
+ description: '那时候我只会想自己想要什么,从不想自己拥有什么',
+ updatedAt: new Date('2017-07-23'),
+ member: '程序员日常',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx5',
+ title: titles[4],
+ logo: avatars[4],
+ description: '凛冬将至',
+ updatedAt: new Date('2017-07-23'),
+ member: '高逼格设计天团',
+ href: '',
+ memberLink: '',
+ },
+ {
+ id: 'xxx6',
+ title: titles[5],
+ logo: avatars[5],
+ description: '生命就像一盒巧克力,结果往往出人意料',
+ updatedAt: new Date('2017-07-23'),
+ member: '骗你来学计算机',
+ href: '',
+ memberLink: '',
+ },
+ ],
+ notifyCount: 12,
+ unreadCount: 11,
+ country: 'China',
+ geographic: {
+ province: {
+ label: '浙江省',
+ key: '330000',
+ },
+ city: {
+ label: '杭州市',
+ key: '330100',
+ },
+ },
+ address: '西湖区工专路 77 号',
+ phone: '0752-268888888',
+};
+
+function fakeList(count: number): ListItemDataType[] {
+ const list = [];
+ for (let i = 0; i < count; i += 1) {
+ list.push({
+ id: `fake-list-${i}`,
+ owner: user[i % 10],
+ title: titles[i % 8],
+ avatar: avatars[i % 8],
+ cover: parseInt(`${i / 4}`, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
+ status: ['active', 'exception', 'normal'][i % 3] as
+ | 'normal'
+ | 'exception'
+ | 'active'
+ | 'success',
+ percent: Math.ceil(Math.random() * 50) + 50,
+ logo: avatars[i % 8],
+ href: 'https://ant.design',
+ updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
+ createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
+ subDescription: desc[i % 5],
+ description:
+ '在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
+ activeUser: Math.ceil(Math.random() * 100000) + 100000,
+ newUser: Math.ceil(Math.random() * 1000) + 1000,
+ star: Math.ceil(Math.random() * 100) + 100,
+ like: Math.ceil(Math.random() * 100) + 100,
+ message: Math.ceil(Math.random() * 10) + 10,
+ content:
+ '段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
+ members: [
+ {
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
+ name: '曲丽丽',
+ id: 'member1',
+ },
+ {
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
+ name: '王昭君',
+ id: 'member2',
+ },
+ {
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
+ name: '董娜娜',
+ id: 'member3',
+ },
+ ],
+ });
+ }
+
+ return list;
+}
+
+function getFakeList(req: Request, res: Response) {
+ const params = req.query as any;
+
+ const count = Number(params.count) * 1 || 5;
+
+ const result = fakeList(count);
+ return res.json({
+ data: {
+ list: result,
+ },
+ });
+}
+
+// 获取用户信息
+function getCurrentUser(req: Request, res: Response) {
+ return res.json({
+ data: currentUseDetail,
+ });
+}
+
+export default {
+ 'GET /api/fake_list_Detail': getFakeList,
+ // 支持值为 Object 和 Array
+ 'GET /api/currentUserDetail': getCurrentUser,
+};
diff --git a/src/pages/account/center/components/AvatarCropper/index.less b/src/pages/account/center/components/AvatarCropper/index.less
new file mode 100644
index 0000000..dc3fecf
--- /dev/null
+++ b/src/pages/account/center/components/AvatarCropper/index.less
@@ -0,0 +1,10 @@
+.avatarPreview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/src/pages/account/center/components/AvatarCropper/index.tsx b/src/pages/account/center/components/AvatarCropper/index.tsx
new file mode 100644
index 0000000..9cb1365
--- /dev/null
+++ b/src/pages/account/center/components/AvatarCropper/index.tsx
@@ -0,0 +1,144 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Modal, Row, Col, Button, Space, Upload, message } from 'antd';
+import { useIntl } from 'umi';
+import 'cropperjs/dist/cropper.css';
+import { Cropper } from 'react-cropper';
+import { uploadAvatar } from '@/pages/system/user/service';
+import styles from './index.less';
+import {
+ MinusOutlined,
+ PlusOutlined,
+ RedoOutlined,
+ UndoOutlined,
+ UploadOutlined,
+} from '@ant-design/icons';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2022/02/24
+ *
+ * */
+
+export type AvatarCropperProps = {
+ onFinished: (isSuccess: boolean) => void;
+ visible: boolean;
+ data: any;
+};
+
+const AvatarCropperForm: React.FC = (props) => {
+ const cropperRef = useRef(null);
+ const [avatarData, setAvatarData] = useState();
+ const [previewData, setPreviewData] = useState();
+
+ useEffect(() => {
+ setAvatarData(props.data);
+ }, [props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.getCroppedCanvas().toBlob((blob: Blob) => {
+ const formData = new FormData();
+ formData.append('avatarfile', blob);
+ uploadAvatar(formData).then((res) => {
+ if (res.code === 200) {
+ message.success(res.msg);
+ props.onFinished(true);
+ } else {
+ message.warn(res.msg);
+ }
+ });
+ }, 'image/png');
+ };
+ const handleCancel = () => {
+ props.onFinished(false);
+ };
+ const onCrop = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ setPreviewData(cropper.getCroppedCanvas().toDataURL());
+ };
+ const onRotateRight = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.rotate(90);
+ };
+ const onRotateLeft = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.rotate(-90);
+ };
+ const onZoomIn = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.zoom(0.1);
+ };
+ const onZoomOut = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.zoom(-0.1);
+ };
+ const beforeUpload = (file: any) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ setAvatarData(reader.result);
+ };
+ };
+ return (
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+ } onClick={onRotateRight} />
+ } onClick={onRotateLeft} />
+ } onClick={onZoomIn} />
+ } onClick={onZoomOut} />
+
+
+
+
+ );
+};
+
+export default AvatarCropperForm;
diff --git a/src/pages/account/center/components/BaseInfo/index.tsx b/src/pages/account/center/components/BaseInfo/index.tsx
new file mode 100644
index 0000000..bf6599b
--- /dev/null
+++ b/src/pages/account/center/components/BaseInfo/index.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { Form, message, Row } from 'antd';
+import { FormattedMessage } from 'react-intl';
+import ProForm, { ProFormRadio, ProFormText } from '@ant-design/pro-form';
+import { useIntl } from 'umi';
+import { updateUserProfile } from '@/pages/system/user/service';
+
+export type BaseInfoProps = {
+ values: Partial | undefined;
+};
+
+const BaseInfo: React.FC = (props) => {
+ const [form] = Form.useForm();
+ const intl = useIntl();
+
+ const handleFinish = async (values: Record) => {
+ const data = { ...props.values, ...values } as API.CurrentUser;
+ const resp = await updateUserProfile(data);
+ if (resp.code === 200) {
+ message.success('修改成功');
+ } else {
+ message.warn(resp.msg);
+ }
+ };
+
+ return (
+ <>
+
+
+
+ ),
+ },
+ ]}
+ />
+
+
+
+ ),
+ },
+ ]}
+ />
+
+
+ ,
+ },
+ ]}
+ />
+
+
+ ,
+ },
+ ]}
+ />
+
+
+ >
+ );
+};
+
+export default BaseInfo;
diff --git a/src/pages/account/center/components/ResetPassword/index.tsx b/src/pages/account/center/components/ResetPassword/index.tsx
new file mode 100644
index 0000000..2cd30e5
--- /dev/null
+++ b/src/pages/account/center/components/ResetPassword/index.tsx
@@ -0,0 +1,85 @@
+import React from 'react';
+import { Form, message } from 'antd';
+import { FormattedMessage } from 'react-intl';
+import ProForm, { ProFormText } from '@ant-design/pro-form';
+import { useIntl } from 'umi';
+import { updateUserPwd } from '@/pages/system/user/service';
+
+const ResetPassword: React.FC = () => {
+ const [form] = Form.useForm();
+ const intl = useIntl();
+
+ const handleFinish = async (values: Record) => {
+ const resp = await updateUserPwd(values.oldPassword, values.newPassword);
+ if (resp.code === 200) {
+ message.success('密码重置成功。');
+ } else {
+ message.warn(resp.msg);
+ }
+ };
+
+ const checkPassword = (rule: any, value: string) => {
+ const login_password = form.getFieldValue('newPassword');
+ if (value === login_password) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('两次密码输入不一致'));
+ };
+
+ return (
+ <>
+
+ ,
+ },
+ ]}
+ />
+ ,
+ },
+ ]}
+ />
+
+ ),
+ },
+ { validator: checkPassword },
+ ]}
+ />
+
+ >
+ );
+};
+
+export default ResetPassword;
diff --git a/src/pages/account/center/data.d.ts b/src/pages/account/center/data.d.ts
new file mode 100644
index 0000000..84292e7
--- /dev/null
+++ b/src/pages/account/center/data.d.ts
@@ -0,0 +1,57 @@
+export type tabKeyType = 'base' | 'password';
+export interface TagType {
+ key: string;
+ label: string;
+}
+
+export type GeographicType = {
+ province: {
+ label: string;
+ key: string;
+ };
+ city: {
+ label: string;
+ key: string;
+ };
+};
+
+export type NoticeType = {
+ id: string;
+ title: string;
+ logo: string;
+ description: string;
+ updatedAt: string;
+ member: string;
+ href: string;
+ memberLink: string;
+};
+
+export type Member = {
+ avatar: string;
+ name: string;
+ id: string;
+};
+
+export type ListItemDataType = {
+ id: string;
+ owner: string;
+ title: string;
+ avatar: string;
+ cover: string;
+ status: 'normal' | 'exception' | 'active' | 'success';
+ percent: number;
+ logo: string;
+ href: string;
+ body?: any;
+ updatedAt: number;
+ createdAt: number;
+ subDescription: string;
+ description: string;
+ activeUser: number;
+ newUser: number;
+ star: number;
+ like: number;
+ message: number;
+ content: string;
+ members: Member[];
+};
diff --git a/src/pages/account/center/index.tsx b/src/pages/account/center/index.tsx
new file mode 100644
index 0000000..0efc26d
--- /dev/null
+++ b/src/pages/account/center/index.tsx
@@ -0,0 +1,197 @@
+import {
+ ClusterOutlined,
+ MailOutlined,
+ TeamOutlined,
+ UserOutlined,
+ MobileOutlined,
+ ManOutlined,
+} from '@ant-design/icons';
+import { Card, Col, Divider, List, Row } from 'antd';
+import React, { useState } from 'react';
+import { useRequest } from 'umi';
+import type { tabKeyType } from './data.d';
+import { queryCurrentUserInfo } from './service';
+import styles from './Center.less';
+import BaseInfo from './components/BaseInfo';
+import ResetPassword from './components/ResetPassword';
+import AvatarCropper from './components/AvatarCropper';
+import WrapContent from '@/components/WrapContent';
+import PageLoading from '@/pages/dashboard/analysis/components/PageLoading';
+
+const operationTabList = [
+ {
+ key: 'base',
+ tab: (
+
+ 基本资料
+
+ ),
+ },
+ {
+ key: 'password',
+ tab: (
+
+ 重置密码
+
+ ),
+ },
+];
+
+const Center: React.FC = () => {
+
+ const [tabKey, setTabKey] = useState('base');
+
+ const [cropperModalVisible, setCropperModalVisible] = useState(false);
+
+ // 获取用户信息
+ const { data: userInfo, loading } = useRequest(() => {
+ return queryCurrentUserInfo();
+ });
+
+ const currentUser = userInfo?.user;
+
+ // 渲染用户信息
+ const renderUserInfo = ({
+ userName,
+ phonenumber,
+ email,
+ sex,
+ dept,
+ }: Partial) => {
+ return (
+
+
+
+
+ 用户名
+
+ {userName}
+
+
+
+
+ 性别
+
+ {sex === '1' ? '女' : '男'}
+
+
+
+
+ 电话
+
+ {phonenumber}
+
+
+
+
+ 邮箱
+
+ {email}
+
+
+
+
+ 部门
+
+ {dept?.deptName}
+
+
+ );
+ };
+
+ // 渲染tab切换
+ const renderChildrenByTabKey = (tabValue: tabKeyType) => {
+ if (tabValue === 'base') {
+ return ;
+ }
+ if (tabValue === 'password') {
+ return ;
+ }
+ return null;
+ };
+
+ if (!currentUser) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ {!loading && (
+
+
{setCropperModalVisible(true)}}>
+

+
+ {renderUserInfo(currentUser)}
+
+
+
角色
+
+ {currentUser.roles &&
+ currentUser.roles.map((item: any) => (
+
+
+ {item.roleName}
+
+ ))}
+
+
+
+ )}
+
+
+
+ {
+ setTabKey(_tabKey as tabKeyType);
+ }}
+ >
+ {renderChildrenByTabKey(tabKey)}
+
+
+
+ {
+ setCropperModalVisible(false);
+ }}
+ visible={cropperModalVisible}
+ data={currentUser.avatar}
+ />
+
+ );
+};
+
+export default Center;
diff --git a/src/pages/account/center/service.ts b/src/pages/account/center/service.ts
new file mode 100644
index 0000000..615855b
--- /dev/null
+++ b/src/pages/account/center/service.ts
@@ -0,0 +1,14 @@
+import { request } from 'umi';
+import type { ListItemDataType } from './data.d';
+
+export async function queryCurrentUserInfo(): Promise<{ data: API.GetUserInfoResult }> {
+ return { data: await request('/api/getInfo') }
+}
+
+export async function queryFakeList(params: {
+ count: number;
+}): Promise<{ data: { list: ListItemDataType[] } }> {
+ return request('/api/fake_list_Detail', {
+ params,
+ });
+}
diff --git a/src/pages/account/settings/_mock.ts b/src/pages/account/settings/_mock.ts
new file mode 100644
index 0000000..8e4ee13
--- /dev/null
+++ b/src/pages/account/settings/_mock.ts
@@ -0,0 +1,79 @@
+// eslint-disable-next-line import/no-extraneous-dependencies
+import type { Request, Response } from 'express';
+
+const city = require('./geographic/city.json');
+const province = require('./geographic/province.json');
+
+function getProvince(_: Request, res: Response) {
+ return res.json({
+ data: province,
+ });
+}
+
+function getCity(req: Request, res: Response) {
+ return res.json({
+ data: city[req.params.province],
+ });
+}
+
+function getCurrentUse(req: Request, res: Response) {
+ return res.json({
+ data: {
+ name: 'Serati Ma',
+ avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
+ userid: '00000001',
+ email: 'antdesign@alipay.com',
+ signature: '海纳百川,有容乃大',
+ title: '交互专家',
+ group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
+ tags: [
+ {
+ key: '0',
+ label: '很有想法的',
+ },
+ {
+ key: '1',
+ label: '专注设计',
+ },
+ {
+ key: '2',
+ label: '辣~',
+ },
+ {
+ key: '3',
+ label: '大长腿',
+ },
+ {
+ key: '4',
+ label: '川妹子',
+ },
+ {
+ key: '5',
+ label: '海纳百川',
+ },
+ ],
+ notifyCount: 12,
+ unreadCount: 11,
+ country: 'China',
+ geographic: {
+ province: {
+ label: '浙江省',
+ key: '330000',
+ },
+ city: {
+ label: '杭州市',
+ key: '330100',
+ },
+ },
+ address: '西湖区工专路 77 号',
+ phone: '0752-268888888',
+ },
+ });
+}
+// 代码中会兼容本地 service mock 以及部署站点的静态数据
+export default {
+ // 支持值为 Object 和 Array
+ 'GET /api/accountSettingCurrentUser': getCurrentUse,
+ 'GET /api/geographic/province': getProvince,
+ 'GET /api/geographic/city/:province': getCity,
+};
diff --git a/src/pages/account/settings/components/BaseView.less b/src/pages/account/settings/components/BaseView.less
new file mode 100644
index 0000000..ca328b4
--- /dev/null
+++ b/src/pages/account/settings/components/BaseView.less
@@ -0,0 +1,65 @@
+@import '~antd/es/style/themes/default.less';
+
+.baseView {
+ display: flex;
+ padding-top: 12px;
+
+ :global {
+ .ant-legacy-form-item .ant-legacy-form-item-control-wrapper {
+ width: 100%;
+ }
+ }
+
+ .left {
+ min-width: 224px;
+ max-width: 448px;
+ }
+ .right {
+ flex: 1;
+ padding-left: 104px;
+ .avatar_title {
+ height: 22px;
+ margin-bottom: 8px;
+ color: @heading-color;
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .avatar {
+ width: 144px;
+ height: 144px;
+ margin-bottom: 12px;
+ overflow: hidden;
+ img {
+ width: 100%;
+ }
+ }
+ .button_view {
+ width: 144px;
+ text-align: center;
+ }
+ }
+}
+
+.area_code {
+ width: 72px;
+}
+.phone_number {
+ width: 214px;
+}
+
+@media screen and (max-width: @screen-xl) {
+ .baseView {
+ flex-direction: column-reverse;
+
+ .right {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-width: 448px;
+ padding: 20px;
+ .avatar_title {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/src/pages/account/settings/components/PhoneView.tsx b/src/pages/account/settings/components/PhoneView.tsx
new file mode 100644
index 0000000..43894f3
--- /dev/null
+++ b/src/pages/account/settings/components/PhoneView.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+
+import { Input } from 'antd';
+import styles from './PhoneView.less';
+
+type PhoneViewProps = {
+ value?: string;
+ onChange?: (value: string) => void;
+};
+
+const PhoneView: React.FC = (props) => {
+ const { value, onChange } = props;
+ let values = ['', ''];
+ if (value) {
+ values = value.split('-');
+ }
+
+ return (
+ <>
+ {
+ if (onChange) {
+ onChange(`${e.target.value}-${values[1]}`);
+ }
+ }}
+ />
+ {
+ if (onChange) {
+ onChange(`${values[0]}-${e.target.value}`);
+ }
+ }}
+ value={values[1]}
+ />
+ >
+ );
+};
+
+export default PhoneView;
diff --git a/src/pages/account/settings/components/base.tsx b/src/pages/account/settings/components/base.tsx
new file mode 100644
index 0000000..09a7187
--- /dev/null
+++ b/src/pages/account/settings/components/base.tsx
@@ -0,0 +1,186 @@
+import React from 'react';
+import { UploadOutlined } from '@ant-design/icons';
+import { Button, Upload, message, Form } from 'antd';
+import ProForm, {
+ ProFormSelect,
+ ProFormText,
+ ProFormTextArea,
+} from '@ant-design/pro-form';
+import { queryCurrentUserInfo } from '../service';
+
+import styles from './BaseView.less';
+import { useRequest } from 'umi';
+
+// const validatorPhone = (rule: any, value: string[], callback: (message?: string) => void) => {
+// if (!value[0]) {
+// callback('Please input your area code!');
+// }
+// if (!value[1]) {
+// callback('Please input your phone number!');
+// }
+// callback();
+// };
+// 头像组件 方便以后独立,增加裁剪之类的功能
+const AvatarView = ({ avatar }: { avatar: string }) => (
+ <>
+ 头像
+
+

+
+
+
+
+
+
+ >
+);
+
+const BaseView: React.FC = () => {
+
+ const [form] = Form.useForm();
+ // 获取用户信息
+ const { data: userInfo, loading } = useRequest(() => {
+ return queryCurrentUserInfo();
+ });
+
+ const currentUser = userInfo?.user;
+
+ if (!currentUser) {
+ return loading;
+ }
+
+ const getAvatarURL = () => {
+ if (currentUser) {
+ if (currentUser.avatar) {
+ return currentUser.avatar;
+ }
+ return 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
+ }
+ return '';
+ };
+
+ const handleFinish = async () => {
+ message.success('更新基本信息成功');
+ };
+
+ return (
+
+ );
+};
+
+export default BaseView;
diff --git a/src/pages/account/settings/components/binding.tsx b/src/pages/account/settings/components/binding.tsx
new file mode 100644
index 0000000..002c459
--- /dev/null
+++ b/src/pages/account/settings/components/binding.tsx
@@ -0,0 +1,46 @@
+import { AlipayOutlined, DingdingOutlined, TaobaoOutlined } from '@ant-design/icons';
+import { List } from 'antd';
+import React, { Fragment } from 'react';
+
+const BindingView: React.FC = () => {
+ const getData = () => [
+ {
+ title: '绑定淘宝',
+ description: '当前未绑定淘宝账号',
+ actions: [绑定],
+ avatar: ,
+ },
+ {
+ title: '绑定支付宝',
+ description: '当前未绑定支付宝账号',
+ actions: [绑定],
+ avatar: ,
+ },
+ {
+ title: '绑定钉钉',
+ description: '当前未绑定钉钉账号',
+ actions: [绑定],
+ avatar: ,
+ },
+ ];
+
+ return (
+
+ (
+
+
+
+ )}
+ />
+
+ );
+};
+
+export default BindingView;
diff --git a/src/pages/account/settings/components/notification.tsx b/src/pages/account/settings/components/notification.tsx
new file mode 100644
index 0000000..177c38c
--- /dev/null
+++ b/src/pages/account/settings/components/notification.tsx
@@ -0,0 +1,44 @@
+import { List, Switch } from 'antd';
+import React, { Fragment } from 'react';
+
+type Unpacked = T extends (infer U)[] ? U : T;
+
+const NotificationView: React.FC = () => {
+ const getData = () => {
+ const Action = ;
+ return [
+ {
+ title: '账户密码',
+ description: '其他用户的消息将以站内信的形式通知',
+ actions: [Action],
+ },
+ {
+ title: '系统消息',
+ description: '系统消息将以站内信的形式通知',
+ actions: [Action],
+ },
+ {
+ title: '待办任务',
+ description: '待办任务将以站内信的形式通知',
+ actions: [Action],
+ },
+ ];
+ };
+
+ const data = getData();
+ return (
+
+ >
+ itemLayout="horizontal"
+ dataSource={data}
+ renderItem={(item) => (
+
+
+
+ )}
+ />
+
+ );
+};
+
+export default NotificationView;
diff --git a/src/pages/account/settings/components/security.tsx b/src/pages/account/settings/components/security.tsx
new file mode 100644
index 0000000..c0c4020
--- /dev/null
+++ b/src/pages/account/settings/components/security.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { List } from 'antd';
+
+type Unpacked = T extends (infer U)[] ? U : T;
+
+const passwordStrength = {
+ strong: 强,
+ medium: 中,
+ weak: 弱 Weak,
+};
+
+const SecurityView: React.FC = () => {
+ const getData = () => [
+ {
+ title: '账户密码',
+ description: (
+ <>
+ 当前密码强度:
+ {passwordStrength.strong}
+ >
+ ),
+ actions: [修改],
+ },
+ {
+ title: '密保手机',
+ description: `已绑定手机:138****8293`,
+ actions: [修改],
+ },
+ {
+ title: '密保问题',
+ description: '未设置密保问题,密保问题可有效保护账户安全',
+ actions: [设置],
+ },
+ {
+ title: '备用邮箱',
+ description: `已绑定邮箱:ant***sign.com`,
+ actions: [修改],
+ },
+ {
+ title: 'MFA 设备',
+ description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
+ actions: [绑定],
+ },
+ ];
+
+ const data = getData();
+ return (
+ <>
+ >
+ itemLayout="horizontal"
+ dataSource={data}
+ renderItem={(item) => (
+
+
+
+ )}
+ />
+ >
+ );
+};
+
+export default SecurityView;
diff --git a/src/pages/account/settings/data.d.ts b/src/pages/account/settings/data.d.ts
new file mode 100644
index 0000000..bf58a3f
--- /dev/null
+++ b/src/pages/account/settings/data.d.ts
@@ -0,0 +1,26 @@
+export type TagType = {
+ key: string;
+ label: string;
+};
+
+export type GeographicItemType = {
+ name: string;
+ id: string;
+};
+
+export type GeographicType = {
+ province: GeographicItemType;
+ city: GeographicItemType;
+};
+
+export type NoticeType = {
+ id: string;
+ title: string;
+ logo: string;
+ description: string;
+ updatedAt: string;
+ member: string;
+ href: string;
+ memberLink: string;
+};
+
diff --git a/src/pages/account/settings/geographic/city.json b/src/pages/account/settings/geographic/city.json
new file mode 100644
index 0000000..2978374
--- /dev/null
+++ b/src/pages/account/settings/geographic/city.json
@@ -0,0 +1,1784 @@
+{
+ "110000": [
+ {
+ "province": "北京市",
+ "name": "市辖区",
+ "id": "110100"
+ }
+ ],
+ "120000": [
+ {
+ "province": "天津市",
+ "name": "市辖区",
+ "id": "120100"
+ }
+ ],
+ "130000": [
+ {
+ "province": "河北省",
+ "name": "石家庄市",
+ "id": "130100"
+ },
+ {
+ "province": "河北省",
+ "name": "唐山市",
+ "id": "130200"
+ },
+ {
+ "province": "河北省",
+ "name": "秦皇岛市",
+ "id": "130300"
+ },
+ {
+ "province": "河北省",
+ "name": "邯郸市",
+ "id": "130400"
+ },
+ {
+ "province": "河北省",
+ "name": "邢台市",
+ "id": "130500"
+ },
+ {
+ "province": "河北省",
+ "name": "保定市",
+ "id": "130600"
+ },
+ {
+ "province": "河北省",
+ "name": "张家口市",
+ "id": "130700"
+ },
+ {
+ "province": "河北省",
+ "name": "承德市",
+ "id": "130800"
+ },
+ {
+ "province": "河北省",
+ "name": "沧州市",
+ "id": "130900"
+ },
+ {
+ "province": "河北省",
+ "name": "廊坊市",
+ "id": "131000"
+ },
+ {
+ "province": "河北省",
+ "name": "衡水市",
+ "id": "131100"
+ },
+ {
+ "province": "河北省",
+ "name": "省直辖县级行政区划",
+ "id": "139000"
+ }
+ ],
+ "140000": [
+ {
+ "province": "山西省",
+ "name": "太原市",
+ "id": "140100"
+ },
+ {
+ "province": "山西省",
+ "name": "大同市",
+ "id": "140200"
+ },
+ {
+ "province": "山西省",
+ "name": "阳泉市",
+ "id": "140300"
+ },
+ {
+ "province": "山西省",
+ "name": "长治市",
+ "id": "140400"
+ },
+ {
+ "province": "山西省",
+ "name": "晋城市",
+ "id": "140500"
+ },
+ {
+ "province": "山西省",
+ "name": "朔州市",
+ "id": "140600"
+ },
+ {
+ "province": "山西省",
+ "name": "晋中市",
+ "id": "140700"
+ },
+ {
+ "province": "山西省",
+ "name": "运城市",
+ "id": "140800"
+ },
+ {
+ "province": "山西省",
+ "name": "忻州市",
+ "id": "140900"
+ },
+ {
+ "province": "山西省",
+ "name": "临汾市",
+ "id": "141000"
+ },
+ {
+ "province": "山西省",
+ "name": "吕梁市",
+ "id": "141100"
+ }
+ ],
+ "150000": [
+ {
+ "province": "内蒙古自治区",
+ "name": "呼和浩特市",
+ "id": "150100"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "包头市",
+ "id": "150200"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "乌海市",
+ "id": "150300"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "赤峰市",
+ "id": "150400"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "通辽市",
+ "id": "150500"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "鄂尔多斯市",
+ "id": "150600"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "呼伦贝尔市",
+ "id": "150700"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "巴彦淖尔市",
+ "id": "150800"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "乌兰察布市",
+ "id": "150900"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "兴安盟",
+ "id": "152200"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "锡林郭勒盟",
+ "id": "152500"
+ },
+ {
+ "province": "内蒙古自治区",
+ "name": "阿拉善盟",
+ "id": "152900"
+ }
+ ],
+ "210000": [
+ {
+ "province": "辽宁省",
+ "name": "沈阳市",
+ "id": "210100"
+ },
+ {
+ "province": "辽宁省",
+ "name": "大连市",
+ "id": "210200"
+ },
+ {
+ "province": "辽宁省",
+ "name": "鞍山市",
+ "id": "210300"
+ },
+ {
+ "province": "辽宁省",
+ "name": "抚顺市",
+ "id": "210400"
+ },
+ {
+ "province": "辽宁省",
+ "name": "本溪市",
+ "id": "210500"
+ },
+ {
+ "province": "辽宁省",
+ "name": "丹东市",
+ "id": "210600"
+ },
+ {
+ "province": "辽宁省",
+ "name": "锦州市",
+ "id": "210700"
+ },
+ {
+ "province": "辽宁省",
+ "name": "营口市",
+ "id": "210800"
+ },
+ {
+ "province": "辽宁省",
+ "name": "阜新市",
+ "id": "210900"
+ },
+ {
+ "province": "辽宁省",
+ "name": "辽阳市",
+ "id": "211000"
+ },
+ {
+ "province": "辽宁省",
+ "name": "盘锦市",
+ "id": "211100"
+ },
+ {
+ "province": "辽宁省",
+ "name": "铁岭市",
+ "id": "211200"
+ },
+ {
+ "province": "辽宁省",
+ "name": "朝阳市",
+ "id": "211300"
+ },
+ {
+ "province": "辽宁省",
+ "name": "葫芦岛市",
+ "id": "211400"
+ }
+ ],
+ "220000": [
+ {
+ "province": "吉林省",
+ "name": "长春市",
+ "id": "220100"
+ },
+ {
+ "province": "吉林省",
+ "name": "吉林市",
+ "id": "220200"
+ },
+ {
+ "province": "吉林省",
+ "name": "四平市",
+ "id": "220300"
+ },
+ {
+ "province": "吉林省",
+ "name": "辽源市",
+ "id": "220400"
+ },
+ {
+ "province": "吉林省",
+ "name": "通化市",
+ "id": "220500"
+ },
+ {
+ "province": "吉林省",
+ "name": "白山市",
+ "id": "220600"
+ },
+ {
+ "province": "吉林省",
+ "name": "松原市",
+ "id": "220700"
+ },
+ {
+ "province": "吉林省",
+ "name": "白城市",
+ "id": "220800"
+ },
+ {
+ "province": "吉林省",
+ "name": "延边朝鲜族自治州",
+ "id": "222400"
+ }
+ ],
+ "230000": [
+ {
+ "province": "黑龙江省",
+ "name": "哈尔滨市",
+ "id": "230100"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "齐齐哈尔市",
+ "id": "230200"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "鸡西市",
+ "id": "230300"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "鹤岗市",
+ "id": "230400"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "双鸭山市",
+ "id": "230500"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "大庆市",
+ "id": "230600"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "伊春市",
+ "id": "230700"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "佳木斯市",
+ "id": "230800"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "七台河市",
+ "id": "230900"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "牡丹江市",
+ "id": "231000"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "黑河市",
+ "id": "231100"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "绥化市",
+ "id": "231200"
+ },
+ {
+ "province": "黑龙江省",
+ "name": "大兴安岭地区",
+ "id": "232700"
+ }
+ ],
+ "310000": [
+ {
+ "province": "上海市",
+ "name": "市辖区",
+ "id": "310100"
+ }
+ ],
+ "320000": [
+ {
+ "province": "江苏省",
+ "name": "南京市",
+ "id": "320100"
+ },
+ {
+ "province": "江苏省",
+ "name": "无锡市",
+ "id": "320200"
+ },
+ {
+ "province": "江苏省",
+ "name": "徐州市",
+ "id": "320300"
+ },
+ {
+ "province": "江苏省",
+ "name": "常州市",
+ "id": "320400"
+ },
+ {
+ "province": "江苏省",
+ "name": "苏州市",
+ "id": "320500"
+ },
+ {
+ "province": "江苏省",
+ "name": "南通市",
+ "id": "320600"
+ },
+ {
+ "province": "江苏省",
+ "name": "连云港市",
+ "id": "320700"
+ },
+ {
+ "province": "江苏省",
+ "name": "淮安市",
+ "id": "320800"
+ },
+ {
+ "province": "江苏省",
+ "name": "盐城市",
+ "id": "320900"
+ },
+ {
+ "province": "江苏省",
+ "name": "扬州市",
+ "id": "321000"
+ },
+ {
+ "province": "江苏省",
+ "name": "镇江市",
+ "id": "321100"
+ },
+ {
+ "province": "江苏省",
+ "name": "泰州市",
+ "id": "321200"
+ },
+ {
+ "province": "江苏省",
+ "name": "宿迁市",
+ "id": "321300"
+ }
+ ],
+ "330000": [
+ {
+ "province": "浙江省",
+ "name": "杭州市",
+ "id": "330100"
+ },
+ {
+ "province": "浙江省",
+ "name": "宁波市",
+ "id": "330200"
+ },
+ {
+ "province": "浙江省",
+ "name": "温州市",
+ "id": "330300"
+ },
+ {
+ "province": "浙江省",
+ "name": "嘉兴市",
+ "id": "330400"
+ },
+ {
+ "province": "浙江省",
+ "name": "湖州市",
+ "id": "330500"
+ },
+ {
+ "province": "浙江省",
+ "name": "绍兴市",
+ "id": "330600"
+ },
+ {
+ "province": "浙江省",
+ "name": "金华市",
+ "id": "330700"
+ },
+ {
+ "province": "浙江省",
+ "name": "衢州市",
+ "id": "330800"
+ },
+ {
+ "province": "浙江省",
+ "name": "舟山市",
+ "id": "330900"
+ },
+ {
+ "province": "浙江省",
+ "name": "台州市",
+ "id": "331000"
+ },
+ {
+ "province": "浙江省",
+ "name": "丽水市",
+ "id": "331100"
+ }
+ ],
+ "340000": [
+ {
+ "province": "安徽省",
+ "name": "合肥市",
+ "id": "340100"
+ },
+ {
+ "province": "安徽省",
+ "name": "芜湖市",
+ "id": "340200"
+ },
+ {
+ "province": "安徽省",
+ "name": "蚌埠市",
+ "id": "340300"
+ },
+ {
+ "province": "安徽省",
+ "name": "淮南市",
+ "id": "340400"
+ },
+ {
+ "province": "安徽省",
+ "name": "马鞍山市",
+ "id": "340500"
+ },
+ {
+ "province": "安徽省",
+ "name": "淮北市",
+ "id": "340600"
+ },
+ {
+ "province": "安徽省",
+ "name": "铜陵市",
+ "id": "340700"
+ },
+ {
+ "province": "安徽省",
+ "name": "安庆市",
+ "id": "340800"
+ },
+ {
+ "province": "安徽省",
+ "name": "黄山市",
+ "id": "341000"
+ },
+ {
+ "province": "安徽省",
+ "name": "滁州市",
+ "id": "341100"
+ },
+ {
+ "province": "安徽省",
+ "name": "阜阳市",
+ "id": "341200"
+ },
+ {
+ "province": "安徽省",
+ "name": "宿州市",
+ "id": "341300"
+ },
+ {
+ "province": "安徽省",
+ "name": "六安市",
+ "id": "341500"
+ },
+ {
+ "province": "安徽省",
+ "name": "亳州市",
+ "id": "341600"
+ },
+ {
+ "province": "安徽省",
+ "name": "池州市",
+ "id": "341700"
+ },
+ {
+ "province": "安徽省",
+ "name": "宣城市",
+ "id": "341800"
+ }
+ ],
+ "350000": [
+ {
+ "province": "福建省",
+ "name": "福州市",
+ "id": "350100"
+ },
+ {
+ "province": "福建省",
+ "name": "厦门市",
+ "id": "350200"
+ },
+ {
+ "province": "福建省",
+ "name": "莆田市",
+ "id": "350300"
+ },
+ {
+ "province": "福建省",
+ "name": "三明市",
+ "id": "350400"
+ },
+ {
+ "province": "福建省",
+ "name": "泉州市",
+ "id": "350500"
+ },
+ {
+ "province": "福建省",
+ "name": "漳州市",
+ "id": "350600"
+ },
+ {
+ "province": "福建省",
+ "name": "南平市",
+ "id": "350700"
+ },
+ {
+ "province": "福建省",
+ "name": "龙岩市",
+ "id": "350800"
+ },
+ {
+ "province": "福建省",
+ "name": "宁德市",
+ "id": "350900"
+ }
+ ],
+ "360000": [
+ {
+ "province": "江西省",
+ "name": "南昌市",
+ "id": "360100"
+ },
+ {
+ "province": "江西省",
+ "name": "景德镇市",
+ "id": "360200"
+ },
+ {
+ "province": "江西省",
+ "name": "萍乡市",
+ "id": "360300"
+ },
+ {
+ "province": "江西省",
+ "name": "九江市",
+ "id": "360400"
+ },
+ {
+ "province": "江西省",
+ "name": "新余市",
+ "id": "360500"
+ },
+ {
+ "province": "江西省",
+ "name": "鹰潭市",
+ "id": "360600"
+ },
+ {
+ "province": "江西省",
+ "name": "赣州市",
+ "id": "360700"
+ },
+ {
+ "province": "江西省",
+ "name": "吉安市",
+ "id": "360800"
+ },
+ {
+ "province": "江西省",
+ "name": "宜春市",
+ "id": "360900"
+ },
+ {
+ "province": "江西省",
+ "name": "抚州市",
+ "id": "361000"
+ },
+ {
+ "province": "江西省",
+ "name": "上饶市",
+ "id": "361100"
+ }
+ ],
+ "370000": [
+ {
+ "province": "山东省",
+ "name": "济南市",
+ "id": "370100"
+ },
+ {
+ "province": "山东省",
+ "name": "青岛市",
+ "id": "370200"
+ },
+ {
+ "province": "山东省",
+ "name": "淄博市",
+ "id": "370300"
+ },
+ {
+ "province": "山东省",
+ "name": "枣庄市",
+ "id": "370400"
+ },
+ {
+ "province": "山东省",
+ "name": "东营市",
+ "id": "370500"
+ },
+ {
+ "province": "山东省",
+ "name": "烟台市",
+ "id": "370600"
+ },
+ {
+ "province": "山东省",
+ "name": "潍坊市",
+ "id": "370700"
+ },
+ {
+ "province": "山东省",
+ "name": "济宁市",
+ "id": "370800"
+ },
+ {
+ "province": "山东省",
+ "name": "泰安市",
+ "id": "370900"
+ },
+ {
+ "province": "山东省",
+ "name": "威海市",
+ "id": "371000"
+ },
+ {
+ "province": "山东省",
+ "name": "日照市",
+ "id": "371100"
+ },
+ {
+ "province": "山东省",
+ "name": "莱芜市",
+ "id": "371200"
+ },
+ {
+ "province": "山东省",
+ "name": "临沂市",
+ "id": "371300"
+ },
+ {
+ "province": "山东省",
+ "name": "德州市",
+ "id": "371400"
+ },
+ {
+ "province": "山东省",
+ "name": "聊城市",
+ "id": "371500"
+ },
+ {
+ "province": "山东省",
+ "name": "滨州市",
+ "id": "371600"
+ },
+ {
+ "province": "山东省",
+ "name": "菏泽市",
+ "id": "371700"
+ }
+ ],
+ "410000": [
+ {
+ "province": "河南省",
+ "name": "郑州市",
+ "id": "410100"
+ },
+ {
+ "province": "河南省",
+ "name": "开封市",
+ "id": "410200"
+ },
+ {
+ "province": "河南省",
+ "name": "洛阳市",
+ "id": "410300"
+ },
+ {
+ "province": "河南省",
+ "name": "平顶山市",
+ "id": "410400"
+ },
+ {
+ "province": "河南省",
+ "name": "安阳市",
+ "id": "410500"
+ },
+ {
+ "province": "河南省",
+ "name": "鹤壁市",
+ "id": "410600"
+ },
+ {
+ "province": "河南省",
+ "name": "新乡市",
+ "id": "410700"
+ },
+ {
+ "province": "河南省",
+ "name": "焦作市",
+ "id": "410800"
+ },
+ {
+ "province": "河南省",
+ "name": "濮阳市",
+ "id": "410900"
+ },
+ {
+ "province": "河南省",
+ "name": "许昌市",
+ "id": "411000"
+ },
+ {
+ "province": "河南省",
+ "name": "漯河市",
+ "id": "411100"
+ },
+ {
+ "province": "河南省",
+ "name": "三门峡市",
+ "id": "411200"
+ },
+ {
+ "province": "河南省",
+ "name": "南阳市",
+ "id": "411300"
+ },
+ {
+ "province": "河南省",
+ "name": "商丘市",
+ "id": "411400"
+ },
+ {
+ "province": "河南省",
+ "name": "信阳市",
+ "id": "411500"
+ },
+ {
+ "province": "河南省",
+ "name": "周口市",
+ "id": "411600"
+ },
+ {
+ "province": "河南省",
+ "name": "驻马店市",
+ "id": "411700"
+ },
+ {
+ "province": "河南省",
+ "name": "省直辖县级行政区划",
+ "id": "419000"
+ }
+ ],
+ "420000": [
+ {
+ "province": "湖北省",
+ "name": "武汉市",
+ "id": "420100"
+ },
+ {
+ "province": "湖北省",
+ "name": "黄石市",
+ "id": "420200"
+ },
+ {
+ "province": "湖北省",
+ "name": "十堰市",
+ "id": "420300"
+ },
+ {
+ "province": "湖北省",
+ "name": "宜昌市",
+ "id": "420500"
+ },
+ {
+ "province": "湖北省",
+ "name": "襄阳市",
+ "id": "420600"
+ },
+ {
+ "province": "湖北省",
+ "name": "鄂州市",
+ "id": "420700"
+ },
+ {
+ "province": "湖北省",
+ "name": "荆门市",
+ "id": "420800"
+ },
+ {
+ "province": "湖北省",
+ "name": "孝感市",
+ "id": "420900"
+ },
+ {
+ "province": "湖北省",
+ "name": "荆州市",
+ "id": "421000"
+ },
+ {
+ "province": "湖北省",
+ "name": "黄冈市",
+ "id": "421100"
+ },
+ {
+ "province": "湖北省",
+ "name": "咸宁市",
+ "id": "421200"
+ },
+ {
+ "province": "湖北省",
+ "name": "随州市",
+ "id": "421300"
+ },
+ {
+ "province": "湖北省",
+ "name": "恩施土家族苗族自治州",
+ "id": "422800"
+ },
+ {
+ "province": "湖北省",
+ "name": "省直辖县级行政区划",
+ "id": "429000"
+ }
+ ],
+ "430000": [
+ {
+ "province": "湖南省",
+ "name": "长沙市",
+ "id": "430100"
+ },
+ {
+ "province": "湖南省",
+ "name": "株洲市",
+ "id": "430200"
+ },
+ {
+ "province": "湖南省",
+ "name": "湘潭市",
+ "id": "430300"
+ },
+ {
+ "province": "湖南省",
+ "name": "衡阳市",
+ "id": "430400"
+ },
+ {
+ "province": "湖南省",
+ "name": "邵阳市",
+ "id": "430500"
+ },
+ {
+ "province": "湖南省",
+ "name": "岳阳市",
+ "id": "430600"
+ },
+ {
+ "province": "湖南省",
+ "name": "常德市",
+ "id": "430700"
+ },
+ {
+ "province": "湖南省",
+ "name": "张家界市",
+ "id": "430800"
+ },
+ {
+ "province": "湖南省",
+ "name": "益阳市",
+ "id": "430900"
+ },
+ {
+ "province": "湖南省",
+ "name": "郴州市",
+ "id": "431000"
+ },
+ {
+ "province": "湖南省",
+ "name": "永州市",
+ "id": "431100"
+ },
+ {
+ "province": "湖南省",
+ "name": "怀化市",
+ "id": "431200"
+ },
+ {
+ "province": "湖南省",
+ "name": "娄底市",
+ "id": "431300"
+ },
+ {
+ "province": "湖南省",
+ "name": "湘西土家族苗族自治州",
+ "id": "433100"
+ }
+ ],
+ "440000": [
+ {
+ "province": "广东省",
+ "name": "广州市",
+ "id": "440100"
+ },
+ {
+ "province": "广东省",
+ "name": "韶关市",
+ "id": "440200"
+ },
+ {
+ "province": "广东省",
+ "name": "深圳市",
+ "id": "440300"
+ },
+ {
+ "province": "广东省",
+ "name": "珠海市",
+ "id": "440400"
+ },
+ {
+ "province": "广东省",
+ "name": "汕头市",
+ "id": "440500"
+ },
+ {
+ "province": "广东省",
+ "name": "佛山市",
+ "id": "440600"
+ },
+ {
+ "province": "广东省",
+ "name": "江门市",
+ "id": "440700"
+ },
+ {
+ "province": "广东省",
+ "name": "湛江市",
+ "id": "440800"
+ },
+ {
+ "province": "广东省",
+ "name": "茂名市",
+ "id": "440900"
+ },
+ {
+ "province": "广东省",
+ "name": "肇庆市",
+ "id": "441200"
+ },
+ {
+ "province": "广东省",
+ "name": "惠州市",
+ "id": "441300"
+ },
+ {
+ "province": "广东省",
+ "name": "梅州市",
+ "id": "441400"
+ },
+ {
+ "province": "广东省",
+ "name": "汕尾市",
+ "id": "441500"
+ },
+ {
+ "province": "广东省",
+ "name": "河源市",
+ "id": "441600"
+ },
+ {
+ "province": "广东省",
+ "name": "阳江市",
+ "id": "441700"
+ },
+ {
+ "province": "广东省",
+ "name": "清远市",
+ "id": "441800"
+ },
+ {
+ "province": "广东省",
+ "name": "东莞市",
+ "id": "441900"
+ },
+ {
+ "province": "广东省",
+ "name": "中山市",
+ "id": "442000"
+ },
+ {
+ "province": "广东省",
+ "name": "潮州市",
+ "id": "445100"
+ },
+ {
+ "province": "广东省",
+ "name": "揭阳市",
+ "id": "445200"
+ },
+ {
+ "province": "广东省",
+ "name": "云浮市",
+ "id": "445300"
+ }
+ ],
+ "450000": [
+ {
+ "province": "广西壮族自治区",
+ "name": "南宁市",
+ "id": "450100"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "柳州市",
+ "id": "450200"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "桂林市",
+ "id": "450300"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "梧州市",
+ "id": "450400"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "北海市",
+ "id": "450500"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "防城港市",
+ "id": "450600"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "钦州市",
+ "id": "450700"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "贵港市",
+ "id": "450800"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "玉林市",
+ "id": "450900"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "百色市",
+ "id": "451000"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "贺州市",
+ "id": "451100"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "河池市",
+ "id": "451200"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "来宾市",
+ "id": "451300"
+ },
+ {
+ "province": "广西壮族自治区",
+ "name": "崇左市",
+ "id": "451400"
+ }
+ ],
+ "460000": [
+ {
+ "province": "海南省",
+ "name": "海口市",
+ "id": "460100"
+ },
+ {
+ "province": "海南省",
+ "name": "三亚市",
+ "id": "460200"
+ },
+ {
+ "province": "海南省",
+ "name": "三沙市",
+ "id": "460300"
+ },
+ {
+ "province": "海南省",
+ "name": "儋州市",
+ "id": "460400"
+ },
+ {
+ "province": "海南省",
+ "name": "省直辖县级行政区划",
+ "id": "469000"
+ }
+ ],
+ "500000": [
+ {
+ "province": "重庆市",
+ "name": "市辖区",
+ "id": "500100"
+ },
+ {
+ "province": "重庆市",
+ "name": "县",
+ "id": "500200"
+ }
+ ],
+ "510000": [
+ {
+ "province": "四川省",
+ "name": "成都市",
+ "id": "510100"
+ },
+ {
+ "province": "四川省",
+ "name": "自贡市",
+ "id": "510300"
+ },
+ {
+ "province": "四川省",
+ "name": "攀枝花市",
+ "id": "510400"
+ },
+ {
+ "province": "四川省",
+ "name": "泸州市",
+ "id": "510500"
+ },
+ {
+ "province": "四川省",
+ "name": "德阳市",
+ "id": "510600"
+ },
+ {
+ "province": "四川省",
+ "name": "绵阳市",
+ "id": "510700"
+ },
+ {
+ "province": "四川省",
+ "name": "广元市",
+ "id": "510800"
+ },
+ {
+ "province": "四川省",
+ "name": "遂宁市",
+ "id": "510900"
+ },
+ {
+ "province": "四川省",
+ "name": "内江市",
+ "id": "511000"
+ },
+ {
+ "province": "四川省",
+ "name": "乐山市",
+ "id": "511100"
+ },
+ {
+ "province": "四川省",
+ "name": "南充市",
+ "id": "511300"
+ },
+ {
+ "province": "四川省",
+ "name": "眉山市",
+ "id": "511400"
+ },
+ {
+ "province": "四川省",
+ "name": "宜宾市",
+ "id": "511500"
+ },
+ {
+ "province": "四川省",
+ "name": "广安市",
+ "id": "511600"
+ },
+ {
+ "province": "四川省",
+ "name": "达州市",
+ "id": "511700"
+ },
+ {
+ "province": "四川省",
+ "name": "雅安市",
+ "id": "511800"
+ },
+ {
+ "province": "四川省",
+ "name": "巴中市",
+ "id": "511900"
+ },
+ {
+ "province": "四川省",
+ "name": "资阳市",
+ "id": "512000"
+ },
+ {
+ "province": "四川省",
+ "name": "阿坝藏族羌族自治州",
+ "id": "513200"
+ },
+ {
+ "province": "四川省",
+ "name": "甘孜藏族自治州",
+ "id": "513300"
+ },
+ {
+ "province": "四川省",
+ "name": "凉山彝族自治州",
+ "id": "513400"
+ }
+ ],
+ "520000": [
+ {
+ "province": "贵州省",
+ "name": "贵阳市",
+ "id": "520100"
+ },
+ {
+ "province": "贵州省",
+ "name": "六盘水市",
+ "id": "520200"
+ },
+ {
+ "province": "贵州省",
+ "name": "遵义市",
+ "id": "520300"
+ },
+ {
+ "province": "贵州省",
+ "name": "安顺市",
+ "id": "520400"
+ },
+ {
+ "province": "贵州省",
+ "name": "毕节市",
+ "id": "520500"
+ },
+ {
+ "province": "贵州省",
+ "name": "铜仁市",
+ "id": "520600"
+ },
+ {
+ "province": "贵州省",
+ "name": "黔西南布依族苗族自治州",
+ "id": "522300"
+ },
+ {
+ "province": "贵州省",
+ "name": "黔东南苗族侗族自治州",
+ "id": "522600"
+ },
+ {
+ "province": "贵州省",
+ "name": "黔南布依族苗族自治州",
+ "id": "522700"
+ }
+ ],
+ "530000": [
+ {
+ "province": "云南省",
+ "name": "昆明市",
+ "id": "530100"
+ },
+ {
+ "province": "云南省",
+ "name": "曲靖市",
+ "id": "530300"
+ },
+ {
+ "province": "云南省",
+ "name": "玉溪市",
+ "id": "530400"
+ },
+ {
+ "province": "云南省",
+ "name": "保山市",
+ "id": "530500"
+ },
+ {
+ "province": "云南省",
+ "name": "昭通市",
+ "id": "530600"
+ },
+ {
+ "province": "云南省",
+ "name": "丽江市",
+ "id": "530700"
+ },
+ {
+ "province": "云南省",
+ "name": "普洱市",
+ "id": "530800"
+ },
+ {
+ "province": "云南省",
+ "name": "临沧市",
+ "id": "530900"
+ },
+ {
+ "province": "云南省",
+ "name": "楚雄彝族自治州",
+ "id": "532300"
+ },
+ {
+ "province": "云南省",
+ "name": "红河哈尼族彝族自治州",
+ "id": "532500"
+ },
+ {
+ "province": "云南省",
+ "name": "文山壮族苗族自治州",
+ "id": "532600"
+ },
+ {
+ "province": "云南省",
+ "name": "西双版纳傣族自治州",
+ "id": "532800"
+ },
+ {
+ "province": "云南省",
+ "name": "大理白族自治州",
+ "id": "532900"
+ },
+ {
+ "province": "云南省",
+ "name": "德宏傣族景颇族自治州",
+ "id": "533100"
+ },
+ {
+ "province": "云南省",
+ "name": "怒江傈僳族自治州",
+ "id": "533300"
+ },
+ {
+ "province": "云南省",
+ "name": "迪庆藏族自治州",
+ "id": "533400"
+ }
+ ],
+ "540000": [
+ {
+ "province": "西藏自治区",
+ "name": "拉萨市",
+ "id": "540100"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "日喀则市",
+ "id": "540200"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "昌都市",
+ "id": "540300"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "林芝市",
+ "id": "540400"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "山南市",
+ "id": "540500"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "那曲地区",
+ "id": "542400"
+ },
+ {
+ "province": "西藏自治区",
+ "name": "阿里地区",
+ "id": "542500"
+ }
+ ],
+ "610000": [
+ {
+ "province": "陕西省",
+ "name": "西安市",
+ "id": "610100"
+ },
+ {
+ "province": "陕西省",
+ "name": "铜川市",
+ "id": "610200"
+ },
+ {
+ "province": "陕西省",
+ "name": "宝鸡市",
+ "id": "610300"
+ },
+ {
+ "province": "陕西省",
+ "name": "咸阳市",
+ "id": "610400"
+ },
+ {
+ "province": "陕西省",
+ "name": "渭南市",
+ "id": "610500"
+ },
+ {
+ "province": "陕西省",
+ "name": "延安市",
+ "id": "610600"
+ },
+ {
+ "province": "陕西省",
+ "name": "汉中市",
+ "id": "610700"
+ },
+ {
+ "province": "陕西省",
+ "name": "榆林市",
+ "id": "610800"
+ },
+ {
+ "province": "陕西省",
+ "name": "安康市",
+ "id": "610900"
+ },
+ {
+ "province": "陕西省",
+ "name": "商洛市",
+ "id": "611000"
+ }
+ ],
+ "620000": [
+ {
+ "province": "甘肃省",
+ "name": "兰州市",
+ "id": "620100"
+ },
+ {
+ "province": "甘肃省",
+ "name": "嘉峪关市",
+ "id": "620200"
+ },
+ {
+ "province": "甘肃省",
+ "name": "金昌市",
+ "id": "620300"
+ },
+ {
+ "province": "甘肃省",
+ "name": "白银市",
+ "id": "620400"
+ },
+ {
+ "province": "甘肃省",
+ "name": "天水市",
+ "id": "620500"
+ },
+ {
+ "province": "甘肃省",
+ "name": "武威市",
+ "id": "620600"
+ },
+ {
+ "province": "甘肃省",
+ "name": "张掖市",
+ "id": "620700"
+ },
+ {
+ "province": "甘肃省",
+ "name": "平凉市",
+ "id": "620800"
+ },
+ {
+ "province": "甘肃省",
+ "name": "酒泉市",
+ "id": "620900"
+ },
+ {
+ "province": "甘肃省",
+ "name": "庆阳市",
+ "id": "621000"
+ },
+ {
+ "province": "甘肃省",
+ "name": "定西市",
+ "id": "621100"
+ },
+ {
+ "province": "甘肃省",
+ "name": "陇南市",
+ "id": "621200"
+ },
+ {
+ "province": "甘肃省",
+ "name": "临夏回族自治州",
+ "id": "622900"
+ },
+ {
+ "province": "甘肃省",
+ "name": "甘南藏族自治州",
+ "id": "623000"
+ }
+ ],
+ "630000": [
+ {
+ "province": "青海省",
+ "name": "西宁市",
+ "id": "630100"
+ },
+ {
+ "province": "青海省",
+ "name": "海东市",
+ "id": "630200"
+ },
+ {
+ "province": "青海省",
+ "name": "海北藏族自治州",
+ "id": "632200"
+ },
+ {
+ "province": "青海省",
+ "name": "黄南藏族自治州",
+ "id": "632300"
+ },
+ {
+ "province": "青海省",
+ "name": "海南藏族自治州",
+ "id": "632500"
+ },
+ {
+ "province": "青海省",
+ "name": "果洛藏族自治州",
+ "id": "632600"
+ },
+ {
+ "province": "青海省",
+ "name": "玉树藏族自治州",
+ "id": "632700"
+ },
+ {
+ "province": "青海省",
+ "name": "海西蒙古族藏族自治州",
+ "id": "632800"
+ }
+ ],
+ "640000": [
+ {
+ "province": "宁夏回族自治区",
+ "name": "银川市",
+ "id": "640100"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "石嘴山市",
+ "id": "640200"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "吴忠市",
+ "id": "640300"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "固原市",
+ "id": "640400"
+ },
+ {
+ "province": "宁夏回族自治区",
+ "name": "中卫市",
+ "id": "640500"
+ }
+ ],
+ "650000": [
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "乌鲁木齐市",
+ "id": "650100"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "克拉玛依市",
+ "id": "650200"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "吐鲁番市",
+ "id": "650400"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "哈密市",
+ "id": "650500"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "昌吉回族自治州",
+ "id": "652300"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "博尔塔拉蒙古自治州",
+ "id": "652700"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "巴音郭楞蒙古自治州",
+ "id": "652800"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "阿克苏地区",
+ "id": "652900"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "克孜勒苏柯尔克孜自治州",
+ "id": "653000"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "喀什地区",
+ "id": "653100"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "和田地区",
+ "id": "653200"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "伊犁哈萨克自治州",
+ "id": "654000"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "塔城地区",
+ "id": "654200"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "阿勒泰地区",
+ "id": "654300"
+ },
+ {
+ "province": "新疆维吾尔自治区",
+ "name": "自治区直辖县级行政区划",
+ "id": "659000"
+ }
+ ]
+}
diff --git a/src/pages/account/settings/geographic/province.json b/src/pages/account/settings/geographic/province.json
new file mode 100644
index 0000000..910c83f
--- /dev/null
+++ b/src/pages/account/settings/geographic/province.json
@@ -0,0 +1,138 @@
+[
+ {
+ "name": "北京市",
+ "id": "110000"
+ },
+ {
+ "name": "天津市",
+ "id": "120000"
+ },
+ {
+ "name": "河北省",
+ "id": "130000"
+ },
+ {
+ "name": "山西省",
+ "id": "140000"
+ },
+ {
+ "name": "内蒙古自治区",
+ "id": "150000"
+ },
+ {
+ "name": "辽宁省",
+ "id": "210000"
+ },
+ {
+ "name": "吉林省",
+ "id": "220000"
+ },
+ {
+ "name": "黑龙江省",
+ "id": "230000"
+ },
+ {
+ "name": "上海市",
+ "id": "310000"
+ },
+ {
+ "name": "江苏省",
+ "id": "320000"
+ },
+ {
+ "name": "浙江省",
+ "id": "330000"
+ },
+ {
+ "name": "安徽省",
+ "id": "340000"
+ },
+ {
+ "name": "福建省",
+ "id": "350000"
+ },
+ {
+ "name": "江西省",
+ "id": "360000"
+ },
+ {
+ "name": "山东省",
+ "id": "370000"
+ },
+ {
+ "name": "河南省",
+ "id": "410000"
+ },
+ {
+ "name": "湖北省",
+ "id": "420000"
+ },
+ {
+ "name": "湖南省",
+ "id": "430000"
+ },
+ {
+ "name": "广东省",
+ "id": "440000"
+ },
+ {
+ "name": "广西壮族自治区",
+ "id": "450000"
+ },
+ {
+ "name": "海南省",
+ "id": "460000"
+ },
+ {
+ "name": "重庆市",
+ "id": "500000"
+ },
+ {
+ "name": "四川省",
+ "id": "510000"
+ },
+ {
+ "name": "贵州省",
+ "id": "520000"
+ },
+ {
+ "name": "云南省",
+ "id": "530000"
+ },
+ {
+ "name": "西藏自治区",
+ "id": "540000"
+ },
+ {
+ "name": "陕西省",
+ "id": "610000"
+ },
+ {
+ "name": "甘肃省",
+ "id": "620000"
+ },
+ {
+ "name": "青海省",
+ "id": "630000"
+ },
+ {
+ "name": "宁夏回族自治区",
+ "id": "640000"
+ },
+ {
+ "name": "新疆维吾尔自治区",
+ "id": "650000"
+ },
+ {
+ "name": "台湾省",
+ "id": "710000"
+ },
+ {
+ "name": "香港特别行政区",
+ "id": "810000"
+ },
+ {
+ "name": "澳门特别行政区",
+ "id": "820000"
+ }
+]
diff --git a/src/pages/account/settings/index.tsx b/src/pages/account/settings/index.tsx
new file mode 100644
index 0000000..d5131ec
--- /dev/null
+++ b/src/pages/account/settings/index.tsx
@@ -0,0 +1,111 @@
+import React, { useState, useRef, useLayoutEffect } from 'react';
+import { GridContent } from '@ant-design/pro-layout';
+import { Menu } from 'antd';
+import BaseView from './components/base';
+import BindingView from './components/binding';
+import NotificationView from './components/notification';
+import SecurityView from './components/security';
+import styles from './style.less';
+
+const { Item } = Menu;
+
+type SettingsStateKeys = 'base' | 'security' | 'binding' | 'notification';
+type SettingsState = {
+ mode: 'inline' | 'horizontal';
+ selectKey: SettingsStateKeys;
+};
+
+const Settings: React.FC = () => {
+ const menuMap: Record = {
+ base: '基本设置',
+ security: '安全设置',
+ binding: '账号绑定',
+ notification: '新消息通知',
+ };
+
+ const [initConfig, setInitConfig] = useState({
+ mode: 'inline',
+ selectKey: 'base',
+ });
+ const dom = useRef();
+
+ const resize = () => {
+ requestAnimationFrame(() => {
+ if (!dom.current) {
+ return;
+ }
+ let mode: 'inline' | 'horizontal' = 'inline';
+ const { offsetWidth } = dom.current;
+ if (dom.current.offsetWidth < 641 && offsetWidth > 400) {
+ mode = 'horizontal';
+ }
+ if (window.innerWidth < 768 && offsetWidth > 400) {
+ mode = 'horizontal';
+ }
+ setInitConfig({ ...initConfig, mode: mode as SettingsState['mode'] });
+ });
+ };
+
+ useLayoutEffect(() => {
+ if (dom.current) {
+ window.addEventListener('resize', resize);
+ resize();
+ }
+ return () => {
+ window.removeEventListener('resize', resize);
+ };
+ }, [dom.current]);
+
+ const getMenu = () => {
+ return Object.keys(menuMap).map((item) => - {menuMap[item]}
);
+ };
+
+ const renderChildren = () => {
+ const { selectKey } = initConfig;
+ switch (selectKey) {
+ case 'base':
+ return ;
+ case 'security':
+ return ;
+ case 'binding':
+ return ;
+ case 'notification':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {
+ if (ref) {
+ dom.current = ref;
+ }
+ }}
+ >
+
+
+
+
+
{menuMap[initConfig.selectKey]}
+ {renderChildren()}
+
+
+
+ );
+};
+export default Settings;
diff --git a/src/pages/account/settings/service.ts b/src/pages/account/settings/service.ts
new file mode 100644
index 0000000..5982865
--- /dev/null
+++ b/src/pages/account/settings/service.ts
@@ -0,0 +1,18 @@
+import { request } from 'umi';
+import type { GeographicItemType } from './data';
+
+export async function queryCurrentUserInfo(): Promise<{ data: API.GetUserInfoResult }> {
+ return { data: await request('/api/getInfo') }
+}
+
+export async function queryProvince(): Promise<{ data: GeographicItemType[] }> {
+ return request('/api/geographic/province');
+}
+
+export async function queryCity(province: string): Promise<{ data: GeographicItemType[] }> {
+ return request(`/api/geographic/city/${province}`);
+}
+
+export async function query() {
+ return request('/api/users');
+}
diff --git a/src/pages/account/settings/style.less b/src/pages/account/settings/style.less
new file mode 100644
index 0000000..fee54c2
--- /dev/null
+++ b/src/pages/account/settings/style.less
@@ -0,0 +1,93 @@
+@import '~antd/es/style/themes/default.less';
+
+.main {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ padding-top: 16px;
+ padding-bottom: 16px;
+ background-color: @menu-bg;
+ .leftMenu {
+ width: 224px;
+ border-right: @border-width-base @border-style-base @border-color-split;
+ :global {
+ .ant-menu-inline {
+ border: none;
+ }
+ .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
+ font-weight: bold;
+ }
+ }
+ }
+ .right {
+ flex: 1;
+ padding: 8px 40px;
+ .title {
+ margin-bottom: 12px;
+ color: @heading-color;
+ font-weight: 500;
+ font-size: 20px;
+ line-height: 28px;
+ }
+ }
+ :global {
+ .ant-list-split .ant-list-item:last-child {
+ border-bottom: 1px solid @border-color-split;
+ }
+ .ant-list-item {
+ padding-top: 14px;
+ padding-bottom: 14px;
+ }
+ }
+}
+:global {
+ .ant-list-item-meta {
+ // 账号绑定图标
+ .taobao {
+ display: block;
+ color: #ff4000;
+ font-size: 48px;
+ line-height: 48px;
+ border-radius: @border-radius-base;
+ }
+ .dingding {
+ margin: 2px;
+ padding: 6px;
+ color: #fff;
+ font-size: 32px;
+ line-height: 32px;
+ background-color: #2eabff;
+ border-radius: @border-radius-base;
+ }
+ .alipay {
+ color: #2eabff;
+ font-size: 48px;
+ line-height: 48px;
+ border-radius: @border-radius-base;
+ }
+ }
+
+ // 密码强度
+ font.strong {
+ color: @success-color;
+ }
+ font.medium {
+ color: @warning-color;
+ }
+ font.weak {
+ color: @error-color;
+ }
+}
+
+@media screen and (max-width: @screen-md) {
+ .main {
+ flex-direction: column;
+ .leftMenu {
+ width: 100%;
+ border: none;
+ }
+ .right {
+ padding: 40px;
+ }
+ }
+}
diff --git a/src/pages/dashboard/analysis/_mock.ts b/src/pages/dashboard/analysis/_mock.ts
new file mode 100644
index 0000000..e811c58
--- /dev/null
+++ b/src/pages/dashboard/analysis/_mock.ts
@@ -0,0 +1,210 @@
+import moment from 'moment';
+import type { Request, Response } from 'express';
+import type { AnalysisData, RadarData, DataItem } from './data.d';
+
+// mock data
+const visitData: DataItem[] = [];
+const beginDay = new Date().getTime();
+
+const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
+for (let i = 0; i < fakeY.length; i += 1) {
+ visitData.push({
+ x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY[i],
+ });
+}
+
+const visitData2 = [];
+const fakeY2 = [1, 6, 4, 8, 3, 7, 2];
+for (let i = 0; i < fakeY2.length; i += 1) {
+ visitData2.push({
+ x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format('YYYY-MM-DD'),
+ y: fakeY2[i],
+ });
+}
+
+const salesData = [];
+for (let i = 0; i < 12; i += 1) {
+ salesData.push({
+ x: `${i + 1}月`,
+ y: Math.floor(Math.random() * 1000) + 200,
+ });
+}
+const searchData = [];
+for (let i = 0; i < 50; i += 1) {
+ searchData.push({
+ index: i + 1,
+ keyword: `搜索关键词-${i}`,
+ count: Math.floor(Math.random() * 1000),
+ range: Math.floor(Math.random() * 100),
+ status: Math.floor((Math.random() * 10) % 2),
+ });
+}
+const salesTypeData = [
+ {
+ x: '家用电器',
+ y: 4544,
+ },
+ {
+ x: '食用酒水',
+ y: 3321,
+ },
+ {
+ x: '个护健康',
+ y: 3113,
+ },
+ {
+ x: '服饰箱包',
+ y: 2341,
+ },
+ {
+ x: '母婴产品',
+ y: 1231,
+ },
+ {
+ x: '其他',
+ y: 1231,
+ },
+];
+
+const salesTypeDataOnline = [
+ {
+ x: '家用电器',
+ y: 244,
+ },
+ {
+ x: '食用酒水',
+ y: 321,
+ },
+ {
+ x: '个护健康',
+ y: 311,
+ },
+ {
+ x: '服饰箱包',
+ y: 41,
+ },
+ {
+ x: '母婴产品',
+ y: 121,
+ },
+ {
+ x: '其他',
+ y: 111,
+ },
+];
+
+const salesTypeDataOffline = [
+ {
+ x: '家用电器',
+ y: 99,
+ },
+ {
+ x: '食用酒水',
+ y: 188,
+ },
+ {
+ x: '个护健康',
+ y: 344,
+ },
+ {
+ x: '服饰箱包',
+ y: 255,
+ },
+ {
+ x: '其他',
+ y: 65,
+ },
+];
+
+const offlineData = [];
+for (let i = 0; i < 10; i += 1) {
+ offlineData.push({
+ name: `Stores ${i}`,
+ cvr: Math.ceil(Math.random() * 9) / 10,
+ });
+}
+const offlineChartData = [];
+for (let i = 0; i < 20; i += 1) {
+ const date = moment(new Date().getTime() + 1000 * 60 * 30 * i).format('HH:mm');
+ offlineChartData.push({
+ date,
+ type: '客流量',
+ value: Math.floor(Math.random() * 100) + 10,
+ });
+ offlineChartData.push({
+ date,
+ type: '支付笔数',
+ value: Math.floor(Math.random() * 100) + 10,
+ });
+}
+
+const radarOriginData = [
+ {
+ name: '个人',
+ ref: 10,
+ koubei: 8,
+ output: 4,
+ contribute: 5,
+ hot: 7,
+ },
+ {
+ name: '团队',
+ ref: 3,
+ koubei: 9,
+ output: 6,
+ contribute: 3,
+ hot: 1,
+ },
+ {
+ name: '部门',
+ ref: 4,
+ koubei: 1,
+ output: 6,
+ contribute: 5,
+ hot: 7,
+ },
+];
+
+const radarData: RadarData[] = [];
+const radarTitleMap = {
+ ref: '引用',
+ koubei: '口碑',
+ output: '产量',
+ contribute: '贡献',
+ hot: '热度',
+};
+radarOriginData.forEach((item) => {
+ Object.keys(item).forEach((key) => {
+ if (key !== 'name') {
+ radarData.push({
+ name: item.name,
+ label: radarTitleMap[key],
+ value: item[key],
+ });
+ }
+ });
+});
+
+const getFakeChartData: AnalysisData = {
+ visitData,
+ visitData2,
+ salesData,
+ searchData,
+ offlineData,
+ offlineChartData,
+ salesTypeData,
+ salesTypeDataOnline,
+ salesTypeDataOffline,
+ radarData,
+};
+
+const fakeChartData = (_: Request, res: Response) => {
+ return res.json({
+ data: getFakeChartData,
+ });
+};
+
+export default {
+ 'GET /api/fake_analysis_chart_data': fakeChartData,
+};
diff --git a/src/pages/dashboard/analysis/components/Charts/Bar/index.tsx b/src/pages/dashboard/analysis/components/Charts/Bar/index.tsx
new file mode 100644
index 0000000..c2650ff
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Bar/index.tsx
@@ -0,0 +1,133 @@
+import { Axis, Chart, Geom, Tooltip } from 'bizcharts';
+import React, { Component } from 'react';
+
+import Debounce from 'lodash.debounce';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export type BarProps = {
+ title: React.ReactNode;
+ color?: string;
+ padding?: [number, number, number, number];
+ height?: number;
+ data: {
+ x: string;
+ y: number;
+ }[];
+ forceFit?: boolean;
+ autoLabel?: boolean;
+ style?: React.CSSProperties;
+};
+
+class Bar extends Component<
+ BarProps,
+ {
+ autoHideXLabels: boolean;
+ }
+> {
+ state = {
+ autoHideXLabels: false,
+ };
+
+ root: HTMLDivElement | undefined = undefined;
+
+ node: HTMLDivElement | undefined = undefined;
+
+ resize = Debounce(() => {
+ if (!this.node || !this.node.parentNode) {
+ return;
+ }
+ const canvasWidth = (this.node.parentNode as HTMLDivElement).clientWidth;
+ const { data = [], autoLabel = true } = this.props;
+ if (!autoLabel) {
+ return;
+ }
+ const minWidth = data.length * 30;
+ const { autoHideXLabels } = this.state;
+
+ if (canvasWidth <= minWidth) {
+ if (!autoHideXLabels) {
+ this.setState({
+ autoHideXLabels: true,
+ });
+ }
+ } else if (autoHideXLabels) {
+ this.setState({
+ autoHideXLabels: false,
+ });
+ }
+ }, 500);
+
+ componentDidMount() {
+ window.addEventListener('resize', this.resize, { passive: true });
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.resize);
+ }
+
+ handleRoot = (n: HTMLDivElement) => {
+ this.root = n;
+ };
+
+ handleRef = (n: HTMLDivElement) => {
+ this.node = n;
+ };
+
+ render() {
+ const {
+ height = 1,
+ title,
+ forceFit = true,
+ data,
+ color = 'rgba(24, 144, 255, 0.85)',
+ padding,
+ } = this.props;
+
+ const { autoHideXLabels } = this.state;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ return (
+
+
+ {title &&
{title}
}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default autoHeight()(Bar);
diff --git a/src/pages/dashboard/analysis/components/Charts/ChartCard/index.less b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.less
new file mode 100644
index 0000000..d7bf6dd
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.less
@@ -0,0 +1,75 @@
+@import '~antd/es/style/themes/default.less';
+
+.chartCard {
+ position: relative;
+ .chartTop {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+ }
+ .chartTopMargin {
+ margin-bottom: 12px;
+ }
+ .chartTopHasMargin {
+ margin-bottom: 20px;
+ }
+ .metaWrap {
+ float: left;
+ }
+ .avatar {
+ position: relative;
+ top: 4px;
+ float: left;
+ margin-right: 20px;
+ img {
+ border-radius: 100%;
+ }
+ }
+ .meta {
+ height: 22px;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .action {
+ position: absolute;
+ top: 4px;
+ right: 0;
+ line-height: 1;
+ cursor: pointer;
+ }
+ .total {
+ height: 38px;
+ margin-top: 4px;
+ margin-bottom: 0;
+ overflow: hidden;
+ color: @heading-color;
+ font-size: 30px;
+ line-height: 38px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+ .content {
+ position: relative;
+ width: 100%;
+ margin-bottom: 12px;
+ }
+ .contentFixed {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ }
+ .footer {
+ margin-top: 8px;
+ padding-top: 9px;
+ border-top: 1px solid @border-color-split;
+ & > * {
+ position: relative;
+ }
+ }
+ .footerMargin {
+ margin-top: 20px;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
new file mode 100644
index 0000000..e860582
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
@@ -0,0 +1,97 @@
+import { Card } from 'antd';
+import type { CardProps } from 'antd/es/card';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+type totalType = () => React.ReactNode;
+
+const renderTotal = (total?: number | totalType | React.ReactNode) => {
+ if (!total && total !== 0) {
+ return null;
+ }
+ let totalDom;
+ switch (typeof total) {
+ case 'undefined':
+ totalDom = null;
+ break;
+ case 'function':
+ totalDom = {total()}
;
+ break;
+ default:
+ totalDom = {total}
;
+ }
+ return totalDom;
+};
+
+export type ChartCardProps = {
+ title: React.ReactNode;
+ action?: React.ReactNode;
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
+ footer?: React.ReactNode;
+ contentHeight?: number;
+ avatar?: React.ReactNode;
+ style?: React.CSSProperties;
+} & CardProps;
+
+class ChartCard extends React.Component {
+ renderContent = () => {
+ const { contentHeight, title, avatar, action, total, footer, children, loading } = this.props;
+ if (loading) {
+ return false;
+ }
+ return (
+
+
+
{avatar}
+
+
+ {title}
+ {action}
+
+ {renderTotal(total)}
+
+
+ {children && (
+
+ )}
+ {footer && (
+
+ {footer}
+
+ )}
+
+ );
+ };
+
+ render() {
+ const {
+ loading = false,
+ contentHeight,
+ title,
+ avatar,
+ action,
+ total,
+ footer,
+ children,
+ ...rest
+ } = this.props;
+ return (
+
+ {this.renderContent()}
+
+ );
+ }
+}
+
+export default ChartCard;
diff --git a/src/pages/dashboard/analysis/components/Charts/Field/index.less b/src/pages/dashboard/analysis/components/Charts/Field/index.less
new file mode 100644
index 0000000..4fe0d1f
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Field/index.less
@@ -0,0 +1,17 @@
+@import '~antd/es/style/themes/default.less';
+
+.field {
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ .label,
+ .number {
+ font-size: @font-size-base;
+ line-height: 22px;
+ }
+ .number {
+ margin-left: 8px;
+ color: @heading-color;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Charts/Field/index.tsx b/src/pages/dashboard/analysis/components/Charts/Field/index.tsx
new file mode 100644
index 0000000..a6fb64a
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Field/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import styles from './index.less';
+
+export type FieldProps = {
+ label: React.ReactNode;
+ value: React.ReactNode;
+ style?: React.CSSProperties;
+};
+
+const Field: React.FC = ({ label, value, ...rest }) => (
+
+ {label}
+ {value}
+
+);
+
+export default Field;
diff --git a/src/pages/dashboard/analysis/components/Charts/Gauge/index.tsx b/src/pages/dashboard/analysis/components/Charts/Gauge/index.tsx
new file mode 100644
index 0000000..b3e5047
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Gauge/index.tsx
@@ -0,0 +1,179 @@
+import { Axis, Chart, Coord, Geom, Guide, Shape } from 'bizcharts';
+
+import React from 'react';
+import autoHeight from '../autoHeight';
+
+const { Arc, Html, Line } = Guide;
+
+export type GaugeProps = {
+ title: React.ReactNode;
+ color?: string;
+ height?: number;
+ bgColor?: number;
+ percent: number;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+ formatter: (value: string) => string;
+};
+
+const defaultFormatter = (val: string): string => {
+ switch (val) {
+ case '2':
+ return '差';
+ case '4':
+ return '中';
+ case '6':
+ return '良';
+ case '8':
+ return '优';
+ default:
+ return '';
+ }
+};
+
+if (Shape.registerShape) {
+ Shape.registerShape('point', 'pointer', {
+ drawShape(cfg: any, group: any) {
+ let point = cfg.points[0];
+ point = (this as any).parsePoint(point);
+ const center = (this as any).parsePoint({
+ x: 0,
+ y: 0,
+ });
+ group.addShape('line', {
+ attrs: {
+ x1: center.x,
+ y1: center.y,
+ x2: point.x,
+ y2: point.y,
+ stroke: cfg.color,
+ lineWidth: 2,
+ lineCap: 'round',
+ },
+ });
+ return group.addShape('circle', {
+ attrs: {
+ x: center.x,
+ y: center.y,
+ r: 6,
+ stroke: cfg.color,
+ lineWidth: 3,
+ fill: '#fff',
+ },
+ });
+ },
+ });
+}
+
+const Gauge: React.FC = (props) => {
+ const {
+ title,
+ height = 1,
+ percent,
+ forceFit = true,
+ formatter = defaultFormatter,
+ color = '#2F9CFF',
+ bgColor = '#F0F2F5',
+ } = props;
+ const cols = {
+ value: {
+ type: 'linear',
+ min: 0,
+ max: 10,
+ tickCount: 6,
+ nice: true,
+ },
+ };
+ const data = [{ value: percent / 10 }];
+ const renderHtml = () => `
+
+
${title}
+
+ ${(data[0].value * 10).toFixed(2)}%
+
+
`;
+ const textStyle: {
+ fontSize: number;
+ fill: string;
+ textAlign: 'center';
+ } = {
+ fontSize: 12,
+ fill: 'rgba(0, 0, 0, 0.65)',
+ textAlign: 'center',
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default autoHeight()(Gauge);
diff --git a/src/pages/dashboard/analysis/components/Charts/MiniArea/index.tsx b/src/pages/dashboard/analysis/components/Charts/MiniArea/index.tsx
new file mode 100644
index 0000000..5b20ae8
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/MiniArea/index.tsx
@@ -0,0 +1,131 @@
+import type { AxisProps } from 'bizcharts';
+import { Axis, Chart, Geom, Tooltip } from 'bizcharts';
+
+import React from 'react';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export type MiniAreaProps = {
+ color?: string;
+ height?: number;
+ borderColor?: string;
+ line?: boolean;
+ animate?: boolean;
+ xAxis?: AxisProps;
+ forceFit?: boolean;
+ scale?: {
+ x?: {
+ tickCount: number;
+ };
+ y?: {
+ tickCount: number;
+ };
+ };
+ yAxis?: Partial;
+ borderWidth?: number;
+ data: {
+ x: number | string;
+ y: number;
+ }[];
+};
+
+const MiniArea: React.FC = (props) => {
+ const {
+ height = 1,
+ data = [],
+ forceFit = true,
+ color = 'rgba(24, 144, 255, 0.2)',
+ borderColor = '#1089ff',
+ scale = { x: {}, y: {} },
+ borderWidth = 2,
+ line,
+ xAxis,
+ yAxis,
+ animate = true,
+ } = props;
+
+ const padding: [number, number, number, number] = [36, 5, 30, 5];
+
+ const scaleProps = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ ...scale.x,
+ },
+ y: {
+ min: 0,
+ ...scale.y,
+ },
+ };
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ const chartHeight = height + 54;
+
+ return (
+
+
+ {height > 0 && (
+
+
+
+
+
+ {line ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default autoHeight()(MiniArea);
diff --git a/src/pages/dashboard/analysis/components/Charts/MiniBar/index.tsx b/src/pages/dashboard/analysis/components/Charts/MiniBar/index.tsx
new file mode 100644
index 0000000..46be276
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/MiniBar/index.tsx
@@ -0,0 +1,54 @@
+import { Chart, Geom, Tooltip } from 'bizcharts';
+
+import React from 'react';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export type MiniBarProps = {
+ color?: string;
+ height?: number;
+ data: {
+ x: number | string;
+ y: number;
+ }[];
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+};
+
+const MiniBar: React.FC = (props) => {
+ const { height = 0, forceFit = true, color = '#1890FF', data = [] } = props;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ const padding: [number, number, number, number] = [36, 5, 30, 5];
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ // for tooltip not to be hide
+ const chartHeight = height + 54;
+
+ return (
+
+ );
+};
+export default autoHeight()(MiniBar);
diff --git a/src/pages/dashboard/analysis/components/Charts/MiniProgress/index.tsx b/src/pages/dashboard/analysis/components/Charts/MiniProgress/index.tsx
new file mode 100644
index 0000000..ad0a2d2
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/MiniProgress/index.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { Tooltip } from 'antd';
+import styles from './index.less';
+
+export type MiniProgressProps = {
+ target: number;
+ targetLabel?: string;
+ color?: string;
+ strokeWidth?: number;
+ percent?: number;
+ style?: React.CSSProperties;
+};
+
+const MiniProgress: React.FC = ({
+ targetLabel,
+ target,
+ color = 'rgb(19, 194, 194)',
+ strokeWidth,
+ percent,
+}) => (
+
+);
+
+export default MiniProgress;
diff --git a/src/pages/dashboard/analysis/components/Charts/Pie/index.tsx b/src/pages/dashboard/analysis/components/Charts/Pie/index.tsx
new file mode 100644
index 0000000..a8928de
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/Pie/index.tsx
@@ -0,0 +1,309 @@
+import { Chart, Coord, Geom, Tooltip } from 'bizcharts';
+import React, { Component } from 'react';
+
+import { DataView } from '@antv/data-set';
+import Debounce from 'lodash.debounce';
+import { Divider } from 'antd';
+import ReactFitText from 'react-fittext';
+import classNames from 'classnames';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export type PieProps = {
+ animate?: boolean;
+ color?: string;
+ colors?: string[];
+ selected?: boolean;
+ height?: number;
+ margin?: [number, number, number, number];
+ hasLegend?: boolean;
+ padding?: [number, number, number, number];
+ percent?: number;
+ data?: {
+ x: string | string;
+ y: number;
+ }[];
+ inner?: number;
+ lineWidth?: number;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+ className?: string;
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
+ title?: React.ReactNode;
+ tooltip?: boolean;
+ valueFormat?: (value: string) => string | React.ReactNode;
+ subTitle?: React.ReactNode;
+};
+type PieState = {
+ legendData: { checked: boolean; x: string; color: string; percent: number; y: string }[];
+ legendBlock: boolean;
+};
+class Pie extends Component {
+ state: PieState = {
+ legendData: [],
+ legendBlock: false,
+ };
+
+ requestRef: number | undefined = undefined;
+
+ root: HTMLDivElement | undefined = undefined;
+
+ chart: G2.Chart | undefined = undefined;
+
+ // for window resize auto responsive legend
+ resize = Debounce(() => {
+ const { hasLegend } = this.props;
+ const { legendBlock } = this.state;
+ if (!hasLegend || !this.root) {
+ window.removeEventListener('resize', this.resize);
+ return;
+ }
+ if (
+ this.root &&
+ this.root.parentNode &&
+ (this.root.parentNode as HTMLElement).clientWidth <= 380
+ ) {
+ if (!legendBlock) {
+ this.setState({
+ legendBlock: true,
+ });
+ }
+ } else if (legendBlock) {
+ this.setState({
+ legendBlock: false,
+ });
+ }
+ }, 400);
+
+ componentDidMount() {
+ window.addEventListener(
+ 'resize',
+ () => {
+ this.requestRef = requestAnimationFrame(() => this.resize());
+ },
+ { passive: true },
+ );
+ }
+
+ componentDidUpdate(preProps: PieProps) {
+ const { data } = this.props;
+ if (data !== preProps.data) {
+ // because of charts data create when rendered
+ // so there is a trick for get rendered time
+ this.getLegendData();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.requestRef) {
+ window.cancelAnimationFrame(this.requestRef);
+ }
+ window.removeEventListener('resize', this.resize);
+ if (this.resize) {
+ (this.resize as any).cancel();
+ }
+ }
+
+ getG2Instance = (chart: G2.Chart) => {
+ this.chart = chart;
+ requestAnimationFrame(() => {
+ this.getLegendData();
+ this.resize();
+ });
+ };
+
+ // for custom lengend view
+ getLegendData = () => {
+ if (!this.chart) return;
+ const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
+ if (!geom) return;
+ const items = (geom as any).get('dataArray') || []; // 获取图形对应的
+
+ const legendData = items.map((item: { color: any; _origin: any }[]) => {
+ /* eslint no-underscore-dangle:0 */
+ const origin = item[0]._origin;
+ origin.color = item[0].color;
+ origin.checked = true;
+ return origin;
+ });
+
+ this.setState({
+ legendData,
+ });
+ };
+
+ handleRoot = (n: HTMLDivElement) => {
+ this.root = n;
+ };
+
+ handleLegendClick = (item: any, i: string | number) => {
+ const newItem = item;
+ newItem.checked = !newItem.checked;
+
+ const { legendData } = this.state;
+ legendData[i] = newItem;
+
+ const filteredLegendData = legendData.filter((l) => l.checked).map((l) => l.x);
+
+ if (this.chart) {
+ this.chart.filter('x', (val) => filteredLegendData.indexOf(`${val}`) > -1);
+ }
+
+ this.setState({
+ legendData,
+ });
+ };
+
+ render() {
+ const {
+ valueFormat,
+ subTitle,
+ total,
+ hasLegend = false,
+ className,
+ style,
+ height = 0,
+ forceFit = true,
+ percent,
+ color,
+ inner = 0.75,
+ animate = true,
+ colors,
+ lineWidth = 1,
+ } = this.props;
+
+ const { legendData, legendBlock } = this.state;
+ const pieClassName = classNames(styles.pie, className, {
+ [styles.hasLegend]: !!hasLegend,
+ [styles.legendBlock]: legendBlock,
+ });
+
+ const {
+ data: propsData,
+ selected: propsSelected = true,
+ tooltip: propsTooltip = true,
+ } = this.props;
+
+ let data = propsData || [];
+ let selected = propsSelected;
+ let tooltip = propsTooltip;
+
+ const defaultColors = colors;
+ data = data || [];
+ selected = selected || true;
+ tooltip = tooltip || true;
+ let formatColor;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ if (percent || percent === 0) {
+ selected = false;
+ tooltip = false;
+ formatColor = (value: string) => {
+ if (value === '占比') {
+ return color || 'rgba(24, 144, 255, 0.85)';
+ }
+ return '#F0F2F5';
+ };
+
+ data = [
+ {
+ x: '占比',
+ y: parseFloat(`${percent}`),
+ },
+ {
+ x: '反比',
+ y: 100 - parseFloat(`${percent}`),
+ },
+ ];
+ }
+
+ const tooltipFormat: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*percent',
+ (x: string, p: number) => ({
+ name: x,
+ value: `${(p * 100).toFixed(2)}%`,
+ }),
+ ];
+
+ const padding = [12, 0, 12, 0] as [number, number, number, number];
+
+ const dv = new DataView();
+ dv.source(data).transform({
+ type: 'percent',
+ field: 'y',
+ dimension: 'x',
+ as: 'percent',
+ });
+
+ return (
+
+
+
+
+ {!!tooltip && }
+
+
+
+
+ {(subTitle || total) && (
+
+ {subTitle &&
{subTitle}
}
+ {/* eslint-disable-next-line */}
+ {total && (
+
{typeof total === 'function' ? total() : total}
+ )}
+
+ )}
+
+
+
+ {hasLegend && (
+
+ {legendData.map((item, i) => (
+ - this.handleLegendClick(item, i)}>
+
+ {item.x}
+
+
+ {`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
+
+ {valueFormat ? valueFormat(item.y) : item.y}
+
+ ))}
+
+ )}
+
+ );
+ }
+}
+
+export default autoHeight()(Pie);
diff --git a/src/pages/dashboard/analysis/components/Charts/TagCloud/index.tsx b/src/pages/dashboard/analysis/components/Charts/TagCloud/index.tsx
new file mode 100644
index 0000000..56e8df4
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/TagCloud/index.tsx
@@ -0,0 +1,212 @@
+import { Chart, Coord, Geom, Shape, Tooltip } from 'bizcharts';
+import React, { Component } from 'react';
+
+import DataSet from '@antv/data-set';
+import Debounce from 'lodash.debounce';
+import classNames from 'classnames';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+/* eslint no-underscore-dangle: 0 */
+/* eslint no-param-reassign: 0 */
+
+const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
+
+export type TagCloudProps = {
+ data: {
+ name: string;
+ value: number;
+ }[];
+ height?: number;
+ className?: string;
+ style?: React.CSSProperties;
+};
+
+type TagCloudState = {
+ dv: any;
+ height?: number;
+ width: number;
+};
+
+class TagCloud extends Component {
+ state = {
+ dv: null,
+ height: 0,
+ width: 0,
+ };
+
+ isUnmount: boolean = false;
+
+ requestRef: number = 0;
+
+ root: HTMLDivElement | undefined = undefined;
+
+ imageMask: HTMLImageElement | undefined = undefined;
+
+ componentDidMount() {
+ requestAnimationFrame(() => {
+ this.initTagCloud();
+ this.renderChart(this.props);
+ });
+ window.addEventListener('resize', this.resize, { passive: true });
+ }
+
+ componentDidUpdate(preProps?: TagCloudProps) {
+ const { data } = this.props;
+ if (preProps && JSON.stringify(preProps.data) !== JSON.stringify(data)) {
+ this.renderChart(this.props);
+ }
+ }
+
+ componentWillUnmount() {
+ this.isUnmount = true;
+ window.cancelAnimationFrame(this.requestRef);
+ window.removeEventListener('resize', this.resize);
+ }
+
+ resize = () => {
+ this.requestRef = requestAnimationFrame(() => {
+ this.renderChart(this.props);
+ });
+ };
+
+ saveRootRef = (node: HTMLDivElement) => {
+ this.root = node;
+ };
+
+ initTagCloud = () => {
+ function getTextAttrs(cfg: {
+ x?: any;
+ y?: any;
+ style?: any;
+ opacity?: any;
+ origin?: any;
+ color?: any;
+ }) {
+ return {
+ ...cfg.style,
+ fillOpacity: cfg.opacity,
+ fontSize: cfg.origin._origin.size,
+ rotate: cfg.origin._origin.rotate,
+ text: cfg.origin._origin.text,
+ textAlign: 'center',
+ fontFamily: cfg.origin._origin.font,
+ fill: cfg.color,
+ textBaseline: 'Alphabetic',
+ };
+ }
+
+ (Shape as any).registerShape('point', 'cloud', {
+ drawShape(
+ cfg: { x: any; y: any },
+ container: { addShape: (arg0: string, arg1: { attrs: any }) => void },
+ ) {
+ const attrs = getTextAttrs(cfg);
+ return container.addShape('text', {
+ attrs: {
+ ...attrs,
+ x: cfg.x,
+ y: cfg.y,
+ },
+ });
+ },
+ });
+ };
+
+ renderChart = Debounce((nextProps: TagCloudProps) => {
+ // const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
+ const { data, height } = nextProps || this.props;
+
+ if (data.length < 1 || !this.root) {
+ return;
+ }
+
+ const h = height;
+ const w = this.root.offsetWidth;
+
+ const onload = () => {
+ const dv = new DataSet.View().source(data);
+ const range = dv.range('value');
+ const [min, max] = range;
+ dv.transform({
+ type: 'tag-cloud',
+ fields: ['name', 'value'],
+ imageMask: this.imageMask,
+ font: 'Verdana',
+ size: [w, h], // 宽高设置最好根据 imageMask 做调整
+ padding: 0,
+ timeInterval: 5000, // max execute time
+ rotate() {
+ return 0;
+ },
+ fontSize(d: { value: number }) {
+ const size = ((d.value - min) / (max - min)) ** 2;
+ return size * (17.5 - 5) + 5;
+ },
+ });
+
+ if (this.isUnmount) {
+ return;
+ }
+
+ this.setState({
+ dv,
+ width: w,
+ height: h,
+ });
+ };
+
+ if (!this.imageMask) {
+ this.imageMask = new Image();
+ this.imageMask.crossOrigin = '';
+ this.imageMask.src = imgUrl;
+
+ this.imageMask.onload = onload;
+ } else {
+ onload();
+ }
+ }, 500);
+
+ render() {
+ const { className, height } = this.props;
+ const { dv, width, height: stateHeight } = this.state;
+
+ return (
+
+ {dv && (
+
+
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default autoHeight()(TagCloud);
diff --git a/src/pages/dashboard/analysis/components/Charts/TimelineChart/index.tsx b/src/pages/dashboard/analysis/components/Charts/TimelineChart/index.tsx
new file mode 100644
index 0000000..fa17bfa
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/TimelineChart/index.tsx
@@ -0,0 +1,132 @@
+import { Axis, Chart, Geom, Legend, Tooltip } from 'bizcharts';
+
+import DataSet from '@antv/data-set';
+import React from 'react';
+import Slider from 'bizcharts-plugin-slider';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+export type TimelineChartProps = {
+ data: {
+ x: number;
+ y1: number;
+ y2: number;
+ }[];
+ title?: string;
+ titleMap: { y1: string; y2: string };
+ padding?: [number, number, number, number];
+ height?: number;
+ style?: React.CSSProperties;
+ borderWidth?: number;
+};
+
+const TimelineChart: React.FC = (props) => {
+ const {
+ title,
+ height = 400,
+ padding = [60, 20, 40, 40] as [number, number, number, number],
+ titleMap = {
+ y1: 'y1',
+ y2: 'y2',
+ },
+ borderWidth = 2,
+ data: sourceData,
+ } = props;
+
+ const data = Array.isArray(sourceData) ? sourceData : [{ x: 0, y1: 0, y2: 0 }];
+
+ data.sort((a, b) => a.x - b.x);
+
+ let max;
+ if (data[0] && data[0].y1 && data[0].y2) {
+ max = Math.max(
+ [...data].sort((a, b) => b.y1 - a.y1)[0].y1,
+ [...data].sort((a, b) => b.y2 - a.y2)[0].y2,
+ );
+ }
+
+ const ds = new DataSet({
+ state: {
+ start: data[0].x,
+ end: data[data.length - 1].x,
+ },
+ });
+
+ const dv = ds.createView();
+ dv.source(data)
+ .transform({
+ type: 'filter',
+ callback: (obj: { x: string }) => {
+ const date = obj.x;
+ return date <= ds.state.end && date >= ds.state.start;
+ },
+ })
+ .transform({
+ type: 'map',
+ callback(row: { y1: string; y2: string }) {
+ const newRow = { ...row };
+ newRow[titleMap.y1] = row.y1;
+ newRow[titleMap.y2] = row.y2;
+ return newRow;
+ },
+ })
+ .transform({
+ type: 'fold',
+ fields: [titleMap.y1, titleMap.y2], // 展开字段集
+ key: 'key', // key字段
+ value: 'value', // value字段
+ });
+
+ const timeScale = {
+ type: 'time',
+ tickInterval: 60 * 60 * 1000,
+ mask: 'HH:mm',
+ range: [0, 1],
+ };
+
+ const cols = {
+ x: timeScale,
+ value: {
+ max,
+ min: 0,
+ },
+ };
+
+ const SliderGen = () => (
+ {
+ ds.setState('start', startValue);
+ ds.setState('end', endValue);
+ }}
+ />
+ );
+
+ return (
+
+
+ {title &&
{title}
}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default autoHeight()(TimelineChart);
diff --git a/src/pages/dashboard/analysis/components/Charts/WaterWave/index.tsx b/src/pages/dashboard/analysis/components/Charts/WaterWave/index.tsx
new file mode 100644
index 0000000..a9b6411
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/WaterWave/index.tsx
@@ -0,0 +1,235 @@
+import React, { Component } from 'react';
+
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+/* eslint no-return-assign: 0 */
+/* eslint no-mixed-operators: 0 */
+// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
+
+export type WaterWaveProps = {
+ title: React.ReactNode;
+ color?: string;
+ height?: number;
+ percent: number;
+ style?: React.CSSProperties;
+};
+
+class WaterWave extends Component {
+ state = {
+ radio: 1,
+ };
+
+ timer: number = 0;
+
+ root: HTMLDivElement | undefined | null = null;
+
+ node: HTMLCanvasElement | undefined | null = null;
+
+ componentDidMount() {
+ this.renderChart();
+ this.resize();
+ window.addEventListener(
+ 'resize',
+ () => {
+ requestAnimationFrame(() => this.resize());
+ },
+ { passive: true },
+ );
+ }
+
+ componentDidUpdate(props: WaterWaveProps) {
+ const { percent } = this.props;
+ if (props.percent !== percent) {
+ // 不加这个会造成绘制缓慢
+ this.renderChart('update');
+ }
+ }
+
+ componentWillUnmount() {
+ cancelAnimationFrame(this.timer);
+ if (this.node) {
+ this.node.innerHTML = '';
+ }
+ window.removeEventListener('resize', this.resize);
+ }
+
+ resize = () => {
+ if (this.root) {
+ const { height = 1 } = this.props;
+ const { offsetWidth } = this.root.parentNode as HTMLElement;
+ this.setState({
+ radio: offsetWidth < height ? offsetWidth / height : 1,
+ });
+ }
+ };
+
+ renderChart(type?: string) {
+ const { percent, color = '#1890FF' } = this.props;
+ const data = percent / 100;
+ cancelAnimationFrame(this.timer);
+
+ if (!this.node || (data !== 0 && !data)) {
+ return;
+ }
+
+ const canvas = this.node;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ return;
+ }
+ const canvasWidth = canvas.width;
+ const canvasHeight = canvas.height;
+ const radius = canvasWidth / 2;
+ const lineWidth = 2;
+ const cR = radius - lineWidth;
+
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth * 2;
+
+ const axisLength = canvasWidth - lineWidth;
+ const unit = axisLength / 8;
+ const range = 0.2; // 振幅
+ let currRange = range;
+ const xOffset = lineWidth;
+ let sp = 0; // 周期偏移量
+ let currData = 0;
+ const waveupsp = 0.005; // 水波上涨速度
+
+ let arcStack: number[][] = [];
+ const bR = radius - lineWidth;
+ const circleOffset = -(Math.PI / 2);
+ let circleLock = true;
+
+ for (let i = circleOffset; i < circleOffset + 2 * Math.PI; i += 1 / (8 * Math.PI)) {
+ arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]);
+ }
+
+ const cStartPoint = arcStack.shift() as number[];
+ ctx.strokeStyle = color;
+ ctx.moveTo(cStartPoint[0], cStartPoint[1]);
+
+ const drawSin = () => {
+ if (!ctx) {
+ return;
+ }
+ ctx.beginPath();
+ ctx.save();
+
+ const sinStack = [];
+ for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
+ const x = sp + (xOffset + i) / unit;
+ const y = Math.sin(x) * currRange;
+ const dx = i;
+ const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
+
+ ctx.lineTo(dx, dy);
+ sinStack.push([dx, dy]);
+ }
+
+ const startPoint = sinStack.shift() as number[];
+
+ ctx.lineTo(xOffset + axisLength, canvasHeight);
+ ctx.lineTo(xOffset, canvasHeight);
+ ctx.lineTo(startPoint[0], startPoint[1]);
+
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
+ gradient.addColorStop(0, '#ffffff');
+ gradient.addColorStop(1, color);
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ ctx.restore();
+ };
+
+ const render = () => {
+ if (!ctx) {
+ return;
+ }
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+ if (circleLock && type !== 'update') {
+ if (arcStack.length) {
+ const temp = arcStack.shift() as number[];
+ ctx.lineTo(temp[0], temp[1]);
+ ctx.stroke();
+ } else {
+ circleLock = false;
+ ctx.lineTo(cStartPoint[0], cStartPoint[1]);
+ ctx.stroke();
+ arcStack = [];
+
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth;
+ ctx.arc(radius, radius, bR, 0, 2 * Math.PI, true);
+
+ ctx.beginPath();
+ ctx.save();
+ ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true);
+
+ ctx.restore();
+ ctx.clip();
+ ctx.fillStyle = color;
+ }
+ } else {
+ if (data >= 0.85) {
+ if (currRange > range / 4) {
+ const t = range * 0.01;
+ currRange -= t;
+ }
+ } else if (data <= 0.1) {
+ if (currRange < range * 1.5) {
+ const t = range * 0.01;
+ currRange += t;
+ }
+ } else {
+ if (currRange <= range) {
+ const t = range * 0.01;
+ currRange += t;
+ }
+ if (currRange >= range) {
+ const t = range * 0.01;
+ currRange -= t;
+ }
+ }
+ if (data - currData > 0) {
+ currData += waveupsp;
+ }
+ if (data - currData < 0) {
+ currData -= waveupsp;
+ }
+
+ sp += 0.07;
+ drawSin();
+ }
+ this.timer = requestAnimationFrame(render);
+ };
+ render();
+ }
+
+ render() {
+ const { radio } = this.state;
+ const { percent, title, height = 1 } = this.props;
+ return (
+ (this.root = n)}
+ style={{ transform: `scale(${radio})` }}
+ >
+
+
+
+ {title && {title}}
+
{percent}%
+
+
+ );
+ }
+}
+
+export default autoHeight()(WaterWave);
diff --git a/src/pages/dashboard/analysis/components/Charts/autoHeight.tsx b/src/pages/dashboard/analysis/components/Charts/autoHeight.tsx
new file mode 100644
index 0000000..5ec7bf3
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/autoHeight.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+
+export type IReactComponent =
+ | React.StatelessComponent
+ | React.ComponentClass
+ | React.ClassicComponentClass
;
+
+function computeHeight(node: HTMLDivElement) {
+ const { style } = node;
+ style.height = '100%';
+ const totalHeight = parseInt(`${getComputedStyle(node).height}`, 10);
+ const padding =
+ parseInt(`${getComputedStyle(node).paddingTop}`, 10) +
+ parseInt(`${getComputedStyle(node).paddingBottom}`, 10);
+ return totalHeight - padding;
+}
+
+function getAutoHeight(n: HTMLDivElement | undefined) {
+ if (!n) {
+ return 0;
+ }
+
+ const node = n;
+
+ let height = computeHeight(node);
+ const parentNode = node.parentNode as HTMLDivElement;
+ if (parentNode) {
+ height = computeHeight(parentNode);
+ }
+
+ return height;
+}
+
+type AutoHeightProps = {
+ height?: number;
+};
+
+function autoHeight() {
+ return
(
+ WrappedComponent: React.ComponentClass
| React.FC
,
+ ): React.ComponentClass
=> {
+ class AutoHeightComponent extends React.Component
{
+ state = {
+ computedHeight: 0,
+ };
+
+ root: HTMLDivElement | undefined = undefined;
+
+ componentDidMount() {
+ const { height } = this.props;
+ if (!height) {
+ let h = getAutoHeight(this.root);
+ this.setState({ computedHeight: h });
+ if (h < 1) {
+ h = getAutoHeight(this.root);
+ this.setState({ computedHeight: h });
+ }
+ }
+ }
+
+ handleRoot = (node: HTMLDivElement) => {
+ this.root = node;
+ };
+
+ render() {
+ const { height } = this.props;
+ const { computedHeight } = this.state;
+ const h = height || computedHeight;
+ return (
+
+ {h > 0 && }
+
+ );
+ }
+ }
+ return AutoHeightComponent;
+ };
+}
+export default autoHeight;
diff --git a/src/pages/dashboard/analysis/components/Charts/index.less b/src/pages/dashboard/analysis/components/Charts/index.less
new file mode 100644
index 0000000..190428b
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/index.less
@@ -0,0 +1,19 @@
+.miniChart {
+ position: relative;
+ width: 100%;
+ .chartContent {
+ position: absolute;
+ bottom: -28px;
+ width: 100%;
+ > div {
+ margin: 0 -5px;
+ overflow: hidden;
+ }
+ }
+ .chartLoading {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ margin-left: -7px;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Charts/index.tsx b/src/pages/dashboard/analysis/components/Charts/index.tsx
new file mode 100644
index 0000000..7ad687f
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Charts/index.tsx
@@ -0,0 +1,13 @@
+import numeral from 'numeral';
+import ChartCard from './ChartCard';
+import Field from './Field';
+
+const yuan = (val: number | string) => `¥ ${numeral(val).format('0,0')}`;
+
+const Charts = {
+ yuan,
+ ChartCard,
+ Field,
+};
+
+export { Charts as default, yuan, ChartCard, Field };
diff --git a/src/pages/dashboard/analysis/components/IntroduceRow.tsx b/src/pages/dashboard/analysis/components/IntroduceRow.tsx
new file mode 100644
index 0000000..cf0f3fd
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/IntroduceRow.tsx
@@ -0,0 +1,135 @@
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { TinyArea, TinyColumn, Progress } from '@ant-design/charts';
+import { Col, Row, Tooltip } from 'antd';
+
+import numeral from 'numeral';
+import { ChartCard, Field } from './Charts';
+import type { DataItem } from '../data.d';
+import Trend from './Trend';
+import Yuan from '../utils/Yuan';
+import styles from '../style.less';
+
+const topColResponsiveProps = {
+ xs: 24,
+ sm: 12,
+ md: 12,
+ lg: 12,
+ xl: 6,
+ style: { marginBottom: 24 },
+};
+
+const IntroduceRow = ({ loading, visitData }: { loading: boolean; visitData: DataItem[] }) => (
+
+
+
+
+
+ }
+ loading={loading}
+ total={() => 126560}
+ footer={}
+ contentHeight={46}
+ >
+
+ 周同比
+ 12%
+
+
+ 日同比
+ 11%
+
+
+
+
+
+
+
+
+ }
+ total={numeral(8846).format('0,0')}
+ footer={}
+ contentHeight={46}
+ >
+
+
+
+
+
+
+
+ }
+ total={numeral(6560).format('0,0')}
+ footer={}
+ contentHeight={46}
+ >
+
+
+
+
+
+
+
+ }
+ total="78%"
+ footer={
+
+
+ 周同比
+ 12%
+
+
+ 日同比
+ 11%
+
+
+ }
+ contentHeight={46}
+ >
+
+
+
+
+);
+
+export default IntroduceRow;
diff --git a/src/pages/dashboard/analysis/components/NumberInfo/index.less b/src/pages/dashboard/analysis/components/NumberInfo/index.less
new file mode 100644
index 0000000..847d25e
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/NumberInfo/index.less
@@ -0,0 +1,68 @@
+@import '~antd/es/style/themes/default.less';
+
+.numberInfo {
+ .suffix {
+ margin-left: 4px;
+ color: @text-color;
+ font-size: 16px;
+ font-style: normal;
+ }
+ .numberInfoTitle {
+ margin-bottom: 16px;
+ color: @text-color;
+ font-size: @font-size-lg;
+ transition: all 0.3s;
+ }
+ .numberInfoSubTitle {
+ height: 22px;
+ overflow: hidden;
+ color: @text-color-secondary;
+ font-size: @font-size-base;
+ line-height: 22px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ }
+ .numberInfoValue {
+ margin-top: 4px;
+ overflow: hidden;
+ font-size: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ & > span {
+ display: inline-block;
+ height: 32px;
+ margin-right: 32px;
+ color: @heading-color;
+ font-size: 24px;
+ line-height: 32px;
+ }
+ .subTotal {
+ margin-right: 0;
+ color: @text-color-secondary;
+ font-size: @font-size-lg;
+ vertical-align: top;
+ .anticon {
+ margin-left: 4px;
+ font-size: 12px;
+ transform: scale(0.82);
+ }
+ :global {
+ .anticon-caret-up {
+ color: @red-6;
+ }
+ .anticon-caret-down {
+ color: @green-6;
+ }
+ }
+ }
+ }
+}
+.numberInfolight {
+ .numberInfoValue {
+ & > span {
+ color: @text-color;
+ }
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/NumberInfo/index.tsx b/src/pages/dashboard/analysis/components/NumberInfo/index.tsx
new file mode 100644
index 0000000..60b4e94
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/NumberInfo/index.tsx
@@ -0,0 +1,62 @@
+import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export type NumberInfoProps = {
+ title?: React.ReactNode | string;
+ subTitle?: React.ReactNode | string;
+ total?: React.ReactNode | string;
+ status?: 'up' | 'down';
+ theme?: string;
+ gap?: number;
+ subTotal?: number;
+ suffix?: string;
+ style?: React.CSSProperties;
+};
+const NumberInfo: React.FC = ({
+ theme,
+ title,
+ subTitle,
+ total,
+ subTotal,
+ status,
+ suffix,
+ gap,
+ ...rest
+}) => (
+
+ {title && (
+
+ {title}
+
+ )}
+ {subTitle && (
+
+ {subTitle}
+
+ )}
+
+
+ {total}
+ {suffix && {suffix}}
+
+ {(status || subTotal) && (
+
+ {subTotal}
+ {status && status === 'up' ? : }
+
+ )}
+
+
+);
+
+export default NumberInfo;
diff --git a/src/pages/dashboard/analysis/components/OfflineData.tsx b/src/pages/dashboard/analysis/components/OfflineData.tsx
new file mode 100644
index 0000000..eca987b
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/OfflineData.tsx
@@ -0,0 +1,76 @@
+import { Card, Col, Row, Tabs } from 'antd';
+import { RingProgress, Line } from '@ant-design/charts';
+import type { OfflineDataType, DataItem } from '../data.d';
+
+import NumberInfo from './NumberInfo';
+import styles from '../style.less';
+
+const CustomTab = ({
+ data,
+ currentTabKey: currentKey,
+}: {
+ data: OfflineDataType;
+ currentTabKey: string;
+}) => (
+
+
+
+
+
+
+
+
+);
+
+const { TabPane } = Tabs;
+
+const OfflineData = ({
+ activeKey,
+ loading,
+ offlineData,
+ offlineChartData,
+ handleTabChange,
+}: {
+ activeKey: string;
+ loading: boolean;
+ offlineData: OfflineDataType[];
+ offlineChartData: DataItem[];
+ handleTabChange: (activeKey: string) => void;
+}) => (
+
+
+ {offlineData.map((shop) => (
+ } key={shop.name}>
+
+
+
+
+ ))}
+
+
+);
+
+export default OfflineData;
diff --git a/src/pages/dashboard/analysis/components/PageLoading/index.tsx b/src/pages/dashboard/analysis/components/PageLoading/index.tsx
new file mode 100644
index 0000000..dd96277
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/PageLoading/index.tsx
@@ -0,0 +1,9 @@
+import { Spin } from 'antd';
+
+// loading components from code split
+// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
+export default () => (
+
+
+
+);
diff --git a/src/pages/dashboard/analysis/components/ProportionSales.tsx b/src/pages/dashboard/analysis/components/ProportionSales.tsx
new file mode 100644
index 0000000..f96d0ea
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/ProportionSales.tsx
@@ -0,0 +1,76 @@
+import { Card, Radio, Typography } from 'antd';
+import numeral from 'numeral';
+import type { RadioChangeEvent } from 'antd/es/radio';
+import { Donut } from '@ant-design/charts';
+import type { DonutConfig } from '@ant-design/charts/es/donut';
+import React from 'react';
+import type { DataItem } from '../data.d';
+import styles from '../style.less';
+
+const { Text } = Typography;
+
+const ProportionSales = ({
+ dropdownGroup,
+ salesType,
+ loading,
+ salesPieData,
+ handleChangeSalesType,
+}: {
+ loading: boolean;
+ dropdownGroup: React.ReactNode;
+ salesType: 'all' | 'online' | 'stores';
+ salesPieData: DataItem[];
+ handleChangeSalesType?: (e: RadioChangeEvent) => void;
+}) => (
+
+ {dropdownGroup}
+
+
+ 全部渠道
+ 线上
+ 门店
+
+
+
+ }
+ >
+
+ 销售额
+ {
+ // eslint-disable-next-line no-underscore-dangle
+ return `${item._origin.x}: ${numeral(item._origin.y).format('0,0')}`;
+ },
+ }}
+ statistic={
+ {
+ totalLabel: '销售额',
+ } as DonutConfig['statistic']
+ }
+ />
+
+
+);
+
+export default ProportionSales;
diff --git a/src/pages/dashboard/analysis/components/SalesCard.tsx b/src/pages/dashboard/analysis/components/SalesCard.tsx
new file mode 100644
index 0000000..152d9ce
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/SalesCard.tsx
@@ -0,0 +1,189 @@
+import { Card, Col, DatePicker, Row, Tabs } from 'antd';
+import type { RangePickerProps } from 'antd/es/date-picker/generatePicker';
+import type moment from 'moment';
+import { Column } from '@ant-design/charts';
+
+import numeral from 'numeral';
+import type { DataItem } from '../data.d';
+import styles from '../style.less';
+
+type RangePickerValue = RangePickerProps['value'];
+export type TimeType = 'today' | 'week' | 'month' | 'year';
+
+const { RangePicker } = DatePicker;
+const { TabPane } = Tabs;
+
+const rankingListData: { title: string; total: number }[] = [];
+for (let i = 0; i < 7; i += 1) {
+ rankingListData.push({
+ title: `工专路 ${i} 号店`,
+ total: 323234,
+ });
+}
+
+const SalesCard = ({
+ rangePickerValue,
+ salesData,
+ isActive,
+ handleRangePickerChange,
+ loading,
+ selectDate,
+}: {
+ rangePickerValue: RangePickerValue;
+ isActive: (key: TimeType) => string;
+ salesData: DataItem[];
+ loading: boolean;
+ handleRangePickerChange: (dates: RangePickerValue, dateStrings: [string, string]) => void;
+ selectDate: (key: TimeType) => void;
+}) => (
+
+
+ }
+ size="large"
+ tabBarStyle={{ marginBottom: 24 }}
+ >
+
+
+
+
+
+
+
+
+
+
门店销售额排名
+
+ {rankingListData.map((item, i) => (
+ -
+
+ {i + 1}
+
+
+ {item.title}
+
+
+ {numeral(item.total).format('0,0')}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
门店访问量排名
+
+ {rankingListData.map((item, i) => (
+ -
+
+ {i + 1}
+
+
+ {item.title}
+
+ {numeral(item.total).format('0,0')}
+
+ ))}
+
+
+
+
+
+
+
+
+);
+
+export default SalesCard;
diff --git a/src/pages/dashboard/analysis/components/TopSearch.tsx b/src/pages/dashboard/analysis/components/TopSearch.tsx
new file mode 100644
index 0000000..48bfe03
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/TopSearch.tsx
@@ -0,0 +1,113 @@
+import { InfoCircleOutlined } from '@ant-design/icons';
+import { Card, Col, Row, Table, Tooltip } from 'antd';
+import { TinyArea } from '@ant-design/charts';
+import React from 'react';
+import numeral from 'numeral';
+import type { DataItem } from '../data.d';
+
+import NumberInfo from './NumberInfo';
+import Trend from './Trend';
+import styles from '../style.less';
+
+const columns = [
+ {
+ title: '排名',
+ dataIndex: 'index',
+ key: 'index',
+ },
+ {
+ title: '搜索关键词',
+ dataIndex: 'keyword',
+ key: 'keyword',
+ render: (text: React.ReactNode) => {text},
+ },
+ {
+ title: '用户数',
+ dataIndex: 'count',
+ key: 'count',
+ sorter: (a: { count: number }, b: { count: number }) => a.count - b.count,
+ className: styles.alignRight,
+ },
+ {
+ title: '周涨幅',
+ dataIndex: 'range',
+ key: 'range',
+ sorter: (a: { range: number }, b: { range: number }) => a.range - b.range,
+ render: (text: React.ReactNode, record: { status: number }) => (
+
+ {text}%
+
+ ),
+ },
+];
+
+const TopSearch = ({
+ loading,
+ visitData2,
+ searchData,
+ dropdownGroup,
+}: {
+ loading: boolean;
+ visitData2: DataItem[];
+ dropdownGroup: React.ReactNode;
+ searchData: DataItem[];
+}) => (
+
+
+
+
+ 搜索用户数
+
+
+
+
+ }
+ gap={8}
+ total={numeral(12321).format('0,0')}
+ status="up"
+ subTotal={17.1}
+ />
+
+
+
+
+ 人均搜索次数
+
+
+
+
+ }
+ total={2.7}
+ status="down"
+ subTotal={26.2}
+ gap={8}
+ />
+
+
+
+
+ rowKey={(record) => record.index}
+ size="small"
+ columns={columns}
+ dataSource={searchData}
+ pagination={{
+ style: { marginBottom: 0 },
+ pageSize: 5,
+ }}
+ />
+
+);
+
+export default TopSearch;
diff --git a/src/pages/dashboard/analysis/components/Trend/index.less b/src/pages/dashboard/analysis/components/Trend/index.less
new file mode 100644
index 0000000..3d7fdf9
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Trend/index.less
@@ -0,0 +1,37 @@
+@import '~antd/es/style/themes/default.less';
+
+.trendItem {
+ display: inline-block;
+ font-size: @font-size-base;
+ line-height: 22px;
+
+ .up,
+ .down {
+ position: relative;
+ top: 1px;
+ margin-left: 4px;
+ span {
+ font-size: 12px;
+ transform: scale(0.83);
+ }
+ }
+ .up {
+ color: @red-6;
+ }
+ .down {
+ top: -1px;
+ color: @green-6;
+ }
+
+ &.trendItemGrey .up,
+ &.trendItemGrey .down {
+ color: @text-color;
+ }
+
+ &.reverseColor .up {
+ color: @green-6;
+ }
+ &.reverseColor .down {
+ color: @red-6;
+ }
+}
diff --git a/src/pages/dashboard/analysis/components/Trend/index.tsx b/src/pages/dashboard/analysis/components/Trend/index.tsx
new file mode 100644
index 0000000..58f7196
--- /dev/null
+++ b/src/pages/dashboard/analysis/components/Trend/index.tsx
@@ -0,0 +1,42 @@
+import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons';
+import React from 'react';
+import classNames from 'classnames';
+import styles from './index.less';
+
+export type TrendProps = {
+ colorful?: boolean;
+ flag: 'up' | 'down';
+ style?: React.CSSProperties;
+ reverseColor?: boolean;
+ className?: string;
+};
+
+const Trend: React.FC = ({
+ colorful = true,
+ reverseColor = false,
+ flag,
+ children,
+ className,
+ ...rest
+}) => {
+ const classString = classNames(
+ styles.trendItem,
+ {
+ [styles.trendItemGrey]: !colorful,
+ [styles.reverseColor]: reverseColor && colorful,
+ },
+ className,
+ );
+ return (
+
+ {children}
+ {flag && (
+
+ {flag === 'up' ? : }
+
+ )}
+
+ );
+};
+
+export default Trend;
diff --git a/src/pages/dashboard/analysis/data.d.ts b/src/pages/dashboard/analysis/data.d.ts
new file mode 100644
index 0000000..7604d10
--- /dev/null
+++ b/src/pages/dashboard/analysis/data.d.ts
@@ -0,0 +1,46 @@
+import { DataItem } from '@antv/g2plot/esm/interface/config';
+
+export { DataItem };
+
+export interface VisitDataType {
+ x: string;
+ y: number;
+}
+
+export type SearchDataType = {
+ index: number;
+ keyword: string;
+ count: number;
+ range: number;
+ status: number;
+};
+
+export type OfflineDataType = {
+ name: string;
+ cvr: number;
+};
+
+export interface OfflineChartData {
+ date: number;
+ type: number;
+ value: number;
+}
+
+export type RadarData = {
+ name: string;
+ label: string;
+ value: number;
+};
+
+export interface AnalysisData {
+ visitData: DataItem[];
+ visitData2: DataItem[];
+ salesData: DataItem[];
+ searchData: DataItem[];
+ offlineData: OfflineDataType[];
+ offlineChartData: DataItem[];
+ salesTypeData: DataItem[];
+ salesTypeDataOnline: DataItem[];
+ salesTypeDataOffline: DataItem[];
+ radarData: RadarData[];
+}
diff --git a/src/pages/dashboard/analysis/index.tsx b/src/pages/dashboard/analysis/index.tsx
new file mode 100644
index 0000000..e45017b
--- /dev/null
+++ b/src/pages/dashboard/analysis/index.tsx
@@ -0,0 +1,165 @@
+import type { FC } from 'react';
+import { Suspense, useState } from 'react';
+import { EllipsisOutlined } from '@ant-design/icons';
+import { Col, Dropdown, Menu, Row } from 'antd';
+import { GridContent } from '@ant-design/pro-layout';
+import type { RadioChangeEvent } from 'antd/es/radio';
+import type { RangePickerProps } from 'antd/es/date-picker/generatePicker';
+import type moment from 'moment';
+import IntroduceRow from './components/IntroduceRow';
+import SalesCard from './components/SalesCard';
+import TopSearch from './components/TopSearch';
+import ProportionSales from './components/ProportionSales';
+import OfflineData from './components/OfflineData';
+import { useRequest } from 'umi';
+
+import { fakeChartData } from './service';
+import PageLoading from './components/PageLoading';
+import type { TimeType } from './components/SalesCard';
+import { getTimeDistance } from './utils/utils';
+import type { AnalysisData } from './data.d';
+import styles from './style.less';
+import WrapContent from '@/components/WrapContent';
+
+type RangePickerValue = RangePickerProps['value'];
+
+type AnalysisProps = {
+ dashboardAndanalysis: AnalysisData;
+ loading: boolean;
+};
+
+type SalesType = 'all' | 'online' | 'stores';
+
+const Analysis: FC = () => {
+ const [salesType, setSalesType] = useState('all');
+ const [currentTabKey, setCurrentTabKey] = useState('');
+ const [rangePickerValue, setRangePickerValue] = useState(
+ getTimeDistance('year'),
+ );
+
+ const { loading, data } = useRequest(fakeChartData);
+
+ const selectDate = (type: TimeType) => {
+ setRangePickerValue(getTimeDistance(type));
+ };
+
+ const handleRangePickerChange = (value: RangePickerValue) => {
+ setRangePickerValue(value);
+ };
+
+ const isActive = (type: TimeType) => {
+ if (!rangePickerValue) {
+ return '';
+ }
+ const value = getTimeDistance(type);
+ if (!value) {
+ return '';
+ }
+ if (!rangePickerValue[0] || !rangePickerValue[1]) {
+ return '';
+ }
+ if (
+ rangePickerValue[0].isSame(value[0] as moment.Moment, 'day') &&
+ rangePickerValue[1].isSame(value[1] as moment.Moment, 'day')
+ ) {
+ return styles.currentDate;
+ }
+ return '';
+ };
+
+ let salesPieData;
+ if (salesType === 'all') {
+ salesPieData = data?.salesTypeData;
+ } else {
+ salesPieData = salesType === 'online' ? data?.salesTypeDataOnline : data?.salesTypeDataOffline;
+ }
+
+ const menu = (
+
+ );
+
+ const dropdownGroup = (
+
+
+
+
+
+ );
+
+ const handleChangeSalesType = (e: RadioChangeEvent) => {
+ setSalesType(e.target.value);
+ };
+
+ const handleTabChange = (key: string) => {
+ setCurrentTabKey(key);
+ };
+
+ const activeKey = currentTabKey || (data?.offlineData[0] && data?.offlineData[0].name) || '';
+
+ return (
+
+
+ <>
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+ );
+};
+
+export default Analysis;
diff --git a/src/pages/dashboard/analysis/service.ts b/src/pages/dashboard/analysis/service.ts
new file mode 100644
index 0000000..c8a6b08
--- /dev/null
+++ b/src/pages/dashboard/analysis/service.ts
@@ -0,0 +1,6 @@
+import { request } from 'umi';
+import type { AnalysisData } from './data';
+
+export async function fakeChartData(): Promise<{ data: AnalysisData }> {
+ return request('/api/fake_analysis_chart_data');
+}
diff --git a/src/pages/dashboard/analysis/style.less b/src/pages/dashboard/analysis/style.less
new file mode 100644
index 0000000..38a790b
--- /dev/null
+++ b/src/pages/dashboard/analysis/style.less
@@ -0,0 +1,189 @@
+@import '~antd/es/style/themes/default.less';
+
+.iconGroup {
+ span.anticon {
+ margin-left: 16px;
+ color: @text-color-secondary;
+ cursor: pointer;
+ transition: color 0.32s;
+ &:hover {
+ color: @text-color;
+ }
+ }
+}
+
+.rankingList {
+ margin: 25px 0 0;
+ padding: 0;
+ list-style: none;
+ li {
+ display: flex;
+ align-items: center;
+ margin-top: 16px;
+ zoom: 1;
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+ span {
+ color: @text-color;
+ font-size: 14px;
+ line-height: 22px;
+ }
+ .rankingItemNumber {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-top: 1.5px;
+ margin-right: 16px;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 20px;
+ text-align: center;
+ background-color: @tag-default-bg;
+ border-radius: 20px;
+ &.active {
+ color: #fff;
+ background-color: #314659;
+ }
+ }
+ .rankingItemTitle {
+ flex: 1;
+ margin-right: 8px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ }
+}
+
+.salesExtra {
+ display: inline-block;
+ margin-right: 24px;
+ a {
+ margin-left: 24px;
+ color: @text-color;
+ &:hover {
+ color: @primary-color;
+ }
+ &.currentDate {
+ color: @primary-color;
+ }
+ }
+}
+
+.salesCard {
+ .salesBar {
+ padding: 0 0 32px 32px;
+ }
+ .salesRank {
+ padding: 0 32px 32px 72px;
+ }
+ :global {
+ .ant-tabs-bar,
+ .ant-tabs-nav-wrap {
+ padding-left: 16px;
+ .ant-tabs-nav .ant-tabs-tab {
+ padding-top: 16px;
+ padding-bottom: 14px;
+ line-height: 24px;
+ }
+ }
+ .ant-tabs-extra-content {
+ padding-right: 24px;
+ line-height: 55px;
+ }
+ .ant-card-head {
+ position: relative;
+ }
+ .ant-card-head-title {
+ align-items: normal;
+ }
+ }
+}
+
+.salesCardExtra {
+ height: inherit;
+}
+
+.salesTypeRadio {
+ position: absolute;
+ right: 54px;
+ bottom: 12px;
+}
+
+.offlineCard {
+ :global {
+ .ant-tabs-ink-bar {
+ bottom: auto;
+ }
+ .ant-tabs-bar {
+ border-bottom: none;
+ }
+ .ant-tabs-nav-container-scrolling {
+ padding-right: 40px;
+ padding-left: 40px;
+ }
+ .ant-tabs-tab-prev-icon::before {
+ position: relative;
+ left: 6px;
+ }
+ .ant-tabs-tab-next-icon::before {
+ position: relative;
+ right: 6px;
+ }
+ .ant-tabs-tab-active h4 {
+ color: @primary-color;
+ }
+ }
+}
+
+.trendText {
+ margin-left: 8px;
+ color: @heading-color;
+}
+
+@media screen and (max-width: @screen-lg) {
+ .salesExtra {
+ display: none;
+ }
+
+ .rankingList {
+ li {
+ span:first-child {
+ margin-right: 8px;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: @screen-md) {
+ .rankingTitle {
+ margin-top: 16px;
+ }
+
+ .salesCard .salesBar {
+ padding: 16px;
+ }
+}
+
+@media screen and (max-width: @screen-sm) {
+ .salesExtraWrap {
+ display: none;
+ }
+
+ .salesCard {
+ :global {
+ .ant-tabs-content {
+ padding-top: 30px;
+ }
+ }
+ }
+}
diff --git a/src/pages/dashboard/analysis/utils/Yuan.tsx b/src/pages/dashboard/analysis/utils/Yuan.tsx
new file mode 100644
index 0000000..7027d3c
--- /dev/null
+++ b/src/pages/dashboard/analysis/utils/Yuan.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { yuan } from '../components/Charts';
+/** 减少使用 dangerouslySetInnerHTML */
+export default class Yuan extends React.Component<{
+ children: React.ReactText;
+}> {
+ main: HTMLSpanElement | undefined | null = null;
+
+ componentDidMount() {
+ this.renderToHtml();
+ }
+
+ componentDidUpdate() {
+ this.renderToHtml();
+ }
+
+ renderToHtml = () => {
+ const { children } = this.props;
+ if (this.main) {
+ this.main.innerHTML = yuan(children);
+ }
+ };
+
+ render() {
+ return (
+ {
+ this.main = ref;
+ }}
+ />
+ );
+ }
+}
diff --git a/src/pages/dashboard/analysis/utils/utils.less b/src/pages/dashboard/analysis/utils/utils.less
new file mode 100644
index 0000000..de1aa64
--- /dev/null
+++ b/src/pages/dashboard/analysis/utils/utils.less
@@ -0,0 +1,50 @@
+.textOverflow() {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+}
+
+.textOverflowMulti(@line: 3, @bg: #fff) {
+ position: relative;
+ max-height: @line * 1.5em;
+ margin-right: -1em;
+ padding-right: 1em;
+ overflow: hidden;
+ line-height: 1.5em;
+ text-align: justify;
+ &::before {
+ position: absolute;
+ right: 14px;
+ bottom: 0;
+ padding: 0 1px;
+ background: @bg;
+ content: '...';
+ }
+ &::after {
+ position: absolute;
+ right: 14px;
+ width: 1em;
+ height: 1em;
+ margin-top: 0.2em;
+ background: white;
+ content: '';
+ }
+}
+
+// mixins for clearfix
+// ------------------------
+.clearfix() {
+ zoom: 1;
+ &::before,
+ &::after {
+ display: table;
+ content: ' ';
+ }
+ &::after {
+ clear: both;
+ height: 0;
+ font-size: 0;
+ visibility: hidden;
+ }
+}
diff --git a/src/pages/dashboard/analysis/utils/utils.ts b/src/pages/dashboard/analysis/utils/utils.ts
new file mode 100644
index 0000000..6a0da9a
--- /dev/null
+++ b/src/pages/dashboard/analysis/utils/utils.ts
@@ -0,0 +1,52 @@
+import moment from 'moment';
+import type { RangePickerProps } from 'antd/es/date-picker/generatePicker';
+
+type RangePickerValue = RangePickerProps['value'];
+
+export function fixedZero(val: number) {
+ return val * 1 < 10 ? `0${val}` : val;
+}
+
+export function getTimeDistance(type: 'today' | 'week' | 'month' | 'year'): RangePickerValue {
+ const now = new Date();
+ const oneDay = 1000 * 60 * 60 * 24;
+
+ if (type === 'today') {
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+ return [moment(now), moment(now.getTime() + (oneDay - 1000))];
+ }
+
+ if (type === 'week') {
+ let day = now.getDay();
+ now.setHours(0);
+ now.setMinutes(0);
+ now.setSeconds(0);
+
+ if (day === 0) {
+ day = 6;
+ } else {
+ day -= 1;
+ }
+
+ const beginTime = now.getTime() - day * oneDay;
+
+ return [moment(beginTime), moment(beginTime + (7 * oneDay - 1000))];
+ }
+ const year = now.getFullYear();
+
+ if (type === 'month') {
+ const month = now.getMonth();
+ const nextDate = moment(now).add(1, 'months');
+ const nextYear = nextDate.year();
+ const nextMonth = nextDate.month();
+
+ return [
+ moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`),
+ moment(moment(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).valueOf() - 1000),
+ ];
+ }
+
+ return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
+}
diff --git a/src/pages/dashboard/monitor/_mock.ts b/src/pages/dashboard/monitor/_mock.ts
new file mode 100644
index 0000000..7ab870e
--- /dev/null
+++ b/src/pages/dashboard/monitor/_mock.ts
@@ -0,0 +1,14 @@
+import mockjs from 'mockjs';
+import type { Request, Response } from 'express';
+
+const getTags = (_: Request, res: Response) => {
+ return res.json({
+ data: mockjs.mock({
+ 'list|100': [{ name: '@city', 'value|1-100': 150, 'type|0-2': 1 }],
+ }),
+ });
+};
+
+export default {
+ 'GET /api/tags': getTags,
+};
diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.less b/src/pages/dashboard/monitor/components/ActiveChart/index.less
new file mode 100644
index 0000000..2f5d15f
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/ActiveChart/index.less
@@ -0,0 +1,51 @@
+.activeChart {
+ position: relative;
+}
+.activeChartGrid {
+ p {
+ position: absolute;
+ top: 80px;
+ }
+ p:last-child {
+ top: 115px;
+ }
+}
+.activeChartLegend {
+ position: relative;
+ height: 20px;
+ margin-top: 8px;
+ font-size: 0;
+ line-height: 20px;
+ span {
+ display: inline-block;
+ width: 33.33%;
+ font-size: 12px;
+ text-align: center;
+ }
+ span:first-child {
+ text-align: left;
+ }
+ span:last-child {
+ text-align: right;
+ }
+}
+.dashedLine {
+ position: relative;
+ top: -70px;
+ left: -3px;
+ height: 1px;
+
+ .line {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-image: linear-gradient(to right, transparent 50%, #e9e9e9 50%);
+ background-size: 6px;
+ }
+}
+
+.dashedLine:last-child {
+ top: -36px;
+}
diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.tsx b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx
new file mode 100644
index 0000000..9f0df84
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx
@@ -0,0 +1,90 @@
+import { Component } from 'react';
+import { TinyArea } from '@ant-design/charts';
+
+import { Statistic } from 'antd';
+import styles from './index.less';
+
+function fixedZero(val: number) {
+ return val * 1 < 10 ? `0${val}` : val;
+}
+
+function getActiveData() {
+ const activeData = [];
+ for (let i = 0; i < 24; i += 1) {
+ activeData.push({
+ x: `${fixedZero(i)}:00`,
+ y: Math.floor(Math.random() * 200) + i * 50,
+ });
+ }
+ return activeData;
+}
+
+export default class ActiveChart extends Component {
+ state = {
+ activeData: getActiveData(),
+ };
+
+ timer: number | undefined = undefined;
+
+ requestRef: number | undefined = undefined;
+
+ componentDidMount() {
+ this.loopData();
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.timer);
+ if (this.requestRef) {
+ cancelAnimationFrame(this.requestRef);
+ }
+ }
+
+ loopData = () => {
+ this.requestRef = requestAnimationFrame(() => {
+ this.timer = window.setTimeout(() => {
+ this.setState(
+ {
+ activeData: getActiveData(),
+ },
+ () => {
+ this.loopData();
+ },
+ );
+ }, 1000);
+ });
+ };
+
+ render() {
+ const { activeData = [] } = this.state;
+
+ return (
+
+
+
+
+
+ {activeData && (
+
+
+
{[...activeData].sort()[activeData.length - 1].y + 200} 亿元
+
{[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元
+
+
+
+
+ )}
+ {activeData && (
+
+ 00:00
+ {activeData[Math.floor(activeData.length / 2)].x}
+ {activeData[activeData.length - 1].x}
+
+ )}
+
+ );
+ }
+}
diff --git a/src/pages/dashboard/monitor/components/Charts/Gauge/index.tsx b/src/pages/dashboard/monitor/components/Charts/Gauge/index.tsx
new file mode 100644
index 0000000..a4975fa
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Charts/Gauge/index.tsx
@@ -0,0 +1,180 @@
+import { Axis, Chart, Coord, Geom, Guide, Shape } from 'bizcharts';
+
+import React from 'react';
+import autoHeight from '../autoHeight';
+
+const { Arc, Html, Line } = Guide;
+
+export type GaugeProps = {
+ title: React.ReactNode;
+ color?: string;
+ height?: number;
+ bgColor?: number;
+ percent: number;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+ formatter?: (value: string) => string;
+};
+
+const defaultFormatter = (val: string): string => {
+ switch (val) {
+ case '2':
+ return '差';
+ case '4':
+ return '中';
+ case '6':
+ return '良';
+ case '8':
+ return '优';
+ default:
+ return '';
+ }
+};
+
+if (Shape.registerShape) {
+ Shape.registerShape('point', 'pointer', {
+ drawShape(cfg: any, group: any) {
+ let point = cfg.points[0];
+ point = (this as any).parsePoint(point);
+ const center = (this as any).parsePoint({
+ x: 0,
+ y: 0,
+ });
+ group.addShape('line', {
+ attrs: {
+ x1: center.x,
+ y1: center.y,
+ x2: point.x,
+ y2: point.y,
+ stroke: cfg.color,
+ lineWidth: 2,
+ lineCap: 'round',
+ },
+ });
+ return group.addShape('circle', {
+ attrs: {
+ x: center.x,
+ y: center.y,
+ r: 6,
+ stroke: cfg.color,
+ lineWidth: 3,
+ fill: '#fff',
+ },
+ });
+ },
+ });
+}
+
+const Gauge: React.FC = (props) => {
+ const {
+ title,
+ height = 1,
+ percent,
+ forceFit = true,
+ formatter = defaultFormatter,
+ color = '#2F9CFF',
+ bgColor = '#F0F2F5',
+ } = props;
+ const cols = {
+ value: {
+ type: 'linear',
+ min: 0,
+ max: 10,
+ tickCount: 6,
+ nice: true,
+ },
+ };
+ const data = [{ value: percent / 10 }];
+
+ const renderHtml = () => `
+
+
${title}
+
+ ${(data[0].value * 10).toFixed(2)}%
+
+
`;
+
+ const textStyle: {
+ fontSize: number;
+ fill: string;
+ textAlign: 'center';
+ } = {
+ fontSize: 12,
+ fill: 'rgba(0, 0, 0, 0.65)',
+ textAlign: 'center',
+ };
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default autoHeight()(Gauge);
diff --git a/src/pages/dashboard/monitor/components/Charts/MiniArea/index.tsx b/src/pages/dashboard/monitor/components/Charts/MiniArea/index.tsx
new file mode 100644
index 0000000..5b20ae8
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Charts/MiniArea/index.tsx
@@ -0,0 +1,131 @@
+import type { AxisProps } from 'bizcharts';
+import { Axis, Chart, Geom, Tooltip } from 'bizcharts';
+
+import React from 'react';
+import autoHeight from '../autoHeight';
+import styles from '../index.less';
+
+export type MiniAreaProps = {
+ color?: string;
+ height?: number;
+ borderColor?: string;
+ line?: boolean;
+ animate?: boolean;
+ xAxis?: AxisProps;
+ forceFit?: boolean;
+ scale?: {
+ x?: {
+ tickCount: number;
+ };
+ y?: {
+ tickCount: number;
+ };
+ };
+ yAxis?: Partial;
+ borderWidth?: number;
+ data: {
+ x: number | string;
+ y: number;
+ }[];
+};
+
+const MiniArea: React.FC = (props) => {
+ const {
+ height = 1,
+ data = [],
+ forceFit = true,
+ color = 'rgba(24, 144, 255, 0.2)',
+ borderColor = '#1089ff',
+ scale = { x: {}, y: {} },
+ borderWidth = 2,
+ line,
+ xAxis,
+ yAxis,
+ animate = true,
+ } = props;
+
+ const padding: [number, number, number, number] = [36, 5, 30, 5];
+
+ const scaleProps = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ ...scale.x,
+ },
+ y: {
+ min: 0,
+ ...scale.y,
+ },
+ };
+
+ const tooltip: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*y',
+ (x: string, y: string) => ({
+ name: x,
+ value: y,
+ }),
+ ];
+
+ const chartHeight = height + 54;
+
+ return (
+
+
+ {height > 0 && (
+
+
+
+
+
+ {line ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default autoHeight()(MiniArea);
diff --git a/src/pages/dashboard/monitor/components/Charts/Pie/index.tsx b/src/pages/dashboard/monitor/components/Charts/Pie/index.tsx
new file mode 100644
index 0000000..520c5c5
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Charts/Pie/index.tsx
@@ -0,0 +1,310 @@
+import { Chart, Coord, Geom, Tooltip } from 'bizcharts';
+import React, { Component } from 'react';
+
+import { DataView } from '@antv/data-set';
+import Debounce from 'lodash.debounce';
+import { Divider } from 'antd';
+import ReactFitText from 'react-fittext';
+import classNames from 'classnames';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+export type PieProps = {
+ animate?: boolean;
+ color?: string;
+ colors?: string[];
+ selected?: boolean;
+ height?: number;
+ margin?: [number, number, number, number];
+ hasLegend?: boolean;
+ padding?: [number, number, number, number];
+ percent?: number;
+ data?: {
+ x: string | string;
+ y: number;
+ }[];
+ inner?: number;
+ lineWidth?: number;
+ forceFit?: boolean;
+ style?: React.CSSProperties;
+ className?: string;
+ total?: React.ReactNode | number | (() => React.ReactNode | number);
+ title?: React.ReactNode;
+ tooltip?: boolean;
+ valueFormat?: (value: string) => string | React.ReactNode;
+ subTitle?: React.ReactNode;
+};
+type PieState = {
+ legendData: { checked: boolean; x: string; color: string; percent: number; y: string }[];
+ legendBlock: boolean;
+};
+class Pie extends Component {
+ state: PieState = {
+ legendData: [],
+ legendBlock: false,
+ };
+
+ chart: G2.Chart | undefined = undefined;
+
+ root: HTMLDivElement | undefined = undefined;
+
+ requestRef: number | undefined = 0;
+
+ // for window resize auto responsive legend
+ resize = Debounce(() => {
+ const { hasLegend } = this.props;
+ const { legendBlock } = this.state;
+ if (!hasLegend || !this.root) {
+ window.removeEventListener('resize', this.resize);
+ return;
+ }
+ if (
+ this.root &&
+ this.root.parentNode &&
+ (this.root.parentNode as HTMLElement).clientWidth <= 380
+ ) {
+ if (!legendBlock) {
+ this.setState({
+ legendBlock: true,
+ });
+ }
+ } else if (legendBlock) {
+ this.setState({
+ legendBlock: false,
+ });
+ }
+ }, 300);
+
+ componentDidMount() {
+ window.addEventListener(
+ 'resize',
+ () => {
+ this.requestRef = requestAnimationFrame(() => this.resize());
+ },
+ { passive: true },
+ );
+ }
+
+ componentDidUpdate(preProps: PieProps) {
+ const { data } = this.props;
+ if (data !== preProps.data) {
+ // because of charts data create when rendered
+ // so there is a trick for get rendered time
+ this.getLegendData();
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.requestRef) {
+ window.cancelAnimationFrame(this.requestRef);
+ }
+ window.removeEventListener('resize', this.resize);
+ if (this.resize) {
+ (this.resize as any).cancel();
+ }
+ }
+
+ getG2Instance = (chart: G2.Chart) => {
+ this.chart = chart;
+ requestAnimationFrame(() => {
+ this.getLegendData();
+ this.resize();
+ });
+ };
+
+ // for custom lengend view
+ getLegendData = () => {
+ if (!this.chart) return;
+ const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
+ if (!geom) return;
+ // g2 的类型有问题
+ const items = (geom as any).get('dataArray') || []; // 获取图形对应的
+
+ const legendData = items.map((item: { color: any; _origin: any }[]) => {
+ /* eslint no-underscore-dangle:0 */
+ const origin = item[0]._origin;
+ origin.color = item[0].color;
+ origin.checked = true;
+ return origin;
+ });
+
+ this.setState({
+ legendData,
+ });
+ };
+
+ handleRoot = (n: HTMLDivElement) => {
+ this.root = n;
+ };
+
+ handleLegendClick = (item: { checked: boolean }, i: string | number) => {
+ const newItem = item;
+ newItem.checked = !newItem.checked;
+
+ const { legendData } = this.state;
+ legendData[i] = newItem;
+
+ const filteredLegendData = legendData.filter((l) => l.checked).map((l) => l.x);
+
+ if (this.chart) {
+ this.chart.filter('x', (val) => filteredLegendData.indexOf(`${val}`) > -1);
+ }
+
+ this.setState({
+ legendData,
+ });
+ };
+
+ render() {
+ const {
+ valueFormat,
+ subTitle,
+ total,
+ hasLegend = false,
+ className,
+ style,
+ height = 0,
+ forceFit = true,
+ percent,
+ color,
+ inner = 0.75,
+ animate = true,
+ colors,
+ lineWidth = 1,
+ } = this.props;
+
+ const { legendData, legendBlock } = this.state;
+ const pieClassName = classNames(styles.pie, className, {
+ [styles.hasLegend]: !!hasLegend,
+ [styles.legendBlock]: legendBlock,
+ });
+
+ const {
+ data: propsData,
+ selected: propsSelected = true,
+ tooltip: propsTooltip = true,
+ } = this.props;
+
+ let data = propsData || [];
+ let selected = propsSelected;
+ let tooltip = propsTooltip;
+
+ const defaultColors = colors;
+ data = data || [];
+ selected = selected || true;
+ tooltip = tooltip || true;
+ let formatColor;
+
+ const scale = {
+ x: {
+ type: 'cat',
+ range: [0, 1],
+ },
+ y: {
+ min: 0,
+ },
+ };
+
+ if (percent || percent === 0) {
+ selected = false;
+ tooltip = false;
+ formatColor = (value: string) => {
+ if (value === '占比') {
+ return color || 'rgba(24, 144, 255, 0.85)';
+ }
+ return '#F0F2F5';
+ };
+
+ data = [
+ {
+ x: '占比',
+ y: parseFloat(`${percent}`),
+ },
+ {
+ x: '反比',
+ y: 100 - parseFloat(`${percent}`),
+ },
+ ];
+ }
+
+ const tooltipFormat: [string, (...args: any[]) => { name?: string; value: string }] = [
+ 'x*percent',
+ (x: string, p: number) => ({
+ name: x,
+ value: `${(p * 100).toFixed(2)}%`,
+ }),
+ ];
+
+ const padding = [12, 0, 12, 0] as [number, number, number, number];
+
+ const dv = new DataView();
+ dv.source(data).transform({
+ type: 'percent',
+ field: 'y',
+ dimension: 'x',
+ as: 'percent',
+ });
+
+ return (
+
+
+
+
+ {!!tooltip && }
+
+
+
+
+ {(subTitle || total) && (
+
+ {subTitle &&
{subTitle}
}
+ {/* eslint-disable-next-line */}
+ {total && (
+
{typeof total === 'function' ? total() : total}
+ )}
+
+ )}
+
+
+
+ {hasLegend && (
+
+ {legendData.map((item, i) => (
+ - this.handleLegendClick(item, i)}>
+
+ {item.x}
+
+
+ {`${(Number.isNaN(item.percent) ? 0 : item.percent * 100).toFixed(2)}%`}
+
+ {valueFormat ? valueFormat(item.y) : item.y}
+
+ ))}
+
+ )}
+
+ );
+ }
+}
+
+export default autoHeight()(Pie);
diff --git a/src/pages/dashboard/monitor/components/Charts/TagCloud/index.tsx b/src/pages/dashboard/monitor/components/Charts/TagCloud/index.tsx
new file mode 100644
index 0000000..f3ee519
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Charts/TagCloud/index.tsx
@@ -0,0 +1,211 @@
+import { Chart, Coord, Geom, Shape, Tooltip } from 'bizcharts';
+import React, { Component } from 'react';
+
+import DataSet from '@antv/data-set';
+import Debounce from 'lodash.debounce';
+import classNames from 'classnames';
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+/* eslint no-underscore-dangle: 0 */
+/* eslint no-param-reassign: 0 */
+
+const imgUrl = 'https://gw.alipayobjects.com/zos/rmsportal/gWyeGLCdFFRavBGIDzWk.png';
+
+export type TagCloudProps = {
+ data: {
+ name: string;
+ value: string;
+ }[];
+ height?: number;
+ className?: string;
+ style?: React.CSSProperties;
+};
+
+type TagCloudState = {
+ dv: any;
+ height?: number;
+ width: number;
+};
+
+class TagCloud extends Component {
+ state = {
+ dv: null,
+ height: 0,
+ width: 0,
+ };
+
+ requestRef: number = 0;
+
+ isUnmount: boolean = false;
+
+ root: HTMLDivElement | undefined = undefined;
+
+ imageMask: HTMLImageElement | undefined = undefined;
+
+ componentDidMount() {
+ requestAnimationFrame(() => {
+ this.initTagCloud();
+ this.renderChart(this.props);
+ });
+ window.addEventListener('resize', this.resize, { passive: true });
+ }
+
+ componentDidUpdate(preProps?: TagCloudProps) {
+ const { data } = this.props;
+ if (preProps && JSON.stringify(preProps.data) !== JSON.stringify(data)) {
+ this.renderChart(this.props);
+ }
+ }
+
+ componentWillUnmount() {
+ this.isUnmount = true;
+ window.cancelAnimationFrame(this.requestRef);
+ window.removeEventListener('resize', this.resize);
+ }
+
+ resize = () => {
+ this.requestRef = requestAnimationFrame(() => {
+ this.renderChart(this.props);
+ });
+ };
+
+ saveRootRef = (node: HTMLDivElement) => {
+ this.root = node;
+ };
+
+ initTagCloud = () => {
+ function getTextAttrs(cfg: {
+ x?: any;
+ y?: any;
+ style?: any;
+ opacity?: any;
+ origin?: any;
+ color?: any;
+ }) {
+ return {
+ ...cfg.style,
+ fillOpacity: cfg.opacity,
+ fontSize: cfg.origin._origin.size,
+ rotate: cfg.origin._origin.rotate,
+ text: cfg.origin._origin.text,
+ textAlign: 'center',
+ fontFamily: cfg.origin._origin.font,
+ fill: cfg.color,
+ textBaseline: 'Alphabetic',
+ };
+ }
+
+ (Shape as any).registerShape('point', 'cloud', {
+ drawShape(
+ cfg: { x: any; y: any },
+ container: { addShape: (arg0: string, arg1: { attrs: any }) => void },
+ ) {
+ const attrs = getTextAttrs(cfg);
+ return container.addShape('text', {
+ attrs: {
+ ...attrs,
+ x: cfg.x,
+ y: cfg.y,
+ },
+ });
+ },
+ });
+ };
+
+ renderChart = Debounce((nextProps: TagCloudProps) => {
+ // const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
+ const { data, height } = nextProps || this.props;
+ if (data.length < 1 || !this.root) {
+ return;
+ }
+
+ const h = height;
+ const w = this.root.offsetWidth;
+
+ const onload = () => {
+ const dv = new DataSet.View().source(data);
+ const range = dv.range('value');
+ const [min, max] = range;
+ dv.transform({
+ type: 'tag-cloud',
+ fields: ['name', 'value'],
+ imageMask: this.imageMask,
+ font: 'Verdana',
+ size: [w, h], // 宽高设置最好根据 imageMask 做调整
+ padding: 0,
+ timeInterval: 5000, // max execute time
+ rotate() {
+ return 0;
+ },
+ fontSize(d: { value: number }) {
+ const size = ((d.value - min) / (max - min)) ** 2;
+ return size * (17.5 - 5) + 5;
+ },
+ });
+
+ if (this.isUnmount) {
+ return;
+ }
+
+ this.setState({
+ dv,
+ width: w,
+ height: h,
+ });
+ };
+
+ if (!this.imageMask) {
+ this.imageMask = new Image();
+ this.imageMask.crossOrigin = '';
+ this.imageMask.src = imgUrl;
+
+ this.imageMask.onload = onload;
+ } else {
+ onload();
+ }
+ }, 200);
+
+ render() {
+ const { className, height } = this.props;
+ const { dv, width, height: stateHeight } = this.state;
+
+ return (
+
+ {dv && (
+
+
+
+
+
+ )}
+
+ );
+ }
+}
+
+export default autoHeight()(TagCloud);
diff --git a/src/pages/dashboard/monitor/components/Charts/WaterWave/index.tsx b/src/pages/dashboard/monitor/components/Charts/WaterWave/index.tsx
new file mode 100644
index 0000000..a9b6411
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Charts/WaterWave/index.tsx
@@ -0,0 +1,235 @@
+import React, { Component } from 'react';
+
+import autoHeight from '../autoHeight';
+import styles from './index.less';
+
+/* eslint no-return-assign: 0 */
+/* eslint no-mixed-operators: 0 */
+// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90
+
+export type WaterWaveProps = {
+ title: React.ReactNode;
+ color?: string;
+ height?: number;
+ percent: number;
+ style?: React.CSSProperties;
+};
+
+class WaterWave extends Component {
+ state = {
+ radio: 1,
+ };
+
+ timer: number = 0;
+
+ root: HTMLDivElement | undefined | null = null;
+
+ node: HTMLCanvasElement | undefined | null = null;
+
+ componentDidMount() {
+ this.renderChart();
+ this.resize();
+ window.addEventListener(
+ 'resize',
+ () => {
+ requestAnimationFrame(() => this.resize());
+ },
+ { passive: true },
+ );
+ }
+
+ componentDidUpdate(props: WaterWaveProps) {
+ const { percent } = this.props;
+ if (props.percent !== percent) {
+ // 不加这个会造成绘制缓慢
+ this.renderChart('update');
+ }
+ }
+
+ componentWillUnmount() {
+ cancelAnimationFrame(this.timer);
+ if (this.node) {
+ this.node.innerHTML = '';
+ }
+ window.removeEventListener('resize', this.resize);
+ }
+
+ resize = () => {
+ if (this.root) {
+ const { height = 1 } = this.props;
+ const { offsetWidth } = this.root.parentNode as HTMLElement;
+ this.setState({
+ radio: offsetWidth < height ? offsetWidth / height : 1,
+ });
+ }
+ };
+
+ renderChart(type?: string) {
+ const { percent, color = '#1890FF' } = this.props;
+ const data = percent / 100;
+ cancelAnimationFrame(this.timer);
+
+ if (!this.node || (data !== 0 && !data)) {
+ return;
+ }
+
+ const canvas = this.node;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) {
+ return;
+ }
+ const canvasWidth = canvas.width;
+ const canvasHeight = canvas.height;
+ const radius = canvasWidth / 2;
+ const lineWidth = 2;
+ const cR = radius - lineWidth;
+
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth * 2;
+
+ const axisLength = canvasWidth - lineWidth;
+ const unit = axisLength / 8;
+ const range = 0.2; // 振幅
+ let currRange = range;
+ const xOffset = lineWidth;
+ let sp = 0; // 周期偏移量
+ let currData = 0;
+ const waveupsp = 0.005; // 水波上涨速度
+
+ let arcStack: number[][] = [];
+ const bR = radius - lineWidth;
+ const circleOffset = -(Math.PI / 2);
+ let circleLock = true;
+
+ for (let i = circleOffset; i < circleOffset + 2 * Math.PI; i += 1 / (8 * Math.PI)) {
+ arcStack.push([radius + bR * Math.cos(i), radius + bR * Math.sin(i)]);
+ }
+
+ const cStartPoint = arcStack.shift() as number[];
+ ctx.strokeStyle = color;
+ ctx.moveTo(cStartPoint[0], cStartPoint[1]);
+
+ const drawSin = () => {
+ if (!ctx) {
+ return;
+ }
+ ctx.beginPath();
+ ctx.save();
+
+ const sinStack = [];
+ for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
+ const x = sp + (xOffset + i) / unit;
+ const y = Math.sin(x) * currRange;
+ const dx = i;
+ const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
+
+ ctx.lineTo(dx, dy);
+ sinStack.push([dx, dy]);
+ }
+
+ const startPoint = sinStack.shift() as number[];
+
+ ctx.lineTo(xOffset + axisLength, canvasHeight);
+ ctx.lineTo(xOffset, canvasHeight);
+ ctx.lineTo(startPoint[0], startPoint[1]);
+
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
+ gradient.addColorStop(0, '#ffffff');
+ gradient.addColorStop(1, color);
+ ctx.fillStyle = gradient;
+ ctx.fill();
+ ctx.restore();
+ };
+
+ const render = () => {
+ if (!ctx) {
+ return;
+ }
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
+ if (circleLock && type !== 'update') {
+ if (arcStack.length) {
+ const temp = arcStack.shift() as number[];
+ ctx.lineTo(temp[0], temp[1]);
+ ctx.stroke();
+ } else {
+ circleLock = false;
+ ctx.lineTo(cStartPoint[0], cStartPoint[1]);
+ ctx.stroke();
+ arcStack = [];
+
+ ctx.globalCompositeOperation = 'destination-over';
+ ctx.beginPath();
+ ctx.lineWidth = lineWidth;
+ ctx.arc(radius, radius, bR, 0, 2 * Math.PI, true);
+
+ ctx.beginPath();
+ ctx.save();
+ ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true);
+
+ ctx.restore();
+ ctx.clip();
+ ctx.fillStyle = color;
+ }
+ } else {
+ if (data >= 0.85) {
+ if (currRange > range / 4) {
+ const t = range * 0.01;
+ currRange -= t;
+ }
+ } else if (data <= 0.1) {
+ if (currRange < range * 1.5) {
+ const t = range * 0.01;
+ currRange += t;
+ }
+ } else {
+ if (currRange <= range) {
+ const t = range * 0.01;
+ currRange += t;
+ }
+ if (currRange >= range) {
+ const t = range * 0.01;
+ currRange -= t;
+ }
+ }
+ if (data - currData > 0) {
+ currData += waveupsp;
+ }
+ if (data - currData < 0) {
+ currData -= waveupsp;
+ }
+
+ sp += 0.07;
+ drawSin();
+ }
+ this.timer = requestAnimationFrame(render);
+ };
+ render();
+ }
+
+ render() {
+ const { radio } = this.state;
+ const { percent, title, height = 1 } = this.props;
+ return (
+ (this.root = n)}
+ style={{ transform: `scale(${radio})` }}
+ >
+
+
+
+ {title && {title}}
+
{percent}%
+
+
+ );
+ }
+}
+
+export default autoHeight()(WaterWave);
diff --git a/src/pages/dashboard/monitor/components/Charts/autoHeight.tsx b/src/pages/dashboard/monitor/components/Charts/autoHeight.tsx
new file mode 100644
index 0000000..f37acac
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Charts/autoHeight.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+
+export type IReactComponent =
+ | React.StatelessComponent
+ | React.ComponentClass
+ | React.ClassicComponentClass
;
+
+function computeHeight(node: HTMLDivElement) {
+ const { style } = node;
+ style.height = '100%';
+ const totalHeight = parseInt(`${getComputedStyle(node).height}`, 10);
+ const padding =
+ parseInt(`${getComputedStyle(node).paddingTop}`, 10) +
+ parseInt(`${getComputedStyle(node).paddingBottom}`, 10);
+ return totalHeight - padding;
+}
+
+function getAutoHeight(n: HTMLDivElement) {
+ if (!n) {
+ return 0;
+ }
+
+ const node = n;
+
+ let height = computeHeight(node);
+ const parentNode = node.parentNode as HTMLDivElement;
+ if (parentNode) {
+ height = computeHeight(parentNode);
+ }
+
+ return height;
+}
+
+type AutoHeightProps = {
+ height?: number;
+};
+
+function autoHeight() {
+ return
(
+ WrappedComponent: React.ComponentClass
| React.FC
,
+ ): React.ComponentClass
=> {
+ class AutoHeightComponent extends React.Component
{
+ state = {
+ computedHeight: 0,
+ };
+
+ root: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ const { height } = this.props;
+ if (!height && this.root) {
+ let h = getAutoHeight(this.root);
+ this.setState({ computedHeight: h });
+ if (h < 1) {
+ h = getAutoHeight(this.root);
+ this.setState({ computedHeight: h });
+ }
+ }
+ }
+
+ handleRoot = (node: HTMLDivElement) => {
+ this.root = node;
+ };
+
+ render() {
+ const { height } = this.props;
+ const { computedHeight } = this.state;
+ const h = height || computedHeight;
+ return (
+
+ {h > 0 && }
+
+ );
+ }
+ }
+ return AutoHeightComponent;
+ };
+}
+export default autoHeight;
diff --git a/src/pages/dashboard/monitor/components/Map/index.tsx b/src/pages/dashboard/monitor/components/Map/index.tsx
new file mode 100644
index 0000000..ff9d924
--- /dev/null
+++ b/src/pages/dashboard/monitor/components/Map/index.tsx
@@ -0,0 +1,145 @@
+import * as React from 'react';
+import { HeatmapLayer, MapboxScene, PointLayer } from '@antv/l7-react';
+import { PageLoading } from '@ant-design/pro-layout';
+
+const colors = ['#eff3ff', '#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#084594'];
+export default class Map extends React.Component {
+ state = {
+ data: null,
+ grid: null,
+ loading: false,
+ };
+
+ public async componentDidMount() {
+ const [geoData, gridData] = await Promise.all([
+ fetch(
+ 'https://gw.alipayobjects.com/os/bmw-prod/c5dba875-b6ea-4e88-b778-66a862906c93.json',
+ ).then((d) => d.json()),
+ fetch(
+ 'https://gw.alipayobjects.com/os/bmw-prod/8990e8b4-c58e-419b-afb9-8ea3daff2dd1.json',
+ ).then((d) => d.json()),
+ ]);
+ this.setState({
+ data: geoData,
+ grid: gridData,
+ loading: true,
+ });
+ }
+
+ public render() {
+ const { data, grid, loading } = this.state;
+ return loading === false ? (
+
+ ) : (
+
+ {grid && (
+
+ )}
+ {data && [
+ ,
+ {
+ return v > 2000;
+ },
+ }}
+ size={{
+ values: 12,
+ }}
+ style={{
+ opacity: 1,
+ strokeOpacity: 1,
+ strokeWidth: 0,
+ }}
+ />,
+ ]}
+
+ );
+ }
+}
diff --git a/src/pages/dashboard/monitor/data.d.ts b/src/pages/dashboard/monitor/data.d.ts
new file mode 100644
index 0000000..b6efef3
--- /dev/null
+++ b/src/pages/dashboard/monitor/data.d.ts
@@ -0,0 +1,5 @@
+export type TagType = {
+ name: string;
+ value: number;
+ type: string;
+};
diff --git a/src/pages/dashboard/monitor/index.tsx b/src/pages/dashboard/monitor/index.tsx
new file mode 100644
index 0000000..779d200
--- /dev/null
+++ b/src/pages/dashboard/monitor/index.tsx
@@ -0,0 +1,152 @@
+import { Card, Col, Row, Statistic } from 'antd';
+import { useRequest } from 'umi';
+import type { FC } from 'react';
+import { Gauge, WordCloud, Liquid, RingProgress } from '@ant-design/charts';
+import type { WordCloudData } from '@antv/g2plot/esm/plots/word-cloud/layer';
+
+import { GridContent } from '@ant-design/pro-layout';
+import numeral from 'numeral';
+import Map from './components/Map';
+import ActiveChart from './components/ActiveChart';
+import { queryTags } from './service';
+import styles from './style.less';
+
+const { Countdown } = Statistic;
+
+const deadline = Date.now() + 1000 * 60 * 60 * 24 * 2 + 1000 * 30; // Moment is also OK
+
+const Monitor: FC = () => {
+ const { loading, data } = useRequest(queryTags);
+
+ const wordCloudData: WordCloudData[] = (data?.list || []).map((item) => {
+ return {
+ id: +Date.now(),
+ word: item.name,
+ weight: item.value,
+ };
+ });
+
+ return (
+
+ <>
+
+
+
+
+