;
export type IReactComponent =
| React.StatelessComponent
| React.ComponentClass
| React.ClassicComponentClass
;
interface Secured {
(authority: authority, error?: React.ReactNode): (target: T) => T;
}
export interface AuthorizedRouteProps extends RouteProps {
authority: authority;
}
export class AuthorizedRoute extends React.Component {}
interface check {
(
authority: authority,
target: T,
Exception: S
): T | S;
}
export interface AuthorizedProps {
authority: authority;
noMatch?: React.ReactNode;
}
export class Authorized extends React.Component {
static Secured: Secured;
static AuthorizedRoute: typeof AuthorizedRoute;
static check: check;
}
declare function renderAuthorize(currentAuthority: string): typeof Authorized;
export default renderAuthorize;
================================================
FILE: src/components/Authorized/index.js
================================================
import Authorized from './Authorized';
import AuthorizedRoute from './AuthorizedRoute';
import Secured from './Secured';
import check from './CheckPermissions';
import renderAuthorize from './renderAuthorize';
Authorized.Secured = Secured;
Authorized.AuthorizedRoute = AuthorizedRoute;
Authorized.check = check;
export default renderAuthorize(Authorized);
================================================
FILE: src/components/Authorized/index.md
================================================
---
title:
en-US: Authorized
zh-CN: Authorized
subtitle: 权限
cols: 1
order: 15
---
权限组件,通过比对现有权限与准入权限,决定相关元素的展示。
## API
### RenderAuthorized
`RenderAuthorized: (currentAuthority: string | () => string) => Authorized`
权限组件默认 export RenderAuthorized 函数,它接收当前权限作为参数,返回一个权限对象,该对象提供以下几种使用方式。
### Authorized
最基础的权限控制。
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| children | 正常渲染的元素,权限判断通过时展示 | ReactNode | - |
| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean | Promise` | - |
| noMatch | 权限异常渲染元素,权限判断不通过时展示 | ReactNode | - |
### Authorized.AuthorizedRoute
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | array | Promise | (currentAuthority) => boolean | Promise` | - |
| redirectPath | 权限异常时重定向的页面路由 | string | - |
其余参数与 `Route` 相同。
### Authorized.Secured
注解方式,`@Authorized.Secured(authority, error)`
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean | Promise` | - |
| error | 权限异常时渲染元素 | ReactNode | |
### Authorized.check
函数形式的 Authorized,用于某些不能被 HOC 包裹的组件。 `Authorized.check(authority, target, Exception)`
注意:传入一个 Promise 时,无论正确还是错误返回的都是一个 ReactClass。
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| authority | 准入权限/权限判断 | `string | Promise | (currentAuthority) => boolean | Promise` | - |
| target | 权限判断通过时渲染的元素 | ReactNode | - |
| Exception | 权限异常时渲染元素 | ReactNode | - |
================================================
FILE: src/components/Authorized/renderAuthorize.js
================================================
/* eslint-disable import/no-mutable-exports */
let CURRENT = 'NULL';
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
const renderAuthorize = Authorized => currentAuthority => {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (
Object.prototype.toString.call(currentAuthority) === '[object String]' ||
Array.isArray(currentAuthority)
) {
CURRENT = currentAuthority;
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
export { CURRENT };
export default Authorized => renderAuthorize(Authorized);
================================================
FILE: src/components/Charts/Bar/index.d.ts
================================================
import * as React from 'react';
export interface IBarProps {
title: React.ReactNode;
color?: string;
padding?: [number, number, number, number];
height: number;
data: Array<{
x: string;
y: number;
}>;
autoLabel?: boolean;
style?: React.CSSProperties;
}
export default class Bar extends React.Component {}
================================================
FILE: src/components/Charts/Bar/index.js
================================================
import React, { Component } from 'react';
import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import autoHeight from '../autoHeight';
import styles from '../index.less';
@autoHeight()
class Bar extends Component {
state = {
autoHideXLabels: false,
};
componentDidMount() {
window.addEventListener('resize', this.resize, { passive: true });
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
handleRoot = n => {
this.root = n;
};
handleRef = n => {
this.node = n;
};
@Bind()
@Debounce(400)
resize() {
if (!this.node) {
return;
}
const canvasWidth = this.node.parentNode.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,
});
}
}
render() {
const {
height,
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 = [
'x*y',
(x, y) => ({
name: x,
value: y,
}),
];
return (
);
}
}
export default Bar;
================================================
FILE: src/components/Charts/ChartCard/index.d.ts
================================================
import * as React from 'react';
import { CardProps } from 'antd/lib/card';
export interface IChartCardProps extends CardProps {
title: React.ReactNode;
action?: React.ReactNode;
total?: React.ReactNode | number | (() => React.ReactNode | number);
footer?: React.ReactNode;
contentHeight?: number;
avatar?: React.ReactNode;
style?: React.CSSProperties;
}
export default class ChartCard extends React.Component {}
================================================
FILE: src/components/Charts/ChartCard/index.js
================================================
import React from 'react';
import { Card } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
const renderTotal = total => {
let totalDom;
switch (typeof total) {
case 'undefined':
totalDom = null;
break;
case 'function':
totalDom = {total()}
;
break;
default:
totalDom = {total}
;
}
return totalDom;
};
class ChartCard extends React.PureComponent {
renderConnet = () => {
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.renderConnet()}
);
}
}
export default ChartCard;
================================================
FILE: src/components/Charts/ChartCard/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.chartCard {
position: relative;
.chartTop {
position: relative;
overflow: hidden;
width: 100%;
}
.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 {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
height: 22px;
}
.action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: @heading-color;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
.content {
margin-bottom: 12px;
position: relative;
width: 100%;
}
.contentFixed {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.footer {
border-top: 1px solid @border-color-split;
padding-top: 9px;
margin-top: 8px;
& > * {
position: relative;
}
}
.footerMargin {
margin-top: 20px;
}
}
================================================
FILE: src/components/Charts/Field/index.d.ts
================================================
import * as React from 'react';
export interface IFieldProps {
label: React.ReactNode;
value: React.ReactNode;
style?: React.CSSProperties;
}
export default class Field extends React.Component {}
================================================
FILE: src/components/Charts/Field/index.js
================================================
import React from 'react';
import styles from './index.less';
const Field = ({ label, value, ...rest }) => (
{label}
{value}
);
export default Field;
================================================
FILE: src/components/Charts/Field/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
span {
font-size: @font-size-base;
line-height: 22px;
}
span:last-child {
margin-left: 8px;
color: @heading-color;
}
}
================================================
FILE: src/components/Charts/Gauge/index.d.ts
================================================
import * as React from 'react';
export interface IGaugeProps {
title: React.ReactNode;
color?: string;
height: number;
bgColor?: number;
percent: number;
style?: React.CSSProperties;
}
export default class Gauge extends React.Component {}
================================================
FILE: src/components/Charts/Gauge/index.js
================================================
import React from 'react';
import { Chart, Geom, Axis, Coord, Guide, Shape } from 'bizcharts';
import autoHeight from '../autoHeight';
const { Arc, Html, Line } = Guide;
const defaultFormatter = val => {
switch (val) {
case '2':
return '差';
case '4':
return '中';
case '6':
return '良';
case '8':
return '优';
default:
return '';
}
};
Shape.registerShape('point', 'pointer', {
drawShape(cfg, group) {
let point = cfg.points[0];
point = this.parsePoint(point);
const center = this.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',
},
});
},
});
@autoHeight()
class Gauge extends React.Component {
render() {
const {
title,
height,
percent,
forceFit = true,
formatter = defaultFormatter,
color = '#2F9CFF',
bgColor = '#F0F2F5',
} = this.props;
const cols = {
value: {
type: 'linear',
min: 0,
max: 10,
tickCount: 6,
nice: true,
},
};
const data = [{ value: percent / 10 }];
return (
`
${title}
${data[0].value * 10}%
`}
/>
);
}
}
export default Gauge;
================================================
FILE: src/components/Charts/MiniArea/index.d.ts
================================================
import * as React from 'react';
// g2已经更新到3.0
// 不带的写了
export interface IAxis {
title: any;
line: any;
gridAlign: any;
labels: any;
tickLine: any;
grid: any;
}
export interface IMiniAreaProps {
color?: string;
height: number;
borderColor?: string;
line?: boolean;
animate?: boolean;
xAxis?: IAxis;
yAxis?: IAxis;
data: Array<{
x: number | string;
y: number;
}>;
}
export default class MiniArea extends React.Component {}
================================================
FILE: src/components/Charts/MiniArea/index.js
================================================
import React from 'react';
import { Chart, Axis, Tooltip, Geom } from 'bizcharts';
import autoHeight from '../autoHeight';
import styles from '../index.less';
@autoHeight()
class MiniArea extends React.PureComponent {
render() {
const {
height,
data = [],
forceFit = true,
color = 'rgba(24, 144, 255, 0.2)',
borderColor = '#1089ff',
scale = {},
borderWidth = 2,
line,
xAxis,
yAxis,
animate = true,
} = this.props;
const padding = [36, 5, 30, 5];
const scaleProps = {
x: {
type: 'cat',
range: [0, 1],
...scale.x,
},
y: {
min: 0,
...scale.y,
},
};
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y,
}),
];
const chartHeight = height + 54;
return (
{height > 0 && (
{line ? (
) : (
)}
)}
);
}
}
export default MiniArea;
================================================
FILE: src/components/Charts/MiniBar/index.d.ts
================================================
import * as React from 'react';
export interface IMiniBarProps {
color?: string;
height: number;
data: Array<{
x: number | string;
y: number;
}>;
style?: React.CSSProperties;
}
export default class MiniBar extends React.Component {}
================================================
FILE: src/components/Charts/MiniBar/index.js
================================================
import React from 'react';
import { Chart, Tooltip, Geom } from 'bizcharts';
import autoHeight from '../autoHeight';
import styles from '../index.less';
@autoHeight()
class MiniBar extends React.Component {
render() {
const { height, forceFit = true, color = '#1890FF', data = [] } = this.props;
const scale = {
x: {
type: 'cat',
},
y: {
min: 0,
},
};
const padding = [36, 5, 30, 5];
const tooltip = [
'x*y',
(x, y) => ({
name: x,
value: y,
}),
];
// for tooltip not to be hide
const chartHeight = height + 54;
return (
);
}
}
export default MiniBar;
================================================
FILE: src/components/Charts/MiniProgress/index.d.ts
================================================
import * as React from 'react';
export interface IMiniProgressProps {
target: number;
color?: string;
strokeWidth?: number;
percent?: number;
style?: React.CSSProperties;
}
export default class MiniProgress extends React.Component {}
================================================
FILE: src/components/Charts/MiniProgress/index.js
================================================
import React from 'react';
import { Tooltip } from 'antd';
import styles from './index.less';
const MiniProgress = ({ target, color = 'rgb(19, 194, 194)', strokeWidth, percent }) => (
);
export default MiniProgress;
================================================
FILE: src/components/Charts/MiniProgress/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.miniProgress {
padding: 5px 0;
position: relative;
width: 100%;
.progressWrap {
background-color: @background-color-base;
position: relative;
}
.progress {
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: @primary-color;
width: 0;
height: 100%;
}
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
}
span:last-child {
top: auto;
bottom: 0;
}
}
}
================================================
FILE: src/components/Charts/Pie/index.d.ts
================================================
import * as React from 'react';
export interface IPieProps {
animate?: boolean;
color?: string;
colors?: string[];
height: number;
hasLegend?: boolean;
padding?: [number, number, number, number];
percent?: number;
data?: Array<{
x: string | string;
y: number;
}>;
total?: React.ReactNode | number | (() => React.ReactNode | number);
title?: React.ReactNode;
tooltip?: boolean;
valueFormat?: (value: string) => string | React.ReactNode;
subTitle?: React.ReactNode;
}
export default class Pie extends React.Component {}
================================================
FILE: src/components/Charts/Pie/index.js
================================================
import React, { Component } from 'react';
import { Chart, Tooltip, Geom, Coord } from 'bizcharts';
import { DataView } from '@antv/data-set';
import { Divider } from 'antd';
import classNames from 'classnames';
import ReactFitText from 'react-fittext';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import autoHeight from '../autoHeight';
import styles from './index.less';
/* eslint react/no-danger:0 */
@autoHeight()
class Pie extends Component {
state = {
legendData: [],
legendBlock: false,
};
componentDidMount() {
window.addEventListener(
'resize',
() => {
this.requestRef = requestAnimationFrame(() => this.resize());
},
{ passive: true }
);
}
componentDidUpdate(preProps) {
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() {
window.cancelAnimationFrame(this.requestRef);
window.removeEventListener('resize', this.resize);
this.resize.cancel();
}
getG2Instance = 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.get('dataArray') || []; // 获取图形对应的
const legendData = items.map(item => {
/* 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 => {
this.root = n;
};
handleLegendClick = (item, i) => {
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,
});
};
// for window resize auto responsive legend
@Bind()
@Debounce(300)
resize() {
const { hasLegend } = this.props;
const { legendBlock } = this.state;
if (!hasLegend || !this.root) {
window.removeEventListener('resize', this.resize);
return;
}
if (this.root.parentNode.clientWidth <= 380) {
if (!legendBlock) {
this.setState({
legendBlock: true,
});
}
} else if (legendBlock) {
this.setState({
legendBlock: false,
});
}
}
render() {
const {
valueFormat,
subTitle,
total,
hasLegend = false,
className,
style,
height,
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 => {
if (value === '占比') {
return color || 'rgba(24, 144, 255, 0.85)';
}
return '#F0F2F5';
};
data = [
{
x: '占比',
y: parseFloat(percent),
},
{
x: '反比',
y: 100 - parseFloat(percent),
},
];
}
const tooltipFormat = [
'x*percent',
(x, p) => ({
name: x,
value: `${(p * 100).toFixed(2)}%`,
}),
];
const padding = [12, 0, 12, 0];
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 Pie;
================================================
FILE: src/components/Charts/Pie/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.pie {
position: relative;
.chart {
position: relative;
}
&.hasLegend .chart {
width: ~'calc(100% - 240px)';
}
.legend {
position: absolute;
right: 0;
min-width: 200px;
top: 50%;
transform: translateY(-50%);
margin: 0 20px;
list-style: none;
padding: 0;
li {
cursor: pointer;
margin-bottom: 16px;
height: 22px;
line-height: 22px;
&:last-child {
margin-bottom: 0;
}
}
}
.dot {
border-radius: 8px;
display: inline-block;
margin-right: 8px;
position: relative;
top: -1px;
height: 8px;
width: 8px;
}
.line {
background-color: @border-color-split;
display: inline-block;
margin-right: 8px;
width: 1px;
height: 16px;
}
.legendTitle {
color: @text-color;
}
.percent {
color: @text-color-secondary;
}
.value {
position: absolute;
right: 0;
}
.title {
margin-bottom: 8px;
}
.total {
position: absolute;
left: 50%;
top: 50%;
text-align: center;
max-height: 62px;
transform: translate(-50%, -50%);
& > h4 {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
height: 22px;
margin-bottom: 8px;
font-weight: normal;
}
& > p {
color: @heading-color;
display: block;
font-size: 1.2em;
height: 32px;
line-height: 32px;
white-space: nowrap;
}
}
}
.legendBlock {
&.hasLegend .chart {
width: 100%;
margin: 0 0 32px 0;
}
.legend {
position: relative;
transform: none;
}
}
================================================
FILE: src/components/Charts/Radar/index.d.ts
================================================
import * as React from 'react';
export interface IRadarProps {
title?: React.ReactNode;
height: number;
padding?: [number, number, number, number];
hasLegend?: boolean;
data: Array<{
name: string;
label: string;
value: string;
}>;
style?: React.CSSProperties;
}
export default class Radar extends React.Component {}
================================================
FILE: src/components/Charts/Radar/index.js
================================================
import React, { Component } from 'react';
import { Chart, Tooltip, Geom, Coord, Axis } from 'bizcharts';
import { Row, Col } from 'antd';
import autoHeight from '../autoHeight';
import styles from './index.less';
/* eslint react/no-danger:0 */
@autoHeight()
class Radar extends Component {
state = {
legendData: [],
};
componentDidMount() {
this.getLegendData();
}
componentDidUpdate(preProps) {
const { data } = this.props;
if (data !== preProps.data) {
this.getLegendData();
}
}
getG2Instance = chart => {
this.chart = chart;
};
// for custom lengend view
getLegendData = () => {
if (!this.chart) return;
const geom = this.chart.getAllGeoms()[0]; // 获取所有的图形
if (!geom) return;
const items = geom.get('dataArray') || []; // 获取图形对应的
const legendData = items.map(item => {
// eslint-disable-next-line
const origins = item.map(t => t._origin);
const result = {
name: origins[0].name,
color: item[0].color,
checked: true,
value: origins.reduce((p, n) => p + n.value, 0),
};
return result;
});
this.setState({
legendData,
});
};
handleRef = n => {
this.node = n;
};
handleLegendClick = (item, i) => {
const newItem = item;
newItem.checked = !newItem.checked;
const { legendData } = this.state;
legendData[i] = newItem;
const filteredLegendData = legendData.filter(l => l.checked).map(l => l.name);
if (this.chart) {
this.chart.filter('name', val => filteredLegendData.indexOf(val) > -1);
this.chart.repaint();
}
this.setState({
legendData,
});
};
render() {
const defaultColors = [
'#1890FF',
'#FACC14',
'#2FC25B',
'#8543E0',
'#F04864',
'#13C2C2',
'#fa8c16',
'#a0d911',
];
const {
data = [],
height = 0,
title,
hasLegend = false,
forceFit = true,
tickCount = 4,
padding = [35, 30, 16, 30],
animate = true,
colors = defaultColors,
} = this.props;
const { legendData } = this.state;
const scale = {
value: {
min: 0,
tickCount,
},
};
const chartHeight = height - (hasLegend ? 80 : 22);
return (
{title &&
{title} }
{hasLegend && (
{legendData.map((item, i) => (
this.handleLegendClick(item, i)}
>
))}
)}
);
}
}
export default Radar;
================================================
FILE: src/components/Charts/Radar/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.radar {
.legend {
margin-top: 16px;
.legendItem {
position: relative;
text-align: center;
cursor: pointer;
color: @text-color-secondary;
line-height: 22px;
p {
margin: 0;
}
h6 {
color: @heading-color;
padding-left: 16px;
font-size: 24px;
line-height: 32px;
margin-top: 4px;
margin-bottom: 0;
}
&:after {
background-color: @border-color-split;
position: absolute;
top: 8px;
right: 0;
height: 40px;
width: 1px;
content: '';
}
}
> :last-child .legendItem:after {
display: none;
}
.dot {
border-radius: 6px;
display: inline-block;
margin-right: 6px;
position: relative;
top: -1px;
height: 6px;
width: 6px;
}
}
}
================================================
FILE: src/components/Charts/TagCloud/index.d.ts
================================================
import * as React from 'react';
export interface ITagCloudProps {
data: Array<{
name: string;
value: number;
}>;
height: number;
style?: React.CSSProperties;
}
export default class TagCloud extends React.Component {}
================================================
FILE: src/components/Charts/TagCloud/index.js
================================================
import React, { Component } from 'react';
import { Chart, Geom, Coord, Shape } from 'bizcharts';
import DataSet from '@antv/data-set';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
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';
@autoHeight()
class TagCloud extends Component {
state = {
dv: null,
};
componentDidMount() {
requestAnimationFrame(() => {
this.initTagCloud();
this.renderChart();
});
window.addEventListener('resize', this.resize, { passive: true });
}
componentDidUpdate(preProps) {
const { data } = this.props;
if (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();
});
};
saveRootRef = node => {
this.root = node;
};
initTagCloud = () => {
function getTextAttrs(cfg) {
return Object.assign(
{},
{
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',
},
cfg.style
);
}
// 给point注册一个词云的shape
Shape.registerShape('point', 'cloud', {
drawShape(cfg, container) {
const attrs = getTextAttrs(cfg);
return container.addShape('text', {
attrs: Object.assign(attrs, {
x: cfg.x,
y: cfg.y,
}),
});
},
});
};
@Bind()
@Debounce(500)
renderChart(nextProps) {
// const colors = ['#1890FF', '#41D9C7', '#2FC25B', '#FACC14', '#9AE65C'];
const { data, height } = nextProps || this.props;
if (data.length < 1 || !this.root) {
return;
}
const h = height * 4;
const w = this.root.offsetWidth * 4;
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: 5,
timeInterval: 5000, // max execute time
rotate() {
return 0;
},
fontSize(d) {
// eslint-disable-next-line
return Math.pow((d.value - min) / (max - min), 2) * (70 - 20) + 20;
},
});
if (this.isUnmount) {
return;
}
this.setState({
dv,
w,
h,
});
};
if (!this.imageMask) {
this.imageMask = new Image();
this.imageMask.crossOrigin = '';
this.imageMask.src = imgUrl;
this.imageMask.onload = onload;
} else {
onload();
}
}
render() {
const { className, height } = this.props;
const { dv, w, h } = this.state;
return (
{dv && (
)}
);
}
}
export default TagCloud;
================================================
FILE: src/components/Charts/TagCloud/index.less
================================================
.tagCloud {
overflow: hidden;
canvas {
transform: scale(0.25);
transform-origin: 0 0;
}
}
================================================
FILE: src/components/Charts/TimelineChart/index.d.ts
================================================
import * as React from 'react';
export interface ITimelineChartProps {
data: Array<{
x: number;
y1: number;
y2?: number;
}>;
titleMap: { y1: string; y2?: string };
padding?: [number, number, number, number];
height?: number;
style?: React.CSSProperties;
}
export default class TimelineChart extends React.Component {}
================================================
FILE: src/components/Charts/TimelineChart/index.js
================================================
import React from 'react';
import { Chart, Tooltip, Geom, Legend, Axis } from 'bizcharts';
import DataSet from '@antv/data-set';
import Slider from 'bizcharts-plugin-slider';
import autoHeight from '../autoHeight';
import styles from './index.less';
@autoHeight()
class TimelineChart extends React.Component {
render() {
const {
title,
height = 400,
padding = [60, 20, 40, 40],
titleMap = {
y1: 'y1',
y2: 'y2',
},
borderWidth = 2,
data = [
{
x: 0,
y1: 0,
y2: 0,
},
],
} = this.props;
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 => {
const date = obj.x;
return date <= ds.state.end && date >= ds.state.start;
},
})
.transform({
type: 'map',
callback(row) {
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 (
);
}
}
export default TimelineChart;
================================================
FILE: src/components/Charts/TimelineChart/index.less
================================================
.timelineChart {
background: #fff;
}
================================================
FILE: src/components/Charts/WaterWave/index.d.ts
================================================
import * as React from 'react';
export interface IWaterWaveProps {
title: React.ReactNode;
color?: string;
height: number;
percent: number;
style?: React.CSSProperties;
}
export default class WaterWave extends React.Component {}
================================================
FILE: src/components/Charts/WaterWave/index.js
================================================
import React, { PureComponent } 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
@autoHeight()
class WaterWave extends PureComponent {
state = {
radio: 1,
};
componentDidMount() {
this.renderChart();
this.resize();
window.addEventListener(
'resize',
() => {
requestAnimationFrame(() => this.resize());
},
{ passive: true }
);
}
componentDidUpdate(props) {
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 } = this.props;
const { offsetWidth } = this.root.parentNode;
this.setState({
radio: offsetWidth < height ? offsetWidth / height : 1,
});
}
};
renderChart(type) {
const { percent, color = '#1890FF' } = this.props;
const data = percent / 100;
const self = this;
cancelAnimationFrame(this.timer);
if (!this.node || (data !== 0 && !data)) {
return;
}
const canvas = this.node;
const ctx = canvas.getContext('2d');
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 = [];
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();
ctx.strokeStyle = color;
ctx.moveTo(cStartPoint[0], cStartPoint[1]);
function drawSin() {
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();
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();
}
function render() {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (circleLock && type !== 'update') {
if (arcStack.length) {
const temp = arcStack.shift();
ctx.lineTo(temp[0], temp[1]);
ctx.stroke();
} else {
circleLock = false;
ctx.lineTo(cStartPoint[0], cStartPoint[1]);
ctx.stroke();
arcStack = null;
ctx.globalCompositeOperation = 'destination-over';
ctx.beginPath();
ctx.lineWidth = lineWidth;
ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1);
ctx.beginPath();
ctx.save();
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, 1);
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();
}
self.timer = requestAnimationFrame(render);
}
render();
}
render() {
const { radio } = this.state;
const { percent, title, height } = this.props;
return (
(this.root = n)}
style={{ transform: `scale(${radio})` }}
>
(this.node = n)}
width={height * 2}
height={height * 2}
/>
{title && {title} }
{percent}%
);
}
}
export default WaterWave;
================================================
FILE: src/components/Charts/WaterWave/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.waterWave {
display: inline-block;
position: relative;
transform-origin: left;
.text {
position: absolute;
left: 0;
top: 32px;
text-align: center;
width: 100%;
span {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
h4 {
color: @heading-color;
line-height: 32px;
font-size: 24px;
}
}
.waterWaveCanvasWrapper {
transform: scale(0.5);
transform-origin: 0 0;
}
}
================================================
FILE: src/components/Charts/autoHeight.js
================================================
/* eslint eqeqeq: 0 */
import React from 'react';
function computeHeight(node) {
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) {
if (!n) {
return 0;
}
let node = n;
let height = computeHeight(node);
while (!height) {
node = node.parentNode;
if (node) {
height = computeHeight(node);
} else {
break;
}
}
return height;
}
const autoHeight = () => WrappedComponent =>
class extends React.Component {
state = {
computedHeight: 0,
};
componentDidMount() {
const { height } = this.props;
if (!height) {
const h = getAutoHeight(this.root);
// eslint-disable-next-line
this.setState({ computedHeight: h });
}
}
handleRoot = node => {
this.root = node;
};
render() {
const { height } = this.props;
const { computedHeight } = this.state;
const h = height || computedHeight;
return (
{h > 0 && }
);
}
};
export default autoHeight;
================================================
FILE: src/components/Charts/bizcharts.d.ts
================================================
import * as BizChart from 'bizcharts';
export = BizChart;
================================================
FILE: src/components/Charts/bizcharts.js
================================================
import * as BizChart from 'bizcharts';
export default BizChart;
================================================
FILE: src/components/Charts/demo/bar.md
================================================
---
order: 4
title: 柱状图
---
通过设置 `x`,`y` 属性,可以快速的构建出一个漂亮的柱状图,各种纬度的关系则是通过自定义的数据展现。
````jsx
import { Bar } from 'ant-design-pro/lib/Charts';
const salesData = [];
for (let i = 0; i < 12; i += 1) {
salesData.push({
x: `${i + 1}月`,
y: Math.floor(Math.random() * 1000) + 200,
});
}
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/chart-card.md
================================================
---
order: 1
title: 图表卡片
---
用于展示图表的卡片容器,可以方便的配合其它图表套件展示丰富信息。
```jsx
import { ChartCard, yuan, Field } from 'ant-design-pro/lib/Charts';
import Trend from 'ant-design-pro/lib/Trend';
import { Row, Col, Icon, Tooltip } from 'antd';
import numeral from 'numeral';
ReactDOM.render(
}
total={() => (
)}
footer={
}
contentHeight={46}
>
周同比
12%
日环比
11%
}
action={
}
total={() => (
)}
footer={
}
/>
}
action={
}
total={() => (
)}
/>
,
mountNode,
);
```
================================================
FILE: src/components/Charts/demo/gauge.md
================================================
---
order: 7
title: 仪表盘
---
仪表盘是一种进度展示方式,可以更直观的展示当前的进展情况,通常也可表示占比。
````jsx
import { Gauge } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/mini-area.md
================================================
---
order: 2
col: 2
title: 迷你区域图
---
````jsx
import { MiniArea } from 'ant-design-pro/lib/Charts';
import moment from 'moment';
const visitData = [];
const beginDay = new Date().getTime();
for (let i = 0; i < 20; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/mini-bar.md
================================================
---
order: 2
col: 2
title: 迷你柱状图
---
迷你柱状图更适合展示简单的区间数据,简洁的表现方式可以很好的减少大数据量的视觉展现压力。
````jsx
import { MiniBar } from 'ant-design-pro/lib/Charts';
import moment from 'moment';
const visitData = [];
const beginDay = new Date().getTime();
for (let i = 0; i < 20; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/mini-pie.md
================================================
---
order: 6
title: 迷你饼状图
---
通过简化 `Pie` 属性的设置,可以快速的实现极简的饼状图,可配合 `ChartCard` 组合展
现更多业务场景。
```jsx
import { Pie } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
,
mountNode
);
```
================================================
FILE: src/components/Charts/demo/mini-progress.md
================================================
---
order: 3
title: 迷你进度条
---
````jsx
import { MiniProgress } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/mix.md
================================================
---
order: 0
title: 图表套件组合展示
---
利用 Ant Design Pro 提供的图表套件,可以灵活组合符合设计规范的图表来满足复杂的业务需求。
````jsx
import { ChartCard, Field, MiniArea, MiniBar, MiniProgress } from 'ant-design-pro/lib/Charts';
import Trend from 'ant-design-pro/lib/Trend';
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
import { Row, Col, Icon, Tooltip } from 'antd';
import numeral from 'numeral';
import moment from 'moment';
const visitData = [];
const beginDay = new Date().getTime();
for (let i = 0; i < 20; i += 1) {
visitData.push({
x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'),
y: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
本周访问}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
}
total={numeral(8846).format('0,0')}
footer={ }
contentHeight={46}
>
}
total="78%"
footer={
周同比
12%
日环比
11%
}
contentHeight={46}
>
, mountNode);
````
================================================
FILE: src/components/Charts/demo/pie.md
================================================
---
order: 5
title: 饼状图
---
```jsx
import { Pie, yuan } from 'ant-design-pro/lib/Charts';
const salesPieData = [
{
x: '家用电器',
y: 4544,
},
{
x: '食用酒水',
y: 3321,
},
{
x: '个护健康',
y: 3113,
},
{
x: '服饰箱包',
y: 2341,
},
{
x: '母婴产品',
y: 1231,
},
{
x: '其他',
y: 1231,
},
];
ReactDOM.render(
(
now.y + pre, 0))
}}
/>
)}
data={salesPieData}
valueFormat={val => }
height={294}
/>,
mountNode,
);
```
================================================
FILE: src/components/Charts/demo/radar.md
================================================
---
order: 7
title: 雷达图
---
````jsx
import { Radar, ChartCard } from 'ant-design-pro/lib/Charts';
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 = [];
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],
});
}
});
});
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/tag-cloud.md
================================================
---
order: 9
title: 标签云
---
标签云是一套相关的标签以及与此相应的权重展示方式,一般典型的标签云有 30 至 150 个标签,而权重影响使用的字体大小或其他视觉效果。
````jsx
import { TagCloud } from 'ant-design-pro/lib/Charts';
const tags = [];
for (let i = 0; i < 50; i += 1) {
tags.push({
name: `TagClout-Title-${i}`,
value: Math.floor((Math.random() * 50)) + 20,
});
}
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/timeline-chart.md
================================================
---
order: 9
title: 带有时间轴的图表
---
使用 `TimelineChart` 组件可以实现带有时间轴的柱状图展现,而其中的 `x` 属性,则是时间值的指向,默认最多支持同时展现两个指标,分别是 `y1` 和 `y2`。
````jsx
import { TimelineChart } from 'ant-design-pro/lib/Charts';
const chartData = [];
for (let i = 0; i < 20; i += 1) {
chartData.push({
x: (new Date().getTime()) + (1000 * 60 * 30 * i),
y1: Math.floor(Math.random() * 100) + 1000,
y2: Math.floor(Math.random() * 100) + 10,
});
}
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/demo/waterwave.md
================================================
---
order: 8
title: 水波图
---
水波图是一种比例的展示方式,可以更直观的展示关键值的占比。
````jsx
import { WaterWave } from 'ant-design-pro/lib/Charts';
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Charts/g2.js
================================================
// 全局 G2 设置
import { track, setTheme } from 'bizcharts';
track(false);
const config = {
defaultColor: '#1089ff',
shape: {
interval: {
fillOpacity: 1,
},
},
};
setTheme(config);
================================================
FILE: src/components/Charts/index.d.ts
================================================
import * as numeral from 'numeral';
export { default as ChartCard } from './ChartCard';
export { default as Bar } from './Bar';
export { default as Pie } from './Pie';
export { default as Radar } from './Radar';
export { default as Gauge } from './Gauge';
export { default as MiniArea } from './MiniArea';
export { default as MiniBar } from './MiniBar';
export { default as MiniProgress } from './MiniProgress';
export { default as Field } from './Field';
export { default as WaterWave } from './WaterWave';
export { default as TagCloud } from './TagCloud';
export { default as TimelineChart } from './TimelineChart';
declare const yuan: (value: number | string) => string;
export { yuan };
================================================
FILE: src/components/Charts/index.js
================================================
import numeral from 'numeral';
import './g2';
import ChartCard from './ChartCard';
import Bar from './Bar';
import Pie from './Pie';
import Radar from './Radar';
import Gauge from './Gauge';
import MiniArea from './MiniArea';
import MiniBar from './MiniBar';
import MiniProgress from './MiniProgress';
import Field from './Field';
import WaterWave from './WaterWave';
import TagCloud from './TagCloud';
import TimelineChart from './TimelineChart';
const yuan = val => `¥ ${numeral(val).format('0,0')}`;
const Charts = {
yuan,
Bar,
Pie,
Gauge,
Radar,
MiniBar,
MiniArea,
MiniProgress,
ChartCard,
Field,
WaterWave,
TagCloud,
TimelineChart,
};
export {
Charts as default,
yuan,
Bar,
Pie,
Gauge,
Radar,
MiniBar,
MiniArea,
MiniProgress,
ChartCard,
Field,
WaterWave,
TagCloud,
TimelineChart,
};
================================================
FILE: src/components/Charts/index.less
================================================
.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;
}
}
================================================
FILE: src/components/Charts/index.md
================================================
---
title:
en-US: Charts
zh-CN: Charts
subtitle: 图表
order: 2
cols: 2
---
Ant Design Pro 提供的业务中常用的图表类型,都是基于 [G2](https://antv.alipay.com/g2/doc/index.html) 按照 Ant Design 图表规范封装,需要注意的是 Ant Design Pro 的图表组件以套件形式提供,可以任意组合实现复杂的业务需求。
因为结合了 Ant Design 的标准设计,本着极简的设计思想以及开箱即用的理念,简化了大量 API 配置,所以如果需要灵活定制图表,可以参考 Ant Design Pro 图表实现,自行基于 [G2](https://antv.alipay.com/g2/doc/index.html) 封装图表组件使用。
## API
### ChartCard
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | 卡片标题 | ReactNode\|string | - |
| action | 卡片操作 | ReactNode | - |
| total | 数据总量 | ReactNode \| number \| function | - |
| footer | 卡片底部 | ReactNode | - |
| contentHeight | 内容区域高度 | number | - |
| avatar | 右侧图标 | React.ReactNode | - |
### MiniBar
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| color | 图表颜色 | string | `#1890FF` |
| height | 图表高度 | number | - |
| data | 数据 | array<{x, y}> | - |
### MiniArea
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.2)` |
| borderColor | 图表边颜色 | string | `#1890FF` |
| height | 图表高度 | number | - |
| line | 是否显示描边 | boolean | false |
| animate | 是否显示动画 | boolean | true |
| xAxis | [x 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
| yAxis | [y 轴配置](http://antvis.github.io/g2/doc/tutorial/start/axis.html) | object | - |
| data | 数据 | array<{x, y}> | - |
### MiniProgress
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| target | 目标比例 | number | - |
| color | 进度条颜色 | string | - |
| strokeWidth | 进度条高度 | number | - |
| percent | 进度比例 | number | - |
### Bar
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | 图表标题 | ReactNode\|string | - |
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
| height | 图表高度 | number | - |
| data | 数据 | array<{x, y}> | - |
| autoLabel | 在宽度不足时,自动隐藏 x 轴的 label | boolean | `true` |
### Pie
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| animate | 是否显示动画 | boolean | true |
| color | 图表颜色 | string | `rgba(24, 144, 255, 0.85)` |
| height | 图表高度 | number | - |
| hasLegend | 是否显示 legend | boolean | `false` |
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
| percent | 占比 | number | - |
| tooltip | 是否显示 tooltip | boolean | true |
| valueFormat | 显示值的格式化函数 | function | - |
| title | 图表标题 | ReactNode\|string | - |
| subTitle | 图表子标题 | ReactNode\|string | - |
| total | 图标中央的总数 | string | function | - |
### Radar
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | 图表标题 | ReactNode\|string | - |
| height | 图表高度 | number | - |
| hasLegend | 是否显示 legend | boolean | `false` |
| padding | 图表内部间距 | [array](https://github.com/alibaba/BizCharts/blob/master/doc/api/chart.md#7padding-object--number--array-) | `'auto'` |
| data | 图标数据 | array<{name,label,value}> | - |
### Gauge
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | 图表标题 | ReactNode\|string | - |
| height | 图表高度 | number | - |
| color | 图表颜色 | string | `#2F9CFF` |
| bgColor | 图表背景颜色 | string | `#F0F2F5` |
| percent | 进度比例 | number | - |
### WaterWave
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | 图表标题 | ReactNode\|string | - |
| height | 图表高度 | number | - |
| color | 图表颜色 | string | `#1890FF` |
| percent | 进度比例 | number | - |
### TagCloud
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| data | 标题 | Array | - |
| height | 高度值 | number | - |
### TimelineChart
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| data | 标题 | Array | - |
| titleMap | 指标别名 | Object{y1: '客流量', y2: '支付笔数'} | - |
| height | 高度值 | number | 400 |
### Field
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| label | 标题 | ReactNode\|string | - |
| value | 值 | ReactNode\|string | - |
================================================
FILE: src/components/Exception/demo/403.md
================================================
---
order: 2
title:
zh-CN: 403
en-US: 403
---
## zh-CN
403 页面,配合自定义操作。
## en-US
403 page with custom operations.
````jsx
import Exception from 'ant-design-pro/lib/Exception';
import { Button } from 'antd';
const actions = (
Home
Detail
);
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Exception/demo/404.md
================================================
---
order: 0
title:
zh-CN: 404
en-US: 404
---
## zh-CN
404 页面。
## en-US
404 page.
````jsx
import Exception from 'ant-design-pro/lib/Exception';
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Exception/demo/500.md
================================================
---
order: 1
title:
zh-CN: 500
en-US: 500
---
## zh-CN
500 页面。
## en-US
500 page.
````jsx
import Exception from 'ant-design-pro/lib/Exception';
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Exception/index.d.ts
================================================
import * as React from 'react';
export interface IExceptionProps {
type?: '403' | '404' | '500';
title?: React.ReactNode;
desc?: React.ReactNode;
img?: string;
actions?: React.ReactNode;
linkElement?: React.ReactNode;
style?: React.CSSProperties;
className?: string;
backText?: React.ReactNode;
redirect?: string;
}
export default class Exception extends React.Component {}
================================================
FILE: src/components/Exception/index.en-US.md
================================================
---
title: Exception
cols: 1
order: 5
---
Exceptions page is used to provide feedback on specific abnormal state. Usually, it contains an explanation of the error status, and provides users with suggestions or operations, to prevent users from feeling lost and confused.
## API
Property | Description | Type | Default
---------|-------------|------|--------
| backText | default return button text | ReactNode | back to home |
type | type of exception, the corresponding default `title`, `desc`, `img` will be given if set, which can be overridden by explicit setting of `title`, `desc`, `img` | Enum {'403', '404', '500'} | -
title | title | ReactNode | -
desc | supplementary description | ReactNode | -
img | the url of background image | string | -
actions | suggested operations, a default 'Home' link will show if not set | ReactNode | -
linkElement | to specify the element of link | string\|ReactElement | 'a'
redirect | redirect path | string | '/'
================================================
FILE: src/components/Exception/index.js
================================================
import React, { createElement } from 'react';
import classNames from 'classnames';
import { Button } from 'antd';
import config from './typeConfig';
import styles from './index.less';
class Exception extends React.PureComponent {
static defaultProps = {
backText: 'back to home',
redirect: '/',
};
constructor(props) {
super(props);
this.state = {};
}
render() {
const {
className,
backText,
linkElement = 'a',
type,
title,
desc,
img,
actions,
redirect,
...rest
} = this.props;
const pageType = type in config ? type : '404';
const clsString = classNames(styles.exception, className);
return (
{title || config[pageType].title}
{desc || config[pageType].desc}
{actions ||
createElement(
linkElement,
{
to: redirect,
href: redirect,
},
{backText}
)}
);
}
}
export default Exception;
================================================
FILE: src/components/Exception/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.exception {
display: flex;
align-items: center;
height: 80%;
min-height: 500px;
.imgBlock {
flex: 0 0 62.5%;
width: 62.5%;
padding-right: 152px;
zoom: 1;
&:before,
&:after {
content: ' ';
display: table;
}
&:after {
clear: both;
visibility: hidden;
font-size: 0;
height: 0;
}
}
.imgEle {
height: 360px;
width: 100%;
max-width: 430px;
float: right;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: contain;
}
.content {
flex: auto;
h1 {
color: #434e59;
font-size: 72px;
font-weight: 600;
line-height: 72px;
margin-bottom: 24px;
}
.desc {
color: @text-color-secondary;
font-size: 20px;
line-height: 28px;
margin-bottom: 16px;
}
.actions {
button:not(:last-child) {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-xl) {
.exception {
.imgBlock {
padding-right: 88px;
}
}
}
@media screen and (max-width: @screen-sm) {
.exception {
display: block;
text-align: center;
.imgBlock {
padding-right: 0;
margin: 0 auto 24px;
}
}
}
@media screen and (max-width: @screen-xs) {
.exception {
.imgBlock {
margin-bottom: -24px;
overflow: hidden;
}
}
}
================================================
FILE: src/components/Exception/index.zh-CN.md
================================================
---
title: Exception
subtitle: 异常
cols: 1
order: 5
---
异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。
## API
| 参数 | 说明| 类型 | 默认值 |
|-------------|------------------------------------------|-------------|-------|
| backText| 默认的返回按钮文本 | ReactNode| back to home |
| type| 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - |
| title | 标题 | ReactNode| -|
| desc| 补充描述| ReactNode| -|
| img | 背景图片地址 | string| -|
| actions | 建议操作,配置此属性时默认的『返回首页』按钮不生效| ReactNode| -|
| linkElement | 定义链接的元素 | string\|ReactElement | 'a' |
| redirect | 返回按钮的跳转地址 | string | '/'
================================================
FILE: src/components/Exception/typeConfig.js
================================================
const config = {
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
title: '403',
desc: '抱歉,你无权访问该页面',
},
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '404',
desc: '抱歉,你访问的页面不存在',
},
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
title: '500',
desc: '抱歉,服务器出错了',
},
};
export default config;
================================================
FILE: src/components/FooterToolbar/demo/basic.md
================================================
---
order: 0
title:
zh-CN: 演示
en-US: demo
iframe: 400
---
## zh-CN
浮动固定页脚。
## en-US
Fixed to the footer.
````jsx
import FooterToolbar from 'ant-design-pro/lib/FooterToolbar';
import { Button } from 'antd';
ReactDOM.render(
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Content Content Content Content
Cancel
Submit
, mountNode);
````
================================================
FILE: src/components/FooterToolbar/index.d.ts
================================================
import * as React from 'react';
export interface IFooterToolbarProps {
extra: React.ReactNode;
style?: React.CSSProperties;
}
export default class FooterToolbar extends React.Component {}
================================================
FILE: src/components/FooterToolbar/index.en-US.md
================================================
---
title: FooterToolbar
cols: 1
order: 6
---
A toolbar fixed at the bottom.
## Usage
It is fixed at the bottom of the content area and does not move along with the scroll bar, which is usually used for data collection and submission for long pages.
## API
Property | Description | Type | Default
---------|-------------|------|--------
children | toolbar content, align to the right | ReactNode | -
extra | extra information, align to the left | ReactNode | -
================================================
FILE: src/components/FooterToolbar/index.js
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import styles from './index.less';
export default class FooterToolbar extends Component {
static contextTypes = {
isMobile: PropTypes.bool,
};
state = {
width: undefined,
};
componentDidMount() {
window.addEventListener('resize', this.resizeFooterToolbar);
this.resizeFooterToolbar();
}
componentWillUnmount() {
window.removeEventListener('resize', this.resizeFooterToolbar);
}
resizeFooterToolbar = () => {
const sider = document.querySelector('.ant-layout-sider');
if (sider == null) {
return;
}
const { isMobile } = this.context;
const width = isMobile ? null : `calc(100% - ${sider.style.width})`;
const { width: stateWidth } = this.state;
if (stateWidth !== width) {
this.setState({ width });
}
};
render() {
const { children, className, extra, ...restProps } = this.props;
const { width } = this.state;
return (
);
}
}
================================================
FILE: src/components/FooterToolbar/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.toolbar {
position: fixed;
width: 100%;
bottom: 0;
right: 0;
height: 56px;
line-height: 56px;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
background: #fff;
border-top: 1px solid @border-color-split;
padding: 0 24px;
z-index: 9;
&:after {
content: '';
display: block;
clear: both;
}
.left {
float: left;
}
.right {
float: right;
}
button + button {
margin-left: 8px;
}
}
================================================
FILE: src/components/FooterToolbar/index.zh-CN.md
================================================
---
title: FooterToolbar
subtitle: 底部工具栏
cols: 1
order: 6
---
固定在底部的工具栏。
## 何时使用
固定在内容区域的底部,不随滚动条移动,常用于长页面的数据搜集和提交工作。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
children | 工具栏内容,向右对齐 | ReactNode | -
extra | 额外信息,向左对齐 | ReactNode | -
================================================
FILE: src/components/GlobalFooter/demo/basic.md
================================================
---
order: 0
title: 演示
iframe: 400
---
基本页脚。
````jsx
import GlobalFooter from 'ant-design-pro/lib/GlobalFooter';
import { Icon } from 'antd';
const links = [{
key: '帮助',
title: '帮助',
href: '',
}, {
key: 'github',
title: ,
href: 'https://github.com/ant-design/ant-design-pro',
blankTarget: true,
}, {
key: '条款',
title: '条款',
href: '',
blankTarget: true,
}];
const copyright = Copyright 2017 蚂蚁金服体验技术部出品
;
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/GlobalFooter/index.d.ts
================================================
import * as React from 'react';
export interface IGlobalFooterProps {
links?: Array<{
key?: string;
title: React.ReactNode;
href: string;
blankTarget?: boolean;
}>;
copyright?: React.ReactNode;
style?: React.CSSProperties;
}
export default class GlobalFooter extends React.Component {}
================================================
FILE: src/components/GlobalFooter/index.js
================================================
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
const GlobalFooter = ({ className, links, copyright }) => {
const clsString = classNames(styles.globalFooter, className);
return (
{links && (
)}
{copyright &&
{copyright}
}
);
};
export default GlobalFooter;
================================================
FILE: src/components/GlobalFooter/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.globalFooter {
padding: 0 16px;
margin: 48px 0 24px 0;
text-align: center;
.links {
margin-bottom: 8px;
a {
color: @text-color-secondary;
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
}
&:hover {
color: @text-color;
}
}
}
.copyright {
color: @text-color-secondary;
font-size: @font-size-base;
}
}
================================================
FILE: src/components/GlobalFooter/index.md
================================================
---
title:
en-US: GlobalFooter
zh-CN: GlobalFooter
subtitle: 全局页脚
cols: 1
order: 7
---
页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | -
copyright | 版权信息 | ReactNode | -
================================================
FILE: src/components/GlobalHeader/RightContent.js
================================================
import React, { PureComponent } from 'react';
import { FormattedMessage, formatMessage } from 'umi/locale';
import { Spin, Tag, Menu, Icon, Dropdown, Avatar, Tooltip } from 'antd';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import NoticeIcon from '../NoticeIcon';
import SelectLang from '../SelectLang';
import styles from './index.less';
export default class GlobalHeaderRight extends PureComponent {
getNoticeData() {
const { notices = [] } = this.props;
if (notices.length === 0) {
return {};
}
const newNotices = notices.map(notice => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime).fromNow();
}
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = {
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
}[newNotice.status];
newNotice.extra = (
{newNotice.extra}
);
}
return newNotice;
});
return groupBy(newNotices, 'type');
}
render() {
const {
currentUser,
fetchingNotices,
onNoticeVisibleChange,
onMenuClick,
onNoticeClear,
theme,
} = this.props;
const menu = (
);
const noticeData = this.getNoticeData();
let className = styles.right;
if (theme === 'dark') {
className = `${styles.right} ${styles.dark}`;
}
return (
{
console.log(item, tabProps); // eslint-disable-line
}}
locale={{
emptyText: formatMessage({ id: 'component.noticeIcon.empty' }),
clear: formatMessage({ id: 'component.noticeIcon.clear' }),
}}
onClear={onNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange}
loading={fetchingNotices}
popupAlign={{ offset: [20, -16] }}
>
{/* */}
{currentUser.name ? (
{currentUser.name}
) : (
)}
);
}
}
================================================
FILE: src/components/GlobalHeader/index.js
================================================
import React, { PureComponent } from 'react';
import { Icon } from 'antd';
import Link from 'umi/link';
import Debounce from 'lodash-decorators/debounce';
import styles from './index.less';
import RightContent from './RightContent';
export default class GlobalHeader extends PureComponent {
componentWillUnmount() {
this.triggerResizeEvent.cancel();
}
/* eslint-disable*/
@Debounce(600)
triggerResizeEvent() {
// eslint-disable-line
const event = document.createEvent('HTMLEvents');
event.initEvent('resize', true, false);
window.dispatchEvent(event);
}
toggle = () => {
const { collapsed, onCollapse } = this.props;
onCollapse(!collapsed);
this.triggerResizeEvent();
};
render() {
const { collapsed, isMobile, logo } = this.props;
return (
{isMobile && (
)}
);
}
}
================================================
FILE: src/components/GlobalHeader/index.less
================================================
@import '~antd/lib/style/themes/default.less';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.header {
height: 64px;
padding: 0 12px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
}
.logo {
height: 64px;
line-height: 58px;
vertical-align: top;
display: inline-block;
padding: 0 0 0 24px;
cursor: pointer;
font-size: 20px;
img {
display: inline-block;
vertical-align: middle;
}
}
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
width: 160px;
}
}
i.trigger {
font-size: 20px;
height: 64px;
cursor: pointer;
transition: all 0.3s, padding 0s;
padding: 22px 24px;
&:hover {
background: @pro-header-hover-bg;
}
}
.right {
float: right;
height: 100%;
overflow: hidden;
.action {
cursor: pointer;
padding: 0 12px;
display: inline-block;
transition: all 0.3s;
height: 100%;
> i {
vertical-align: middle;
color: @text-color;
}
&:hover {
background: @pro-header-hover-bg;
}
:global(&.ant-popover-open) {
background: @pro-header-hover-bg;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin: 20px 8px 20px 0;
color: @primary-color;
background: rgba(255, 255, 255, 0.85);
vertical-align: middle;
}
}
}
.dark {
height: 64px;
.action {
color: rgba(255, 255, 255, 0.85);
> i {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&:global(.ant-popover-open) {
background: @primary-color;
}
:global(.ant-badge) {
color: rgba(255, 255, 255, 0.85);
}
}
}
@media only screen and (max-width: @screen-md) {
.header {
:global(.ant-divider-vertical) {
vertical-align: unset;
}
.name {
display: none;
}
i.trigger {
padding: 22px 12px;
}
.logo {
padding-left: 12px;
padding-right: 12px;
position: relative;
}
.right {
position: absolute;
right: 12px;
top: 0;
background: #fff;
.account {
.avatar {
margin-right: 0;
}
}
}
}
}
================================================
FILE: src/components/Login/LoginItem.js
================================================
import React, { Component } from 'react';
import { Form, Input, Button, Row, Col } from 'antd';
import omit from 'omit.js';
import styles from './index.less';
import ItemMap from './map';
import LoginContext from './loginContext';
const FormItem = Form.Item;
class WrapFormItem extends Component {
static defaultProps = {
buttonText: '获取验证码',
};
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
componentDidMount() {
const { updateActive, name } = this.props;
if (updateActive) {
updateActive(name);
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onGetCaptcha = () => {
const { onGetCaptcha } = this.props;
const result = onGetCaptcha ? onGetCaptcha() : null;
if (result === false) {
return;
}
if (result instanceof Promise) {
result.then(this.runGetCaptchaCountDown);
} else {
this.runGetCaptchaCountDown();
}
};
getFormItemOptions = ({ onChange, defaultValue, customprops, rules }) => {
const options = {
rules: rules || customprops.rules,
};
if (onChange) {
options.onChange = onChange;
}
if (defaultValue) {
options.initialValue = defaultValue;
}
return options;
};
runGetCaptchaCountDown = () => {
const { countDown } = this.props;
let count = countDown || 59;
this.setState({ count });
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
};
render() {
const { count } = this.state;
const {
form: { getFieldDecorator },
} = this.props;
// 这么写是为了防止restProps中 带入 onChange, defaultValue, rules props
const {
onChange,
customprops,
defaultValue,
rules,
name,
buttonText,
updateActive,
type,
...restProps
} = this.props;
// get getFieldDecorator props
const options = this.getFormItemOptions(this.props);
const otherProps = restProps || {};
if (type === 'Captcha') {
const inputProps = omit(otherProps, ['onGetCaptcha', 'countDown']);
return (
{getFieldDecorator(name, options)( )}
{count ? `${count} s` : buttonText}
);
}
return (
{getFieldDecorator(name, options)( )}
);
}
}
const LoginItem = {};
Object.keys(ItemMap).forEach(key => {
const item = ItemMap[key];
LoginItem[key] = props => (
{context => (
)}
);
});
export default LoginItem;
================================================
FILE: src/components/Login/LoginSubmit.js
================================================
import React from 'react';
import classNames from 'classnames';
import { Button, Form } from 'antd';
import styles from './index.less';
const FormItem = Form.Item;
const LoginSubmit = ({ className, ...rest }) => {
const clsString = classNames(styles.submit, className);
return (
);
};
export default LoginSubmit;
================================================
FILE: src/components/Login/LoginTab.js
================================================
import React, { Component } from 'react';
import { Tabs } from 'antd';
import LoginContext from './loginContext';
const { TabPane } = Tabs;
const generateId = (() => {
let i = 0;
return (prefix = '') => {
i += 1;
return `${prefix}${i}`;
};
})();
class LoginTab extends Component {
constructor(props) {
super(props);
this.uniqueId = generateId('login-tab-');
}
componentDidMount() {
const { tabUtil } = this.props;
tabUtil.addTab(this.uniqueId);
}
render() {
const { children } = this.props;
return {children} ;
}
}
const wrapContext = props => (
{value => }
);
// 标志位 用来判断是不是自定义组件
wrapContext.typeName = 'LoginTab';
export default wrapContext;
================================================
FILE: src/components/Login/demo/basic.md
================================================
---
order: 0
title:
zh-CN: 标准登录
en-US: Standard Login
---
Support login with account and mobile number.
````jsx
import Login from 'ant-design-pro/lib/Login';
import { Alert, Checkbox } from 'antd';
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
class LoginDemo extends React.Component {
state = {
notice: '',
type: 'tab2',
autoLogin: true,
}
onSubmit = (err, values) => {
console.log('value collected ->', { ...values, autoLogin: this.state.autoLogin });
if (this.state.type === 'tab1') {
this.setState({
notice: '',
}, () => {
if (!err && (values.username !== 'admin' || values.password !== '888888')) {
setTimeout(() => {
this.setState({
notice: 'The combination of username and password is incorrect!',
});
}, 500);
}
});
}
}
onTabChange = (key) => {
this.setState({
type: key,
});
}
changeAutoLogin = (e) => {
this.setState({
autoLogin: e.target.checked,
});
}
render() {
return (
{
this.state.notice &&
}
console.log('Get captcha!')} name="captcha" />
Login
);
}
}
ReactDOM.render( , mountNode);
````
================================================
FILE: src/components/Login/index.d.ts
================================================
import * as React from 'react';
import Button from 'antd/lib/button';
export interface LoginProps {
defaultActiveKey?: string;
onTabChange?: (key: string) => void;
style?: React.CSSProperties;
onSubmit?: (error: any, values: any) => void;
}
export interface TabProps {
key?: string;
tab?: React.ReactNode;
}
export class Tab extends React.Component {}
export interface LoginItemProps {
name?: string;
rules?: any[];
style?: React.CSSProperties;
onGetCaptcha?: () => void;
placeholder?: string;
buttonText?: React.ReactNode;
}
export class LoginItem extends React.Component {}
export default class Login extends React.Component {
static Tab: typeof Tab;
static UserName: typeof LoginItem;
static Password: typeof LoginItem;
static Mobile: typeof LoginItem;
static Captcha: typeof LoginItem;
static Submit: typeof Button;
}
================================================
FILE: src/components/Login/index.en-US.md
================================================
---
title: Login
cols: 1
order: 15
---
Support multiple common ways of login with built-in controls. You can choose your own combinations and use with your custom controls.
## API
### Login
Property | Description | Type | Default
----|------|-----|------
defaultActiveKey | default key to activate the tab panel | String | -
onTabChange | callback on changing tabs | (key) => void | -
onSubmit | callback on submit | (err, values) => void | -
### Login.Tab
Property | Description | Type | Default
----|------|-----|------
key | key of the tab | String | -
tab | displayed text of the tab | ReactNode | -
### Login.UserName
Property | Description | Type | Default
----|------|-----|------
name | name of the control, also the key of the submitted data | String | -
rules | validation rules, same with [option.rules](getFieldDecorator(id, options)) in Form getFieldDecorator(id, options) | object[] | -
Apart from the above properties, Login.Username also support all properties of antd.Input, together with the default values of basic settings, such as _placeholder_, _size_ and _prefix_. All of these default values can be over-written.
### Login.Password, Login.Mobile are the same as Login.UserName
### Login.Captcha
Property | Description | Type | Default
----|------|-----|------
onGetCaptcha | callback on getting a new Captcha | () => (void \| false \| Promise) | -
countDown | count down | number |-
buttonText | text on getting a new Captcha | ReactNode | '获取验证码'
Apart from the above properties, _Login.Captcha_ support the same properties with _Login.UserName_.
### Login.Submit
Support all properties of _antd.Button_.
================================================
FILE: src/components/Login/index.js
================================================
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Form, Tabs } from 'antd';
import classNames from 'classnames';
import LoginItem from './LoginItem';
import LoginTab from './LoginTab';
import LoginSubmit from './LoginSubmit';
import styles from './index.less';
import LoginContext from './loginContext';
class Login extends Component {
static propTypes = {
className: PropTypes.string,
defaultActiveKey: PropTypes.string,
onTabChange: PropTypes.func,
onSubmit: PropTypes.func,
};
static defaultProps = {
className: '',
defaultActiveKey: '',
onTabChange: () => {},
onSubmit: () => {},
};
constructor(props) {
super(props);
this.state = {
type: props.defaultActiveKey,
tabs: [],
active: {},
};
}
onSwitch = type => {
this.setState({
type,
});
const { onTabChange } = this.props;
onTabChange(type);
};
getContext = () => {
const { tabs } = this.state;
const { form } = this.props;
return {
tabUtil: {
addTab: id => {
this.setState({
tabs: [...tabs, id],
});
},
removeTab: id => {
this.setState({
tabs: tabs.filter(currentId => currentId !== id),
});
},
},
form,
updateActive: activeItem => {
const { type, active } = this.state;
if (active[type]) {
active[type].push(activeItem);
} else {
active[type] = [activeItem];
}
this.setState({
active,
});
},
};
};
handleSubmit = e => {
e.preventDefault();
const { active, type } = this.state;
const { form, onSubmit } = this.props;
const activeFileds = active[type];
form.validateFields(activeFileds, { force: true }, (err, values) => {
onSubmit(err, values);
});
};
render() {
const { className, children } = this.props;
const { type, tabs } = this.state;
const TabChildren = [];
const otherChildren = [];
React.Children.forEach(children, item => {
if (!item) {
return;
}
// eslint-disable-next-line
if (item.type.typeName === 'LoginTab') {
TabChildren.push(item);
} else {
otherChildren.push(item);
}
});
return (
);
}
}
Login.Tab = LoginTab;
Login.Submit = LoginSubmit;
Object.keys(LoginItem).forEach(item => {
Login[item] = LoginItem[item];
});
export default Form.create()(Login);
================================================
FILE: src/components/Login/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.login {
:global {
.ant-tabs .ant-tabs-bar {
border-bottom: 0;
margin-bottom: 24px;
text-align: center;
}
.ant-form-item {
margin: 0 2px 24px;
}
}
.icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
text-align: left;
margin-top: 24px;
line-height: 22px;
.register {
float: right;
}
}
.prefixIcon {
font-size: @font-size-base;
color: @disabled-color;
}
.submit {
width: 100%;
margin-top: 24px;
}
}
================================================
FILE: src/components/Login/index.zh-CN.md
================================================
---
title: Login
subtitle: 登录
cols: 1
order: 15
---
支持多种登录方式切换,内置了几种常见的登录控件,可以灵活组合,也支持和自定义控件配合使用。
## API
### Login
参数 | 说明 | 类型 | 默认值
----|------|-----|------
defaultActiveKey | 默认激活 tab 面板的 key | String | -
onTabChange | 切换页签时的回调 | (key) => void | -
onSubmit | 点击提交时的回调 | (err, values) => void | -
### Login.Tab
参数 | 说明 | 类型 | 默认值
----|------|-----|------
key | 对应选项卡的 key | String | -
tab | 选项卡头显示文字 | ReactNode | -
### Login.UserName
参数 | 说明 | 类型 | 默认值
----|------|-----|------
name | 控件标记,提交数据中同样以此为 key | String | -
rules | 校验规则,同 Form getFieldDecorator(id, options) 中 [option.rules 的规则](getFieldDecorator(id, options)) | object[] | -
除上述属性以外,Login.UserName 还支持 antd.Input 的所有属性,并且自带默认的基础配置,包括 `placeholder` `size` `prefix` 等,这些基础配置均可被覆盖。
## Login.Password、Login.Mobile 同 Login.UserName
### Login.Captcha
参数 | 说明 | 类型 | 默认值
----|------|-----|------
onGetCaptcha | 点击获取校验码的回调 | () => (void \| false \| Promise) | -
countDown | 倒计时 | number |-
buttonText | 点击获取校验码的说明文字 | ReactNode | '获取验证码'
除上述属性以外,Login.Captcha 支持的属性与 Login.UserName 相同。
### Login.Submit
支持 antd.Button 的所有属性。
================================================
FILE: src/components/Login/loginContext.js
================================================
import { createContext } from 'react';
const LoginContext = createContext();
export default LoginContext;
================================================
FILE: src/components/Login/map.js
================================================
import React from 'react';
import { Icon } from 'antd';
import styles from './index.less';
export default {
UserName: {
props: {
size: 'large',
prefix: ,
placeholder: 'admin',
},
rules: [
{
required: true,
message: 'Please enter username!',
},
],
},
Password: {
props: {
size: 'large',
prefix: ,
type: 'password',
placeholder: '888888',
},
rules: [
{
required: true,
message: 'Please enter password!',
},
],
},
Mobile: {
props: {
size: 'large',
prefix: ,
placeholder: 'mobile number',
},
rules: [
{
required: true,
message: 'Please enter mobile number!',
},
{
pattern: /^1\d{10}$/,
message: 'Wrong mobile number format!',
},
],
},
Captcha: {
props: {
size: 'large',
prefix: ,
placeholder: 'captcha',
},
rules: [
{
required: true,
message: 'Please enter Captcha!',
},
],
},
};
================================================
FILE: src/components/NoticeIcon/NoticeIconTab.d.ts
================================================
import * as React from 'react';
export interface INoticeIconData {
avatar?: string|React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
datetime?: React.ReactNode;
extra?: React.ReactNode;
style?: React.CSSProperties;
}
export interface INoticeIconTabProps {
list?: INoticeIconData[];
title?: string;
name?: string;
emptyText?: React.ReactNode;
emptyImage?: string;
style?: React.CSSProperties;
showClear?: boolean;
}
export default class NoticeIconTab extends React.Component {}
================================================
FILE: src/components/NoticeIcon/NoticeList.js
================================================
import React from 'react';
import { Avatar, List } from 'antd';
import classNames from 'classnames';
import styles from './NoticeList.less';
export default function NoticeList({
data = [],
onClick,
onClear,
title,
locale,
emptyText,
emptyImage,
showClear = true,
}) {
if (data.length === 0) {
return (
{emptyImage ?
: null}
{emptyText || locale.emptyText}
);
}
return (
{data.map((item, i) => {
const itemCls = classNames(styles.item, {
[styles.read]: item.read,
});
// eslint-disable-next-line no-nested-ternary
const leftIcon = item.avatar ? (
typeof item.avatar === 'string' ? (
) : (
item.avatar
)
) : null;
return (
onClick(item)}>
{leftIcon}}
title={
{item.title}
{item.extra}
}
description={
{item.description}
{item.datetime}
}
/>
);
})}
{showClear ? (
{locale.clear} {title}
) : null}
);
}
================================================
FILE: src/components/NoticeIcon/NoticeList.less
================================================
@import '~antd/lib/style/themes/default.less';
.list {
max-height: 400px;
overflow: auto;
.item {
transition: all 0.3s;
overflow: hidden;
cursor: pointer;
padding-left: 24px;
padding-right: 24px;
.meta {
width: 100%;
}
.avatar {
background: #fff;
margin-top: 4px;
}
.iconElement {
font-size: 32px;
}
&.read {
opacity: 0.4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
font-weight: normal;
margin-bottom: 8px;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
font-size: 12px;
margin-top: 4px;
line-height: @line-height-base;
}
.extra {
float: right;
color: @text-color-secondary;
font-weight: normal;
margin-right: 0;
margin-top: -1.5px;
}
}
}
.notFound {
text-align: center;
padding: 73px 0 88px 0;
color: @text-color-secondary;
img {
display: inline-block;
margin-bottom: 16px;
height: 76px;
}
}
.clear {
height: 46px;
line-height: 46px;
text-align: center;
color: @text-color;
border-radius: 0 0 @border-radius-base @border-radius-base;
border-top: 1px solid @border-color-split;
transition: all 0.3s;
cursor: pointer;
&:hover {
color: @heading-color;
}
}
================================================
FILE: src/components/NoticeIcon/demo/basic.md
================================================
---
order: 1
title: 通知图标
---
通常用在导航工具栏上。
````jsx
import NoticeIcon from 'ant-design-pro/lib/NoticeIcon';
ReactDOM.render( , mountNode);
````
================================================
FILE: src/components/NoticeIcon/demo/popover.md
================================================
---
order: 2
title: 带浮层卡片
---
点击展开通知卡片,展现多种类型的通知,通常放在导航工具栏。
````jsx
import NoticeIcon from 'ant-design-pro/lib/NoticeIcon';
import moment from 'moment';
import groupBy from 'lodash/groupBy';
import { Tag } from 'antd';
const data = [{
id: '000000001',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
type: '通知',
}, {
id: '000000002',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
type: '通知',
}, {
id: '000000003',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
title: '这种模板可以区分多种通知类型',
datetime: '2017-08-07',
read: true,
type: '通知',
}, {
id: '000000004',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
title: '左侧图标用于区分不同的类型',
datetime: '2017-08-07',
type: '通知',
}, {
id: '000000005',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '内容不要超过两行字,超出时自动截断',
datetime: '2017-08-07',
type: '通知',
}, {
id: '000000006',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '曲丽丽 评论了你',
description: '描述信息描述信息描述信息',
datetime: '2017-08-07',
type: '消息',
}, {
id: '000000007',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '朱偏右 回复了你',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
}, {
id: '000000008',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
title: '标题',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
datetime: '2017-08-07',
type: '消息',
}, {
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
type: '待办',
}, {
id: '000000010',
title: '第三方紧急代码变更',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
status: 'urgent',
type: '待办',
}, {
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
type: '待办',
}, {
id: '000000012',
title: 'ABCD 版本发布',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
type: '待办',
}];
function onItemClick(item, tabProps) {
console.log(item, tabProps);
}
function onClear(tabTitle) {
console.log(tabTitle);
}
function getNoticeData(notices) {
if (notices.length === 0) {
return {};
}
const newNotices = notices.map((notice) => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime).fromNow();
}
// transform id to item key
if (newNotice.id) {
newNotice.key = newNotice.id;
}
if (newNotice.extra && newNotice.status) {
const color = ({
todo: '',
processing: 'blue',
urgent: 'red',
doing: 'gold',
})[newNotice.status];
newNotice.extra = {newNotice.extra} ;
}
return newNotice;
});
return groupBy(newNotices, 'type');
}
const noticeData = getNoticeData(data);
ReactDOM.render(
, mountNode);
````
```css
```
================================================
FILE: src/components/NoticeIcon/index.d.ts
================================================
import * as React from 'react';
import NoticeIconTab, { INoticeIconData } from './NoticeIconTab';
export interface INoticeIconProps {
count?: number;
bell?: React.ReactNode;
className?: string;
loading?: boolean;
onClear?: (tabName: string) => void;
onItemClick?: (item: INoticeIconData, tabProps: INoticeIconProps) => void;
onTabChange?: (tabTile: string) => void;
popupAlign?: {
points?: [string, string];
offset?: [number, number];
targetOffset?: [number, number];
overflow?: any;
useCssRight?: boolean;
useCssBottom?: boolean;
useCssTransform?: boolean;
};
style?: React.CSSProperties;
onPopupVisibleChange?: (visible: boolean) => void;
popupVisible?: boolean;
locale?: { emptyText: string; clear: string };
}
export default class NoticeIcon extends React.Component {
public static Tab: typeof NoticeIconTab;
}
================================================
FILE: src/components/NoticeIcon/index.en-US.md
================================================
---
title: NoticeIcon
subtitle: Notification Menu
cols: 1
order: 9
---
用在导航工具栏上,作为整个产品统一的通知中心。
## API
Property | Description | Type | Default
----|------|-----|------
count | Total number of messages | number | -
bell | Change the bell Icon | ReactNode | ` `
loading | Popup card loading status | boolean | false
onClear | Click to clear button the callback | function(tabName) | -
onItemClick | Click on the list item's callback | function(item, tabProps) | -
onTabChange | Switching callbacks for tabs | function(tabTitle) | -
popupAlign | Popup card location configuration | Object [alignConfig](https://github.com/yiminghe/dom-align#alignconfig-object-details) | -
onPopupVisibleChange | Popup Card Showing or Hiding Callbacks | function(visible) | -
popupVisible | Popup card display state | boolean | -
locale | Default message text | Object | `{ emptyText: '暂无数据', clear: '清空' }`
### NoticeIcon.Tab
Property | Description | Type | Default
----|------|-----|------
title | header for message Tab | string | -
name | identifier for message Tab | string | -
list | List data, format refer to the following table | Array | `[]`
showClear | Clear button display status | boolean | true
emptyText | message text when list is empty | ReactNode | -
emptyImage | image when list is empty | string | -
### Tab data
Property | Description | Type | Default
----|------|-----|------
avatar | avatar img url | string \| ReactNode | -
title | title | ReactNode | -
description | description info | ReactNode | -
datetime | Timestamps | ReactNode | -
extra |Additional information in the upper right corner of the list item | ReactNode | -
================================================
FILE: src/components/NoticeIcon/index.js
================================================
import React, { PureComponent } from 'react';
import { Popover, Icon, Tabs, Badge, Spin } from 'antd';
import classNames from 'classnames';
import List from './NoticeList';
import styles from './index.less';
const { TabPane } = Tabs;
export default class NoticeIcon extends PureComponent {
static Tab = TabPane;
static defaultProps = {
onItemClick: () => {},
onPopupVisibleChange: () => {},
onTabChange: () => {},
onClear: () => {},
loading: false,
locale: {
emptyText: 'No notifications',
clear: 'Clear',
},
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
};
onItemClick = (item, tabProps) => {
const { onItemClick } = this.props;
onItemClick(item, tabProps);
};
onTabChange = tabType => {
const { onTabChange } = this.props;
onTabChange(tabType);
};
getNotificationBox() {
const { children, loading, locale, onClear } = this.props;
if (!children) {
return null;
}
const panes = React.Children.map(children, child => {
const title =
child.props.list && child.props.list.length > 0
? `${child.props.title} (${child.props.list.length})`
: child.props.title;
return (
this.onItemClick(item, child.props)}
onClear={() => onClear(child.props.name)}
title={child.props.title}
locale={locale}
/>
);
});
return (
{panes}
);
}
render() {
const { className, count, popupAlign, popupVisible, onPopupVisibleChange, bell } = this.props;
const noticeButtonClass = classNames(className, styles.noticeButton);
const notificationBox = this.getNotificationBox();
const NoticeBellIcon = bell || ;
const trigger = (
{NoticeBellIcon}
);
if (!notificationBox) {
return trigger;
}
const popoverProps = {};
if ('popupVisible' in this.props) {
popoverProps.visible = popupVisible;
}
return (
{trigger}
);
}
}
================================================
FILE: src/components/NoticeIcon/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.popover {
width: 336px;
:global(.ant-popover-inner-content) {
padding: 0;
}
}
.noticeButton {
cursor: pointer;
display: inline-block;
transition: all 0.3s;
}
.icon {
padding: 4px;
}
.tabs {
:global {
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 4px;
}
}
}
================================================
FILE: src/components/NoticeIcon/index.zh-CN.md
================================================
---
title: NoticeIcon
subtitle: 通知菜单
cols: 1
order: 9
---
用在导航工具栏上,作为整个产品统一的通知中心。
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
count | 图标上的消息总数 | number | -
bell | translate this please -> Change the bell Icon | ReactNode | ` `
loading | 弹出卡片加载状态 | boolean | false
onClear | 点击清空按钮的回调 | function(tabName) | -
onItemClick | 点击列表项的回调 | function(item, tabProps) | -
onTabChange | 切换页签的回调 | function(tabTitle) | -
popupAlign | 弹出卡片的位置配置 | Object [alignConfig](https://github.com/yiminghe/dom-align#alignconfig-object-details) | -
onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | -
popupVisible | 控制弹层显隐 | boolean | -
locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }`
### NoticeIcon.Tab
参数 | 说明 | 类型 | 默认值
----|------|-----|------
title | 消息分类的页签标题 | string | -
name | 消息分类的标识符 | string | -
list | 列表数据,格式参照下表 | Array | `[]`
showClear | 是否显示清空按钮 | boolean | true
emptyText | 针对每个 Tab 定制空数据文案 | ReactNode | -
emptyImage | 针对每个 Tab 定制空数据图片 | string | -
### Tab data
参数 | 说明 | 类型 | 默认值
----|------|-----|------
avatar | 头像图片链接 | string \| ReactNode | -
title | 标题 | ReactNode | -
description | 描述信息 | ReactNode | -
datetime | 时间戳 | ReactNode | -
extra | 额外信息,在列表项右上角 | ReactNode | -
================================================
FILE: src/components/PageHeader/breadcrumb.d.ts
================================================
import * as React from 'react';
import { IPageHeaderProps } from './index'
export default class BreadcrumbView extends React.Component {}
export function getBreadcrumb(breadcrumbNameMap: Object, url: string): Object;
================================================
FILE: src/components/PageHeader/breadcrumb.js
================================================
import React, { PureComponent, createElement } from 'react';
import pathToRegexp from 'path-to-regexp';
import { Breadcrumb } from 'antd';
import styles from './index.less';
import { urlToList } from '../_utils/pathTools';
export const getBreadcrumb = (breadcrumbNameMap, url) => {
let breadcrumb = breadcrumbNameMap[url];
if (!breadcrumb) {
Object.keys(breadcrumbNameMap).forEach(item => {
if (pathToRegexp(item).test(url)) {
breadcrumb = breadcrumbNameMap[item];
}
});
}
return breadcrumb || {};
};
export default class BreadcrumbView extends PureComponent {
state = {
breadcrumb: null,
};
componentDidMount() {
this.getBreadcrumbDom();
}
componentDidUpdate(preProps) {
const { location } = this.props;
if (!location || !preProps.location) {
return;
}
const prePathname = preProps.location.pathname;
if (prePathname !== location.pathname) {
this.getBreadcrumbDom();
}
}
getBreadcrumbDom = () => {
const breadcrumb = this.conversionBreadcrumbList();
this.setState({
breadcrumb,
});
};
getBreadcrumbProps = () => {
const { routes, params, location, breadcrumbNameMap } = this.props;
return {
routes,
params,
routerLocation: location,
breadcrumbNameMap,
};
};
// Generated according to props
conversionFromProps = () => {
const { breadcrumbList, breadcrumbSeparator, itemRender, linkElement = 'a' } = this.props;
return (
{breadcrumbList.map(item => {
const title = itemRender ? itemRender(item) : item.title;
return (
{item.href
? createElement(
linkElement,
{
[linkElement === 'a' ? 'href' : 'to']: item.href,
},
title
)
: title}
);
})}
);
};
conversionFromLocation = (routerLocation, breadcrumbNameMap) => {
const { breadcrumbSeparator, home, itemRender, linkElement = 'a' } = this.props;
// Convert the url to an array
const pathSnippets = urlToList(routerLocation.pathname);
// Loop data mosaic routing
const extraBreadcrumbItems = pathSnippets.map((url, index) => {
const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url);
if (currentBreadcrumb.inherited) {
return null;
}
const isLinkable = index !== pathSnippets.length - 1 && currentBreadcrumb.component;
const name = itemRender ? itemRender(currentBreadcrumb) : currentBreadcrumb.name;
return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? (
{createElement(
isLinkable ? linkElement : 'span',
{ [linkElement === 'a' ? 'href' : 'to']: url },
name
)}
) : null;
});
// Add home breadcrumbs to your head
extraBreadcrumbItems.unshift(
{createElement(
linkElement,
{
[linkElement === 'a' ? 'href' : 'to']: '/',
},
home || 'Home'
)}
);
return (
{extraBreadcrumbItems}
);
};
/**
* 将参数转化为面包屑
* Convert parameters into breadcrumbs
*/
conversionBreadcrumbList = () => {
const { breadcrumbList, breadcrumbSeparator } = this.props;
const { routes, params, routerLocation, breadcrumbNameMap } = this.getBreadcrumbProps();
if (breadcrumbList && breadcrumbList.length) {
return this.conversionFromProps();
}
// 如果传入 routes 和 params 属性
// If pass routes and params attributes
if (routes && params) {
return (
route.breadcrumbName)}
params={params}
itemRender={this.itemRender}
separator={breadcrumbSeparator}
/>
);
}
// 根据 location 生成 面包屑
// Generate breadcrumbs based on location
if (routerLocation && routerLocation.pathname) {
return this.conversionFromLocation(routerLocation, breadcrumbNameMap);
}
return null;
};
// 渲染Breadcrumb 子节点
// Render the Breadcrumb child node
itemRender = (route, params, routes, paths) => {
const { linkElement = 'a' } = this.props;
const last = routes.indexOf(route) === routes.length - 1;
return last || !route.component ? (
{route.breadcrumbName}
) : (
createElement(
linkElement,
{
href: paths.join('/') || '/',
to: paths.join('/') || '/',
},
route.breadcrumbName
)
);
};
render() {
const { breadcrumb } = this.state;
return breadcrumb;
}
}
================================================
FILE: src/components/PageHeader/demo/image.md
================================================
---
order: 2
title: With Image
---
带图片的页头。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
const content = (
段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。
);
const extra = (
);
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/PageHeader/demo/simple.md
================================================
---
order: 3
title: Simple
---
简单的页头。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/PageHeader/demo/standard.md
================================================
---
order: 1
title: Standard
---
标准页头。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
import DescriptionList from 'ant-design-pro/lib/DescriptionList';
import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd';
const { Description } = DescriptionList;
const ButtonGroup = Button.Group;
const description = (
曲丽丽
XX 服务
2017-07-07
12421
);
const menu = (
选项一
选项二
选项三
);
const action = (
操作
操作
主操作
);
const extra = (
状态
待审批
订单金额
¥ 568.08
);
const breadcrumbList = [{
title: '一级菜单',
href: '/',
}, {
title: '二级菜单',
href: '/',
}, {
title: '三级菜单',
}];
const tabList = [{
key: 'detail',
tab: '详情',
}, {
key: 'rule',
tab: '规则',
}];
function onTabChange(key) {
console.log(key);
}
ReactDOM.render(
}
action={action}
content={description}
extraContent={extra}
breadcrumbList={breadcrumbList}
tabList={tabList}
tabActiveKey="detail"
onTabChange={onTabChange}
/>
, mountNode);
````
================================================
FILE: src/components/PageHeader/demo/structure.md
================================================
---
order: 0
title: Structure
---
基本结构,具备响应式布局功能,主要断点为 768px 和 576px,拖动窗口改变大小试试看。
````jsx
import PageHeader from 'ant-design-pro/lib/PageHeader';
const breadcrumbList = [{
title: '面包屑',
}];
const tabList = [{
key: '1',
tab: '页签一',
}, {
key: '2',
tab: '页签二',
}, {
key: '3',
tab: '页签三',
}];
ReactDOM.render(
}
logo={logo
}
action={action
}
content={content
}
extraContent={extraContent
}
breadcrumbList={breadcrumbList}
tabList={tabList}
tabActiveKey="1"
/>
, mountNode);
````
================================================
FILE: src/components/PageHeader/index.d.ts
================================================
import * as React from 'react';
export interface IPageHeaderProps {
title?: React.ReactNode | string;
logo?: React.ReactNode | string;
action?: React.ReactNode | string;
content?: React.ReactNode;
extraContent?: React.ReactNode;
routes?: any[];
params?: any;
breadcrumbList?: Array<{ title: React.ReactNode; href?: string }>;
tabList?: Array<{ key: string; tab: React.ReactNode }>;
tabActiveKey?: string;
tabDefaultActiveKey?: string;
onTabChange?: (key: string) => void;
tabBarExtraContent?: React.ReactNode;
linkElement?: React.ReactNode;
style?: React.CSSProperties;
home?: React.ReactNode;
wide?: boolean;
hiddenBreadcrumb?:boolean;
}
export default class PageHeader extends React.Component {}
================================================
FILE: src/components/PageHeader/index.js
================================================
import React, { PureComponent } from 'react';
import { Tabs, Skeleton } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
import BreadcrumbView from './breadcrumb';
const { TabPane } = Tabs;
export default class PageHeader extends PureComponent {
onChange = key => {
const { onTabChange } = this.props;
if (onTabChange) {
onTabChange(key);
}
};
render() {
const {
title,
logo,
action,
content,
extraContent,
tabList,
className,
tabActiveKey,
tabDefaultActiveKey,
tabBarExtraContent,
loading = false,
wide = false,
hiddenBreadcrumb = false,
} = this.props;
const clsString = classNames(styles.pageHeader, className);
const activeKeyProps = {};
if (tabDefaultActiveKey !== undefined) {
activeKeyProps.defaultActiveKey = tabDefaultActiveKey;
}
if (tabActiveKey !== undefined) {
activeKeyProps.activeKey = tabActiveKey;
}
return (
{hiddenBreadcrumb ? null : }
{logo &&
{logo}
}
{title &&
{title} }
{action &&
{action}
}
{content &&
{content}
}
{extraContent &&
{extraContent}
}
{tabList && tabList.length ? (
{tabList.map(item => (
))}
) : null}
);
}
}
================================================
FILE: src/components/PageHeader/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.pageHeader {
background: @component-background;
padding: 16px 32px 0 32px;
border-bottom: @border-width-base @border-style-base @border-color-split;
.wide {
max-width: 1200px;
margin: auto;
}
.detail {
display: flex;
}
.row {
display: flex;
width: 100%;
}
.breadcrumb {
margin-bottom: 16px;
}
.tabs {
margin: 0 0 0 -8px;
:global {
.ant-tabs-bar {
border-bottom: @border-width-base @border-style-base @border-color-split;
}
}
}
.logo {
flex: 0 1 auto;
margin-right: 16px;
padding-top: 1px;
> img {
width: 28px;
height: 28px;
border-radius: @border-radius-base;
display: block;
}
}
.title {
font-size: 20px;
font-weight: 500;
color: @heading-color;
}
.action {
margin-left: 56px;
min-width: 266px;
:global {
.ant-btn-group:not(:last-child),
.ant-btn:not(:last-child) {
margin-right: 8px;
}
.ant-btn-group > .ant-btn {
margin-right: 0;
}
}
}
.title,
.content {
flex: auto;
}
.action,
.extraContent,
.main {
flex: 0 1 auto;
}
.main {
width: 100%;
}
.title,
.action {
margin-bottom: 16px;
}
.logo,
.content,
.extraContent {
margin-bottom: 16px;
}
.action,
.extraContent {
text-align: right;
}
.extraContent {
margin-left: 88px;
min-width: 242px;
}
}
@media screen and (max-width: @screen-xl) {
.pageHeader {
.extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-lg) {
.pageHeader {
.extraContent {
margin-left: 20px;
}
}
}
@media screen and (max-width: @screen-md) {
.pageHeader {
.row {
display: block;
}
.action,
.extraContent {
margin-left: 0;
text-align: left;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeader {
.detail {
display: block;
}
}
}
@media screen and (max-width: @screen-xs) {
.pageHeader {
.action {
:global {
.ant-btn-group,
.ant-btn {
display: block;
margin-bottom: 8px;
}
.ant-btn-group > .ant-btn {
display: inline-block;
margin-bottom: 0;
}
}
}
}
}
================================================
FILE: src/components/PageHeader/index.md
================================================
---
title:
en-US: PageHeader
zh-CN: PageHeader
subtitle: 页头
cols: 1
order: 11
---
页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| title | title 区域 | ReactNode | - |
| logo | logo区域 | ReactNode | - |
| action | 操作区,位于 title 行的行尾 | ReactNode | - |
| home | 默认的主页说明文字 | ReactNode | - |
| content | 内容区 | ReactNode | - |
| extraContent | 额外内容区,位于content的右侧 | ReactNode | - |
| breadcrumbList | 面包屑数据,配置了此属性时 `routes` `params` `location` `breadcrumbNameMap` 无效 | array<{title: ReactNode, href?: string}> | - |
| hiddenBreadcrumb |隐藏面包屑 | boolean | false |
| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - |
| params | 面包屑相关属性,路由的参数 | object | - |
| location | 面包屑相关属性,当前的路由信息 | object | - |
| breadcrumbNameMap | 面包屑相关属性,路由的地址-名称映射表 | object | - |
| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - |
| tabActiveKey | 当前高亮的 tab 项 | string | - |
| tabDefaultActiveKey | 默认高亮的 tab 项 | string | 第一项 |
| wide | 是否定宽 | boolean | false |
| onTabChange | 切换面板的回调 | (key) => void | - |
| itemRender | 自定义节点方法 | (menuItem) => ReactNode | - |
| linkElement | 定义链接的元素,默认为 `a`,可传入 react-router 的 Link | string\|ReactElement | - |
> 面包屑的配置方式有三种,一是直接配置 `breadcrumbList`,二是结合 `react-router@2` `react-router@3`,配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router),三是结合 `react-router@4`,配置 `location` `breadcrumbNameMap`,优先级依次递减,脚手架中使用最后一种。 对于后两种用法,你也可以将 `routes` `params` 及 `location` `breadcrumbNameMap` 放到 context 中,组件会自动获取。
================================================
FILE: src/components/PageHeader/index.test.js
================================================
import { getBreadcrumb } from './breadcrumb';
import { urlToList } from '../_utils/pathTools';
const routerData = {
'/dashboard/analysis': {
name: '分析页',
},
'/userinfo': {
name: '用户列表',
},
'/userinfo/:id': {
name: '用户信息',
},
'/userinfo/:id/addr': {
name: '收货订单',
},
};
describe('test getBreadcrumb', () => {
it('Simple url', () => {
expect(getBreadcrumb(routerData, '/dashboard/analysis').name).toEqual('分析页');
});
it('Parameters url', () => {
expect(getBreadcrumb(routerData, '/userinfo/2144').name).toEqual('用户信息');
});
it('The middle parameter url', () => {
expect(getBreadcrumb(routerData, '/userinfo/2144/addr').name).toEqual('收货订单');
});
it('Loop through the parameters', () => {
const urlNameList = urlToList('/userinfo/2144/addr').map(
url => getBreadcrumb(routerData, url).name
);
expect(urlNameList).toEqual(['用户列表', '用户信息', '收货订单']);
});
it('a path', () => {
const urlNameList = urlToList('/userinfo').map(url => getBreadcrumb(routerData, url).name);
expect(urlNameList).toEqual(['用户列表']);
});
it('Secondary path', () => {
const urlNameList = urlToList('/userinfo/2144').map(url => getBreadcrumb(routerData, url).name);
expect(urlNameList).toEqual(['用户列表', '用户信息']);
});
});
================================================
FILE: src/components/PageHeaderWrapper/GridContent.js
================================================
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import styles from './GridContent.less';
class GridContent extends PureComponent {
render() {
const { contentWidth, children } = this.props;
let className = `${styles.main}`;
if (contentWidth === 'Fixed') {
className = `${styles.main} ${styles.wide}`;
}
return {children}
;
}
}
export default connect(({ setting }) => ({
contentWidth: setting.contentWidth,
}))(GridContent);
================================================
FILE: src/components/PageHeaderWrapper/GridContent.less
================================================
.main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}
================================================
FILE: src/components/PageHeaderWrapper/index.js
================================================
import React from 'react';
import { FormattedMessage } from 'umi/locale';
import Link from 'umi/link';
import PageHeader from '@/components/PageHeader';
import { connect } from 'dva';
import GridContent from './GridContent';
import styles from './index.less';
import MenuContext from '@/layouts/MenuContext';
const PageHeaderWrapper = ({ children, contentWidth, wrapperClassName, top, ...restProps }) => (
{top}
{value => (
}
{...value}
key="pageheader"
{...restProps}
linkElement={Link}
itemRender={item => {
if (item.locale) {
return ;
}
return item.name;
}}
/>
)}
{children ? (
{children}
) : null}
);
export default connect(({ setting }) => ({
contentWidth: setting.contentWidth,
}))(PageHeaderWrapper);
================================================
FILE: src/components/PageHeaderWrapper/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.content {
margin: 24px 24px 0;
}
@media screen and (max-width: @screen-sm) {
.content {
margin: 24px 0 0;
}
}
================================================
FILE: src/components/PageLoading/index.js
================================================
import React from 'react';
import { Spin } from 'antd';
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default () => (
);
================================================
FILE: src/components/Result/demo/classic.md
================================================
---
order: 1
title: Classic
---
典型结果页面。
````jsx
import Result from 'ant-design-pro/lib/Result';
import { Button, Row, Col, Icon, Steps } from 'antd';
const { Step } = Steps;
const desc1 = (
);
const desc2 = (
);
const extra = (
项目名称
项目 ID:
23421
负责人:
曲丽丽
生效时间:
2016-12-12 ~ 2017-12-12
);
const actions = (
返回列表
查看项目
打 印
);
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Result/demo/error.md
================================================
---
order: 2
title: Failed
---
提交失败。
````jsx
import Result from 'ant-design-pro/lib/Result';
import { Button, Icon } from 'antd';
const extra = (
);
const actions = 返回修改 ;
ReactDOM.render(
, mountNode);
````
================================================
FILE: src/components/Result/demo/structure.md
================================================
---
order: 0
title: Structure
---
结构包含 `处理结果`,`补充信息` 以及 `操作建议` 三个部分,其中 `处理结果` 由 `提示图标`,`标题` 和 `结果描述` 组成。
````jsx
import Result from 'ant-design-pro/lib/Result';
ReactDOM.render(
标题}
description={结果描述
}
extra="其他补充信息,自带灰底效果"
actions={操作建议,一般放置按钮组
}
/>
, mountNode);
````
================================================
FILE: src/components/Result/index.d.ts
================================================
import * as React from 'react';
export interface IResultProps {
type: 'success' | 'error';
title: React.ReactNode;
description?: React.ReactNode;
extra?: React.ReactNode;
actions?: React.ReactNode;
style?: React.CSSProperties;
}
export default class Result extends React.Component {}
================================================
FILE: src/components/Result/index.js
================================================
import React from 'react';
import classNames from 'classnames';
import { Icon } from 'antd';
import styles from './index.less';
export default function Result({
className,
type,
title,
description,
extra,
actions,
...restProps
}) {
const iconMap = {
error: ,
success: ,
};
const clsString = classNames(styles.result, className);
return (
{iconMap[type]}
{title}
{description &&
{description}
}
{extra &&
{extra}
}
{actions &&
{actions}
}
);
}
================================================
FILE: src/components/Result/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.result {
text-align: center;
width: 72%;
margin: 0 auto;
@media screen and (max-width: @screen-xs) {
width: 100%;
}
.icon {
font-size: 72px;
line-height: 72px;
margin-bottom: 24px;
& > .success {
color: @success-color;
}
& > .error {
color: @error-color;
}
}
.title {
font-size: 24px;
color: @heading-color;
font-weight: 500;
line-height: 32px;
margin-bottom: 16px;
}
.description {
font-size: 14px;
line-height: 22px;
color: @text-color-secondary;
margin-bottom: 24px;
}
.extra {
background: #fafafa;
padding: 24px 40px;
border-radius: @border-radius-sm;
text-align: left;
@media screen and (max-width: @screen-xs) {
padding: 18px 20px;
}
}
.actions {
margin-top: 32px;
button:not(:last-child) {
margin-right: 8px;
}
}
}
================================================
FILE: src/components/Result/index.md
================================================
---
title:
en-US: Result
zh-CN: Result
subtitle: 处理结果
cols: 1
order: 12
---
结果页用于对用户进行的一系列任务处理结果进行反馈。
## API
| 参数 | 说明 | 类型 | 默认值 |
|----------|------------------------------------------|-------------|-------|
| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - |
| title | 标题 | ReactNode | - |
| description | 结果描述 | ReactNode | - |
| extra | 补充信息,有默认的灰色背景 | ReactNode | - |
| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - |
================================================
FILE: src/components/SelectLang/index.js
================================================
import React, { PureComponent } from 'react';
import { FormattedMessage, setLocale, getLocale } from 'umi/locale';
import { Menu, Icon, Dropdown } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
export default class SelectLang extends PureComponent {
changLang = ({ key }) => {
setLocale(key);
};
render() {
const { className } = this.props;
const selectedLang = getLocale();
const langMenu = (
);
return (
);
}
}
================================================
FILE: src/components/SelectLang/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
width: 160px;
}
}
.dropDown {
cursor: pointer;
}
================================================
FILE: src/components/SettingDrawer/BlockChecbox.js
================================================
import React from 'react';
import { Tooltip, Icon } from 'antd';
import style from './index.less';
const BlockChecbox = ({ value, onChange, list }) => (
{list.map(item => (
onChange(item.key)}>
))}
);
export default BlockChecbox;
================================================
FILE: src/components/SettingDrawer/ThemeColor.js
================================================
import React from 'react';
import { Tooltip, Icon } from 'antd';
import { formatMessage } from 'umi/locale';
import styles from './ThemeColor.less';
const Tag = ({ color, check, ...rest }) => (
{check ? : ''}
);
const ThemeColor = ({ colors, title, value, onChange }) => {
let colorList = colors;
if (!colors) {
colorList = [
{
key: 'dust',
color: '#F5222D',
},
{
key: 'volcano',
color: '#FA541C',
},
{
key: 'sunset',
color: '#FAAD14',
},
{
key: 'cyan',
color: '#13C2C2',
},
{
key: 'green',
color: '#52C41A',
},
{
key: 'daybreak',
color: '#1890FF',
},
{
key: 'geekblue',
color: '#2F54EB',
},
{
key: 'purple',
color: '#722ED1',
},
];
}
return (
{title}
{colorList.map(({ key, color }) => (
onChange && onChange(color)}
/>
))}
);
};
export default ThemeColor;
================================================
FILE: src/components/SettingDrawer/ThemeColor.less
================================================
.themeColor {
overflow: hidden;
margin-top: 24px;
.title {
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
line-height: 22px;
margin-bottom: 12px;
}
.colorBlock {
width: 20px;
height: 20px;
border-radius: 2px;
float: left;
cursor: pointer;
margin-right: 8px;
text-align: center;
color: #fff;
font-weight: bold;
}
}
================================================
FILE: src/components/SettingDrawer/index.js
================================================
import React, { PureComponent } from 'react';
import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd';
import { formatMessage } from 'umi/locale';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { connect } from 'dva';
import omit from 'omit.js';
import styles from './index.less';
import ThemeColor from './ThemeColor';
import BlockChecbox from './BlockChecbox';
const { Option } = Select;
const Body = ({ children, title, style }) => (
{title}
{children}
);
@connect(({ setting }) => ({ setting }))
class SettingDrawer extends PureComponent {
state = {
collapse: false,
};
getLayoutSetting = () => {
const {
setting: { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar },
} = this.props;
return [
{
title: formatMessage({ id: 'app.setting.content-width' }),
action: (
this.changeSetting('contentWidth', value)}
style={{ width: 80 }}
>
{layout === 'sidemenu' ? null : (
{formatMessage({ id: 'app.setting.content-width.fixed' })}
)}
{formatMessage({ id: 'app.setting.content-width.fluid' })}
),
},
{
title: formatMessage({ id: 'app.setting.fixedheader' }),
action: (
this.changeSetting('fixedHeader', checked)}
/>
),
},
{
title: formatMessage({ id: 'app.setting.hideheader' }),
disabled: !fixedHeader,
disabledReason: formatMessage({ id: 'app.setting.hideheader.hint' }),
action: (
this.changeSetting('autoHideHeader', checked)}
/>
),
},
{
title: formatMessage({ id: 'app.setting.fixedsidebar' }),
disabled: layout === 'topmenu',
disabledReason: formatMessage({ id: 'app.setting.fixedsidebar.hint' }),
action: (
this.changeSetting('fixSiderbar', checked)}
/>
),
},
];
};
changeSetting = (key, value) => {
const { setting } = this.props;
const nextState = { ...setting };
nextState[key] = value;
if (key === 'layout') {
nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid';
} else if (key === 'fixedHeader' && !value) {
nextState.autoHideHeader = false;
}
this.setState(nextState, () => {
const { dispatch } = this.props;
dispatch({
type: 'setting/changeSetting',
payload: this.state,
});
});
};
togglerContent = () => {
const { collapse } = this.state;
this.setState({ collapse: !collapse });
};
renderLayoutSettingItem = item => {
const action = React.cloneElement(item.action, {
disabled: item.disabled,
});
return (
{item.title}
);
};
render() {
const { setting } = this.props;
const { navTheme, primaryColor, layout, colorWeak } = setting;
const { collapse } = this.state;
return (
}
onHandleClick={this.togglerContent}
style={{
zIndex: 999,
}}
>
this.changeSetting('navTheme', value)}
/>
this.changeSetting('primaryColor', color)}
/>
this.changeSetting('layout', value)}
/>
this.changeSetting('colorWeak', checked)}
/>,
]}
>
{formatMessage({ id: 'app.setting.weakmode' })}
message.success(formatMessage({ id: 'app.setting.copyinfo' }))}
>
{formatMessage({ id: 'app.setting.copy' })}
{formatMessage({ id: 'app.setting.production.hint' })}{' '}
src/defaultSettings.js
}
/>
);
}
}
export default SettingDrawer;
================================================
FILE: src/components/SettingDrawer/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.content {
min-height: 100%;
background: #fff;
position: relative;
}
.blockChecbox {
display: flex;
.item {
margin-right: 16px;
position: relative;
// box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
border-radius: @border-radius-base;
cursor: pointer;
img {
width: 48px;
}
}
.selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: @primary-color;
font-size: 14px;
font-weight: bold;
}
}
.color_block {
width: 38px;
height: 22px;
margin: 4px;
border-radius: 4px;
cursor: pointer;
margin-right: 12px;
display: inline-block;
vertical-align: middle;
}
.title {
font-size: 14px;
color: @heading-color;
line-height: 22px;
margin-bottom: 12px;
}
.handle {
position: absolute;
top: 240px;
background: @primary-color;
width: 48px;
height: 48px;
right: 300px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
pointer-events: auto;
z-index: 0;
text-align: center;
font-size: 16px;
border-radius: 4px 0 0 4px;
}
.productionHint {
font-size: 12px;
margin-top: 16px;
}
================================================
FILE: src/components/SiderMenu/BaseMenu.js
================================================
import React, { PureComponent } from 'react';
import { Menu, Icon } from 'antd';
import Link from 'umi/link';
import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one';
import { formatMessage } from 'umi/locale';
import pathToRegexp from 'path-to-regexp';
import { urlToList } from '../_utils/pathTools';
import styles from './index.less';
const { SubMenu } = Menu;
// Allow menu.js config icon as string or ReactNode
// icon: 'setting',
// icon: 'http://demo.com/icon.png',
// icon: ,
const getIcon = icon => {
if (typeof icon === 'string' && icon.indexOf('http') === 0) {
return ;
}
if (typeof icon === 'string') {
return ;
}
return icon;
};
export const getMenuMatches = memoizeOne(
(flatMenuKeys, path) => flatMenuKeys.filter(item => item && pathToRegexp(item).test(path)),
isEqual
);
export default class BaseMenu extends PureComponent {
constructor(props) {
super(props);
this.getSelectedMenuKeys = memoizeOne(this.getSelectedMenuKeys, isEqual);
this.flatMenuKeys = this.getFlatMenuKeys(props.menuData);
}
/**
* Recursively flatten the data
* [{path:string},{path:string}] => {path,path2}
* @param menus
*/
getFlatMenuKeys(menus) {
let keys = [];
menus.forEach(item => {
if (item.children) {
keys = keys.concat(this.getFlatMenuKeys(item.children));
}
keys.push(item.path);
});
return keys;
}
/**
* 获得菜单子节点
* @memberof SiderMenu
*/
getNavMenuItems = (menusData, parent) => {
if (!menusData) {
return [];
}
return menusData
.filter(item => item.name && !item.hideInMenu)
.map(item => {
// make dom
const ItemDom = this.getSubMenuOrItem(item, parent);
return this.checkPermissionItem(item.authority, ItemDom);
})
.filter(item => item);
};
// Get the currently selected menu
getSelectedMenuKeys = pathname =>
urlToList(pathname).map(itemPath => getMenuMatches(this.flatMenuKeys, itemPath).pop());
/**
* get SubMenu or Item
*/
getSubMenuOrItem = item => {
// doc: add hideChildrenInMenu
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
const name = item.locale ? formatMessage({ id: item.locale }) : item.name;
return (
{getIcon(item.icon)}
{name}
) : (
name
)
}
key={item.path}
>
{this.getNavMenuItems(item.children)}
);
}
return {this.getMenuItemPath(item)} ;
};
/**
* 判断是否是http链接.返回 Link 或 a
* Judge whether it is http link.return a or Link
* @memberof SiderMenu
*/
getMenuItemPath = item => {
const name = item.locale ? formatMessage({ id: item.locale }) : item.name;
const itemPath = this.conversionPath(item.path);
const icon = getIcon(item.icon);
const { target } = item;
// Is it a http link
if (/^https?:\/\//.test(itemPath)) {
return (
{icon}
{name}
);
}
const { location, isMobile, onCollapse } = this.props;
return (
{
onCollapse(true);
}
: undefined
}
>
{icon}
{name}
);
};
// permission to check
checkPermissionItem = (authority, ItemDom) => {
const { Authorized } = this.props;
if (Authorized && Authorized.check) {
const { check } = Authorized;
return check(authority, ItemDom);
}
return ItemDom;
};
conversionPath = path => {
if (path && path.indexOf('http') === 0) {
return path;
}
return `/${path || ''}`.replace(/\/+/g, '/');
};
render() {
const {
openKeys,
theme,
mode,
location: { pathname },
} = this.props;
// if pathname can't match, use the nearest parent's key
let selectedKeys = this.getSelectedMenuKeys(pathname);
if (!selectedKeys.length && openKeys) {
selectedKeys = [openKeys[openKeys.length - 1]];
}
let props = {};
if (openKeys) {
props = {
openKeys,
};
}
const { handleOpenChange, style, menuData } = this.props;
return (
{this.getNavMenuItems(menuData)}
);
}
}
================================================
FILE: src/components/SiderMenu/SiderMenu.js
================================================
import React, { PureComponent } from 'react';
import { Layout } from 'antd';
import pathToRegexp from 'path-to-regexp';
import classNames from 'classnames';
import Link from 'umi/link';
import styles from './index.less';
import BaseMenu, { getMenuMatches } from './BaseMenu';
import { urlToList } from '../_utils/pathTools';
const { Sider } = Layout;
/**
* 获得菜单子节点
* @memberof SiderMenu
*/
const getDefaultCollapsedSubMenus = props => {
const {
location: { pathname },
flatMenuKeys,
} = props;
return urlToList(pathname)
.map(item => getMenuMatches(flatMenuKeys, item)[0])
.filter(item => item);
};
/**
* Recursively flatten the data
* [{path:string},{path:string}] => {path,path2}
* @param menu
*/
export const getFlatMenuKeys = menu =>
menu.reduce((keys, item) => {
keys.push(item.path);
if (item.children) {
return keys.concat(getFlatMenuKeys(item.children));
}
return keys;
}, []);
/**
* Find all matched menu keys based on paths
* @param flatMenuKeys: [/abc, /abc/:id, /abc/:id/info]
* @param paths: [/abc, /abc/11, /abc/11/info]
*/
export const getMenuMatchKeys = (flatMenuKeys, paths) =>
paths.reduce(
(matchKeys, path) =>
matchKeys.concat(flatMenuKeys.filter(item => pathToRegexp(item).test(path))),
[]
);
export default class SiderMenu extends PureComponent {
constructor(props) {
super(props);
this.flatMenuKeys = getFlatMenuKeys(props.menuData);
this.state = {
openKeys: getDefaultCollapsedSubMenus(props),
};
}
static getDerivedStateFromProps(props, state) {
const { pathname } = state;
if (props.location.pathname !== pathname) {
return {
pathname: props.location.pathname,
openKeys: getDefaultCollapsedSubMenus(props),
};
}
return null;
}
isMainMenu = key => {
const { menuData } = this.props;
return menuData.some(item => {
if (key) {
return item.key === key || item.path === key;
}
return false;
});
};
handleOpenChange = openKeys => {
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
this.setState({
openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
});
};
render() {
const { logo, collapsed, onCollapse, fixSiderbar, theme } = this.props;
const { openKeys } = this.state;
const defaultProps = collapsed ? {} : { openKeys };
const siderClassName = classNames(styles.sider, {
[styles.fixSiderbar]: fixSiderbar,
[styles.light]: theme === 'light',
});
return (
react-blog
);
}
}
================================================
FILE: src/components/SiderMenu/SiderMenu.test.js
================================================
import { urlToList } from '../_utils/pathTools';
import { getFlatMenuKeys, getMenuMatchKeys } from './SiderMenu';
const menu = [
{
path: '/dashboard',
children: [
{
path: '/dashboard/name',
},
],
},
{
path: '/userinfo',
children: [
{
path: '/userinfo/:id',
children: [
{
path: '/userinfo/:id/info',
},
],
},
],
},
];
const flatMenuKeys = getFlatMenuKeys(menu);
describe('test convert nested menu to flat menu', () => {
it('simple menu', () => {
expect(flatMenuKeys).toEqual([
'/dashboard',
'/dashboard/name',
'/userinfo',
'/userinfo/:id',
'/userinfo/:id/info',
]);
});
});
describe('test menu match', () => {
it('simple path', () => {
expect(getMenuMatchKeys(flatMenuKeys, urlToList('/dashboard'))).toEqual(['/dashboard']);
});
it('error path', () => {
expect(getMenuMatchKeys(flatMenuKeys, urlToList('/dashboardname'))).toEqual([]);
});
it('Secondary path', () => {
expect(getMenuMatchKeys(flatMenuKeys, urlToList('/dashboard/name'))).toEqual([
'/dashboard',
'/dashboard/name',
]);
});
it('Parameter path', () => {
expect(getMenuMatchKeys(flatMenuKeys, urlToList('/userinfo/2144'))).toEqual([
'/userinfo',
'/userinfo/:id',
]);
});
it('three parameter path', () => {
expect(getMenuMatchKeys(flatMenuKeys, urlToList('/userinfo/2144/info'))).toEqual([
'/userinfo',
'/userinfo/:id',
'/userinfo/:id/info',
]);
});
});
================================================
FILE: src/components/SiderMenu/index.js
================================================
import React from 'react';
import { Drawer } from 'antd';
import SiderMenu from './SiderMenu';
/**
* Recursively flatten the data
* [{path:string},{path:string}] => {path,path2}
* @param menus
*/
const getFlatMenuKeys = menuData => {
let keys = [];
menuData.forEach(item => {
if (item.children) {
keys = keys.concat(getFlatMenuKeys(item.children));
}
keys.push(item.path);
});
return keys;
};
const SiderMenuWrapper = props => {
const { isMobile, menuData, collapsed, onCollapse } = props;
return isMobile ? (
onCollapse(true)}
style={{
padding: 0,
height: '100vh',
}}
>
) : (
);
};
export default SiderMenuWrapper;
================================================
FILE: src/components/SiderMenu/index.less
================================================
@import '~antd/lib/style/themes/default.less';
@nav-header-height: 64px;
.logo {
height: @nav-header-height;
position: relative;
line-height: @nav-header-height;
padding-left: (@menu-collapsed-width - 32px) / 2;
transition: all 0.3s;
background: #002140;
overflow: hidden;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: white;
display: inline-block;
vertical-align: middle;
font-size: 20px;
margin: 0 0 0 12px;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
}
}
.sider {
min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
position: relative;
z-index: 9;
&.fixSiderbar {
position: fixed;
top: 0;
left: 0;
:global(.ant-menu-root) {
overflow-y: auto;
height: ~'calc(100vh - @{nav-header-height})';
}
}
&.light {
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
background-color: white;
.logo {
background: white;
box-shadow: 1px 1px 0 0 @border-color-split;
h1 {
color: @primary-color;
}
}
:global(.ant-menu-light) {
border-right-color: transparent;
}
}
}
.icon {
width: 14px;
margin-right: 10px;
}
:global {
.top-nav-menu li.ant-menu-item {
height: @nav-header-height;
line-height: @nav-header-height;
}
.drawer .drawer-content {
background: #001529;
}
.ant-menu-inline-collapsed {
& > .ant-menu-item .sider-menu-item-img + span,
&
> .ant-menu-item-group
> .ant-menu-item-group-list
> .ant-menu-item
.sider-menu-item-img
+ span,
& > .ant-menu-submenu > .ant-menu-submenu-title .sider-menu-item-img + span {
max-width: 0;
display: inline-block;
opacity: 0;
}
}
.ant-menu-item .sider-menu-item-img + span,
.ant-menu-submenu-title .sider-menu-item-img + span {
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
opacity: 1;
}
}
================================================
FILE: src/components/StandardTable/index.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { Table, Alert } from 'antd';
import styles from './index.less';
function initTotalList(columns) {
const totalList = [];
columns.forEach(column => {
if (column.needTotal) {
totalList.push({ ...column, total: 0 });
}
});
return totalList;
}
class StandardTable extends PureComponent {
constructor(props) {
super(props);
const { columns } = props;
const needTotalList = initTotalList(columns);
this.state = {
selectedRowKeys: [],
needTotalList,
};
}
static getDerivedStateFromProps(nextProps) {
// clean state
if (nextProps.selectedRows.length === 0) {
const needTotalList = initTotalList(nextProps.columns);
return {
selectedRowKeys: [],
needTotalList,
};
}
return null;
}
handleRowSelectChange = (selectedRowKeys, selectedRows) => {
let { needTotalList } = this.state;
needTotalList = needTotalList.map(item => ({
...item,
total: selectedRows.reduce((sum, val) => sum + parseFloat(val[item.dataIndex], 10), 0),
}));
const { onSelectRow } = this.props;
if (onSelectRow) {
onSelectRow(selectedRows);
}
this.setState({ selectedRowKeys, needTotalList });
};
handleTableChange = (pagination, filters, sorter) => {
const { onChange } = this.props;
if (onChange) {
onChange(pagination, filters, sorter);
}
};
cleanSelectedKeys = () => {
this.handleRowSelectChange([], []);
};
render() {
const { selectedRowKeys, needTotalList } = this.state;
const {
data: { list, pagination },
loading,
columns,
rowKey,
} = this.props;
const paginationProps = {
showSizeChanger: true,
showQuickJumper: true,
...pagination,
};
const rowSelection = {
selectedRowKeys,
onChange: this.handleRowSelectChange,
getCheckboxProps: record => ({
disabled: record.disabled,
}),
};
return (
已选择 {selectedRowKeys.length} 项
{needTotalList.map(item => (
{item.title}
总计
{item.render ? item.render(item.total) : item.total}
))}
清空
}
type="info"
showIcon
/>
);
}
}
export default StandardTable;
================================================
FILE: src/components/StandardTable/index.less
================================================
@import '~antd/lib/style/themes/default.less';
.standardTable {
:global {
.ant-table-pagination {
margin-top: 24px;
}
}
.tableAlert {
margin-bottom: 16px;
}
}
================================================
FILE: src/components/TopNavHeader/index.js
================================================
import React, { PureComponent } from 'react';
import Link from 'umi/link';
import RightContent from '../GlobalHeader/RightContent';
import BaseMenu from '../SiderMenu/BaseMenu';
import styles from './index.less';
export default class TopNavHeader extends PureComponent {
constructor(props) {
super(props);
this.state = {
maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 330 - 165 - 4 - 36,
};
}
static getDerivedStateFromProps(props) {
return {
maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 330 - 165 - 4 - 36,
};
}
render() {
const { theme, contentWidth, logo } = this.props;
const { maxWidth } = this.state;
return (
{
this.maim = ref;
}}
className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`}
>
Ant Design Pro
);
}
}
================================================
FILE: src/components/TopNavHeader/index.less
================================================
.head {
width: 100%;
transition: background 0.3s, width 0.2s;
height: 64px;
padding: 0 12px 0 0;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
:global {
.ant-menu-submenu.ant-menu-submenu-horizontal {
height: 100%;
padding-top: 9px;
.ant-menu-submenu-title {
height: 100%;
}
}
}
&.light {
background-color: #fff;
}
.main {
display: flex;
height: 64px;
padding-left: 24px;
&.wide {
max-width: 1200px;
margin: auto;
padding-left: 4px;
}
.left {
flex: 1;
display: flex;
}
.right {
width: 324px;
}
}
}
.logo {
width: 165px;
height: 64px;
position: relative;
line-height: 64px;
transition: all 0.3s;
overflow: hidden;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: #fff;
display: inline-block;
vertical-align: middle;
font-size: 16px;
margin: 0 0 0 12px;
font-weight: 400;
}
}
.light {
h1 {
color: #002140;
}
}
================================================
FILE: src/components/_utils/pathTools.js
================================================
// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
// eslint-disable-next-line import/prefer-default-export
export function urlToList(url) {
const urllist = url.split('/').filter(i => i);
return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`);
}
================================================
FILE: src/components/_utils/pathTools.test.js
================================================
import { urlToList } from './pathTools';
describe('test urlToList', () => {
it('A path', () => {
expect(urlToList('/userinfo')).toEqual(['/userinfo']);
});
it('Secondary path', () => {
expect(urlToList('/userinfo/2144')).toEqual(['/userinfo', '/userinfo/2144']);
});
it('Three paths', () => {
expect(urlToList('/userinfo/2144/addr')).toEqual([
'/userinfo',
'/userinfo/2144',
'/userinfo/2144/addr',
]);
});
});
================================================
FILE: src/defaultSettings.js
================================================
module.exports = {
navTheme: 'dark', // theme for nav menu
primaryColor: '#1890FF', // primary color of ant design
layout: 'sidemenu', // nav menu position: sidemenu or topmenu
contentWidth: 'Fluid', // layout of content: Fluid or Fixed, only works when layout is topmenu
fixedHeader: false, // sticky header
autoHideHeader: false, // auto hide header
fixSiderbar: false, // sticky siderbar
};
================================================
FILE: src/e2e/home.e2e.js
================================================
import puppeteer from 'puppeteer';
describe('Homepage', () => {
it('it should have logo text', async () => {
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.goto('http://localhost:8000', { waitUntil: 'networkidle2' });
await page.waitForSelector('#logo h1');
const text = await page.evaluate(() => document.body.innerHTML);
expect(text).toContain('Ant Design Pro ');
await page.close();
browser.close();
});
});
================================================
FILE: src/e2e/login.e2e.js
================================================
import puppeteer from 'puppeteer';
describe('Login', () => {
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch({ args: ['--no-sandbox'] });
});
beforeEach(async () => {
page = await browser.newPage();
await page.goto('http://localhost:8000/user/login', { waitUntil: 'networkidle2' });
await page.evaluate(() => window.localStorage.setItem('antd-pro-authority', 'guest'));
});
afterEach(() => page.close());
it('should login with failure', async () => {
await page.waitForSelector('#userName', {
timeout: 2000,
});
await page.type('#userName', 'mockuser');
await page.type('#password', 'wrong_password');
await page.click('button[type="submit"]');
await page.waitForSelector('.ant-alert-error'); // should display error
});
it('should login successfully', async () => {
await page.waitForSelector('#userName', {
timeout: 2000,
});
await page.type('#userName', 'admin');
await page.type('#password', '888888');
await page.click('button[type="submit"]');
await page.waitForSelector('.ant-layout-sider h1'); // should display error
const text = await page.evaluate(() => document.body.innerHTML);
expect(text).toContain('Ant Design Pro ');
});
afterAll(() => browser.close());
});
================================================
FILE: src/global.less
================================================
html,
body,
#root {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.globalSpin {
width: 100%;
margin: 40px 0 !important;
}
ul,
ol {
list-style: none;
}
.ant-modal-title {
margin: 0;
font-size: 16px;
line-height: 22px;
font-weight: 500;
text-align: center;
color: rgba(0, 0, 0, 0.85);
}
================================================
FILE: src/layouts/BasicLayout.js
================================================
import React from 'react';
import { Layout } from 'antd';
import DocumentTitle from 'react-document-title';
import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one';
import { connect } from 'dva';
import { ContainerQuery } from 'react-container-query';
import classNames from 'classnames';
import pathToRegexp from 'path-to-regexp';
import { enquireScreen, unenquireScreen } from 'enquire-js';
import { formatMessage } from 'umi/locale';
import SiderMenu from '@/components/SiderMenu';
import Authorized from '@/utils/Authorized';
import SettingDrawer from '@/components/SettingDrawer';
import logo from '../assets/logo.svg';
// import logo from '../assets/all.png';
import Footer from './Footer';
import Header from './Header';
import Context from './MenuContext';
import Exception403 from '../pages/Exception/403';
const { Content } = Layout;
// Conversion router to menu.
function formatter(data, parentPath = '', parentAuthority, parentName) {
return data
.map(item => {
let locale = 'menu';
if (parentName && item.name) {
locale = `${parentName}.${item.name}`;
} else if (item.name) {
locale = `menu.${item.name}`;
} else if (parentName) {
locale = parentName;
}
if (item.path) {
const result = {
...item,
locale,
authority: item.authority || parentAuthority,
};
if (item.routes) {
const children = formatter(
item.routes,
`${parentPath}${item.path}/`,
item.authority,
locale
);
// Reduce memory usage
result.children = children;
}
delete result.routes;
return result;
}
return null;
})
.filter(item => item);
}
const memoizeOneFormatter = memoizeOne(formatter, isEqual);
const query = {
'screen-xs': {
maxWidth: 575,
},
'screen-sm': {
minWidth: 576,
maxWidth: 767,
},
'screen-md': {
minWidth: 768,
maxWidth: 991,
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199,
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599,
},
'screen-xxl': {
minWidth: 1600,
},
};
class BasicLayout extends React.PureComponent {
constructor(props) {
super(props);
this.getPageTitle = memoizeOne(this.getPageTitle);
this.getBreadcrumbNameMap = memoizeOne(this.getBreadcrumbNameMap, isEqual);
this.breadcrumbNameMap = this.getBreadcrumbNameMap();
this.matchParamsPath = memoizeOne(this.matchParamsPath, isEqual);
}
state = {
rendering: true,
isMobile: false,
menuData: this.getMenuData(),
};
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'user/fetchCurrent',
});
dispatch({
type: 'setting/getSetting',
});
this.renderRef = requestAnimationFrame(() => {
this.setState({
rendering: false,
});
});
this.enquireHandler = enquireScreen(mobile => {
const { isMobile } = this.state;
if (isMobile !== mobile) {
this.setState({
isMobile: mobile,
});
}
});
}
componentDidUpdate(preProps) {
// After changing to phone mode,
// if collapsed is true, you need to click twice to display
this.breadcrumbNameMap = this.getBreadcrumbNameMap();
const { isMobile } = this.state;
const { collapsed } = this.props;
if (isMobile && !preProps.isMobile && !collapsed) {
this.handleMenuCollapse(false);
}
}
componentWillUnmount() {
cancelAnimationFrame(this.renderRef);
unenquireScreen(this.enquireHandler);
}
getContext() {
const { location } = this.props;
return {
location,
breadcrumbNameMap: this.breadcrumbNameMap,
};
}
getMenuData() {
const {
route: { routes },
} = this.props;
return memoizeOneFormatter(routes);
}
/**
* 获取面包屑映射
* @param {Object} menuData 菜单配置
*/
getBreadcrumbNameMap() {
const routerMap = {};
const mergeMenuAndRouter = data => {
data.forEach(menuItem => {
if (menuItem.children) {
mergeMenuAndRouter(menuItem.children);
}
// Reduce memory usage
routerMap[menuItem.path] = menuItem;
});
};
mergeMenuAndRouter(this.getMenuData());
return routerMap;
}
matchParamsPath = pathname => {
const pathKey = Object.keys(this.breadcrumbNameMap).find(key =>
pathToRegexp(key).test(pathname)
);
return this.breadcrumbNameMap[pathKey];
};
getPageTitle = pathname => {
const currRouterData = this.matchParamsPath(pathname);
if (!currRouterData) {
return 'Ant Design Pro';
}
const message = formatMessage({
id: currRouterData.locale || currRouterData.name,
defaultMessage: currRouterData.name,
});
return `${message} - Ant Design Pro`;
};
getLayoutStyle = () => {
const { isMobile } = this.state;
const { fixSiderbar, collapsed, layout } = this.props;
if (fixSiderbar && layout !== 'topmenu' && !isMobile) {
return {
paddingLeft: collapsed ? '80px' : '256px',
};
}
return null;
};
getContentStyle = () => {
const { fixedHeader } = this.props;
return {
margin: '24px 24px 0',
paddingTop: fixedHeader ? 64 : 0,
};
};
handleMenuCollapse = collapsed => {
const { dispatch } = this.props;
dispatch({
type: 'global/changeLayoutCollapsed',
payload: collapsed,
});
};
renderSettingDrawer() {
// Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo
const { rendering } = this.state;
if ((rendering || process.env.NODE_ENV === 'production') && APP_TYPE !== 'site') {
return null;
}
return ;
}
render() {
const {
navTheme,
layout: PropsLayout,
children,
location: { pathname },
} = this.props;
const { isMobile, menuData } = this.state;
const isTop = PropsLayout === 'topmenu';
const routerConfig = this.matchParamsPath(pathname);
const layout = (
{isTop && !isMobile ? null : (
)}
}>
{children}
);
return (
{params => (
{layout}
)}
{this.renderSettingDrawer()}
);
}
}
export default connect(({ global, setting }) => ({
collapsed: global.collapsed,
layout: setting.layout,
...setting,
}))(BasicLayout);
================================================
FILE: src/layouts/BlankLayout.js
================================================
import React from 'react';
export default props =>
;
================================================
FILE: src/layouts/Footer.js
================================================
import React, { Fragment } from 'react';
import { Layout, Icon } from 'antd';
import GlobalFooter from '@/components/GlobalFooter';
const { Footer } = Layout;
const FooterView = () => (
,
href: 'https://github.com/biaochenxuying/blog-react-admin',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
copyright={
Copyright BiaoChenXuYing
}
/>
);
export default FooterView;
================================================
FILE: src/layouts/Header.js
================================================
import React, { PureComponent } from 'react';
import { formatMessage } from 'umi/locale';
import { Layout, message } from 'antd';
import Animate from 'rc-animate';
import { connect } from 'dva';
import router from 'umi/router';
import GlobalHeader from '@/components/GlobalHeader';
import TopNavHeader from '@/components/TopNavHeader';
import styles from './Header.less';
import Authorized from '@/utils/Authorized';
const { Header } = Layout;
class HeaderView extends PureComponent {
state = {
visible: true,
};
static getDerivedStateFromProps(props, state) {
if (!props.autoHideHeader && !state.visible) {
return {
visible: true,
};
}
return null;
}
componentDidMount() {
document.addEventListener('scroll', this.handScroll, { passive: true });
}
componentWillUnmount() {
document.removeEventListener('scroll', this.handScroll);
}
getHeadWidth = () => {
const { isMobile, collapsed, setting } = this.props;
const { fixedHeader, layout } = setting;
if (isMobile || !fixedHeader || layout === 'topmenu') {
return '100%';
}
return collapsed ? 'calc(100% - 80px)' : 'calc(100% - 256px)';
};
handleNoticeClear = type => {
message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${formatMessage({ id: `component.globalHeader.${type}` })}`);
const { dispatch } = this.props;
dispatch({
type: 'global/clearNotices',
payload: type,
});
};
handleMenuClick = ({ key }) => {
const { dispatch } = this.props;
if (key === 'userCenter') {
router.push('/account/center');
return;
}
if (key === 'triggerError') {
router.push('/exception/trigger');
return;
}
if (key === 'userinfo') {
router.push('/account/settings/base');
return;
}
if (key === 'logout') {
dispatch({
type: 'login/logout',
});
}
};
handleNoticeVisibleChange = visible => {
if (visible) {
const { dispatch } = this.props;
dispatch({
type: 'global/fetchNotices',
});
}
};
handScroll = () => {
const { autoHideHeader } = this.props;
const { visible } = this.state;
if (!autoHideHeader) {
return;
}
const scrollTop = document.body.scrollTop + document.documentElement.scrollTop;
if (!this.ticking) {
requestAnimationFrame(() => {
if (this.oldScrollTop > scrollTop) {
this.setState({
visible: true,
});
this.scrollTop = scrollTop;
return;
}
if (scrollTop > 300 && visible) {
this.setState({
visible: false,
});
}
if (scrollTop < 300 && !visible) {
this.setState({
visible: true,
});
}
this.oldScrollTop = scrollTop;
this.ticking = false;
});
}
this.ticking = false;
};
render() {
const { isMobile, handleMenuCollapse, setting } = this.props;
const { navTheme, layout, fixedHeader } = setting;
const { visible } = this.state;
const isTop = layout === 'topmenu';
const width = this.getHeadWidth();
const HeaderDom = visible ? (
{isTop && !isMobile ? (
) : (
)}
) : null;
return (
{HeaderDom}
);
}
}
export default connect(({ user, global, setting, loading }) => ({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingNotices: loading.effects['global/fetchNotices'],
notices: global.notices,
setting,
}))(HeaderView);
================================================
FILE: src/layouts/Header.less
================================================
.fixedHeader {
position: fixed;
top: 0;
right: 0;
width: 100%;
z-index: 9;
transition: width 0.2s;
}
================================================
FILE: src/layouts/MenuContext.js
================================================
import { createContext } from 'react';
export default createContext();
================================================
FILE: src/layouts/UserLayout.js
================================================
import React, { Fragment } from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import { Icon } from 'antd';
import GlobalFooter from '@/components/GlobalFooter';
import SelectLang from '@/components/SelectLang';
import styles from './UserLayout.less';
import logo from '../assets/logo.svg';
const links = [
{
key: 'help',
title: formatMessage({ id: 'layout.user.link.help' }),
href: '',
},
{
key: 'privacy',
title: formatMessage({ id: 'layout.user.link.privacy' }),
href: '',
},
{
key: 'terms',
title: formatMessage({ id: 'layout.user.link.terms' }),
href: '',
},
];
const copyright = (
Copyright 2018 蚂蚁金服体验技术部出品
);
class UserLayout extends React.PureComponent {
// @TODO title
// getPageTitle() {
// const { routerData, location } = this.props;
// const { pathname } = location;
// let title = 'Ant Design Pro';
// if (routerData[pathname] && routerData[pathname].name) {
// title = `${routerData[pathname].name} - Ant Design Pro`;
// }
// return title;
// }
render() {
const { children } = this.props;
return (
// @TODO
Ant Design
Ant Design 是西湖区最具影响力的 Web 设计规范
{children}
);
}
}
export default UserLayout;
================================================
FILE: src/layouts/UserLayout.less
================================================
@import '~antd/lib/style/themes/default.less';
.container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
}
.lang {
text-align: right;
width: 100%;
height: 40px;
line-height: 44px;
:global(.ant-dropdown-trigger) {
margin-right: 24px;
}
}
.content {
padding: 32px 0;
flex: 1;
}
@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: 72px 0 24px 0;
}
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
}
.title {
font-size: 33px;
color: @heading-color;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
.desc {
font-size: @font-size-base;
color: @text-color-secondary;
margin-top: 12px;
margin-bottom: 40px;
}
================================================
FILE: src/locales/en-US.js
================================================
export default {
'navBar.lang': 'Languages',
'lang.simplified-chinese': '简体中文',
'lang.traditional-chinese': '繁体中文',
'lang.english': 'English',
'lang.portuguese': 'Portuguese',
'layout.user.link.help': 'Help',
'layout.user.link.privacy': 'Privacy',
'layout.user.link.terms': 'Terms',
'validation.email.required': 'Please enter your email!',
'validation.email.wrong-format': 'The email address is in the wrong format!',
'validation.password.required': 'Please enter your password!',
'validation.password.twice': 'The passwords entered twice do not match!',
'validation.password.strength.msg':
"Please enter at least 6 characters and don't use passwords that are easy to guess.",
'validation.password.strength.strong': 'Strength: strong',
'validation.password.strength.medium': 'Strength: medium',
'validation.password.strength.short': 'Strength: too short',
'validation.confirm-password.required': 'Please confirm your password!',
'validation.phone-number.required': 'Please enter your phone number!',
'validation.phone-number.wrong-format': 'Malformed phone number!',
'validation.verification-code.required': 'Please enter the verification code!',
'validation.title.required': 'Please enter a title',
'validation.date.required': 'Please select the start and end date',
'validation.goal.required': 'Please enter a description of the goal',
'validation.standard.required': 'Please enter a metric',
'form.optional': ' (optional) ',
'form.submit': 'Submit',
'form.save': 'Save',
'form.email.placeholder': 'Email',
'form.password.placeholder': 'Password',
'form.confirm-password.placeholder': 'Confirm password',
'form.phone-number.placeholder': 'Phone number',
'form.verification-code.placeholder': 'Verification code',
'form.title.label': 'Title',
'form.title.placeholder': 'Give the target a name',
'form.date.label': 'Start and end date',
'form.date.placeholder.start': 'Start date',
'form.date.placeholder.end': 'End date',
'form.goal.label': 'Goal description',
'form.goal.placeholder': 'Please enter your work goals',
'form.standard.label': 'Metrics',
'form.standard.placeholder': 'Please enter a metric',
'form.client.label': 'Client',
'form.client.label.tooltip': 'Target service object',
'form.client.placeholder':
'Please describe your customer service, internal customers directly @ Name / job number',
'form.invites.label': 'Inviting critics',
'form.invites.placeholder': 'Please direct @ Name / job number, you can invite up to 5 people',
'form.weight.label': 'Weight',
'form.weight.placeholder': 'Please enter weight',
'form.public.label': 'Target disclosure',
'form.public.label.help': 'Customers and invitees are shared by default',
'form.public.radio.public': 'Public',
'form.public.radio.partially-public': 'Partially public',
'form.public.radio.private': 'Private',
'form.publicUsers.placeholder': 'Open to',
'form.publicUsers.option.A': 'Colleague A',
'form.publicUsers.option.B': 'Colleague B',
'form.publicUsers.option.C': 'Colleague C',
'component.globalHeader.search': 'Search',
'component.globalHeader.search.example1': 'Search example 1',
'component.globalHeader.search.example2': 'Search example 2',
'component.globalHeader.search.example3': 'Search example 3',
'component.globalHeader.help': 'Help',
'component.globalHeader.notification': 'Notification',
'component.globalHeader.notification.empty': 'You have viewed all notifications.',
'component.globalHeader.message': 'Messages',
'component.globalHeader.message.empty': 'You have viewed all messsages.',
'component.globalHeader.event': 'Event',
'component.globalHeader.event.empty': 'You have viewed all events.',
'component.noticeIcon.clear': 'Clear',
'component.noticeIcon.cleared': 'Cleared',
'component.noticeIcon.empty': 'No notifications',
'menu.article': 'article',
'menu.article.list': 'list',
'menu.article.create': 'create',
'menu.timeAxis': 'timeAxis',
'menu.timeAxis.list': 'list',
'menu.project': 'project',
'menu.project.list': 'list',
'menu.tag': 'tag',
'menu.tag.list': 'list',
'menu.otherUser': 'user',
'menu.otherUser.list': 'list',
'menu.message': 'message',
'menu.message.list': 'list',
'menu.link': 'link',
'menu.link.list': 'list',
'menu.category': 'category',
'menu.category.list': 'list',
'menu.home': 'Home',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Analysis',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Workplace',
'menu.form': 'Form',
'menu.form.basicform': 'Basic Form',
'menu.form.stepform': 'Step Form',
'menu.form.stepform.info': 'Step Form(write transfer information)',
'menu.form.stepform.confirm': 'Step Form(confirm transfer information)',
'menu.form.stepform.result': 'Step Form(finished)',
'menu.form.advancedform': 'Advanced Form',
'menu.list': 'List',
'menu.list.searchtable': 'Search Table',
'menu.list.basiclist': 'Basic List',
'menu.list.cardlist': 'Card List',
'menu.list.searchlist': 'Search List',
'menu.list.searchlist.articles': 'Search List(articles)',
'menu.list.searchlist.projects': 'Search List(projects)',
'menu.list.searchlist.applications': 'Search List(applications)',
'menu.profile': 'Profile',
'menu.profile.basic': 'Basic Profile',
'menu.profile.advanced': 'Advanced Profile',
'menu.result': 'Result',
'menu.result.success': 'Success',
'menu.result.fail': 'Fail',
'menu.exception': 'Exception',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Trigger',
'menu.account': 'Account',
'menu.account.center': 'Account Center',
'menu.account.settings': 'Account Settings',
'menu.account.trigger': 'Trigger Error',
'menu.account.logout': 'Logout',
'app.login.tab-login-credentials': 'Credentials',
'app.login.tab-login-mobile': 'Mobile number',
'app.login.remember-me': 'Remember me',
'app.login.forgot-password': 'Forgot your password?',
'app.login.sign-in-with': 'Sign in with',
'app.login.signup': 'Sign up',
'app.login.login': 'Login',
'app.register.register': 'Register',
'app.register.get-verification-code': 'Get code',
'app.register.sing-in': 'Already have an account?',
'app.register-result.msg': 'Account:registered at {email}',
'app.register-result.activation-email':
'The activation email has been sent to your email address and is valid for 24 hours. Please log in to the email in time and click on the link in the email to activate the account.',
'app.register-result.back-home': 'Back to home',
'app.register-result.view-mailbox': 'View mailbox',
'app.home.introduce': 'introduce',
'app.analysis.test': 'Gongzhuan No.{no} shop',
'app.analysis.introduce': 'Introduce',
'app.analysis.total-sales': 'Total Sales',
'app.analysis.day-sales': 'Day Sales',
'app.analysis.visits': 'Visits',
'app.analysis.visits-trend': 'Visits Trend',
'app.analysis.visits-ranking': 'Visits Ranking',
'app.analysis.day-visits': 'Day Visits',
'app.analysis.week': 'Week Ratio',
'app.analysis.day': 'Day Ratio',
'app.analysis.payments': 'Payments',
'app.analysis.conversion-rate': 'Conversion Rate',
'app.analysis.operational-effect': 'Operational Effect',
'app.analysis.sales-trend': 'Stores Sales Trend',
'app.analysis.sales-ranking': 'Sales Ranking',
'app.analysis.all-year': 'All Year',
'app.analysis.all-month': 'All Month',
'app.analysis.all-week': 'All Week',
'app.analysis.all-day': 'All day',
'app.analysis.search-users': 'Search Users',
'app.analysis.per-capita-search': 'Per Capita Search',
'app.analysis.online-top-search': 'Online Top Search',
'app.analysis.the-proportion-of-sales': 'The Proportion Of Sales',
'app.analysis.channel.all': 'ALL',
'app.analysis.channel.online': 'Online',
'app.analysis.channel.stores': 'Stores',
'app.analysis.sales': 'Sales',
'app.analysis.traffic': 'Traffic',
'app.analysis.table.rank': 'Rank',
'app.analysis.table.search-keyword': 'Keyword',
'app.analysis.table.users': 'Users',
'app.analysis.table.weekly-range': 'Weekly Range',
'app.forms.basic.title': 'Basic form',
'app.forms.basic.description':
'Form pages are used to collect or verify information to users, and basic forms are common in scenarios where there are fewer data items.',
'app.monitor.trading-activity': 'Real-Time Trading Activity',
'app.monitor.total-transactions': 'Total transactions today',
'app.monitor.sales-target': 'Sales target completion rate',
'app.monitor.remaining-time': 'Remaining time of activity',
'app.monitor.total-transactions-per-second': 'Total transactions per second',
'app.monitor.activity-forecast': 'Activity forecast',
'app.monitor.efficiency': 'Efficiency',
'app.monitor.ratio': 'Ratio',
'app.monitor.proportion-per-category': 'Proportion Per Category',
'app.monitor.fast-food': 'Fast food',
'app.monitor.western-food': 'Western food',
'app.monitor.hot-pot': 'Hot pot',
'app.monitor.waiting-for-implementation': 'Waiting for implementation',
'app.monitor.popular-searches': 'Popular Searches',
'app.monitor.resource-surplus': 'Resource Surplus',
'app.monitor.fund-surplus': 'Fund Surplus',
'app.settings.menuMap.basic': 'Basic Settings',
'app.settings.menuMap.security': 'Security Settings',
'app.settings.menuMap.binding': 'Account Binding',
'app.settings.menuMap.notification': 'New Message Notification',
'app.settings.menuMap.personalLink': 'personal link',
'app.settings.basic.avatar': 'Change avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Please input your email!',
'app.settings.basic.nickname': 'Nickname',
'app.settings.basic.nickname-message': 'Please input your Nickname!',
'app.settings.basic.profile': 'Personal profile',
'app.settings.basic.profile-message': 'Please input your personal profile!',
'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
'app.settings.basic.country': 'Country/Region',
'app.settings.basic.country-message': 'Please input your country!',
'app.settings.basic.geographic': 'Province or city',
'app.settings.basic.geographic-message': 'Please input your geographic info!',
'app.settings.basic.address': 'Street Address',
'app.settings.basic.address-message': 'Please input your address!',
'app.settings.basic.phone': 'Phone Number',
'app.settings.basic.phone-message': 'Please input your phone!',
'app.settings.basic.update': 'Update Information',
'app.settings.security.strong': 'Strong',
'app.settings.security.medium': 'Medium',
'app.settings.security.weak': 'Weak',
'app.settings.security.password': 'Account Password',
'app.settings.security.password-description': 'Current password strength:',
'app.settings.security.phone': 'Security Phone',
'app.settings.security.phone-description': 'Bound phone:',
'app.settings.security.question': 'Security Question',
'app.settings.security.question-description':
'The security question is not set, and the security policy can effectively protect the account security',
'app.settings.security.email': 'Backup Email',
'app.settings.security.email-description': 'Bound Email:',
'app.settings.security.mfa': 'MFA Device',
'app.settings.security.mfa-description':
'Unbound MFA device, after binding, can be confirmed twice',
'app.settings.security.modify': 'Modify',
'app.settings.security.set': 'Set',
'app.settings.security.bind': 'Bind',
'app.settings.binding.taobao': 'Binding Taobao',
'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
'app.settings.binding.alipay': 'Binding Alipay',
'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
'app.settings.binding.dingding': 'Binding DingTalk',
'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
'app.settings.binding.bind': 'Bind',
'app.settings.notification.password': 'Account Password',
'app.settings.notification.password-description':
'Messages from other users will be notified in the form of a station letter',
'app.settings.notification.messages': 'System Messages',
'app.settings.notification.messages-description':
'System messages will be notified in the form of a station letter',
'app.settings.notification.todo': 'To-do Notification',
'app.settings.notification.todo-description':
'The to-do list will be notified in the form of a letter from the station',
'app.settings.open': 'Open',
'app.settings.close': 'Close',
'app.exception.back': 'Back to home',
'app.exception.description.403': "Sorry, you don't have access to this page",
'app.exception.description.404': 'Sorry, the page you visited does not exist',
'app.exception.description.500': 'Sorry, the server is reporting an error',
'app.result.error.title': 'Submission Failed',
'app.result.error.description':
'Please check and modify the following information before resubmitting.',
'app.result.error.hint-title': 'The content you submitted has the following error:',
'app.result.error.hint-text1': 'Your account has been frozen',
'app.result.error.hint-btn1': 'Thaw immediately',
'app.result.error.hint-text2': 'Your account is not yet eligible to apply',
'app.result.error.hint-btn2': 'Upgrade immediately',
'app.result.error.btn-text': 'Return to modify',
'app.result.success.title': 'Submission Success',
'app.result.success.description':
'The submission results page is used to feed back the results of a series of operational tasks. If it is a simple operation, use the Message global prompt feedback. This text area can show a simple supplementary explanation. If there is a similar requirement for displaying “documents”, the following gray area can present more complicated content.',
'app.result.success.operate-title': 'Project Name',
'app.result.success.operate-id': 'Project ID:',
'app.result.success.principal': 'Principal:',
'app.result.success.operate-time': 'Effective time:',
'app.result.success.step1-title': 'Create project',
'app.result.success.step1-operator': 'Qu Lili',
'app.result.success.step2-title': 'Departmental preliminary review',
'app.result.success.step2-operator': 'Zhou Maomao',
'app.result.success.step2-extra': 'Urge',
'app.result.success.step3-title': 'Financial review',
'app.result.success.step4-title': 'Finish',
'app.result.success.btn-return': 'Back to list',
'app.result.success.btn-project': 'View project',
'app.result.success.btn-print': 'Print',
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Content Width',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.themecolor': 'Theme Color',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Navigation Mode',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
'app.setting.hideheader': 'Hidden Header when scrolling',
'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
'app.setting.production.hint':
'Setting panel shows in development environment only, please manually modify',
};
================================================
FILE: src/locales/pt-BR.js
================================================
export default {
'navBar.lang': 'Idiomas',
'lang.simplified-chinese': '简体中文',
'lang.traditional-chinese': '繁体中文',
'lang.english': 'English',
'lang.portuguese': 'Portuguese',
'layout.user.link.help': 'ajuda',
'layout.user.link.privacy': 'política de privacidade',
'layout.user.link.terms': 'termos de serviços',
'validation.email.required': 'Por favor insira seu email!',
'validation.email.wrong-format': 'O email está errado!',
'validation.password.required': 'Por favor insira sua senha!',
'validation.password.twice': 'As senhas não estão iguais!',
'validation.password.strength.msg':
'Por favor insira pelo menos 6 caracteres e não use senhas fáceis de adivinhar.',
'validation.password.strength.strong': 'Força: forte',
'validation.password.strength.medium': 'Força: média',
'validation.password.strength.short': 'Força: curta',
'validation.confirm-password.required': 'Por favor confirme sua senha!',
'validation.phone-number.required': 'Por favor insira seu telefone!',
'validation.phone-number.wrong-format': 'Formato de telefone errado!',
'validation.verification-code.required': 'Por favor insira seu código de verificação!',
'form.email.placeholder': 'Email',
'form.password.placeholder': 'Senha',
'form.confirm-password.placeholder': 'Confirme a senha',
'form.phone-number.placeholder': 'Telefone',
'form.verification-code.placeholder': 'Código de verificação',
'component.globalHeader.search': 'Busca',
'component.globalHeader.search.example1': 'Exemplo de busca 1',
'component.globalHeader.search.example2': 'Exemplo de busca 2',
'component.globalHeader.search.example3': 'Exemplo de busca 3',
'component.globalHeader.help': 'Ajuda',
'component.globalHeader.notification': 'Notificação',
'component.globalHeader.notification.empty': 'Você visualizou todas as notificações.',
'component.globalHeader.message': 'Mensagem',
'component.globalHeader.message.empty': 'Você visualizou todas as mensagens.',
'component.globalHeader.event': 'Evento',
'component.globalHeader.event.empty': 'Você visualizou todos os eventos.',
'component.noticeIcon.clear': 'Limpar',
'component.noticeIcon.cleared': 'Limpo',
'component.noticeIcon.empty': 'Sem notificações',
'menu.home': 'Início',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': 'Análise',
'menu.dashboard.monitor': 'Monitor',
'menu.dashboard.workplace': 'Ambiente de Trabalho',
'menu.form': 'Formulário',
'menu.form.basicform': 'Formulário Básico',
'menu.form.stepform': 'Formulário Assistido',
'menu.form.stepform.info': 'Formulário Assistido(gravar informações de transferência)',
'menu.form.stepform.confirm': 'Formulário Assistido(confirmar informações de transferência)',
'menu.form.stepform.result': 'Formulário Assistido(finalizado)',
'menu.form.advancedform': 'Formulário Avançado',
'menu.list': 'Lista',
'menu.list.searchtable': 'Tabela de Busca',
'menu.list.basiclist': 'Lista Básica',
'menu.list.cardlist': 'Lista de Card',
'menu.list.searchlist': 'Lista de Busca',
'menu.list.searchlist.articles': 'Lista de Busca(artigos)',
'menu.list.searchlist.projects': 'Lista de Busca(projetos)',
'menu.list.searchlist.applications': 'Lista de Busca(aplicações)',
'menu.profile': 'Perfil',
'menu.profile.basic': 'Perfil Básico',
'menu.profile.advanced': 'Perfil Avançado',
'menu.result': 'Resultado',
'menu.result.success': 'Sucesso',
'menu.result.fail': 'Falha',
'menu.exception': 'Exceção',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': 'Disparar',
'menu.account': 'Conta',
'menu.account.center': 'Central da Conta',
'menu.account.settings': 'Configurar Conta',
'menu.account.trigger': 'Disparar Erro',
'menu.account.logout': 'Sair',
'app.login.tab-login-credentials': 'Credenciais',
'app.login.tab-login-mobile': 'Telefone',
'app.login.remember-me': 'Lembre-me',
'app.login.forgot-password': 'Esqueceu sua senha?',
'app.login.sign-in-with': 'Login com',
'app.login.signup': 'Cadastre-se',
'app.login.login': 'Login',
'app.register.register': 'Cadastro',
'app.register.get-verification-code': 'Recuperar código',
'app.register.sing-in': 'Já tem uma conta?',
'app.register-result.msg': 'Conta:registrada em {email}',
'app.register-result.activation-email':
'Um email de ativação foi enviado para o seu email e é válido por 24 horas. Por favor entre no seu email e clique no link de ativação da conta.',
'app.register-result.back-home': 'Voltar ao Início',
'app.register-result.view-mailbox': 'Visualizar a caixa de email',
'app.home.introduce': 'introduzir',
'app.analysis.test': 'Gongzhuan No.{no} shop',
'app.analysis.introduce': 'Introduzir',
'app.analysis.total-sales': 'Vendas Totais',
'app.analysis.day-sales': 'Vendas do Dia',
'app.analysis.visits': 'Visitas',
'app.analysis.visits-trend': 'Tendência de Visitas',
'app.analysis.visits-ranking': 'Ranking de Visitas',
'app.analysis.day-visits': 'Visitas do Dia',
'app.analysis.week': 'Taxa Semanal',
'app.analysis.day': 'Taxa Diária',
'app.analysis.payments': 'Pagamentos',
'app.analysis.conversion-rate': 'Taxa de Conversão',
'app.analysis.operational-effect': 'Efeito Operacional',
'app.analysis.sales-trend': 'Tendência de Vendas das Lojas',
'app.analysis.sales-ranking': 'Ranking de Vendas',
'app.analysis.all-year': 'Todo ano',
'app.analysis.all-month': 'Todo mês',
'app.analysis.all-week': 'Toda semana',
'app.analysis.all-day': 'Todo dia',
'app.analysis.search-users': 'Pesquisa de Usuários',
'app.analysis.per-capita-search': 'Busca Per Capta',
'app.analysis.online-top-search': 'Mais Buscadas Online',
'app.analysis.the-proportion-of-sales': 'The Proportion Of Sales',
'app.analysis.channel.all': 'Tudo',
'app.analysis.channel.online': 'Online',
'app.analysis.channel.stores': 'Lojas',
'app.analysis.sales': 'Vendas',
'app.analysis.traffic': 'Tráfego',
'app.analysis.table.rank': 'Rank',
'app.analysis.table.search-keyword': 'Palavra chave',
'app.analysis.table.users': 'Usuários',
'app.analysis.table.weekly-range': 'Faixa Semanal',
'app.settings.menuMap.basic': 'Configurações Básicas',
'app.settings.menuMap.security': 'Configurações de Segurança',
'app.settings.menuMap.binding': 'Vinculação de Conta',
'app.settings.menuMap.notification': 'Mensagens de Notificação',
'app.settings.basic.avatar': 'Alterar avatar',
'app.settings.basic.email': 'Email',
'app.settings.basic.email-message': 'Por favor insira seu email!',
'app.settings.basic.nickname': 'Nome de usuário',
'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!',
'app.settings.basic.profile': 'Perfil pessoal',
'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!',
'app.settings.basic.profile-placeholder': 'Breve introdução sua',
'app.settings.basic.country': 'País/Região',
'app.settings.basic.country-message': 'Por favor insira país!',
'app.settings.basic.geographic': 'Província, estado ou cidade',
'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!',
'app.settings.basic.address': 'Endereço',
'app.settings.basic.address-message': 'Por favor insira seu endereço!',
'app.settings.basic.phone': 'Número de telefone',
'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!',
'app.settings.basic.update': 'Atualizar Informações',
'app.settings.security.strong': 'Forte',
'app.settings.security.medium': 'Média',
'app.settings.security.weak': 'Fraca',
'app.settings.security.password': 'Senha da Conta',
'app.settings.security.password-description': 'Força da senha',
'app.settings.security.phone': 'Telefone de Seguraça',
'app.settings.security.phone-description': 'Telefone vinculado',
'app.settings.security.question': 'Pergunta de Segurança',
'app.settings.security.question-description':
'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta',
'app.settings.security.email': 'Email de Backup',
'app.settings.security.email-description': 'Email vinculado',
'app.settings.security.mfa': 'Dispositivo MFA',
'app.settings.security.mfa-description':
'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes',
'app.settings.security.modify': 'Modificar',
'app.settings.security.set': 'Atribuir',
'app.settings.security.bind': 'Vincular',
'app.settings.binding.taobao': 'Vincular Taobao',
'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao',
'app.settings.binding.alipay': 'Vincular Alipay',
'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay',
'app.settings.binding.dingding': 'Vincular DingTalk',
'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk',
'app.settings.binding.bind': 'Vincular',
'app.settings.notification.password': 'Senha da Conta',
'app.settings.notification.password-description':
'Mensagens de outros usuários serão notificadas na forma de uma estação de letra',
'app.settings.notification.messages': 'Mensagens de Sistema',
'app.settings.notification.messages-description':
'Mensagens de sistema serão notificadas na forma de uma estação de letra',
'app.settings.notification.todo': 'Notificação de To-do',
'app.settings.notification.todo-description':
'A lista de to-do será notificada na forma de uma estação de letra',
'app.settings.open': 'Aberto',
'app.settings.close': 'Fechado',
'app.exception.back': 'Voltar para Início',
'app.exception.description.403': 'Desculpe, você não tem acesso a esta página',
'app.exception.description.404': 'Desculpe, a página que você visitou não existe',
'app.exception.description.500': 'Desculpe, o servidor está reportando um erro',
'app.result.error.title': 'A Submissão Falhou',
'app.result.error.description':
'Por favor, verifique e modifique as seguintes informações antes de reenviar.',
'app.result.error.hint-title': 'O conteúdo que você enviou tem o seguinte erro:',
'app.result.error.hint-text1': 'Sua conta foi congelada',
'app.result.error.hint-btn1': 'Descongele imediatamente',
'app.result.error.hint-text2': 'Sua conta ainda não está qualificada para se candidatar',
'app.result.error.hint-btn2': 'Atualizar imediatamente',
'app.result.error.btn-text': 'Retornar para modificar',
'app.result.success.title': 'A Submissão foi um Sucesso',
'app.result.success.description':
'A página de resultados de envio é usada para fornecer os resultados de uma série de tarefas operacionais. Se for uma operação simples, use o prompt de feedback de Mensagem global. Esta área de texto pode mostrar uma explicação suplementar simples. Se houver um requisito semelhante para exibir "documentos", a área cinza a seguir pode apresentar um conteúdo mais complicado.',
'app.result.success.operate-title': 'Nome do Projeto',
'app.result.success.operate-id': 'ID do Projeto:',
'app.result.success.principal': 'Principal:',
'app.result.success.operate-time': 'Tempo efetivo:',
'app.result.success.step1-title': 'Criar projeto',
'app.result.success.step1-operator': 'Qu Lili',
'app.result.success.step2-title': 'Revisão preliminar do departamento',
'app.result.success.step2-operator': 'Zhou Maomao',
'app.result.success.step2-extra': 'Urge',
'app.result.success.step3-title': 'Revisão financeira',
'app.result.success.step4-title': 'Terminar',
'app.result.success.btn-return': 'Voltar a lista',
'app.result.success.btn-project': 'Ver projeto',
'app.result.success.btn-print': 'imprimir',
'app.setting.pagestyle': 'Configuração de estilo da página',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.light': 'Light style',
'app.setting.content-width': 'Largura do conteúdo',
'app.setting.content-width.fixed': 'Fixo',
'app.setting.content-width.fluid': 'Fluido',
'app.setting.themecolor': 'Cor do Tema',
'app.setting.themecolor.dust': 'Dust Red',
'app.setting.themecolor.volcano': 'Volcano',
'app.setting.themecolor.sunset': 'Sunset Orange',
'app.setting.themecolor.cyan': 'Cyan',
'app.setting.themecolor.green': 'Polar Green',
'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
'app.setting.themecolor.geekblue': 'Geek Glue',
'app.setting.themecolor.purple': 'Golden Purple',
'app.setting.navigationmode': 'Modo de Navegação',
'app.setting.sidemenu': 'Layout do Menu Lateral',
'app.setting.topmenu': 'Layout do Menu Superior',
'app.setting.fixedheader': 'Cabeçalho fixo',
'app.setting.fixedsidebar': 'Barra lateral fixa',
'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral',
'app.setting.hideheader': 'Esconder o cabeçalho quando rolar',
'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado',
'app.setting.othersettings': 'Outras configurações',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copiar Configuração',
'app.setting.copyinfo':
'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js',
'app.setting.production.hint':
'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o',
};
================================================
FILE: src/locales/zh-CN.js
================================================
export default {
'navBar.lang': '语言',
'lang.simplified-chinese': '简体中文',
'lang.traditional-chinese': '繁体中文',
'lang.english': 'English',
'lang.portuguese': 'Portuguese',
'layout.user.link.help': '帮助',
'layout.user.link.privacy': '隐私',
'layout.user.link.terms': '条款',
'validation.email.required': '请输入邮箱地址!',
'validation.email.wrong-format': '邮箱地址格式错误!',
'validation.password.required': '请输入密码!',
'validation.password.twice': '两次输入的密码不匹配!',
'validation.password.strength.msg': '请至少输入 6 个字符。请不要使用容易被猜到的密码。',
'validation.password.strength.strong': '强度:强',
'validation.password.strength.medium': '强度:中',
'validation.password.strength.short': '强度:太短',
'validation.confirm-password.required': '请确认密码!',
'validation.phone-number.required': '请输入手机号!',
'validation.phone-number.wrong-format': '手机号格式错误!',
'validation.verification-code.required': '请输入验证码!',
'validation.title.required': '请输入标题',
'validation.date.required': '请选择起止日期',
'validation.goal.required': '请输入目标描述',
'validation.standard.required': '请输入衡量标准',
'form.optional': '(选填)',
'form.submit': '提交',
'form.save': '保存',
'form.email.placeholder': '邮箱',
'form.password.placeholder': '至少6位密码,区分大小写',
'form.confirm-password.placeholder': '确认密码',
'form.phone-number.placeholder': '位手机号',
'form.verification-code.placeholder': '验证码',
'form.title.label': '标题',
'form.title.placeholder': '给目标起个名字',
'form.date.label': '起止日期',
'form.date.placeholder.start': '开始日期',
'form.date.placeholder.end': '结束日期',
'component.globalHeader.search': '站内搜索',
'component.globalHeader.search.example1': '搜索提示一',
'component.globalHeader.search.example2': '搜索提示二',
'component.globalHeader.search.example3': '搜索提示三',
'component.globalHeader.help': '使用文档',
'component.globalHeader.notification': '通知',
'component.globalHeader.notification.empty': '你已查看所有通知',
'component.globalHeader.message': '消息',
'component.globalHeader.message.empty': '您已读完所有消息',
'component.noticeIcon.clear': '清空',
'component.noticeIcon.cleared': '清空了',
'component.noticeIcon.empty': '暂无数据',
'menu.home': '首页',
'menu.article': '文章',
'menu.article.list': '文章列表',
'menu.article.create': '文章创作',
'menu.timeAxis': '时间轴',
'menu.timeAxis.list': '时间轴列表',
'menu.project': '项目',
'menu.project.list': '项目列表',
'menu.tag': '标签',
'menu.tag.list': '标签列表',
'menu.otherUser': '用户管理',
'menu.otherUser.list': '用户列表',
'menu.message': '留言',
'menu.message.list': '留言列表',
'menu.link': '友情链接',
'menu.link.list': '链接列表',
'menu.category': '分类',
'menu.category.list': '分类列表',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': '分析页',
'menu.dashboard.monitor': '监控页',
'menu.dashboard.workplace': '工作台',
'menu.result': '结果页',
'menu.result.success': '成功页',
'menu.result.fail': '失败页',
'menu.exception': '异常页',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': '触发错误',
'menu.account': '个人中心',
'menu.account.center': '个人中心',
'menu.account.settings': '个人设置',
'menu.account.trigger': '触发报错',
'menu.account.logout': '退出登录',
'app.login.tab-login-credentials': '账户密码登录',
'app.login.tab-login-mobile': '手机号登录',
'app.login.remember-me': '自动登录',
'app.login.forgot-password': '忘记密码',
'app.login.sign-in-with': '其他登录方式',
'app.login.signup': '注册账户',
'app.login.login': '登录',
'app.register.register': '注册',
'app.register.get-verification-code': '获取验证码',
'app.register.sing-in': '使用已有账户登录',
'app.register-result.msg': '你的账户:{email} 注册成功',
'app.register-result.activation-email':
'激活邮件已发送到你的邮箱中,邮件有效期为24小时。请及时登录邮箱,点击邮件中的链接激活帐户。',
'app.register-result.back-home': '返回首页',
'app.register-result.view-mailbox': '查看邮箱',
'app.home.introduce': '介绍',
'app.settings.menuMap.basic': '基本设置',
'app.settings.menuMap.security': '安全设置',
'app.settings.menuMap.notification': '新消息通知',
'app.settings.menuMap.personalLink': '个人链接',
'app.settings.basic.avatar': '更换头像',
'app.settings.basic.email': '邮箱',
'app.settings.basic.email-message': '请输入您的邮箱!',
'app.settings.basic.nickname': '昵称',
'app.settings.basic.nickname-message': '请输入您的昵称!',
'app.settings.basic.profile': '个人简介',
'app.settings.basic.profile-message': '请输入个人简介!',
'app.settings.basic.profile-placeholder': '个人简介',
'app.settings.basic.phone': '联系电话',
'app.settings.basic.phone-message': '请输入您的联系电话!',
'app.settings.basic.update': '更新基本信息',
'app.settings.notification.password': '账户密码',
'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
'app.settings.notification.messages': '系统消息',
'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
'app.settings.notification.todo': '账户密码',
'app.settings.notification.todo-description': '账户密码',
'app.settings.open': '开',
'app.settings.close': '关',
'app.exception.back': '返回首页',
'app.exception.description.403': '抱歉,你无权访问该页面',
'app.exception.description.404': '抱歉,你访问的页面不存在',
'app.exception.description.500': '抱歉,服务器出错了',
'app.setting.pagestyle': '整体风格设置',
'app.setting.pagestyle.dark': '暗色菜单风格',
'app.setting.pagestyle.light': '亮色菜单风格',
'app.setting.content-width': '内容区域宽度',
'app.setting.content-width.fixed': '定宽',
'app.setting.content-width.fluid': '流式',
'app.setting.themecolor': '主题色',
'app.setting.themecolor.dust': '薄暮',
'app.setting.themecolor.volcano': '火山',
'app.setting.themecolor.sunset': '日暮',
'app.setting.themecolor.cyan': '明青',
'app.setting.themecolor.green': '极光绿',
'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
'app.setting.themecolor.geekblue': '极客蓝',
'app.setting.themecolor.purple': '酱紫',
'app.setting.navigationmode': '导航模式',
'app.setting.sidemenu': '侧边菜单布局',
'app.setting.topmenu': '顶部菜单布局',
'app.setting.fixedheader': '固定 Header',
'app.setting.fixedsidebar': '固定侧边菜单',
'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
'app.setting.hideheader': '下滑时隐藏 Header',
'app.setting.hideheader.hint': '固定 Header 时可配置',
'app.setting.othersettings': '其他设置',
'app.setting.weakmode': '色弱模式',
'app.setting.copy': '拷贝设置',
'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
'app.setting.production.hint':
'配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
};
================================================
FILE: src/locales/zh-TW.js
================================================
export default {
'navBar.lang': '語言',
'lang.simplified-chinese': '简体中文',
'lang.traditional-chinese': '繁体中文',
'lang.english': 'English',
'lang.portuguese': 'Portuguese',
'layout.user.link.help': '幫助',
'layout.user.link.privacy': '隱私',
'layout.user.link.terms': '條款',
'validation.email.required': '請輸入郵箱地址!',
'validation.email.wrong-format': '郵箱地址格式錯誤!',
'validation.password.required': '請輸入密碼!',
'validation.password.twice': '兩次輸入的密碼不匹配!',
'validation.password.strength.msg': '請至少輸入 6 個字符。請不要使用容易被猜到的密碼。',
'validation.password.strength.strong': '強度:強',
'validation.password.strength.medium': '強度:中',
'validation.password.strength.short': '強度:太短',
'validation.confirm-password.required': '請確認密碼!',
'validation.phone-number.required': '請輸入手機號!',
'validation.phone-number.wrong-format': '手機號格式錯誤!',
'validation.verification-code.required': '請輸入驗證碼!',
'validation.title.required': '請輸入標題',
'validation.date.required': '請選擇起止日期',
'validation.goal.required': '請輸入目標描述',
'validation.standard.required': '請輸入衡量標淮',
'form.optional': '(選填)',
'form.submit': '提交',
'form.save': '保存',
'form.email.placeholder': '郵箱',
'form.password.placeholder': '至少6位密碼,區分大小寫',
'form.confirm-password.placeholder': '確認密碼',
'form.phone-number.placeholder': '位手機號',
'form.verification-code.placeholder': '驗證碼',
'form.title.label': '標題',
'form.title.placeholder': '給目標起個名字',
'form.date.label': '起止日期',
'form.date.placeholder.start': '開始日期',
'form.date.placeholder.end': '結束日期',
'form.goal.label': '目標描述',
'form.goal.placeholder': '請輸入妳的階段性工作目標',
'form.standard.label': '衡量標淮',
'form.standard.placeholder': '請輸入衡量標淮',
'form.client.label': '客戶',
'form.client.label.tooltip': '目標的服務對象',
'form.client.placeholder': '請描述妳服務的客戶,內部客戶直接 @姓名/工號',
'form.invites.label': '邀評人',
'form.invites.placeholder': '請直接 @姓名/工號,最多可邀請 5 人',
'form.weight.label': '權重',
'form.weight.placeholder': '請輸入',
'form.public.label': '目標公開',
'form.public.label.help': '客戶、邀評人默認被分享',
'form.public.radio.public': '公開',
'form.public.radio.partially-public': '部分公開',
'form.public.radio.private': '不公開',
'form.publicUsers.placeholder': '公開給',
'form.publicUsers.option.A': '同事甲',
'form.publicUsers.option.B': '同事乙',
'form.publicUsers.option.C': '同事丙',
'component.globalHeader.search': '站內搜索',
'component.globalHeader.search.example1': '搜索提示壹',
'component.globalHeader.search.example2': '搜索提示二',
'component.globalHeader.search.example3': '搜索提示三',
'component.globalHeader.help': '使用文檔',
'component.globalHeader.notification': '通知',
'component.globalHeader.notification.empty': '妳已查看所有通知',
'component.globalHeader.message': '消息',
'component.globalHeader.message.empty': '您已讀完所有消息',
'component.globalHeader.event': '待辦',
'component.globalHeader.event.empty': '妳已完成所有待辦',
'component.noticeIcon.clear': '清空',
'component.noticeIcon.cleared': '清空了',
'component.noticeIcon.empty': '暫無數據',
'menu.home': '首頁',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': '分析頁',
'menu.dashboard.monitor': '監控頁',
'menu.dashboard.workplace': '工作臺',
'menu.form': '表單頁',
'menu.form.basicform': '基礎表單',
'menu.form.stepform': '分步表單',
'menu.form.stepform.info': '分步表單(填寫轉賬信息)',
'menu.form.stepform.confirm': '分步表單(確認轉賬信息)',
'menu.form.stepform.result': '分步表單(完成)',
'menu.form.advancedform': '高級表單',
'menu.list': '列表頁',
'menu.list.searchtable': '查詢表格',
'menu.list.basiclist': '標淮列表',
'menu.list.cardlist': '卡片列表',
'menu.list.searchlist': '搜索列表',
'menu.list.searchlist.articles': '搜索列表(文章)',
'menu.list.searchlist.projects': '搜索列表(項目)',
'menu.list.searchlist.applications': '搜索列表(應用)',
'menu.profile': '詳情頁',
'menu.profile.basic': '基礎詳情頁',
'menu.profile.advanced': '高級詳情頁',
'menu.result': '結果頁',
'menu.result.success': '成功頁',
'menu.result.fail': '失敗頁',
'menu.exception': '異常頁',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': '觸發錯誤',
'menu.account': '個人頁',
'menu.account.center': '個人中心',
'menu.account.settings': '個人設置',
'menu.account.trigger': '觸發報錯',
'menu.account.logout': '退出登錄',
'app.login.tab-login-credentials': '賬戶密碼登錄',
'app.login.tab-login-mobile': '手機號登錄',
'app.login.remember-me': '自動登錄',
'app.login.forgot-password': '忘記密碼',
'app.login.sign-in-with': '其他登錄方式',
'app.login.signup': '註冊賬戶',
'app.login.login': '登錄',
'app.register.register': '註冊',
'app.register.get-verification-code': '獲取驗證碼',
'app.register.sing-in': '使用已有賬戶登錄',
'app.register-result.msg': '妳的賬戶:{email} 註冊成功',
'app.register-result.activation-email':
'激活郵件已發送到妳的郵箱中,郵件有效期為24小時。請及時登錄郵箱,點擊郵件中的鏈接激活帳戶。',
'app.register-result.back-home': '返回首頁',
'app.register-result.view-mailbox': '查看郵箱',
'app.home.introduce': '介紹',
'app.analysis.test': '工專路 {no} 號店',
'app.analysis.introduce': '指標說明',
'app.analysis.total-sales': '總銷售額',
'app.analysis.day-sales': '日銷售額',
'app.analysis.visits': '訪問量',
'app.analysis.visits-trend': '訪問量趨勢',
'app.analysis.visits-ranking': '門店訪問量排名',
'app.analysis.day-visits': '日訪問量',
'app.analysis.week': '周同比',
'app.analysis.day': '日同比',
'app.analysis.payments': '支付筆數',
'app.analysis.conversion-rate': '轉化率',
'app.analysis.operational-effect': '運營活動效果',
'app.analysis.sales-trend': '銷售趨勢',
'app.analysis.sales-ranking': '門店銷售額排名',
'app.analysis.all-year': '全年',
'app.analysis.all-month': '本月',
'app.analysis.all-week': '本周',
'app.analysis.all-day': '今日',
'app.analysis.search-users': '搜索用戶數',
'app.analysis.per-capita-search': '人均搜索次數',
'app.analysis.online-top-search': '線上熱門搜索',
'app.analysis.the-proportion-of-sales': '銷售額類別占比',
'app.analysis.channel.all': '全部渠道',
'app.analysis.channel.online': '線上',
'app.analysis.channel.stores': '門店',
'app.analysis.sales': '銷售額',
'app.analysis.traffic': '客流量',
'app.analysis.table.rank': '排名',
'app.analysis.table.search-keyword': '搜索關鍵詞',
'app.analysis.table.users': '用戶數',
'app.analysis.table.weekly-range': '周漲幅',
'app.forms.basic.title': '基礎表單',
'app.forms.basic.description':
'表單頁用於向用戶收集或驗證信息,基礎表單常見於數據項較少的表單場景。',
'app.monitor.trading-activity': '活動實時交易情況',
'app.monitor.total-transactions': '今日交易總額',
'app.monitor.sales-target': '銷售目標完成率',
'app.monitor.remaining-time': '活動剩余時間',
'app.monitor.total-transactions-per-second': '每秒交易總額',
'app.monitor.activity-forecast': '活動情況預測',
'app.monitor.efficiency': '券核效率',
'app.monitor.ratio': '跳出率',
'app.monitor.proportion-per-category': '各品類占比',
'app.monitor.fast-food': '中式快餐',
'app.monitor.western-food': '西餐',
'app.monitor.hot-pot': '火鍋',
'app.monitor.waiting-for-implementation': 'Waiting for implementation',
'app.monitor.popular-searches': '熱門搜索',
'app.monitor.resource-surplus': '資源剩余',
'app.monitor.fund-surplus': '補貼資金剩余',
'app.settings.menuMap.basic': '基本設置',
'app.settings.menuMap.security': '安全設置',
'app.settings.menuMap.binding': '賬號綁定',
'app.settings.menuMap.notification': '新消息通知',
'app.settings.basic.avatar': '更換頭像',
'app.settings.basic.email': '郵箱',
'app.settings.basic.email-message': '請輸入您的郵箱!',
'app.settings.basic.nickname': '昵稱',
'app.settings.basic.nickname-message': '請輸入您的昵稱!',
'app.settings.basic.profile': '個人簡介',
'app.settings.basic.profile-message': '請輸入個人簡介!',
'app.settings.basic.profile-placeholder': '個人簡介',
'app.settings.basic.country': '國家/地區',
'app.settings.basic.country-message': '請輸入您的國家或地區!',
'app.settings.basic.geographic': '所在省市',
'app.settings.basic.geographic-message': '請輸入您的所在省市!',
'app.settings.basic.address': '街道地址',
'app.settings.basic.address-message': '請輸入您的街道地址!',
'app.settings.basic.phone': '聯系電話',
'app.settings.basic.phone-message': '請輸入您的聯系電話!',
'app.settings.basic.update': '更新基本信息',
'app.settings.security.strong': '強',
'app.settings.security.medium': '中',
'app.settings.security.weak': '弱',
'app.settings.security.password': '賬戶密碼',
'app.settings.security.password-description': '當前密碼強度:',
'app.settings.security.phone': '密保手機',
'app.settings.security.phone-description': '已綁定手機:',
'app.settings.security.question': '密保問題',
'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全',
'app.settings.security.email': '備用郵箱',
'app.settings.security.email-description': '已綁定郵箱:',
'app.settings.security.mfa': 'MFA 設備',
'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認',
'app.settings.security.modify': '修改',
'app.settings.security.set': '設置',
'app.settings.security.bind': '綁定',
'app.settings.binding.taobao': '綁定淘寶',
'app.settings.binding.taobao-description': '當前未綁定淘寶賬號',
'app.settings.binding.alipay': '綁定支付寶',
'app.settings.binding.alipay-description': '當前未綁定支付寶賬號',
'app.settings.binding.dingding': '綁定釘釘',
'app.settings.binding.dingding-description': '當前未綁定釘釘賬號',
'app.settings.binding.bind': '綁定',
'app.settings.notification.password': '賬戶密碼',
'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知',
'app.settings.notification.messages': '系統消息',
'app.settings.notification.messages-description': '系統消息將以站內信的形式通知',
'app.settings.notification.todo': '賬戶密碼',
'app.settings.notification.todo-description': '賬戶密碼',
'app.settings.open': '開',
'app.settings.close': '關',
'app.exception.back': '返回首頁',
'app.exception.description.403': '抱歉,妳無權訪問該頁面',
'app.exception.description.404': '抱歉,妳訪問的頁面不存在',
'app.exception.description.500': '抱歉,服務器出錯了',
'app.result.error.title': '提交失敗',
'app.result.error.description': '請核對並修改以下信息後,再重新提交。',
'app.result.error.hint-title': '您提交的內容有如下錯誤:',
'app.result.error.hint-text1': '您的賬戶已被凍結',
'app.result.error.hint-btn1': '立即解凍',
'app.result.error.hint-text2': '您的賬戶還不具備申請資格',
'app.result.error.hint-btn2': '立即升級',
'app.result.error.btn-text': '返回修改',
'app.result.success.title': '提交成功',
'app.result.success.description':
'提交結果頁用於反饋壹系列操作任務的處理結果, 如果僅是簡單操作,使用 Message 全局提示反饋即可。 本文字區域可以展示簡單的補充說明,如果有類似展示 “單據”的需求,下面這個灰色區域可以呈現比較復雜的內容。',
'app.result.success.operate-title': '項目名稱',
'app.result.success.operate-id': '項目 ID:',
'app.result.success.principal': '負責人:',
'app.result.success.operate-time': '生效時間:',
'app.result.success.step1-title': '創建項目',
'app.result.success.step1-operator': '曲麗麗',
'app.result.success.step2-title': '部門初審',
'app.result.success.step2-operator': '周毛毛',
'app.result.success.step2-extra': '催壹下',
'app.result.success.step3-title': '財務復核',
'app.result.success.step4-title': '完成',
'app.result.success.btn-return': '返回列表',
'app.result.success.btn-project': '查看項目',
'app.result.success.btn-print': '打印',
'app.setting.pagestyle': '整體風格設置',
'app.setting.pagestyle.dark': '暗色菜單風格',
'app.setting.pagestyle.light': '亮色菜單風格',
'app.setting.content-width': '內容區域寬度',
'app.setting.content-width.fixed': '定寬',
'app.setting.content-width.fluid': '流式',
'app.setting.themecolor': '主題色',
'app.setting.themecolor.dust': '薄暮',
'app.setting.themecolor.volcano': '火山',
'app.setting.themecolor.sunset': '日暮',
'app.setting.themecolor.cyan': '明青',
'app.setting.themecolor.green': '極光綠',
'app.setting.themecolor.daybreak': '拂曉藍(默認)',
'app.setting.themecolor.geekblue': '極客藍',
'app.setting.themecolor.purple': '醬紫',
'app.setting.navigationmode': '導航模式',
'app.setting.sidemenu': '側邊菜單布局',
'app.setting.topmenu': '頂部菜單布局',
'app.setting.fixedheader': '固定 Header',
'app.setting.fixedsidebar': '固定側邊菜單',
'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置',
'app.setting.hideheader': '下滑時隱藏 Header',
'app.setting.hideheader.hint': '固定 Header 時可配置',
'app.setting.othersettings': '其他設置',
'app.setting.weakmode': '色弱模式',
'app.setting.copy': '拷貝設置',
'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置',
'app.setting.production.hint':
'配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件',
};
================================================
FILE: src/models/article.js
================================================
import {
queryArticle,
delArticle,
updateArticle,
addArticle,
getArticleDetail,
changeComment,
changeThirdComment,
} from '@/services/api';
export default {
namespace: 'article',
state: {
articleList: [],
total: 0,
articleDetail: {
_id: '',
author: 'biaochenxuying',
category: [],
comments: [],
create_time: '',
desc: '',
id: 16,
img_url: '',
keyword: [],
like_users: [],
meta: { views: 0, likes: 0, comments: 0 },
origin: 0,
state: 1,
tags: [],
title: '',
update_time: '',
},
},
effects: {
*queryArticle({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryArticle, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveArticleList',
payload: response.data.list,
});
yield put({
type: 'saveArticleListTotal',
payload: response.data.count,
});
}
},
*delArticle({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delArticle, params);
!!resolve && resolve(response);
},
*addArticle({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addArticle, params);
!!resolve && resolve(response);
},
*updateArticle({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(updateArticle, params);
!!resolve && resolve(response);
},
*getArticleDetail({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(getArticleDetail, params);
!!resolve && resolve(response);
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveArticleDetail',
payload: response.data,
});
}
},
*changeComment({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(changeComment, params);
!!resolve && resolve(response);
},
*changeThirdComment({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(changeThirdComment, params);
!!resolve && resolve(response);
},
},
reducers: {
saveArticleList(state, { payload }) {
return {
...state,
articleList: payload,
};
},
saveArticleListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
saveArticleDetail(state, { payload }) {
return {
...state,
articleDetail: payload,
};
},
},
};
================================================
FILE: src/models/category.js
================================================
import { queryCategory, addCategory, delCategory } from '@/services/api';
export default {
namespace: 'category',
state: {
categoryList: [],
total: 0,
},
effects: {
*queryCategory({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryCategory, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveCategoryList',
payload: response.data.list,
});
yield put({
type: 'saveCategoryListTotal',
payload: response.data.count,
});
} else {
//
}
},
*addCategory({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addCategory, params);
!!resolve && resolve(response);
},
*delCategory({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delCategory, params);
!!resolve && resolve(response);
},
},
reducers: {
saveCategoryList(state, { payload }) {
return {
...state,
categoryList: payload,
};
},
saveCategoryListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
},
};
================================================
FILE: src/models/global.js
================================================
import { queryNotices } from '@/services/api';
export default {
namespace: 'global',
state: {
collapsed: false,
notices: [],
},
effects: {
*fetchNotices(_, { call, put }) {
const data = yield call(queryNotices);
yield put({
type: 'saveNotices',
payload: data,
});
yield put({
type: 'user/changeNotifyCount',
payload: data.length,
});
},
*clearNotices({ payload }, { put, select }) {
yield put({
type: 'saveClearedNotices',
payload,
});
const count = yield select(state => state.global.notices.length);
yield put({
type: 'user/changeNotifyCount',
payload: count,
});
},
},
reducers: {
changeLayoutCollapsed(state, { payload }) {
return {
...state,
collapsed: payload,
};
},
saveNotices(state, { payload }) {
return {
...state,
notices: payload,
};
},
saveClearedNotices(state, { payload }) {
return {
...state,
notices: state.notices.filter(item => item.type !== payload),
};
},
},
subscriptions: {
setup({ history }) {
// Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => {
if (typeof window.ga !== 'undefined') {
window.ga('send', 'pageview', pathname + search);
}
});
},
},
};
================================================
FILE: src/models/link.js
================================================
import { queryLink, addLink, updateLink,delLink } from '@/services/api';
export default {
namespace: 'link',
state: {
linkList: [],
total: 0,
},
effects: {
*queryLink({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryLink, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveLinkList',
payload: response.data.list,
});
yield put({
type: 'saveLinkListTotal',
payload: response.data.count,
});
} else {
//
}
},
*addLink({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addLink, params);
!!resolve && resolve(response);
},
*updateLink({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(updateLink, params);
!!resolve && resolve(response);
},
*delLink({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delLink, params);
!!resolve && resolve(response);
},
},
reducers: {
saveLinkList(state, { payload }) {
return {
...state,
linkList: payload,
};
},
saveLinkListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
},
};
================================================
FILE: src/models/list.js
================================================
import { queryFakeList, removeFakeList, addFakeList, updateFakeList } from '@/services/api';
export default {
namespace: 'list',
state: {
list: [],
},
effects: {
*fetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'queryList',
payload: Array.isArray(response) ? response : [],
});
},
*appendFetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'appendList',
payload: Array.isArray(response) ? response : [],
});
},
*submit({ payload }, { call, put }) {
let callback;
if (payload.id) {
callback = Object.keys(payload).length === 1 ? removeFakeList : updateFakeList;
} else {
callback = addFakeList;
}
const response = yield call(callback, payload); // post
yield put({
type: 'queryList',
payload: response,
});
},
},
reducers: {
queryList(state, action) {
return {
...state,
list: action.payload,
};
},
appendList(state, action) {
return {
...state,
list: state.list.concat(action.payload),
};
},
},
};
================================================
FILE: src/models/login.js
================================================
import { routerRedux } from 'dva/router';
import { stringify } from 'qs';
import { fakeAccountLogin, getFakeCaptcha, loginAdmin } from '@/services/api';
import { setAuthority } from '@/utils/authority';
import { getPageQuery } from '@/utils/utils';
import { reloadAuthorized } from '@/utils/Authorized';
export default {
namespace: 'login',
state: {
status: undefined,
},
effects: {
*loginAdmin({ payload }, { call, put }) {
const response = yield call(loginAdmin, payload);
if(!response){
return
}
if (response.code === 0) {
response.currentAuthority = response.data.name || 'admin';
response.status = 'ok';
response.type = 'account';
yield put({
type: 'changeLoginStatus',
payload: response,
});
}
// Login successfully
if (response.code === 0) {
reloadAuthorized();
const urlParams = new URL(window.location.href);
const params = getPageQuery();
console.log('params :', params);
let { redirect } = params;
if (redirect) {
const redirectUrlParams = new URL(redirect);
if (redirectUrlParams.origin === urlParams.origin) {
redirect = redirect.substr(urlParams.origin.length);
if (redirect.startsWith('/#')) {
redirect = redirect.substr(2);
}
} else {
window.location.href = redirect;
return;
}
}
console.log('redirect :', redirect);
yield put(routerRedux.replace(redirect || '/dashboard/workplace'));
}
},
*login({ payload }, { call, put }) {
const response = yield call(fakeAccountLogin, payload);
console.log('response :', response);
yield put({
type: 'changeLoginStatus',
payload: response,
});
// Login successfully
if (response.status === 'ok') {
reloadAuthorized();
const urlParams = new URL(window.location.href);
const params = getPageQuery();
let { redirect } = params;
console.log('redirect :', redirect);
if (redirect) {
const redirectUrlParams = new URL(redirect);
if (redirectUrlParams.origin === urlParams.origin) {
redirect = redirect.substr(urlParams.origin.length);
if (redirect.startsWith('/#')) {
redirect = redirect.substr(2);
}
} else {
window.location.href = redirect;
return;
}
}
console.log('redirect :', redirect);
yield put(routerRedux.replace(redirect || '/'));
}
},
*getCaptcha({ payload }, { call }) {
yield call(getFakeCaptcha, payload);
},
*logout(_, { put }) {
yield put({
type: 'changeLoginStatus',
payload: {
status: false,
currentAuthority: 'guest',
},
});
reloadAuthorized();
yield put(
routerRedux.push({
pathname: '/user/login',
search: stringify({
redirect: window.location.href,
}),
})
);
},
},
reducers: {
changeLoginStatus(state, { payload }) {
setAuthority(payload.currentAuthority);
return {
...state,
status: payload.status,
type: payload.type,
};
},
},
};
================================================
FILE: src/models/message.js
================================================
import { queryMessage, delMessage, getMessageDetail, addReplyMessage } from '@/services/api';
export default {
namespace: 'message',
state: {
messageList: [],
total: 0,
messageDetail: {
avatar: 'user',
content: '.....留言',
reply_list: [],
create_time: '2018-11-04T12:05:10.761Z',
email: '13800138000',
id: 15,
introduce: 'introduce',
name: '虚影',
phone: '1380013800',
state: 0,
update_time: '2018-11-04T12:05:10.761Z',
user_id: '5bd9a84c2758be723f5ef2cb',
__v: 0,
_id: '5bdee076bc454f49bba03ab0',
},
},
effects: {
*queryMessage({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryMessage, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveMessageList',
payload: response.data.list,
});
yield put({
type: 'saveMessageListTotal',
payload: response.data.count,
});
}
},
*delMessage({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delMessage, params);
!!resolve && resolve(response);
},
*addReplyMessage({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addReplyMessage, params);
!!resolve && resolve(response);
},
*getMessageDetail({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(getMessageDetail, params);
!!resolve && resolve(response);
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveMessageDetail',
payload: response.data,
});
}
},
},
reducers: {
saveMessageList(state, { payload }) {
return {
...state,
messageList: payload,
};
},
saveMessageListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
saveMessageDetail(state, { payload }) {
return {
...state,
messageDetail: payload,
};
},
},
};
================================================
FILE: src/models/otherUser.js
================================================
import { queryUser, addUser, updateUser,delUser } from '@/services/api';
export default {
namespace: 'otherUser',
state: {
userList: [],
total: 0,
},
effects: {
*queryUser({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryUser, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveUserList',
payload: response.data.list,
});
yield put({
type: 'saveUserListTotal',
payload: response.data.count,
});
} else {
//
}
},
*addUser({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addUser, params);
!!resolve && resolve(response);
},
*updateUser({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(updateUser, params);
!!resolve && resolve(response);
},
*delUser({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delUser, params);
!!resolve && resolve(response);
},
},
reducers: {
saveUserList(state, { payload }) {
return {
...state,
userList: payload,
};
},
saveUserListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
},
};
================================================
FILE: src/models/project.js
================================================
import { queryProjectNotice,queryProject, delProject, updateProject, addProject,getProjectDetail } from '@/services/api';
export default {
namespace: 'project',
state: {
notice: [],
projectList: [],
total: 0,
projectDetail: {
title: '',
state: '',
content: '',
_id: '',
},
},
effects: {
*fetchNotice(_, { call, put }) {
const response = yield call(queryProjectNotice);
yield put({
type: 'saveNotice',
payload: Array.isArray(response) ? response : [],
});
},
*queryProject({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryProject, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveProjectList',
payload: response.data.list,
});
yield put({
type: 'saveProjectListTotal',
payload: response.data.count,
});
}
},
*delProject({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delProject, params);
!!resolve && resolve(response);
},
*addProject({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addProject, params);
!!resolve && resolve(response);
},
*updateProject({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(updateProject, params);
!!resolve && resolve(response);
},
*getProjectDetail({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(getProjectDetail, params);
!!resolve && resolve(response);
if (response.code === 0) {
yield put({
type: 'saveProjectDetail',
payload: response.data,
});
}
},
},
reducers: {
saveNotice(state, action) {
return {
...state,
notice: action.payload,
};
},
saveProjectList(state, { payload }) {
return {
...state,
projectList: payload,
};
},
saveProjectListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
saveProjectDetail(state, { payload }) {
return {
...state,
projectDetail: payload,
};
},
},
};
================================================
FILE: src/models/setting.js
================================================
import { message } from 'antd';
import defaultSettings from '../defaultSettings';
let lessNodesAppended;
const updateTheme = primaryColor => {
// Don't compile less in production!
if (APP_TYPE !== 'site') {
return;
}
// Determine if the component is remounted
if (!primaryColor) {
return;
}
const hideMessage = message.loading('正在编译主题!', 0);
function buildIt() {
if (!window.less) {
return;
}
setTimeout(() => {
window.less
.modifyVars({
'@primary-color': primaryColor,
})
.then(() => {
hideMessage();
})
.catch(() => {
message.error('Failed to update theme');
hideMessage();
});
}, 200);
}
if (!lessNodesAppended) {
// insert less.js and color.less
const lessStyleNode = document.createElement('link');
const lessConfigNode = document.createElement('script');
const lessScriptNode = document.createElement('script');
lessStyleNode.setAttribute('rel', 'stylesheet/less');
lessStyleNode.setAttribute('href', '/color.less');
lessConfigNode.innerHTML = `
window.less = {
async: true,
env: 'production',
javascriptEnabled: true
};
`;
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js';
lessScriptNode.async = true;
lessScriptNode.onload = () => {
buildIt();
lessScriptNode.onload = null;
};
document.body.appendChild(lessStyleNode);
document.body.appendChild(lessConfigNode);
document.body.appendChild(lessScriptNode);
lessNodesAppended = true;
} else {
buildIt();
}
};
const updateColorWeak = colorWeak => {
document.body.className = colorWeak ? 'colorWeak' : '';
};
export default {
namespace: 'setting',
state: defaultSettings,
reducers: {
getSetting(state) {
const setting = {};
const urlParams = new URL(window.location.href);
Object.keys(state).forEach(key => {
if (urlParams.searchParams.has(key)) {
const value = urlParams.searchParams.get(key);
setting[key] = value === '1' ? true : value;
}
});
const { primaryColor, colorWeak } = setting;
if (state.primaryColor !== primaryColor) {
updateTheme(primaryColor);
}
updateColorWeak(colorWeak);
return {
...state,
...setting,
};
},
changeSetting(state, { payload }) {
const urlParams = new URL(window.location.href);
Object.keys(defaultSettings).forEach(key => {
if (urlParams.searchParams.has(key)) {
urlParams.searchParams.delete(key);
}
});
Object.keys(payload).forEach(key => {
if (key === 'collapse') {
return;
}
let value = payload[key];
if (value === true) {
value = 1;
}
if (defaultSettings[key] !== value) {
urlParams.searchParams.set(key, value);
}
});
const { primaryColor, colorWeak, contentWidth } = payload;
if (state.primaryColor !== primaryColor) {
updateTheme(primaryColor);
}
if (state.contentWidth !== contentWidth && window.dispatchEvent) {
window.dispatchEvent(new Event('resize'));
}
updateColorWeak(colorWeak);
window.history.replaceState(null, 'setting', urlParams.href);
return {
...state,
...payload,
};
},
},
};
================================================
FILE: src/models/tag.js
================================================
import { queryTag, addTag, delTag } from '@/services/api';
export default {
namespace: 'tag',
state: {
tagList: [],
total: 0,
},
effects: {
*queryTag({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryTag, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveTagList',
payload: response.data.list,
});
yield put({
type: 'saveTagListTotal',
payload: response.data.count,
});
} else {
//
}
},
*addTag({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addTag, params);
!!resolve && resolve(response);
},
*delTag({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delTag, params);
!!resolve && resolve(response);
},
},
reducers: {
saveTagList(state, { payload }) {
return {
...state,
tagList: payload,
};
},
saveTagListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
},
};
================================================
FILE: src/models/timeAxis.js
================================================
import { queryTimeAxis, delTimeAxis, updateTimeAxis, addTimeAxis,getTimeAxisDetail } from '@/services/api';
export default {
namespace: 'timeAxis',
state: {
timeAxisList: [],
total: 0,
timeAxisDetail: {
title: '',
state: '',
content: '',
_id: '',
},
},
effects: {
*queryTimeAxis({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(queryTimeAxis, params);
!!resolve && resolve(response); // 返回数据
// console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveTimeAxisList',
payload: response.data.list,
});
yield put({
type: 'saveTimeAxisListTotal',
payload: response.data.count,
});
}
},
*delTimeAxis({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delTimeAxis, params);
!!resolve && resolve(response);
},
*addTimeAxis({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(addTimeAxis, params);
!!resolve && resolve(response);
},
*updateTimeAxis({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(updateTimeAxis, params);
!!resolve && resolve(response);
},
*getTimeAxisDetail({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(getTimeAxisDetail, params);
!!resolve && resolve(response);
console.log('response :', response)
if (response.code === 0) {
yield put({
type: 'saveTimeAxisDetail',
payload: response.data,
});
}
},
},
reducers: {
saveTimeAxisList(state, { payload }) {
return {
...state,
timeAxisList: payload,
};
},
saveTimeAxisListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
saveTimeAxisDetail(state, { payload }) {
return {
...state,
timeAxisDetail: payload,
};
},
},
};
================================================
FILE: src/models/user.js
================================================
import { query as queryAdmin, queryCurrent } from '@/services/user';
export default {
namespace: 'user',
state: {
list: [],
currentUser: {},
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(queryAdmin);
yield put({
type: 'save',
payload: response,
});
},
*fetchCurrent(_, { call, put }) {
const response = yield call(queryCurrent);
yield put({
type: 'saveCurrentUser',
payload: response.data,
});
},
*delUser({ payload }, { call, put }) {
const { resolve, params } = payload;
const response = yield call(delUser, params);
!!resolve && resolve(response);
},
},
reducers: {
save(state, action) {
return {
...state,
list: action.payload,
};
},
saveCurrentUser(state, action) {
return {
...state,
currentUser: action.payload || {},
};
},
changeNotifyCount(state, action) {
return {
...state,
currentUser: {
...state.currentUser,
notifyCount: action.payload,
},
};
},
saveUserList(state, { payload }) {
return {
...state,
userList: payload,
};
},
saveUserListTotal(state, { payload }) {
return {
...state,
total: payload,
};
},
},
};
================================================
FILE: src/pages/404.js
================================================
import React from 'react';
import Link from 'umi/link';
import Exception from '@/components/Exception';
export default () => (
);
================================================
FILE: src/pages/Account/Settings/BaseView.js
================================================
import React, { Component, Fragment } from 'react';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { Form, Input, Upload, Select, Button } from 'antd';
import { connect } from 'dva';
import styles from './BaseView.less';
// import { getTimeDistance } from '@/utils/utils';
const FormItem = Form.Item;
const { Option } = Select;
// 头像组件 方便以后独立,增加裁剪之类的功能
const AvatarView = ({ avatar }) => (
Avatar
);
@connect(({ user }) => ({
currentUser: user.currentUser,
}))
@Form.create()
class BaseView extends Component {
componentDidMount() {
this.setBaseInfo();
}
setBaseInfo = () => {
const { currentUser, form } = this.props;
Object.keys(form.getFieldsValue()).forEach(key => {
const obj = {};
obj[key] = currentUser[key] || null;
form.setFieldsValue(obj);
});
};
getAvatarURL() {
const { currentUser } = this.props;
if (currentUser.avatar) {
return currentUser.avatar;
}
const url = 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
return url;
}
getViewDom = ref => {
this.view = ref;
};
render() {
const {
form: { getFieldDecorator },
} = this.props;
return (
);
}
}
export default BaseView;
================================================
FILE: src/pages/Account/Settings/BaseView.less
================================================
@import '~antd/lib/style/themes/default.less';
.baseView {
display: flex;
padding-top: 12px;
.left {
max-width: 448px;
min-width: 224px;
}
.right {
flex: 1;
padding-left: 104px;
.avatar_title {
height: 22px;
font-size: @font-size-base;
color: @heading-color;
line-height: 22px;
margin-bottom: 8px;
}
.avatar {
width: 144px;
height: 144px;
margin-bottom: 12px;
overflow: hidden;
img {
width: 100%;
}
}
.button_view {
width: 144px;
text-align: center;
}
}
}
@media screen and (max-width: @screen-xl) {
.baseView {
flex-direction: column-reverse;
.right {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
max-width: 448px;
.avatar_title {
display: none;
}
}
}
}
================================================
FILE: src/pages/Account/Settings/Info.js
================================================
import React, { Component } from 'react';
import { connect } from 'dva';
import router from 'umi/router';
import { FormattedMessage } from 'umi/locale';
import { Menu } from 'antd';
import GridContent from '@/components/PageHeaderWrapper/GridContent';
import styles from './Info.less';
const { Item } = Menu;
@connect(({ user }) => ({
currentUser: user.currentUser,
}))
class Info extends Component {
constructor(props) {
super(props);
const { match, location } = props;
const menuMap = {
base: ,
personalLink: (
),
};
const key = location.pathname.replace(`${match.path}/`, '');
this.state = {
mode: 'inline',
menuMap,
selectKey: menuMap[key] ? key : 'base',
};
}
static getDerivedStateFromProps(props, state) {
const { match, location } = props;
let selectKey = location.pathname.replace(`${match.path}/`, '');
selectKey = state.menuMap[selectKey] ? selectKey : 'base';
if (selectKey !== state.selectKey) {
return { selectKey };
}
return null;
}
componentDidMount() {
window.addEventListener('resize', this.resize);
this.resize();
}
componentWillUnmount() {
window.removeEventListener('resize', this.resize);
}
getmenu = () => {
const { menuMap } = this.state;
return Object.keys(menuMap).map(item => - {menuMap[item]}
);
};
getRightTitle = () => {
const { selectKey, menuMap } = this.state;
return menuMap[selectKey];
};
selectKey = ({ key }) => {
router.push(`/account/settings/${key}`);
this.setState({
selectKey: key,
});
};
resize = () => {
if (!this.main) {
return;
}
requestAnimationFrame(() => {
let mode = 'inline';
const { offsetWidth } = this.main;
if (this.main.offsetWidth < 641 && offsetWidth > 400) {
mode = 'horizontal';
}
if (window.innerWidth < 768 && offsetWidth > 400) {
mode = 'horizontal';
}
this.setState({
mode,
});
});
};
render() {
const { children, currentUser } = this.props;
if (!currentUser.userid) {
return '';
}
const { mode, selectKey } = this.state;
return (
{
this.main = ref;
}}
>
{this.getmenu()}
{this.getRightTitle()}
{children}
);
}
}
export default Info;
================================================
FILE: src/pages/Account/Settings/Info.less
================================================
@import '~antd/lib/style/themes/default.less';
.main {
width: 100%;
height: 100%;
background-color: @body-background;
display: flex;
padding-top: 16px;
padding-bottom: 16px;
overflow: auto;
.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-left: 40px;
padding-right: 40px;
padding-top: 8px;
padding-bottom: 8px;
.title {
font-size: 20px;
color: @heading-color;
line-height: 28px;
font-weight: 500;
margin-bottom: 12px;
}
}
:global {
.ant-list-split .ant-list-item:last-child {
border-bottom: 1px solid #e8e8e8;
}
.ant-list-item {
padding-top: 14px;
padding-bottom: 14px;
}
}
}
:global {
.ant-list-item-meta {
// 账号绑定图标
.taobao {
color: #ff4000;
display: block;
font-size: 48px;
line-height: 48px;
border-radius: @border-radius-base;
}
.dingding {
background-color: #2eabff;
color: #fff;
font-size: 32px;
line-height: 32px;
padding: 6px;
margin: 2px;
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;
}
}
}
================================================
FILE: src/pages/Account/Settings/PersonalLinkView.js
================================================
import React, { Component, Fragment } from 'react';
import { formatMessage } from 'umi/locale';
import { Switch, List, Button, Icon, Modal, Input } from 'antd';
const confirm = Modal.confirm;
class ModelLink extends Component {
constructor(props) {
super(props);
}
render() {
const param = this.props.stateParam;
return (
);
}
}
class NotificationView extends Component {
constructor(props) {
super(props);
this.state = {
name: '',
icon: '',
url: '',
desc: '',
visible: false,
confirmLoading: false,
};
this.handleChange = this.handleChange.bind(this);
this.showModal = this.showModal.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.showDeleteConfirm = this.showDeleteConfirm.bind(this);
}
handleChange(event) {
console.log('event.target.name:', event.target.name);
console.log('event.target.value:', event.target.value);
this.setState({
[event.target.name]: event.target.value,
});
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = () => {
this.setState({
ModalText: 'The modal will be closed after two seconds',
confirmLoading: true,
});
setTimeout(() => {
this.setState({
visible: false,
confirmLoading: false,
});
}, 2000);
};
handleCancel = () => {
console.log('Clicked cancel button');
this.setState({
visible: false,
});
};
showDeleteConfirm = () => {
confirm({
title: 'Are you sure delete this task?',
content: 'Some descriptions',
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
onOk() {
console.log('OK');
},
onCancel() {
console.log('Cancel');
},
});
};
getData = () => {
const Action = (
修改
删除
);
return [
{
title: 'github',
icon: 'github',
description: 'github 链接',
url: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
actions: [Action],
},
{
title: '微信',
icon: 'wechat',
description: '微信 链接',
url: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
actions: [Action],
},
{
title: 'segmentFault',
icon: 'github',
description: 'segmentFault 链接',
url: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
actions: [Action],
},
];
};
render() {
return (
添加
(
{item.url}
)}
/>
);
}
}
export default NotificationView;
================================================
FILE: src/pages/Article/ArticleComponent.js
================================================
import React from 'react';
import { Input, Modal, Select, notification } from 'antd';
import { connect } from 'dva';
@connect(({ article, tag, category }) => ({
article,
tag,
category,
}))
class ArticleComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
keywordCom: '',
pageNum: 1,
pageSize: 50,
};
this.handleSearchTag = this.handleSearchTag.bind(this);
this.handleSearchCategory = this.handleSearchCategory.bind(this);
}
componentDidMount() {
this.handleSearchTag();
this.handleSearchCategory();
}
handleSearchTag = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keywordCom,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'tag/queryTag',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleSearchCategory = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'category/queryCategory',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
render() {
const { tagList } = this.props.tag;
const { categoryList } = this.props.category;
const children = [];
const categoryChildren = [];
for (let i = 0; i < tagList.length; i++) {
const e = tagList[i];
children.push(
{e.name}
);
}
for (let i = 0; i < categoryList.length; i++) {
const e = categoryList[i];
categoryChildren.push(
{e.name}
);
}
const { articleDetail } = this.props.article;
const { changeType } = this.props;
let originDefault = '原创';
let stateDefault = '发布'; // 文章发布状态 => 0 草稿,1 发布
let typeDefault = '普通文章'; // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
let categoryDefault = [];
let tagsDefault = [];
if (changeType) {
originDefault = articleDetail.origin === 0 ? '原创' : '';
stateDefault = articleDetail.state ? '已发布' : '草稿';
typeDefault =
articleDetail.type === 1 ? '普通文章' : articleDetail.type === 2 ? '简历' : '管理员介绍';
categoryDefault = this.props.categoryDefault;
tagsDefault = this.props.tagsDefault;
} else {
originDefault = '原创';
stateDefault = '发布'; // 文章发布状态 => 0 草稿,1 发布
categoryDefault = [];
tagsDefault = [];
}
// console.log('originDefault :', originDefault)
// console.log('stateDefault :', stateDefault)
// console.log('categoryDefault :', categoryDefault)
// console.log('tagsDefault :', tagsDefault)
const { TextArea } = Input;
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
{/* 0 草稿,1 发布 */}
草稿
发布
{/* 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍 */}
普通文章
简历
管理员介绍
{/* 0 原创,1 转载,2 混合 */}
原创
转载
混合
{children}
{categoryChildren}
);
}
}
export default ArticleComponent;
================================================
FILE: src/pages/Article/ArticleCreate.js
================================================
import React from 'react';
import { Input, Select, Button, notification } from 'antd';
import { connect } from 'dva';
import SimpleMDE from 'simplemde';
import marked from 'marked';
import highlight from 'highlight.js';
import 'simplemde/dist/simplemde.min.css';
import './style.less';
@connect(({ article, tag, category }) => ({
article,
tag,
category,
}))
class ArticleCreate extends React.Component {
constructor(props) {
super(props);
this.state = {
smde: null,
loading: false,
keywordCom: '',
pageNum: 1,
pageSize: 50,
changeType: false,
title: '',
author: 'biaochenxuying',
keyword: '',
content: '',
desc: '',
img_url: '',
origin: 0, // 0 原创,1 转载,2 混合
state: 1, // 文章发布状态 => 0 草稿,1 已发布
type: 1, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
tags: '',
category: '',
tagsDefault: [],
categoryDefault: [],
};
this.handleSearchTag = this.handleSearchTag.bind(this);
this.handleSearchCategory = this.handleSearchCategory.bind(this);
this.getSmdeValue = this.getSmdeValue.bind(this);
this.setSmdeValue = this.setSmdeValue.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleCategoryChange = this.handleCategoryChange.bind(this);
this.handleChangeState = this.handleChangeState.bind(this);
this.handleTagChange = this.handleTagChange.bind(this);
this.handleChangeOrigin = this.handleChangeOrigin.bind(this);
this.handleChangeType = this.handleChangeType.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.handleSearchTag();
this.handleSearchCategory();
this.state.smde = new SimpleMDE({
element: document.getElementById('editor').childElementCount,
autofocus: true,
autosave: true,
previewRender(plainText) {
return marked(plainText, {
renderer: new marked.Renderer(),
gfm: true,
pedantic: false,
sanitize: false,
tables: true,
breaks: true,
smartLists: true,
smartypants: true,
highlight(code) {
return highlight.highlightAuto(code).value;
},
});
},
});
}
handleSubmit() {
const { dispatch } = this.props;
const { articleDetail } = this.props.article;
if(!this.state.title){
notification.error({
message: "文章标题不能为空",
});
return
}
if(!this.state.keyword){
notification.error({
message: "文章关键字不能为空",
});
return
}
if(!this.state.smde.value()){
notification.error({
message: "文章内容不能为空",
});
return
}
let keyword = this.state.keyword;
if (keyword instanceof Array) {
keyword = keyword.join(',');
}
this.setState({
loading: true,
});
// 修改
if (this.state.changeType) {
const params = {
id: articleDetail._id,
title: this.state.title,
author: this.state.author,
desc: this.state.desc,
keyword,
content: this.state.content,
img_url: this.state.img_url,
origin: this.state.origin,
state: this.state.state,
type: this.state.type,
tags: this.state.tags,
category: this.state.category,
};
new Promise(resolve => {
dispatch({
type: 'article/updateArticle',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
changeType: false,
title: '',
author: 'biaochenxuying',
keyword: '',
content: '',
desc: '',
img_url: '',
origin: 0, // 0 原创,1 转载,2 混合
state: 1, // 文章发布状态 => 0 草稿,1 已发布
type: 1, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
tags: '',
category: '',
tagsDefault: [],
categoryDefault: [],
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
} else {
// 添加
const params = {
title: this.state.title,
author: this.state.author,
desc: this.state.desc,
keyword: this.state.keyword,
content: this.state.smde.value(),
img_url: this.state.img_url,
origin: this.state.origin,
state: this.state.state,
type: this.state.type,
tags: this.state.tags,
category: this.state.category,
};
new Promise(resolve => {
dispatch({
type: 'article/addArticle',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
loading: false,
chnageType: false,
});
// this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
}
}
getSmdeValue() {
// console.log('this.state.smde.value() :', this.state.smde.value());
return this.state.smde.value();
}
setSmdeValue(value) {
this.state.smde.value(value);
}
handleChange(event) {
this.setState({
[event.target.name]: event.target.value,
});
}
handleTagChange(value) {
const tags = value.join();
console.log('tags :', tags);
this.setState({
tagsDefault: value,
tags,
});
}
handleCategoryChange(value) {
const category = value.join();
console.log('category :', category);
this.setState({
categoryDefault: value,
category,
});
}
handleChangeState(value) {
this.setState({
state: value,
});
}
handleChangeOrigin(value) {
this.setState({
origin: value,
});
}
handleChangeType(value) {
console.log('type :', value);
this.setState({
type: value,
});
}
handleSearchTag = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keywordCom,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'tag/queryTag',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleSearchCategory = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'category/queryCategory',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
render() {
const { tagList } = this.props.tag;
const { categoryList } = this.props.category;
const children = [];
const categoryChildren = [];
for (let i = 0; i < tagList.length; i++) {
const e = tagList[i];
children.push(
{e.name}
,
);
}
for (let i = 0; i < categoryList.length; i++) {
const e = categoryList[i];
categoryChildren.push(
{e.name}
,
);
}
// const { articleDetail } = this.props.article;
// const { changeType } = this.props;
let originDefault = '原创';
let stateDefault = '发布'; // 文章发布状态 => 0 草稿,1 发布
const typeDefault = '普通文章'; // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
let categoryDefault = [];
let tagsDefault = [];
// if (changeType) {
// originDefault = articleDetail.origin === 0 ? '原创' : '';
// stateDefault = articleDetail.state ? '已发布' : '草稿';
// typeDefault = articleDetail.type === 1 ? '普通文章' : articleDetail.type === 2 ? '简历' : '管理员介绍';
// categoryDefault = this.props.categoryDefault;
// tagsDefault = this.props.tagsDefault;
// } else {
originDefault = '原创';
stateDefault = '发布'; // 文章发布状态 => 0 草稿,1 发布
categoryDefault = [];
tagsDefault = [];
// }
const normalCenter = {
textAlign: 'center',
marginBottom: 10,
};
return (
);
}
}
export default ArticleCreate;
================================================
FILE: src/pages/Article/CommentsComponent.js
================================================
import React from 'react';
import { Input, Modal, Select, notification, Comment, Avatar, Tag } from 'antd';
import { connect } from 'dva';
@connect(({ article }) => ({
article,
}))
class CommentsComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
loading: false,
keywordCom: '',
pageNum: 1,
pageSize: 50,
};
this.handleChangeState = this.handleChangeState.bind(this);
}
componentDidMount() {}
handleChangeState = (value, type, index, item) => {
// console.log('value', value)
// console.log('type', type)
// console.log('index', index)
// console.log('item', item)
this.setState({
loading: true,
});
const { dispatch } = this.props;
if (type === 1) {
const params = {
id: item._id,
state: parseInt(value),
};
new Promise(resolve => {
dispatch({
type: 'article/changeComment',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
this.props.getArticleDetail();
notification.success({
message: res.message,
});
} else {
notification.error({
message: res.message,
});
}
});
} else {
const params = {
id: item._id,
state: parseInt(value),
index: index,
};
new Promise(resolve => {
dispatch({
type: 'article/changeThirdComment',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
this.props.getArticleDetail();
notification.success({
message: res.message,
});
} else {
notification.error({
message: res.message,
});
}
});
}
};
render() {
const { articleDetail } = this.props.article;
const ExampleComment = ({ item, children }) => (
{item.user.name}}
avatar={ }
content={
{' '}
{item.to_user ? '@' + item.to_user.name + ': ' : ''} {item.content}
}
>
{children}
);
let list = [];
let length = articleDetail.comments.length;
for (let i = 0; i < length; i++) {
const e = articleDetail.comments[i];
let defaultValue = '';
if (e.state === 0) {
defaultValue = '待审核';
} else if (e.state === 1) {
defaultValue = '正常通过';
} else if (e.state === -1) {
defaultValue = '删除';
} else if (e.state === -2) {
defaultValue = '垃圾评论';
}
const actions = [
{e.is_handle === 2 ? (
未处理过
) : (
已经处理过
)}
{
this.handleChangeState(value, 1, i, e);
}}
>
{/* 状态 => 0 待审核 / 1 正常通过 / -1 已删除 / -2 垃圾评论 */}
待审核
正常通过
删除
垃圾评论
,
];
e.actions = actions;
let len = e.other_comments.length;
if (len) {
let arr = [];
for (let i = 0; i < len; i++) {
let item = e.other_comments[i];
let defaultValue = '';
if (item.state === 0) {
defaultValue = '待审核';
} else if (item.state === 1) {
defaultValue = '正常通过';
} else if (item.state === -1) {
defaultValue = '删除';
} else if (item.state === -2) {
defaultValue = '垃圾评论';
}
const actions2 = [
{
this.handleChangeState(value, 2, i, e);
}}
>
{/* 状态 => 0 待审核 / 1 通过正常 / -1 已删除 / -2 垃圾评论 */}
待审核
通过
删除
垃圾评论
,
];
item.actions = actions2;
arr.push( );
}
list.push(
{arr}
);
} else {
list.push( );
}
}
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
{articleDetail.title}
{/* {articleDetail.desc}
*/}
{list.length ? list :
暂无评论!
}
);
}
}
export default CommentsComponent;
================================================
FILE: src/pages/Article/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import domain from '@/utils/domain.js';
import {
Row,
Col,
Card,
Form,
Input,
Button,
Table,
notification,
Popconfirm,
Divider,
Tag,
Select,
Avatar,
} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import ArticleComponent from './ArticleComponent';
import CommentsComponent from './CommentsComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ article }) => ({
article,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
changeType: false,
title: '',
author: 'biaochenxuying',
keyword: '',
content: '',
desc: '',
img_url: '',
origin: 0, // 0 原创,1 转载,2 混合
state: 1, // 文章发布状态 => 0 草稿,1 已发布
type: 1, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
tags: '',
category: '',
tagsDefault: [],
categoryDefault: [],
searchState: '', // 文章发布状态 => 0 草稿,1 已发布,'' 代表所有文章
searchKeyword: '',
visible: false,
article_id: '',
commentsVisible: false,
loading: false,
pageNum: 1,
pageSize: 10,
columns: [
{
title: '标题',
width: 120,
dataIndex: 'title',
},
{
title: '作者',
width: 80,
dataIndex: 'author',
},
{
title: '关键字',
width: 80,
dataIndex: 'keyword',
render: arr => (
{arr.map(item => (
{item}
))}
),
},
{
title: '封面图',
width: 50,
dataIndex: 'img_url',
render: val => ,
},
{
title: '标签',
dataIndex: 'tags',
width: 60,
render: arr => (
{arr.map(item => (
{item.name}
))}
),
},
{
title: '分类',
dataIndex: 'category',
width: 70,
render: arr => (
{arr.map(item => (
{item.name}
))}
),
},
{
title: '状态',
dataIndex: 'state',
width: 70,
render: val => {
// 文章发布状态 => 0 草稿,1 已发布
if (val === 0) {
return 草稿 ;
}
if (val === 1) {
return 已发布 ;
}
},
},
{
title: '评论是否处理过',
dataIndex: 'comments',
width: 50,
render: comments => {
// console.log('comments',comments)
let flag = 1;
let length = comments.length;
if (length) {
for (let i = 0; i < length; i++) {
flag = comments[i].is_handle;
}
}
// 新添加的评论 是否已经处理过 => 1 是 / 2 否
if (flag === 2) {
return 否 ;
}
return 是 ;
},
},
{
title: '观看/点赞/评论',
width: 120,
dataIndex: 'meta',
render: val => (
{val.views} | {val.likes} | {val.comments}
),
},
{
title: '原创状态',
dataIndex: 'origin',
width: 50,
render: val => {
// 文章转载状态 => 0 原创,1 转载,2 混合
if (val === 0) {
return 原创 ;
}
if (val === 1) {
return 转载 ;
}
return 混合 ;
},
},
{
title: '创建时间',
dataIndex: 'create_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
width: 220,
render: (text, record) => (
),
},
],
};
this.handleChangeSearchKeyword = this.handleChangeSearchKeyword.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showModal = this.showModal.bind(this);
this.showCommentModal = this.showCommentModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleCommentsCancel = this.handleCommentsCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleChangeSearchState = this.handleChangeSearchState.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getArticleDetail = this.getArticleDetail.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleChangeContent = this.handleChangeContent.bind(this);
this.handleChangeState = this.handleChangeState.bind(this);
this.handleChangeType = this.handleChangeType.bind(this);
this.handleChangeOrigin = this.handleChangeOrigin.bind(this);
this.handleTagChange = this.handleTagChange.bind(this);
this.handleChangeAuthor = this.handleChangeAuthor.bind(this);
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleChangeDesc = this.handleChangeDesc.bind(this);
this.handleChangeImgUrl = this.handleChangeImgUrl.bind(this);
this.handleCategoryChange = this.handleCategoryChange.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
handleSubmit() {
const { dispatch } = this.props;
const { articleDetail } = this.props.article;
if (!this.state.title) {
notification.error({
message: '文章标题不能为空',
});
return;
}
if (!this.state.keyword) {
notification.error({
message: '文章关键字不能为空',
});
return;
}
if (!this.state.content) {
notification.error({
message: '文章内容不能为空',
});
return;
}
if (keyword instanceof Array) {
keyword = keyword.join(',');
}
this.setState({
loading: true,
});
let keyword = this.state.keyword;
if (keyword instanceof Array) {
keyword = keyword.join(',');
}
if (this.state.changeType) {
const params = {
id: articleDetail._id,
title: this.state.title,
author: this.state.author,
desc: this.state.desc,
keyword,
content: this.state.content,
img_url: this.state.img_url,
origin: this.state.origin,
state: this.state.state,
type: this.state.type,
tags: this.state.tags,
category: this.state.category,
};
new Promise(resolve => {
dispatch({
type: 'article/updateArticle',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
changeType: false,
title: '',
author: 'biaochenxuying',
keyword: '',
content: '',
desc: '',
img_url: '',
origin: 0, // 0 原创,1 转载,2 混合
state: 1, // 文章发布状态 => 0 草稿,1 已发布
type: 1, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
tags: '',
category: '',
tagsDefault: [],
categoryDefault: [],
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
} else {
const params = {
title: this.state.title,
author: this.state.author,
desc: this.state.desc,
keyword: this.state.keyword,
content: this.state.content,
img_url: this.state.img_url,
origin: this.state.origin,
state: this.state.state,
type: this.state.type,
tags: this.state.tags,
category: this.state.category,
};
new Promise(resolve => {
dispatch({
type: 'article/addArticle',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
chnageType: false,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
}
}
handleChange(event) {
this.setState({
title: event.target.value,
});
}
handleChangeAuthor(event) {
this.setState({
author: event.target.value,
});
}
handleChangeContent(event) {
this.setState({
content: event.target.value,
});
}
handleChangeImgUrl(event) {
this.setState({
img_url: event.target.value,
});
}
handleChangeKeyword(event) {
this.setState({
keyword: event.target.value,
});
}
handleChangeOrigin(value) {
this.setState({
origin: value,
});
}
handleChangeDesc(event) {
this.setState({
desc: event.target.value,
});
}
handleChangeType(value) {
console.log('type :', value);
this.setState({
type: value,
});
}
handleTagChange(value) {
const tags = value.join();
console.log('tags :', tags);
this.setState({
tagsDefault: value,
tags,
});
}
handleCategoryChange(value) {
const category = value.join();
console.log('category :', category);
this.setState({
categoryDefault: value,
category,
});
}
handleChangeState(value) {
this.setState({
state: value,
});
}
handleChangeSearchState(searchState) {
this.setState(
{
searchState,
},
() => {
this.handleSearch();
}
);
}
handleChangeSearchKeyword(event) {
this.setState({
searchKeyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
}
);
}
getArticleDetail(callback) {
const { dispatch } = this.props;
const params = {
id: this.state.article_id,
filter: 2, // 文章的评论过滤 => 1: 过滤,2: 不过滤
};
new Promise(resolve => {
dispatch({
type: 'article/getArticleDetail',
payload: {
resolve,
params,
},
});
}).then(res => {
callback ? callback() : null;
// console.log('callback',callback)
});
}
showCommentModal = record => {
console.log('record._id:', record._id);
if (!record._id) {
return;
}
this.setState(
{
article_id: record._id,
},
() => {
this.getArticleDetail(e => {
this.setState({
commentsVisible: true,
});
});
}
);
};
showModal = record => {
if (record._id) {
const { dispatch } = this.props;
const params = {
id: record._id,
filter: 2, // 文章的评论过滤 => 1: 过滤,2: 不过滤
};
new Promise(resolve => {
dispatch({
type: 'article/getArticleDetail',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res)
const tagsArr = [];
if (res.data.tags.length) {
for (let i = 0; i < res.data.tags.length; i++) {
const e = res.data.tags[i];
tagsArr.push(e._id);
}
}
const tags = tagsArr.length ? tagsArr.join() : '';
const categoryArr = [];
if (res.data.category.length) {
for (let i = 0; i < res.data.category.length; i++) {
const e = res.data.category[i];
categoryArr.push(e._id);
}
}
const category = categoryArr.length ? categoryArr.join() : '';
console.log('tagsArr :', tagsArr);
console.log('categoryArr :', categoryArr);
if (res.code === 0) {
this.setState({
visible: true,
changeType: true,
title: res.data.title,
content: res.data.content,
state: res.data.state,
author: res.data.author,
keyword: res.data.keyword,
desc: res.data.desc,
img_url: res.data.img_url,
origin: res.data.origin, // 0 原创,1 转载,2 混合
tags,
category,
tagsDefault: tagsArr,
categoryDefault: categoryArr,
});
} else {
notification.error({
message: res.message,
});
}
});
} else {
this.setState({
visible: true,
changeType: false,
title: '',
author: 'biaochenxuying',
keyword: '',
content: '',
desc: '',
img_url: '',
origin: 0, // 0 原创,1 转载,2 混合
state: 1, // 文章发布状态 => 0 草稿,1 已发布
type: 1, // 文章类型 => 1: 普通文章,2: 简历,3: 管理员介绍
tags: '',
category: '',
});
}
};
handleOk = () => {
this.handleSubmit();
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleCommentsCancel = e => {
this.setState({
commentsVisible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.searchKeyword,
state: this.state.searchState,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'article/queryArticle',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'article/delArticle',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { articleList, total } = this.props.article;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={articleList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/Article/style.less
================================================
// .editor-toolbar.fullscreen {
// z-index: 99;
// }
// .CodeMirror-fullscreen {
// z-index: 90;
// }
// .editor-preview-side {
// z-index: 99;
// }
/*对 markdown 样式的补充*/
pre {
display: block;
padding: 10px;
margin: 0 0 10px;
font-size: 14px;
line-height: 1.42857143;
color: #abb2bf;
background: #282c34;
word-break: break-all;
word-wrap: break-word;
overflow: auto;
}
.editor-preview pre, .editor-preview-side pre{
background: #282c34;
}
h1,h2,h3,h4,h5,h6{
margin-top: 1em;
/* margin-bottom: 1em; */
}
strong {
font-weight: bold;
}
p > code:not([class]) {
padding: 2px 4px;
font-size: 90%;
color: #c7254e;
background-color: #f9f2f4;
border-radius: 4px;
}
p img{
/* 图片居中 */
margin: 0 auto;
display: flex;
}
.editor-preview-side {
font-family: "Microsoft YaHei", 'sans-serif';
font-size: 16px;
line-height: 30px;
}
.editor-preview-side .desc ul,.editor-preview-side .desc ol {
color: #333333;
margin: 1.5em 0 0 25px;
}
.editor-preview-side .desc h1, .editor-preview-side .desc h2 {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.editor-preview-side .desc a {
color: #009a61;
}
================================================
FILE: src/pages/Authorized.js
================================================
import React from 'react';
import RenderAuthorized from '@/components/Authorized';
import { getAuthority } from '@/utils/authority';
import Redirect from 'umi/redirect';
const Authority = getAuthority();
const Authorized = RenderAuthorized(Authority);
export default ({ children }) => (
}>
{children}
);
================================================
FILE: src/pages/Category/CategoryComponent.js
================================================
import React from 'react';
import { Input, Modal } from 'antd';
class LinkComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
render() {
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
);
}
}
export default LinkComponent;
================================================
FILE: src/pages/Category/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import { Row, Col, Card, Form, Input, Button, Table, notification, Popconfirm, Switch, Tag, Select } from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import CategoryComponent from './CategoryComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ category }) => ({
category,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
keyword: '',
pageNum: 1,
pageSize: 10,
name: '',
desc: '',
columns: [
{
title: '分类名',
dataIndex: 'name',
},
{
title: '描述',
dataIndex: 'desc',
},
{
title: '创建时间',
dataIndex: 'create_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
render: (text, record) => (
this.handleDelete(text, record)}>
Delete
),
},
],
};
this.handleChange = this.handleChange.bind(this);
this.handleDescChange = this.handleDescChange.bind(this);
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showModal = this.showModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
handleChange(event) {
this.setState({
name: event.target.value,
});
}
handleDescChange(event) {
this.setState({
desc: event.target.value,
});
}
handleChangeKeyword(event) {
this.setState({
keyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
},
);
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = () => {
const { dispatch } = this.props;
const params = {
name: this.state.name,
desc: this.state.desc,
};
new Promise(resolve => {
dispatch({
type: 'category/addCategory',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
name: '',
desc: '',
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'category/queryCategory',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'category/delCategory',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { categoryList, total } = this.props.category;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={categoryList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/Dashboard/Workplace.js
================================================
import React, { PureComponent } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import { Row, Col, List, Avatar } from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './Workplace.less';
@connect(({ user,activities, loading }) => ({
currentUser: user.currentUser,
activities,
currentUserLoading: loading.effects['user/fetchCurrent'],
}))
class Workplace extends PureComponent {
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'user/fetchCurrent',
});
// dispatch({
// type: 'activities/fetchList',
// });
}
componentWillUnmount() {
}
renderActivities() {
const {
activities: { list },
} = this.props;
return list.map(item => {
const events = item.template.split(/@\{([^{}]*)\}/gi).map(key => {
if (item[key]) {
return (
{item[key].name}
);
}
return key;
});
return (
}
title={
{item.user.name}
{events}
}
description={
{moment(item.updatedAt).fromNow()}
}
/>
);
});
}
render() {
const {
currentUser,
currentUserLoading,
} = this.props;
const pageHeaderContent =
currentUser && Object.keys(currentUser).length ? (
早安,
{currentUser.name}
,祝你开心每一天!
{currentUser.title} |{currentUser.group}
) : null;
const extraContent = (
);
return (
内容
);
}
}
export default Workplace;
================================================
FILE: src/pages/Dashboard/Workplace.less
================================================
@import '~antd/lib/style/themes/default.less';
@import '~@/utils/utils.less';
.pageHeaderContent {
display: flex;
.avatar {
flex: 0 1 72px;
margin-bottom: 8px;
& > span {
border-radius: 72px;
display: block;
width: 72px;
height: 72px;
}
}
.content {
position: relative;
top: 4px;
margin-left: 24px;
flex: 1 1 auto;
color: @text-color-secondary;
line-height: 22px;
.contentTitle {
font-size: 20px;
line-height: 28px;
font-weight: 500;
color: @heading-color;
margin-bottom: 12px;
}
}
}
.extraContent {
.clearfix();
float: right;
white-space: nowrap;
.statItem {
padding: 0 32px;
position: relative;
display: inline-block;
> p:first-child {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
margin-bottom: 4px;
}
> p {
color: @heading-color;
font-size: 30px;
line-height: 38px;
margin: 0;
> span {
color: @text-color-secondary;
font-size: 20px;
}
}
&:after {
background-color: @border-color-split;
position: absolute;
top: 8px;
right: 0;
width: 1px;
height: 40px;
content: '';
}
&:last-child {
padding-right: 0;
&:after {
display: none;
}
}
}
}
.datetime {
color: @disabled-color;
}
@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.extraContent {
margin-left: -44px;
.statItem {
padding: 0 16px;
}
}
}
@media screen and (max-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.extraContent {
float: none;
margin-right: 0;
.statItem {
padding: 0 16px;
text-align: left;
&:after {
display: none;
}
}
}
}
@media screen and (max-width: @screen-md) {
.extraContent {
margin-left: -16px;
}
}
@media screen and (max-width: @screen-sm) {
.pageHeaderContent {
display: block;
.content {
margin-left: 0;
}
}
.extraContent {
.statItem {
float: none;
}
}
}
================================================
FILE: src/pages/Dashboard/models/activities.js
================================================
import { queryActivities } from '@/services/api';
export default {
namespace: 'activities',
state: {
list: [],
},
effects: {
*fetchList(_, { call, put }) {
const response = yield call(queryActivities);
yield put({
type: 'saveList',
payload: Array.isArray(response) ? response : [],
});
},
},
reducers: {
saveList(state, action) {
return {
...state,
list: action.payload,
};
},
},
};
================================================
FILE: src/pages/Exception/403.js
================================================
import React from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import Exception from '@/components/Exception';
const Exception403 = () => (
);
export default Exception403;
================================================
FILE: src/pages/Exception/404.js
================================================
import React from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import Exception from '@/components/Exception';
const Exception404 = () => (
);
export default Exception404;
================================================
FILE: src/pages/Exception/500.js
================================================
import React from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import Exception from '@/components/Exception';
const Exception500 = () => (
);
export default Exception500;
================================================
FILE: src/pages/Exception/TriggerException.js
================================================
import React, { PureComponent } from 'react';
import { Button, Spin, Card } from 'antd';
import { connect } from 'dva';
import styles from './style.less';
@connect(state => ({
isloading: state.error.isloading,
}))
class TriggerException extends PureComponent {
state = {
isloading: false,
};
triggerError = code => {
this.setState({
isloading: true,
});
const { dispatch } = this.props;
dispatch({
type: 'error/query',
payload: {
code,
},
});
};
render() {
const { isloading } = this.state;
return (
this.triggerError(401)}>
触发401
this.triggerError(403)}>
触发403
this.triggerError(500)}>
触发500
this.triggerError(404)}>
触发404
);
}
}
export default TriggerException;
================================================
FILE: src/pages/Exception/models/error.js
================================================
import queryError from '@/services/error';
export default {
namespace: 'error',
state: {
error: '',
isloading: false,
},
effects: {
*query({ payload }, { call, put }) {
yield call(queryError, payload.code);
yield put({
type: 'trigger',
payload: payload.code,
});
},
},
reducers: {
trigger(state, action) {
return {
error: action.payload,
};
},
},
};
================================================
FILE: src/pages/Exception/style.less
================================================
.trigger {
background: 'red';
:global(.ant-btn) {
margin-right: 8px;
margin-bottom: 12px;
}
}
================================================
FILE: src/pages/Link/LinkComponent.js
================================================
import React from 'react';
import { Input, Modal } from 'antd';
class LinkComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
render() {
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
);
}
}
export default LinkComponent;
================================================
FILE: src/pages/Link/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import { Row, Col, Card, Form, Input, Button, Table, notification, Popconfirm, Switch, Tag, Select } from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import LinkComponent from './LinkComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ link }) => ({
link,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
keyword: '',
type: '', // 1 :其他友情链接 2: 是博主的个人链接 ,'' 代表所有链接
url: '',
icon: '',
pageNum: 1,
pageSize: 10,
name: '',
desc: '',
columns: [
{
title: '链接名',
dataIndex: 'name',
},
{
title: '图标',
dataIndex: 'icon',
},
{
title: 'url',
dataIndex: 'url',
},
{
title: '描述',
dataIndex: 'desc',
},
{
title: '类型',
dataIndex: 'type',
render: val => (val ? 博主链接 : 其他友情链接 ),
},
{
title: '状态',
dataIndex: 'state',
render: val => ,
},
{
title: '创建时间',
dataIndex: 'create_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
render: (text, record) => (
this.handleDelete(text, record)}>
Delete
),
},
],
};
this.createRef = React.createRef();
this.handleChange = this.handleChange.bind(this);
this.handleDescChange = this.handleDescChange.bind(this);
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleIconChange = this.handleIconChange.bind(this);
this.handleTypeChange = this.handleTypeChange.bind(this);
this.handleUrlChange = this.handleUrlChange.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showModal = this.showModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.onChangeState = this.onChangeState.bind(this);
this.handleChangeType = this.handleChangeType.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
onChangeState(text, record, state) {
console.log('text :', text);
console.log('record :', record);
console.log('state :', state);
// updateLink
this.setState(
{
state,
},
() => {
const { dispatch } = this.props;
const params = {
state: this.state.state,
};
return;
new Promise(resolve => {
dispatch({
type: 'link/updateLink',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
});
this.setState({
name: '',
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
},
);
}
handleChangeType(type) {
this.setState(
{
type,
},
() => {
this.handleSearch();
},
);
}
handleChange(event) {
this.setState({
name: event.target.value,
});
}
handleDescChange(event) {
this.setState({
desc: event.target.value,
});
}
handleUrlChange(event) {
this.setState({
url: event.target.value,
});
}
handleIconChange(event) {
this.setState({
icon: event.target.value,
});
}
handleTypeChange(event) {
this.setState({
type: event.target.value,
});
}
handleChangeKeyword(event) {
this.setState({
keyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
},
);
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = () => {
const { dispatch } = this.props;
const params = {
name: this.state.name,
url: this.state.url,
icon: this.state.icon,
type: this.state.type,
desc: this.state.desc,
};
new Promise(resolve => {
dispatch({
type: 'link/addLink',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
});
this.setState({
name: '',
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
type: this.state.type,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'link/queryLink',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'link/delLink',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { linkList, total } = this.props.link;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={linkList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/Message/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import {
Row,
Col,
Card,
Form,
Input,
Button,
Table,
notification,
Popconfirm,
Divider,
Switch,
Tag,
Select,
} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import MessageComponent from './MessageComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ message }) => ({
message,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
keyword: '',
state: '', // 状态 0 是未处理,1 是已处理 ,'' 代表所有留言
pageNum: 1,
pageSize: 10,
columns: [
{
title: '用户名',
dataIndex: 'name',
},
{
title: 'email',
dataIndex: 'email',
},
{
title: '头像',
dataIndex: 'avatar',
},
{
title: 'phone',
dataIndex: 'phone',
},
// {
// title: '用户介绍',
// dataIndex: 'introduce',
// },
{
title: '内容',
width: 300,
dataIndex: 'content',
},
{
title: '状态',
dataIndex: 'state',
render: val => (val ? 已经处理 : 未处理 ),
},
{
title: '创建时间',
dataIndex: 'create_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
width: 150,
render: (text, record) => (
),
},
],
};
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showReplyModal = this.showReplyModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleChangeState = this.handleChangeState.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
handleChangeState(state) {
this.setState(
{
state,
},
() => {
this.handleSearch();
}
);
}
handleChangeKeyword(event) {
this.setState({
keyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
}
);
}
showReplyModal = (text, record) => {
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'message/getMessageDetail',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res)
if (res.code === 0) {
this.setState({
visible: true,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleOk = () => {
this.setState({
visible: false,
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
state: this.state.state,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'message/queryMessage',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'message/delMessage',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { messageList, total } = this.props.message;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={messageList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/Message/MessageComponent.js
================================================
import React from 'react';
import { Row, Col, Input, Modal, Select, notification, Button } from 'antd';
import { connect } from 'dva';
@connect(({ message }) => ({
message,
}))
class MessageComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
state: '',
content: '',
};
this.handleChange = this.handleChange.bind(this);
this.submit = this.submit.bind(this);
this.handleStateChange = this.handleStateChange.bind(this);
}
componentDidMount() {}
submit() {
const { dispatch } = this.props;
const { messageDetail } = this.props.message;
const params = {
id: messageDetail._id,
state: this.state.state,
content: this.state.content,
};
new Promise(resolve => {
dispatch({
type: 'message/addReplyMessage',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
} else {
notification.error({
message: res.message,
});
}
});
}
handleChange(event) {
this.setState({
content: event.target.value,
});
}
handleStateChange(value) {
this.setState({
state: value,
});
}
render() {
const globalStyle = {
marginBottom: 20,
};
const titleStyle = {
textAlign: 'left',
borderLeft: '5px solid #1890FF',
paddingLeft: '10px',
fontSize: '20px',
margin: '20px 0',
};
const contentStyle = {
textAlign: 'center',
padding: '30px 0',
};
const normalLeft = {
textAlign: 'left',
paddingBottom: 20,
borderTop: '1px solid #eee',
};
const { TextArea } = Input;
const { messageDetail } = this.props.message;
const list = messageDetail.reply_list.map(e => (
{' '}
{e.content}
));
return (
用户
用户名:
{messageDetail.name}
手机:
{messageDetail.phone}
邮箱:
{messageDetail.email}
介绍:
{messageDetail.introduce}
留言
{messageDetail.content}
回复内容
{list || 暂无回复!
}
添加回复
未处理
已处理
提交回复
);
}
}
export default MessageComponent;
================================================
FILE: src/pages/OtherUser/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import {
Row,
Col,
Card,
Form,
Input,
Button,
Table,
notification,
Popconfirm,
Switch,
Tag,
Select,
} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ otherUser }) => ({
otherUser,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
keyword: '',
type: '', // 1 :其他友情用户 2: 是管理员的个人用户 ,'' 代表所有用户
pageNum: 1,
pageSize: 10,
columns: [
{
title: '用户名',
dataIndex: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
},
{
title: '手机',
dataIndex: 'phone',
},
{
title: '头像',
dataIndex: 'img_url',
},
{
title: '个人介绍',
width: 250,
dataIndex: 'introduce',
},
{
title: '类型',
dataIndex: 'type',
// 0:管理员 1:其他用户
render: val =>
!val ? 管理员 : 其他用户 ,
},
{
title: '创建时间',
dataIndex: 'create_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
render: (text, record) => (
this.handleDelete(text, record)}>
Delete
),
},
],
};
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleChangeType = this.handleChangeType.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
handleChangeType(type) {
this.setState(
{
type,
},
() => {
this.handleSearch();
}
);
}
handleChangeKeyword(event) {
this.setState({
keyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
}
);
}
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
type: this.state.type,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'otherUser/queryUser',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'otherUser/delUser',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { userList, total } = this.props.otherUser;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={userList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/OtherUser/OtherUserComponent.js
================================================
import React from 'react';
import { Input, Modal } from 'antd';
class OtherUserComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
render() {
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
);
}
}
export default OtherUserComponent;
================================================
FILE: src/pages/OtherUser/style.less
================================================
@import '~antd/lib/style/themes/default.less';
@import '~@/utils/utils.less';
.tableList {
.tableListOperator {
margin-bottom: 16px;
button {
margin-right: 8px;
}
}
}
.tableListForm {
:global {
.ant-form-item {
margin-bottom: 24px;
margin-right: 0;
display: flex;
> .ant-form-item-label {
width: auto;
line-height: 32px;
padding-right: 8px;
}
.ant-form-item-control {
line-height: 32px;
}
}
.ant-form-item-control-wrapper {
flex: 1;
}
}
.submitButtons {
display: block;
white-space: nowrap;
margin-bottom: 24px;
}
}
@media screen and (max-width: @screen-lg) {
.tableListForm :global(.ant-form-item) {
margin-right: 24px;
}
}
@media screen and (max-width: @screen-md) {
.tableListForm :global(.ant-form-item) {
margin-right: 8px;
}
}
================================================
FILE: src/pages/Project/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import {
Row,
Col,
Card,
Form,
Input,
Button,
Table,
notification,
Popconfirm,
Divider,
Tag,
Select,
Avatar
} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import ProjectComponent from './ProjectComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ project }) => ({
project,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
changeType: false,
title: '',
img: '',
url: '',
stateComponent: '',
content: '',
start_time: new Date(),
end_time: new Date(),
visible: false,
loading: false,
keyword: '',
state: '', // 状态 1 是已经完成 ,2 是正在进行,3 是没完成 ,'' 代表所有项目
pageNum: 1,
pageSize: 10,
columns: [
{
title: '标题',
width: 150,
dataIndex: 'title',
},
{
title: '内容',
width: 350,
dataIndex: 'content',
},
{
title: 'url',
width: 100,
dataIndex: 'url',
},
{
title: '封面图',
width: 50,
dataIndex: 'img',
render: val => ,
},
{
title: '状态',
dataIndex: 'state', // 状态 1 是已经完成 ,2 是正在进行,3 是没完成
render: val => {
// 状态 1 是已经完成 ,2 是正在进行,3 是没完成
if (val === 1) {
return 已经完成 ;
}
if (val === 2) {
return 正在进行 ;
}
return 没完成 ;
},
},
{
title: '开始时间',
dataIndex: 'start_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '结束时间',
dataIndex: 'end_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
width: 150,
render: (text, record) => (
),
},
],
};
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showModal = this.showModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleChangeState = this.handleChangeState.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleStateChange = this.handleStateChange.bind(this);
this.onChangeTime = this.onChangeTime.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
onChangeTime(date, dateString) {
console.log(date, dateString);
this.setState({
start_time: new Date(dateString[0]),
end_time: new Date(dateString[1]),
});
}
handleSubmit() {
const { dispatch } = this.props;
const { projectDetail } = this.props.project;
if (this.state.changeType) {
const params = {
id: projectDetail._id,
state: this.state.stateComponent,
title: this.state.title,
img: this.state.img,
url: this.state.url,
content: this.state.content,
start_time: this.state.start_time,
end_time: this.state.end_time,
};
new Promise(resolve => {
dispatch({
type: 'project/updateProject',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
chnageType: false,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
} else {
const params = {
state: this.state.stateComponent,
title: this.state.title,
img: this.state.img,
url: this.state.url,
content: this.state.content,
start_time: this.state.start_time,
end_time: this.state.end_time,
};
new Promise(resolve => {
dispatch({
type: 'project/addProject',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
chnageType: false,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
}
}
handleChange(event) {
console.log('event.target.value :', event.target.name)
this.setState({
[event.target.name]: event.target.value,
});
}
handleStateChange(value) {
this.setState({
stateComponent: value,
});
}
handleChangeState(state) {
this.setState(
{
state,
},
() => {
this.handleSearch();
}
);
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
}
);
}
showModal = record => {
if (record._id) {
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'project/getProjectDetail',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res)
if (res.code === 0) {
this.setState({
visible: true,
changeType: true,
stateComponent: res.data.state,
title: res.data.title,
img: res.data.img,
url: res.data.url,
content: res.data.content,
});
} else {
notification.error({
message: res.message,
});
}
});
} else {
this.setState({
visible: true,
changeType: false,
stateComponent: '',
title: '',
img: '',
url: '',
content: '',
});
}
};
handleOk = () => {
this.handleSubmit();
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
state: this.state.state,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'project/queryProject',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'project/delProject',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { projectList, total } = this.props.project;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={projectList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/Project/ProjectComponent.js
================================================
import React from 'react';
import { Input, Modal, Select, DatePicker } from 'antd';
import { connect } from 'dva';
const { RangePicker } = DatePicker;
@connect(({ project }) => ({
project,
}))
class ProjectComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
checkUpdate() {
const { changeType } = this.props;
const { projectDetail } = this.props.project;
if (changeType) {
this.setState({
title: projectDetail.title,
state: projectDetail.state,
content: projectDetail.content,
});
}
}
render() {
const { TextArea } = Input;
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
{/* 状态 1 是已经完成 ,2 是正在进行,3 是没完成 */}
已完成
正在进行中
没完成
);
}
}
export default ProjectComponent;
================================================
FILE: src/pages/Tag/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import { Row, Col, Card, Form, Input, Button, Table, notification, Popconfirm } from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import TagComponent from './TagComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ tag }) => ({
tag
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
visible: false,
loading: false,
keyword: '',
pageNum: 1,
pageSize: 10,
name: '',
desc: '',
columns: [
{
title: '标签名',
dataIndex: 'name',
},
{
title: '创建时间',
dataIndex: 'create_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
render: (text, record) => (
this.handleDelete(text, record)}>
Delete
),
},
],
};
this.createRef = React.createRef();
this.handleChange = this.handleChange.bind(this);
this.handleDescChange = this.handleDescChange.bind(this);
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showModal = this.showModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
handleChange(event) {
// console.log('event :', event)
this.setState({
name: event.target.value,
});
}
handleDescChange(event) {
// console.log('event :', event)
this.setState({
desc: event.target.value,
});
}
handleChangeKeyword(event) {
// console.log('event :', event)
this.setState({
keyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
},
);
}
showModal = () => {
this.setState({
visible: true,
});
};
handleOk = e => {
const { dispatch } = this.props;
const params = {
name: this.state.name,
desc: this.state.desc,
};
new Promise(resolve => {
dispatch({
type: 'tag/addTag',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
});
this.setState({
name: '',
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'tag/queryTag',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'tag/delTag',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { tagList, total } = this.props.tag;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={tagList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/Tag/TagComponent.js
================================================
import React from 'react';
import { Input, Modal } from 'antd';
class TagComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
render() {
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
);
}
}
export default TagComponent;
================================================
FILE: src/pages/TimeAxis/List.js
================================================
import React, { PureComponent, Fragment } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import {
Row,
Col,
Card,
Form,
Input,
Button,
Table,
notification,
Popconfirm,
Divider,
Tag,
Select,
} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import TimeAxisComponent from './TimeAxisComponent';
const FormItem = Form.Item;
/* eslint react/no-multi-comp:0 */
@connect(({ timeAxis }) => ({
timeAxis,
}))
@Form.create()
class TableList extends PureComponent {
constructor(props) {
super(props);
this.state = {
changeType: false,
title: '',
stateComponent: '',
content: '',
start_time: new Date(),
end_time: new Date(),
visible: false,
loading: false,
keyword: '',
state: '', // 状态 1 是已经完成 ,2 是正在进行,3 是没完成 ,'' 代表所有时间轴
pageNum: 1,
pageSize: 10,
columns: [
{
title: '标题',
width: 150,
dataIndex: 'title',
},
{
title: '内容',
width: 350,
dataIndex: 'content',
},
{
title: '状态',
dataIndex: 'state', // 状态 1 是已经完成 ,2 是正在进行,3 是没完成
render: val => {
// 状态 1 是已经完成 ,2 是正在进行,3 是没完成
if (val === 1) {
return 已经完成 ;
}
if (val === 2) {
return 正在进行 ;
}
return 没完成 ;
},
},
{
title: '开始时间',
dataIndex: 'start_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '结束时间',
dataIndex: 'end_time',
sorter: true,
render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')} ,
},
{
title: '操作',
width: 150,
render: (text, record) => (
),
},
],
};
this.handleChangeKeyword = this.handleChangeKeyword.bind(this);
this.handleOk = this.handleOk.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.showModal = this.showModal.bind(this);
this.handleCancel = this.handleCancel.bind(this);
this.handleSearch = this.handleSearch.bind(this);
this.handleChangeState = this.handleChangeState.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleChangeContent = this.handleChangeContent.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleStateChange = this.handleStateChange.bind(this);
this.onChangeTime = this.onChangeTime.bind(this);
}
componentDidMount() {
this.handleSearch(this.state.pageNum, this.state.pageSize);
}
onChangeTime(date, dateString) {
console.log(date, dateString);
this.setState({
start_time: new Date(dateString[0]),
end_time: new Date(dateString[1]),
});
}
handleSubmit() {
const { dispatch } = this.props;
const { timeAxisDetail } = this.props.timeAxis;
if (this.state.changeType) {
const params = {
id: timeAxisDetail._id,
state: this.state.stateComponent,
title: this.state.title,
content: this.state.content,
start_time: this.state.start_time,
end_time: this.state.end_time,
};
new Promise(resolve => {
dispatch({
type: 'timeAxis/updateTimeAxis',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
chnageType: false,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
} else {
const params = {
state: this.state.stateComponent,
title: this.state.title,
content: this.state.content,
start_time: this.state.start_time,
end_time: this.state.end_time,
};
new Promise(resolve => {
dispatch({
type: 'timeAxis/addTimeAxis',
payload: {
resolve,
params,
},
});
}).then(res => {
if (res.code === 0) {
notification.success({
message: res.message,
});
this.setState({
visible: false,
chnageType: false,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
}
}
handleChange(event) {
this.setState({
title: event.target.value,
});
}
handleChangeContent(event) {
this.setState({
content: event.target.value,
});
}
handleStateChange(value) {
this.setState({
stateComponent: value,
});
}
handleChangeState(state) {
this.setState(
{
state,
},
() => {
this.handleSearch();
}
);
}
handleChangeKeyword(event) {
this.setState({
keyword: event.target.value,
});
}
handleChangePageParam(pageNum, pageSize) {
this.setState(
{
pageNum,
pageSize,
},
() => {
this.handleSearch();
}
);
}
showModal = record => {
if (record._id) {
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'timeAxis/getTimeAxisDetail',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res)
if (res.code === 0) {
this.setState({
visible: true,
changeType: true,
stateComponent: res.data.state,
title: res.data.title,
content: res.data.content,
});
} else {
notification.error({
message: res.message,
});
}
});
} else {
this.setState({
visible: true,
changeType: false,
stateComponent: '',
title: '',
content: '',
});
}
};
handleOk = () => {
this.handleSubmit();
};
handleCancel = e => {
this.setState({
visible: false,
});
};
handleSearch = () => {
this.setState({
loading: true,
});
const { dispatch } = this.props;
const params = {
keyword: this.state.keyword,
state: this.state.state,
pageNum: this.state.pageNum,
pageSize: this.state.pageSize,
};
new Promise(resolve => {
dispatch({
type: 'timeAxis/queryTimeAxis',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
this.setState({
loading: false,
});
} else {
notification.error({
message: res.message,
});
}
});
};
handleDelete = (text, record) => {
// console.log('text :', text);
// console.log('record :', record);
const { dispatch } = this.props;
const params = {
id: record._id,
};
new Promise(resolve => {
dispatch({
type: 'timeAxis/delTimeAxis',
payload: {
resolve,
params,
},
});
}).then(res => {
// console.log('res :', res);
if (res.code === 0) {
notification.success({
message: res.message,
});
this.handleSearch(this.state.pageNum, this.state.pageSize);
} else {
notification.error({
message: res.message,
});
}
});
};
renderSimpleForm() {
return (
);
}
render() {
const { timeAxisList, total } = this.props.timeAxis;
const { pageNum, pageSize } = this.state;
const pagination = {
total,
defaultCurrent: pageNum,
pageSize,
showSizeChanger: true,
onShowSizeChange: (current, pageSize) => {
// console.log('current, pageSize :', current, pageSize);
this.handleChangePageParam(current, pageSize);
},
onChange: (current, pageSize) => {
this.handleChangePageParam(current, pageSize);
},
};
return (
{this.renderSimpleForm()}
record._id}
columns={this.state.columns}
bordered
dataSource={timeAxisList}
/>
);
}
}
export default TableList;
================================================
FILE: src/pages/TimeAxis/TimeAxisComponent.js
================================================
import React from 'react';
import { Input, Modal, Select, DatePicker } from 'antd';
import { connect } from 'dva';
const { RangePicker } = DatePicker;
@connect(({ timeAxis }) => ({
timeAxis,
}))
class TimeAxisComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
checkUpdate(){
const { changeType } = this.props;
const { timeAxisDetail } = this.props.timeAxis;
if (changeType) {
this.setState({
title: timeAxisDetail.title,
state: timeAxisDetail.state,
content: timeAxisDetail.content,
});
}
}
render() {
const { TextArea } = Input;
const normalCenter = {
textAlign: 'center',
marginBottom: 20,
};
return (
{/* 状态 1 是已经完成 ,2 是正在进行,3 是没完成 */}
已完成
正在进行中
没完成
);
}
}
export default TimeAxisComponent;
================================================
FILE: src/pages/User/Login.js
================================================
import React, { Component } from 'react';
import { connect } from 'dva';
import { formatMessage, FormattedMessage } from 'umi/locale';
import Link from 'umi/link';
import { Checkbox, Alert, Icon } from 'antd';
import Login from '@/components/Login';
import styles from './Login.less';
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
@connect(({ login, loading }) => ({
login,
submitting: loading.effects['login/login'],
}))
class LoginPage extends Component {
state = {
type: 'account',
autoLogin: true,
};
onTabChange = type => {
this.setState({ type });
};
onGetCaptcha = () =>
new Promise((resolve, reject) => {
this.loginForm.validateFields(['mobile'], {}, (err, values) => {
if (err) {
reject(err);
} else {
const { dispatch } = this.props;
dispatch({
type: 'login/getCaptcha',
payload: values.mobile,
})
.then(resolve)
.catch(reject);
}
});
});
handleSubmit = (err, values) => {
// console.log('values :', values)
const { type } = this.state;
if (!err) {
const { dispatch } = this.props;
dispatch({
type: 'login/loginAdmin',
// type: 'login/login',
payload: {
...values,
type,
},
});
}
};
changeAutoLogin = e => {
this.setState({
autoLogin: e.target.checked,
});
};
renderMessage = content => (
);
render() {
const { login, submitting } = this.props;
const { type, autoLogin } = this.state;
return (
{
this.loginForm = form;
}}
>
{login.status === 'error' &&
login.type === 'account' &&
!submitting &&
this.renderMessage('账户或密码错误(admin/888888)')}
{/* */}
this.loginForm.validateFields(this.handleSubmit)}
/>
{/*
{login.status === 'error' &&
login.type === 'account' &&
!submitting &&
this.renderMessage('账户或密码错误(admin/888888)')}
this.loginForm.validateFields(this.handleSubmit)}
/>
*/}
{/*
{login.status === 'error' &&
login.type === 'mobile' &&
!submitting &&
this.renderMessage('验证码错误')}
*/}
{/*
*/}
);
}
}
export default LoginPage;
================================================
FILE: src/pages/User/Login.less
================================================
@import '~antd/lib/style/themes/default.less';
.main {
width: 368px;
margin: 0 auto;
@media screen and (max-width: @screen-sm) {
width: 95%;
}
.icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
.other {
text-align: left;
margin-top: 24px;
line-height: 22px;
.register {
float: right;
}
}
}
================================================
FILE: src/pages/User/Register.js
================================================
import React, { Component } from 'react';
import { connect } from 'dva';
import { formatMessage, FormattedMessage } from 'umi/locale';
import Link from 'umi/link';
import router from 'umi/router';
import { Form, Input, Button, Select, Row, Col, Popover, Progress } from 'antd';
import styles from './Register.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',
};
@connect(({ register, loading }) => ({
register,
submitting: loading.effects['register/submit'],
}))
@Form.create()
class Register extends Component {
state = {
count: 0,
confirmDirty: false,
visible: false,
help: '',
prefix: '86',
};
componentDidUpdate() {
const { form, register } = this.props;
const account = form.getFieldValue('mail');
if (register.status === 'ok') {
router.push({
pathname: '/user/register-result',
state: {
account,
},
});
}
}
componentWillUnmount() {
clearInterval(this.interval);
}
onGetCaptcha = () => {
let count = 59;
this.setState({ count });
this.interval = setInterval(() => {
count -= 1;
this.setState({ count });
if (count === 0) {
clearInterval(this.interval);
}
}, 1000);
};
getPasswordStatus = () => {
const { form } = this.props;
const value = form.getFieldValue('password');
if (value && value.length > 9) {
return 'ok';
}
if (value && value.length > 5) {
return 'pass';
}
return 'poor';
};
handleSubmit = e => {
e.preventDefault();
const { form, dispatch } = this.props;
form.validateFields({ force: true }, (err, values) => {
if (!err) {
const { prefix } = this.state;
dispatch({
type: 'register/submit',
payload: {
...values,
prefix,
},
});
}
});
};
handleConfirmBlur = e => {
const { value } = e.target;
const { confirmDirty } = this.state;
this.setState({ confirmDirty: confirmDirty || !!value });
};
checkConfirm = (rule, value, callback) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password')) {
callback(formatMessage({ id: 'validation.password.twice' }));
} else {
callback();
}
};
checkPassword = (rule, value, callback) => {
const { visible, confirmDirty } = this.state;
if (!value) {
this.setState({
help: formatMessage({ id: 'validation.password.required' }),
visible: !!value,
});
callback('error');
} else {
this.setState({
help: '',
});
if (!visible) {
this.setState({
visible: !!value,
});
}
if (value.length < 6) {
callback('error');
} else {
const { form } = this.props;
if (value && confirmDirty) {
form.validateFields(['confirm'], { force: true });
}
callback();
}
}
};
changePrefix = value => {
this.setState({
prefix: value,
});
};
renderPasswordProgress = () => {
const { form } = this.props;
const value = form.getFieldValue('password');
const passwordStatus = this.getPasswordStatus();
return value && value.length ? (
100 ? 100 : value.length * 10}
showInfo={false}
/>
) : null;
};
render() {
const { form, submitting } = this.props;
const { getFieldDecorator } = form;
const { count, prefix, help, visible } = this.state;
return (
}
overlayStyle={{ width: 240 }}
placement="right"
visible={visible}
>
{getFieldDecorator('password', {
rules: [
{
validator: this.checkPassword,
},
],
})(
)}
{getFieldDecorator('confirm', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.confirm-password.required' }),
},
{
validator: this.checkConfirm,
},
],
})(
)}
+86
+87
{getFieldDecorator('mobile', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.phone-number.required' }),
},
{
pattern: /^\d{10}$/,
message: formatMessage({ id: 'validation.phone-number.wrong-format' }),
},
],
})(
)}
{getFieldDecorator('captcha', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.verification-code.required' }),
},
],
})(
)}
{count
? `${count} s`
: formatMessage({ id: 'app.register.get-verification-code' })}
);
}
}
export default Register;
================================================
FILE: src/pages/User/Register.less
================================================
@import '~antd/lib/style/themes/default.less';
.main {
width: 368px;
margin: 0 auto;
:global {
.ant-form-item {
margin-bottom: 24px;
}
}
h3 {
font-size: 16px;
margin-bottom: 20px;
}
.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;
}
}
}
================================================
FILE: src/pages/User/RegisterResult.js
================================================
import React from 'react';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { Button } from 'antd';
import Link from 'umi/link';
import Result from '@/components/Result';
import styles from './RegisterResult.less';
const actions = (
);
const RegisterResult = ({ location }) => (
}
description={formatMessage({ id: 'app.register-result.activation-email' })}
actions={actions}
style={{ marginTop: 56 }}
/>
);
export default RegisterResult;
================================================
FILE: src/pages/User/RegisterResult.less
================================================
.registerResult {
:global {
.anticon {
font-size: 64px;
}
}
.title {
margin-top: 32px;
font-size: 20px;
line-height: 28px;
}
.actions {
margin-top: 40px;
a + a {
margin-left: 8px;
}
}
}
================================================
FILE: src/pages/User/models/register.js
================================================
import { fakeRegister } from '@/services/api';
import { setAuthority } from '@/utils/authority';
import { reloadAuthorized } from '@/utils/Authorized';
export default {
namespace: 'register',
state: {
status: undefined,
},
effects: {
*submit({ payload }, { call, put }) {
const response = yield call(fakeRegister, payload);
yield put({
type: 'registerHandle',
payload: response,
});
},
},
reducers: {
registerHandle(state, { payload }) {
setAuthority('user');
reloadAuthorized();
return {
...state,
status: payload.status,
};
},
},
};
================================================
FILE: src/pages/document.ejs
================================================
react-blog-admin
================================================
FILE: src/services/api.js
================================================
import { stringify } from 'qs';
import request from '@/utils/request';
export async function queryProjectNotice() {
return request('/api/project/notice');
}
export async function queryActivities() {
return request('/api/activities');
}
export async function fakeSubmitForm(params) {
return request('/api/forms', {
method: 'POST',
body: params,
});
}
export async function fakeChartData() {
return request('/api/fake_chart_data');
}
// 分类
export async function queryCategory(params) {
return request(`/api/getCategoryList?${stringify(params)}`);
}
export async function addCategory(params) {
return request('/api/addCategory', {
method: 'POST',
body: params,
});
}
export async function updateCategory(params) {
return request('/api/updateCategory', {
method: 'POST',
body: params,
});
}
export async function delCategory(params) {
return request('/api/delCategory', {
method: 'POST',
body: params,
});
}
// 其他用户
export async function queryUser(params) {
return request(`/api/getUserList?${stringify(params)}`);
}
export async function addUser(params) {
return request('/api/addUser', {
method: 'POST',
body: params,
});
}
export async function updateUser(params) {
return request('/api/updateUser', {
method: 'POST',
body: params,
});
}
export async function delUser(params) {
return request('/api/delUser', {
method: 'POST',
body: params,
});
}
// 链接
export async function queryLink(params) {
return request(`/api/getLinkList?${stringify(params)}`);
}
export async function addLink(params) {
return request('/api/addLink', {
method: 'POST',
body: params,
});
}
export async function updateLink(params) {
return request('/api/updateLink', {
method: 'POST',
body: params,
});
}
export async function delLink(params) {
return request('/api/delLink', {
method: 'POST',
body: params,
});
}
// 留言
export async function queryMessage(params) {
return request(`/api/getMessageList?${stringify(params)}`);
}
export async function delMessage(params) {
return request('/api/delMessage', {
method: 'POST',
body: params,
});
}
export async function getMessageDetail(params) {
return request('/api/getMessageDetail', {
method: 'POST',
body: params,
});
}
export async function addReplyMessage(params) {
return request('/api/addReplyMessage', {
method: 'POST',
body: params,
});
}
// 文章
export async function queryArticle(params) {
return request(`/api/getArticleListAdmin?${stringify(params)}`);
}
export async function addArticle(params) {
return request('/api/addArticle', {
method: 'POST',
body: params,
});
}
export async function delArticle(params) {
return request('/api/delArticle', {
method: 'POST',
body: params,
});
}
export async function updateArticle(params) {
return request('/api/updateArticle', {
method: 'POST',
body: params,
});
}
export async function getArticleDetail(params) {
return request('/api/getArticleDetail', {
method: 'POST',
body: params,
});
}
// 管理一级评论
export async function changeComment(params) {
return request('/api/changeComment', {
method: 'POST',
body: params,
});
}
// 管理第三者评论
export async function changeThirdComment(params) {
return request('/api/changeThirdComment', {
method: 'POST',
body: params,
});
}
// 时间轴
export async function queryTimeAxis(params) {
return request(`/api/getTimeAxisList?${stringify(params)}`);
}
export async function addTimeAxis(params) {
return request('/api/addTimeAxis', {
method: 'POST',
body: params,
});
}
export async function delTimeAxis(params) {
return request('/api/delTimeAxis', {
method: 'POST',
body: params,
});
}
export async function updateTimeAxis(params) {
return request('/api/updateTimeAxis', {
method: 'POST',
body: params,
});
}
export async function getTimeAxisDetail(params) {
return request('/api/getTimeAxisDetail', {
method: 'POST',
body: params,
});
}
// 项目
export async function queryProject(params) {
return request(`/api/getProjectList?${stringify(params)}`);
}
export async function addProject(params) {
return request('/api/addProject', {
method: 'POST',
body: params,
});
}
export async function delProject(params) {
return request('/api/delProject', {
method: 'POST',
body: params,
});
}
export async function updateProject(params) {
return request('/api/updateProject', {
method: 'POST',
body: params,
});
}
export async function getProjectDetail(params) {
return request('/api/getProjectDetail', {
method: 'POST',
body: params,
});
}
// 标签
export async function queryTag(params) {
return request(`/api/getTagList?${stringify(params)}`);
}
export async function addTag(params) {
return request('/api/addTag', {
method: 'POST',
body: params,
});
}
export async function delTag(params) {
return request('/api/delTag', {
method: 'POST',
body: params,
});
}
export async function queryBasicProfile() {
return request('/api/profile/basic');
}
export async function queryAdvancedProfile() {
return request('/api/profile/advanced');
}
export async function queryFakeList(params) {
return request(`/api/fake_list?${stringify(params)}`);
}
export async function removeFakeList(params) {
const { count = 5, ...restParams } = params;
return request(`/api/fake_list?count=${count}`, {
method: 'POST',
body: {
...restParams,
method: 'delete',
},
});
}
export async function addFakeList(params) {
const { count = 5, ...restParams } = params;
return request(`/api/fake_list?count=${count}`, {
method: 'POST',
body: {
...restParams,
method: 'post',
},
});
}
export async function updateFakeList(params) {
const { count = 5, ...restParams } = params;
return request(`/api/fake_list?count=${count}`, {
method: 'POST',
body: {
...restParams,
method: 'update',
},
});
}
export async function loginAdmin(params) {
return request('/api/loginAdmin', {
method: 'POST',
body: params,
});
}
export async function fakeAccountLogin(params) {
return request('/api/login/account', {
method: 'POST',
body: params,
});
}
export async function fakeRegister(params) {
return request('/api/register', {
method: 'POST',
body: params,
});
}
export async function queryNotices() {
return request('/api/notices');
}
export async function getFakeCaptcha(mobile) {
return request(`/api/captcha?mobile=${mobile}`);
}
================================================
FILE: src/services/error.js
================================================
import request from '@/utils/request';
export default async function queryError(code) {
return request(`/api/${code}`);
}
================================================
FILE: src/services/geographic.js
================================================
import request from '@/utils/request';
export async function queryProvince() {
return request('/api/geographic/province');
}
export async function queryCity(province) {
return request(`/api/geographic/city/${province}`);
}
================================================
FILE: src/services/user.js
================================================
import request from '@/utils/request';
export async function query() {
return request('/api/users');
}
export async function queryCurrent() {
return request('/api/currentUser');
}
================================================
FILE: src/utils/Authorized.js
================================================
import RenderAuthorized from '@/components/Authorized';
import { getAuthority } from './authority';
let Authorized = RenderAuthorized(getAuthority()); // eslint-disable-line
// Reload the rights component
const reloadAuthorized = () => {
Authorized = RenderAuthorized(getAuthority());
};
export { reloadAuthorized };
export default Authorized;
================================================
FILE: src/utils/Yuan.js
================================================
import React from 'react';
import { yuan } from '@/components/Charts';
/**
* 减少使用 dangerouslySetInnerHTML
*/
export default class Yuan extends React.PureComponent {
componentDidMount() {
this.rendertoHtml();
}
componentDidUpdate() {
this.rendertoHtml();
}
rendertoHtml = () => {
const { children } = this.props;
if (this.main) {
this.main.innerHTML = yuan(children);
}
};
render() {
return (
{
this.main = ref;
}}
/>
);
}
}
================================================
FILE: src/utils/authority.js
================================================
// use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority(str) {
// return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
const authorityString =
typeof str === 'undefined' ? localStorage.getItem('antd-pro-authority') : str;
// authorityString could be admin, "admin", ["admin"]
let authority;
try {
authority = JSON.parse(authorityString);
} catch (e) {
authority = authorityString;
}
if (typeof authority === 'string') {
return [authority];
}
return authority || ['admin'];
}
export function setAuthority(authority) {
const proAuthority = typeof authority === 'string' ? [authority] : authority;
return localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
}
================================================
FILE: src/utils/authority.test.js
================================================
import { getAuthority } from './authority';
describe('getAuthority should be strong', () => {
it('empty', () => {
expect(getAuthority(null)).toEqual(['admin']); // default value
});
it('string', () => {
expect(getAuthority('admin')).toEqual(['admin']);
});
it('array with double quotes', () => {
expect(getAuthority('"admin"')).toEqual(['admin']);
});
it('array with single item', () => {
expect(getAuthority('["admin"]')).toEqual(['admin']);
});
it('array with multiple items', () => {
expect(getAuthority('["admin", "guest"]')).toEqual(['admin', 'guest']);
});
});
================================================
FILE: src/utils/domain.js
================================================
let domain = 'https://biaochenxuying.cn/'; // 正式域名
if (process.env.NODE_ENV === 'development') {
// 开发环境下,本地地址
domain = 'http://localhost:3001/';
}
export default domain;
================================================
FILE: src/utils/request.js
================================================
import fetch from 'dva/fetch';
import { notification } from 'antd';
import router from 'umi/router';
import hash from 'hash.js';
import { isAntdPro } from './utils';
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
const checkStatus = response => {
if (response.status >= 200 && response.status < 300) {
return response;
}
const errortext = codeMessage[response.status] || response.statusText;
notification.error({
message: `请求错误 ${response.status}: ${response.url}`,
description: errortext,
});
const error = new Error(errortext);
error.name = response.status;
error.response = response;
throw error;
};
const cachedSave = (response, hashcode) => {
/**
* Clone a response data and store it in sessionStorage
* Does not support data other than json, Cache only json
*/
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.match(/application\/json/i)) {
// All data is saved as text
response
.clone()
.text()
.then(content => {
sessionStorage.setItem(hashcode, content);
sessionStorage.setItem(`${hashcode}:timestamp`, Date.now());
});
}
return response;
};
/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @return {object} An object containing either "data" or "err"
*/
export default function request(
url,
options = {
expirys: isAntdPro(),
}
) {
/**
* Produce fingerprints based on url and parameters
* Maybe url has the same parameters
*/
const fingerprint = url + (options.body ? JSON.stringify(options.body) : '');
const hashcode = hash
.sha256()
.update(fingerprint)
.digest('hex');
const defaultOptions = {
credentials: 'include',
};
const newOptions = { ...defaultOptions, ...options };
if (
newOptions.method === 'POST' ||
newOptions.method === 'PUT' ||
newOptions.method === 'DELETE'
) {
if (!(newOptions.body instanceof FormData)) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
...newOptions.headers,
};
newOptions.body = JSON.stringify(newOptions.body);
} else {
// newOptions.body is FormData
newOptions.headers = {
Accept: 'application/json',
...newOptions.headers,
};
}
}
const expirys = options.expirys || 60;
// options.expirys !== false, return the cache,
if (options.expirys !== false) {
const cached = sessionStorage.getItem(hashcode);
const whenCached = sessionStorage.getItem(`${hashcode}:timestamp`);
if (cached !== null && whenCached !== null) {
const age = (Date.now() - whenCached) / 1000;
if (age < expirys) {
const response = new Response(new Blob([cached]));
return response.json();
}
sessionStorage.removeItem(hashcode);
sessionStorage.removeItem(`${hashcode}:timestamp`);
}
}
return fetch(url, newOptions)
.then(checkStatus)
.then(response => cachedSave(response, hashcode))
.then(response => {
// DELETE and 204 do not return data by default
// using .json will report an error.
if (newOptions.method === 'DELETE' || response.status === 204) {
return response.text();
}
return response.json();
})
.catch(e => {
const status = e.name;
if (status === 401) {
// @HACK
/* eslint-disable no-underscore-dangle */
window.g_app._store.dispatch({
type: 'login/logout',
});
return;
}
// environment should not be used
if (status === 403) {
router.push('/exception/403');
return;
}
if (status <= 504 && status >= 500) {
router.push('/exception/500');
return;
}
if (status >= 404 && status < 422) {
router.push('/exception/404');
}
});
}
================================================
FILE: src/utils/utils.js
================================================
import moment from 'moment';
import React from 'react';
import nzh from 'nzh/cn';
import { parse, stringify } from 'qs';
export function fixedZero(val) {
return val * 1 < 10 ? `0${val}` : val;
}
export function getTimeDistance(type) {
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))];
}
if (type === 'month') {
const year = now.getFullYear();
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),
];
}
const year = now.getFullYear();
return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)];
}
export function getPlainNode(nodeList, parentPath = '') {
const arr = [];
nodeList.forEach(node => {
const item = node;
item.path = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/');
item.exact = true;
if (item.children && !item.component) {
arr.push(...getPlainNode(item.children, item.path));
} else {
if (item.children && item.component) {
item.exact = false;
}
arr.push(item);
}
});
return arr;
}
export function digitUppercase(n) {
return nzh.toMoney(n);
}
function getRelation(str1, str2) {
if (str1 === str2) {
console.warn('Two path are equal!'); // eslint-disable-line
}
const arr1 = str1.split('/');
const arr2 = str2.split('/');
if (arr2.every((item, index) => item === arr1[index])) {
return 1;
}
if (arr1.every((item, index) => item === arr2[index])) {
return 2;
}
return 3;
}
function getRenderArr(routes) {
let renderArr = [];
renderArr.push(routes[0]);
for (let i = 1; i < routes.length; i += 1) {
// 去重
renderArr = renderArr.filter(item => getRelation(item, routes[i]) !== 1);
// 是否包含
const isAdd = renderArr.every(item => getRelation(item, routes[i]) === 3);
if (isAdd) {
renderArr.push(routes[i]);
}
}
return renderArr;
}
/**
* Get router routing configuration
* { path:{name,...param}}=>Array<{name,path ...param}>
* @param {string} path
* @param {routerData} routerData
*/
export function getRoutes(path, routerData) {
let routes = Object.keys(routerData).filter(
routePath => routePath.indexOf(path) === 0 && routePath !== path
);
// Replace path to '' eg. path='user' /user/name => name
routes = routes.map(item => item.replace(path, ''));
// Get the route to be rendered to remove the deep rendering
const renderArr = getRenderArr(routes);
// Conversion and stitching parameters
const renderRoutes = renderArr.map(item => {
const exact = !routes.some(route => route !== item && getRelation(route, item) === 1);
return {
exact,
...routerData[`${path}${item}`],
key: `${path}${item}`,
path: `${path}${item}`,
};
});
return renderRoutes;
}
export function getPageQuery() {
return parse(window.location.href.split('?')[1]);
}
export function getQueryPath(path = '', query = {}) {
const search = stringify(query);
if (search.length) {
return `${path}?${search}`;
}
return path;
}
/* eslint no-useless-escape:0 */
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
export function isUrl(path) {
return reg.test(path);
}
export function formatWan(val) {
const v = val * 1;
if (!v || Number.isNaN(v)) return '';
let result = val;
if (val > 10000) {
result = Math.floor(val / 10000);
result = (
{result}
万
);
}
return result;
}
export function isAntdPro() {
return window.location.hostname === 'preview.pro.ant.design';
}
================================================
FILE: src/utils/utils.less
================================================
.textOverflow() {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
overflow: hidden;
position: relative;
line-height: 1.5em;
max-height: @line * 1.5em;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
&:before {
background: @bg;
content: '...';
padding: 0 1px;
position: absolute;
right: 14px;
bottom: 0;
}
&:after {
background: white;
content: '';
margin-top: 0.2em;
position: absolute;
right: 14px;
width: 1em;
height: 1em;
}
}
// mixins for clearfix
// ------------------------
.clearfix() {
zoom: 1;
&:before,
&:after {
content: ' ';
display: table;
}
&:after {
clear: both;
visibility: hidden;
font-size: 0;
height: 0;
}
}
================================================
FILE: tests/fix_puppeteer.sh
================================================
#!/bin/bash
sudo apt-get update
sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
================================================
FILE: tests/run-tests.js
================================================
const { spawn } = require('child_process');
const { kill } = require('cross-port-killer');
const env = Object.create(process.env);
env.BROWSER = 'none';
env.TEST = true;
// flag to prevent multiple test
let once = false;
const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
env,
});
startServer.stderr.on('data', data => {
// eslint-disable-next-line
console.log(data.toString());
});
startServer.on('exit', () => {
kill(process.env.PORT || 8000);
});
// eslint-disable-next-line
console.log('Starting development server for e2e tests...');
startServer.stdout.on('data', data => {
// eslint-disable-next-line
console.log(data.toString());
if (!once && data.toString().indexOf('App running at') >= 0) {
// eslint-disable-next-line
once = true;
console.log('Development server is started, ready to run tests.');
const testCmd = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['test'], {
stdio: 'inherit',
});
testCmd.on('exit', code => {
startServer.kill();
process.exit(code);
});
}
});