Commit 81ee2c12 authored by 愚道's avatar 愚道 Committed by Yu

remove pages

parent 5ba67a51
/functions/mock/** /functions/mock/**
/scripts /scripts
/config /config
\ No newline at end of file
// https://umijs.org/config/ // https://umijs.org/config/
import os from 'os'; import os from 'os';
import pageRoutes from './router.config';
import webpackPlugin from './plugin.config'; import webpackPlugin from './plugin.config';
import defaultSettings from '../src/defaultSettings'; import defaultSettings from '../src/defaultSettings';
import slash from 'slash2'; import slash from 'slash2';
...@@ -63,7 +62,25 @@ export default { ...@@ -63,7 +62,25 @@ export default {
ie: 11, ie: 11,
}, },
// 路由配置 // 路由配置
routes: pageRoutes, routes: [{
path: '/user',
components: ['../layouts/UserLayout'],
routes: [],
}, {
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
routes: [
// dashboard
{
path: '/',
name: 'dashboard',
icon: 'dashboard',
component: './BasicDemo',
},
],
}],
// Theme for antd // Theme for antd
// https://ant.design/docs/react/customize-theme-cn // https://ant.design/docs/react/customize-theme-cn
theme: { theme: {
......
export default [
// user
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{ path: '/user', redirect: '/user/login' },
{ path: '/user/login', component: './User/Login' },
{ path: '/user/register', component: './User/Register' },
{ path: '/user/register-result', component: './User/RegisterResult' },
],
},
// app
{
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
routes: [
// dashboard
{ path: '/', redirect: '/dashboard/analysis' },
{
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{
path: '/dashboard/analysis',
name: 'analysis',
component: './Dashboard/Analysis',
},
{
path: '/dashboard/monitor',
name: 'monitor',
component: './Dashboard/Monitor',
},
{
path: '/dashboard/workplace',
name: 'workplace',
component: './Dashboard/Workplace',
},
],
},
// forms
{
path: '/form',
icon: 'form',
name: 'form',
routes: [
{
path: '/form/basic-form',
name: 'basicform',
component: './Forms/BasicForm',
},
{
path: '/form/step-form',
name: 'stepform',
component: './Forms/StepForm',
hideChildrenInMenu: true,
routes: [
{
path: '/form/step-form',
redirect: '/form/step-form/info',
},
{
path: '/form/step-form/info',
name: 'info',
component: './Forms/StepForm/Step1',
},
{
path: '/form/step-form/confirm',
name: 'confirm',
component: './Forms/StepForm/Step2',
},
{
path: '/form/step-form/result',
name: 'result',
component: './Forms/StepForm/Step3',
},
],
},
{
path: '/form/advanced-form',
name: 'advancedform',
authority: ['admin'],
component: './Forms/AdvancedForm',
},
],
},
// list
{
path: '/list',
icon: 'table',
name: 'list',
routes: [
{
path: '/list/table-list',
name: 'searchtable',
component: './List/TableList',
},
{
path: '/list/basic-list',
name: 'basiclist',
component: './List/BasicList',
},
{
path: '/list/card-list',
name: 'cardlist',
component: './List/CardList',
},
{
path: '/list/search',
name: 'searchlist',
component: './List/List',
routes: [
{
path: '/list/search',
redirect: '/list/search/articles',
},
{
path: '/list/search/articles',
name: 'articles',
component: './List/Articles',
},
{
path: '/list/search/projects',
name: 'projects',
component: './List/Projects',
},
{
path: '/list/search/applications',
name: 'applications',
component: './List/Applications',
},
],
},
],
},
{
path: '/profile',
name: 'profile',
icon: 'profile',
routes: [
// profile
{
path: '/profile/basic',
name: 'basic',
component: './Profile/BasicProfile',
},
{
path: '/profile/advanced',
name: 'advanced',
authority: ['admin'],
component: './Profile/AdvancedProfile',
},
],
},
{
name: 'result',
icon: 'check-circle-o',
path: '/result',
routes: [
// result
{
path: '/result/success',
name: 'success',
component: './Result/Success',
},
{ path: '/result/fail', name: 'fail', component: './Result/Error' },
],
},
{
name: 'exception',
icon: 'warning',
path: '/exception',
routes: [
// exception
{
path: '/exception/403',
name: 'not-permission',
component: './Exception/403',
},
{
path: '/exception/404',
name: 'not-find',
component: './Exception/404',
},
{
path: '/exception/500',
name: 'server-error',
component: './Exception/500',
},
{
path: '/exception/trigger',
name: 'trigger',
hideInMenu: true,
component: './Exception/TriggerException',
},
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
path: '/account/center',
name: 'center',
component: './Account/Center/Center',
routes: [
{
path: '/account/center',
redirect: '/account/center/articles',
},
{
path: '/account/center/articles',
component: './Account/Center/Articles',
},
{
path: '/account/center/applications',
component: './Account/Center/Applications',
},
{
path: '/account/center/projects',
component: './Account/Center/Projects',
},
],
},
{
path: '/account/settings',
name: 'settings',
component: './Account/Settings/Info',
routes: [
{
path: '/account/settings',
redirect: '/account/settings/base',
},
{
path: '/account/settings/base',
component: './Account/Settings/BaseView',
},
{
path: '/account/settings/security',
component: './Account/Settings/SecurityView',
},
{
path: '/account/settings/binding',
component: './Account/Settings/BindingView',
},
{
path: '/account/settings/notification',
component: './Account/Settings/NotificationView',
},
],
},
],
},
{
component: '404',
},
],
},
];
...@@ -14,7 +14,6 @@ import logo from '../assets/logo.svg'; ...@@ -14,7 +14,6 @@ import logo from '../assets/logo.svg';
import Footer from './Footer'; import Footer from './Footer';
import Header from './Header'; import Header from './Header';
import Context from './MenuContext'; import Context from './MenuContext';
import Exception403 from '../pages/Exception/403';
import PageLoading from '@/components/PageLoading'; import PageLoading from '@/components/PageLoading';
import SiderMenu from '@/components/SiderMenu'; import SiderMenu from '@/components/SiderMenu';
import { title } from '../defaultSettings'; import { title } from '../defaultSettings';
...@@ -25,6 +24,8 @@ const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer')); ...@@ -25,6 +24,8 @@ const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
const { Content } = Layout; const { Content } = Layout;
const Exception403 = <p>Exception403</p>;
const query = { const query = {
'screen-xs': { 'screen-xs': {
maxWidth: 575, maxWidth: 575,
......
import React from 'react';
import Link from 'umi/link';
import { formatMessage } from 'umi/locale';
import Exception from 'ant-design-pro/lib/Exception';
export default () => (
<Exception
type="404"
linkElement={Link}
desc={formatMessage({ id: 'app.exception.description.404' })}
backText={formatMessage({ id: 'app.exception.back' })}
/>
);
import React, { PureComponent } from 'react';
import { List, Card, Icon, Dropdown, Menu, Avatar, Tooltip } from 'antd';
import numeral from 'numeral';
import { connect } from 'dva';
import { formatWan } from '@/utils/utils';
import stylesApplications from '../../List/Applications.less';
@connect(({ list }) => ({
list,
}))
class Center extends PureComponent {
render() {
const {
list: { list },
} = this.props;
const itemMenu = (
<Menu>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="https://www.alipay.com/">
1st menu item
</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="https://www.taobao.com/">
2nd menu item
</a>
</Menu.Item>
<Menu.Item>
<a target="_blank" rel="noopener noreferrer" href="https://www.tmall.com/">
3d menu item
</a>
</Menu.Item>
</Menu>
);
const CardInfo = ({ activeUser, newUser }) => (
<div className={stylesApplications.cardInfo}>
<div>
<p>活跃用户</p>
<p>{activeUser}</p>
</div>
<div>
<p>新增用户</p>
<p>{newUser}</p>
</div>
</div>
);
return (
<List
rowKey="id"
className={stylesApplications.filterCardList}
grid={{ gutter: 24, xxl: 3, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }}
dataSource={list}
renderItem={item => (
<List.Item key={item.id}>
<Card
hoverable
bodyStyle={{ paddingBottom: 20 }}
actions={[
<Tooltip title="下载">
<Icon type="download" />
</Tooltip>,
<Tooltip title="编辑">
<Icon type="edit" />
</Tooltip>,
<Tooltip title="分享">
<Icon type="share-alt" />
</Tooltip>,
<Dropdown overlay={itemMenu}>
<Icon type="ellipsis" />
</Dropdown>,
]}
>
<Card.Meta avatar={<Avatar size="small" src={item.avatar} />} title={item.title} />
<div className={stylesApplications.cardItemContent}>
<CardInfo
activeUser={formatWan(item.activeUser)}
newUser={numeral(item.newUser).format('0,0')}
/>
</div>
</Card>
</List.Item>
)}
/>
);
}
}
export default Center;
import React, { PureComponent } from 'react';
import { List, Icon, Tag } from 'antd';
import { connect } from 'dva';
import ArticleListContent from '@/components/ArticleListContent';
import styles from './Articles.less';
@connect(({ list }) => ({
list,
}))
class Center extends PureComponent {
render() {
const {
list: { list },
} = this.props;
const IconText = ({ type, text }) => (
<span>
<Icon type={type} style={{ marginRight: 8 }} />
{text}
</span>
);
return (
<List
size="large"
className={styles.articleList}
rowKey="id"
itemLayout="vertical"
dataSource={list}
renderItem={item => (
<List.Item
key={item.id}
actions={[
<IconText type="star-o" text={item.star} />,
<IconText type="like-o" text={item.like} />,
<IconText type="message" text={item.message} />,
]}
>
<List.Item.Meta
title={
<a className={styles.listItemMetaTitle} href={item.href}>
{item.title}
</a>
}
description={
<span>
<Tag>Ant Design</Tag>
<Tag>设计语言</Tag>
<Tag>蚂蚁金服</Tag>
</span>
}
/>
<ArticleListContent data={item} />
</List.Item>
)}
/>
);
}
}
export default Center;
@import '~antd/lib/style/themes/default.less';
.articleList {
:global {
.ant-list-item:first-child {
padding-top: 0;
}
}
}
a.listItemMetaTitle {
color: @heading-color;
}
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import Link from 'umi/link';
import router from 'umi/router';
import { Card, Row, Col, Icon, Avatar, Tag, Divider, Spin, Input } from 'antd';
import GridContent from '@/components/PageHeaderWrapper/GridContent';
import styles from './Center.less';
@connect(({ loading, user, project }) => ({
listLoading: loading.effects['list/fetch'],
currentUser: user.currentUser,
currentUserLoading: loading.effects['user/fetchCurrent'],
project,
projectLoading: loading.effects['project/fetchNotice'],
}))
class Center extends PureComponent {
state = {
newTags: [],
inputVisible: false,
inputValue: '',
};
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'user/fetchCurrent',
});
dispatch({
type: 'list/fetch',
payload: {
count: 8,
},
});
dispatch({
type: 'project/fetchNotice',
});
}
onTabChange = key => {
const { match } = this.props;
switch (key) {
case 'articles':
router.push(`${match.url}/articles`);
break;
case 'applications':
router.push(`${match.url}/applications`);
break;
case 'projects':
router.push(`${match.url}/projects`);
break;
default:
break;
}
};
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
};
saveInputRef = input => {
this.input = input;
};
handleInputChange = e => {
this.setState({ inputValue: e.target.value });
};
handleInputConfirm = () => {
const { state } = this;
const { inputValue } = state;
let { newTags } = state;
if (inputValue && newTags.filter(tag => tag.label === inputValue).length === 0) {
newTags = [...newTags, { key: `new-${newTags.length}`, label: inputValue }];
}
this.setState({
newTags,
inputVisible: false,
inputValue: '',
});
};
render() {
const { newTags, inputVisible, inputValue } = this.state;
const {
listLoading,
currentUser,
currentUserLoading,
project: { notice },
projectLoading,
match,
location,
children,
} = this.props;
const operationTabList = [
{
key: 'articles',
tab: (
<span>
文章 <span style={{ fontSize: 14 }}>(8)</span>
</span>
),
},
{
key: 'applications',
tab: (
<span>
应用 <span style={{ fontSize: 14 }}>(8)</span>
</span>
),
},
{
key: 'projects',
tab: (
<span>
项目 <span style={{ fontSize: 14 }}>(8)</span>
</span>
),
},
];
return (
<GridContent className={styles.userCenter}>
<Row gutter={24}>
<Col lg={7} md={24}>
<Card bordered={false} style={{ marginBottom: 24 }} loading={currentUserLoading}>
{currentUser && Object.keys(currentUser).length ? (
<div>
<div className={styles.avatarHolder}>
<img alt="" src={currentUser.avatar} />
<div className={styles.name}>{currentUser.name}</div>
<div>{currentUser.signature}</div>
</div>
<div className={styles.detail}>
<p>
<i className={styles.title} />
{currentUser.title}
</p>
<p>
<i className={styles.group} />
{currentUser.group}
</p>
<p>
<i className={styles.address} />
{currentUser.geographic.province.label}
{currentUser.geographic.city.label}
</p>
</div>
<Divider dashed />
<div className={styles.tags}>
<div className={styles.tagsTitle}>标签</div>
{currentUser.tags.concat(newTags).map(item => (
<Tag key={item.key}>{item.label}</Tag>
))}
{inputVisible && (
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={this.handleInputChange}
onBlur={this.handleInputConfirm}
onPressEnter={this.handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={this.showInput}
style={{ background: '#fff', borderStyle: 'dashed' }}
>
<Icon type="plus" />
</Tag>
)}
</div>
<Divider style={{ marginTop: 16 }} dashed />
<div className={styles.team}>
<div className={styles.teamTitle}>团队</div>
<Spin spinning={projectLoading}>
<Row gutter={36}>
{notice.map(item => (
<Col key={item.id} lg={24} xl={12}>
<Link to={item.href}>
<Avatar size="small" src={item.logo} />
{item.member}
</Link>
</Col>
))}
</Row>
</Spin>
</div>
</div>
) : (
'loading...'
)}
</Card>
</Col>
<Col lg={17} md={24}>
<Card
className={styles.tabsCard}
bordered={false}
tabList={operationTabList}
activeTabKey={location.pathname.replace(`${match.path}/`, '')}
onTabChange={this.onTabChange}
loading={listLoading}
>
{children}
</Card>
</Col>
</Row>
</GridContent>
);
}
}
export default Center;
@import '~antd/lib/style/themes/default.less';
@import '~@/utils/utils.less';
.avatarHolder {
text-align: center;
margin-bottom: 24px;
& > img {
width: 104px;
height: 104px;
margin-bottom: 20px;
}
.name {
font-size: 20px;
line-height: 28px;
font-weight: 500;
color: @heading-color;
margin-bottom: 4px;
}
}
.detail {
p {
margin-bottom: 8px;
padding-left: 26px;
position: relative;
&:last-child {
margin-bottom: 0;
}
}
i {
position: absolute;
height: 14px;
width: 14px;
left: 0;
top: 4px;
background: url(https://gw.alipayobjects.com/zos/rmsportal/pBjWzVAHnOOtAUvZmZfy.svg);
&.title {
background-position: 0 0;
}
&.group {
background-position: 0 -22px;
}
&.address {
background-position: 0 -44px;
}
}
}
.tagsTitle,
.teamTitle {
font-weight: 500;
color: @heading-color;
margin-bottom: 12px;
}
.tags {
:global {
.ant-tag {
margin-bottom: 8px;
}
}
}
.team {
:global {
.ant-avatar {
margin-right: 12px;
}
}
a {
display: block;
margin-bottom: 24px;
color: @text-color;
transition: color 0.3s;
.textOverflow();
&:hover {
color: @primary-color;
}
}
}
.tabsCard {
:global {
.ant-card-head {
padding: 0 16px;
}
}
}
import React, { PureComponent } from 'react';
import { List, Card } from 'antd';
import moment from 'moment';
import { connect } from 'dva';
import AvatarList from 'ant-design-pro/lib/AvatarList';
import stylesProjects from '../../List/Projects.less';
@connect(({ list }) => ({
list,
}))
class Center extends PureComponent {
render() {
const {
list: { list },
} = this.props;
return (
<List
className={stylesProjects.coverCardList}
rowKey="id"
grid={{ gutter: 24, xxl: 3, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }}
dataSource={list}
renderItem={item => (
<List.Item>
<Card
className={stylesProjects.card}
hoverable
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta title={<a>{item.title}</a>} description={item.subDescription} />
<div className={stylesProjects.cardItemContent}>
<span>{moment(item.updatedAt).fromNow()}</span>
<div className={stylesProjects.avatarList}>
<AvatarList size="mini">
{item.members.map(member => (
<AvatarList.Item
key={`${item.id}-avatar-${member.id}`}
src={member.avatar}
tips={member.name}
/>
))}
</AvatarList>
</div>
</div>
</Card>
</List.Item>
)}
/>
);
}
}
export default Center;
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 GeographicView from './GeographicView';
import PhoneView from './PhoneView';
// import { getTimeDistance } from '@/utils/utils';
const FormItem = Form.Item;
const { Option } = Select;
// 头像组件 方便以后独立,增加裁剪之类的功能
const AvatarView = ({ avatar }) => (
<Fragment>
<div className={styles.avatar_title}>
<FormattedMessage id="app.settings.basic.avatar" defaultMessage="Avatar" />
</div>
<div className={styles.avatar}>
<img src={avatar} alt="avatar" />
</div>
<Upload fileList={[]}>
<div className={styles.button_view}>
<Button icon="upload">
<FormattedMessage id="app.settings.basic.change-avatar" defaultMessage="Change avatar" />
</Button>
</div>
</Upload>
</Fragment>
);
const validatorGeographic = (rule, value, callback) => {
const { province, city } = value;
if (!province.key) {
callback('Please input your province!');
}
if (!city.key) {
callback('Please input your city!');
}
callback();
};
const validatorPhone = (rule, value, callback) => {
const values = value.split('-');
if (!values[0]) {
callback('Please input your area code!');
}
if (!values[1]) {
callback('Please input your phone number!');
}
callback();
};
@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 (
<div className={styles.baseView} ref={this.getViewDom}>
<div className={styles.left}>
<Form layout="vertical" onSubmit={this.handleSubmit} hideRequiredMark>
<FormItem label={formatMessage({ id: 'app.settings.basic.email' })}>
{getFieldDecorator('email', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.email-message' }, {}),
},
],
})(<Input />)}
</FormItem>
<FormItem label={formatMessage({ id: 'app.settings.basic.nickname' })}>
{getFieldDecorator('name', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.nickname-message' }, {}),
},
],
})(<Input />)}
</FormItem>
<FormItem label={formatMessage({ id: 'app.settings.basic.profile' })}>
{getFieldDecorator('profile', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.profile-message' }, {}),
},
],
})(
<Input.TextArea
placeholder={formatMessage({ id: 'app.settings.basic.profile-placeholder' })}
rows={4}
/>
)}
</FormItem>
<FormItem label={formatMessage({ id: 'app.settings.basic.country' })}>
{getFieldDecorator('country', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.country-message' }, {}),
},
],
})(
<Select style={{ maxWidth: 220 }}>
<Option value="China">中国</Option>
</Select>
)}
</FormItem>
<FormItem label={formatMessage({ id: 'app.settings.basic.geographic' })}>
{getFieldDecorator('geographic', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.geographic-message' }, {}),
},
{
validator: validatorGeographic,
},
],
})(<GeographicView />)}
</FormItem>
<FormItem label={formatMessage({ id: 'app.settings.basic.address' })}>
{getFieldDecorator('address', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.address-message' }, {}),
},
],
})(<Input />)}
</FormItem>
<FormItem label={formatMessage({ id: 'app.settings.basic.phone' })}>
{getFieldDecorator('phone', {
rules: [
{
required: true,
message: formatMessage({ id: 'app.settings.basic.phone-message' }, {}),
},
{ validator: validatorPhone },
],
})(<PhoneView />)}
</FormItem>
<Button type="primary">
<FormattedMessage
id="app.settings.basic.update"
defaultMessage="Update Information"
/>
</Button>
</Form>
</div>
<div className={styles.right}>
<AvatarView avatar={this.getAvatarURL()} />
</div>
</div>
);
}
}
export default BaseView;
@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;
}
}
}
}
import React, { Component, Fragment } from 'react';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { Icon, List } from 'antd';
class BindingView extends Component {
getData = () => [
{
title: formatMessage({ id: 'app.settings.binding.taobao' }, {}),
description: formatMessage({ id: 'app.settings.binding.taobao-description' }, {}),
actions: [
<a>
<FormattedMessage id="app.settings.binding.bind" defaultMessage="Bind" />
</a>,
],
avatar: <Icon type="taobao" className="taobao" />,
},
{
title: formatMessage({ id: 'app.settings.binding.alipay' }, {}),
description: formatMessage({ id: 'app.settings.binding.alipay-description' }, {}),
actions: [
<a>
<FormattedMessage id="app.settings.binding.bind" defaultMessage="Bind" />
</a>,
],
avatar: <Icon type="alipay" className="alipay" />,
},
{
title: formatMessage({ id: 'app.settings.binding.dingding' }, {}),
description: formatMessage({ id: 'app.settings.binding.dingding-description' }, {}),
actions: [
<a>
<FormattedMessage id="app.settings.binding.bind" defaultMessage="Bind" />
</a>,
],
avatar: <Icon type="dingding" className="dingding" />,
},
];
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta
avatar={item.avatar}
title={item.title}
description={item.description}
/>
</List.Item>
)}
/>
</Fragment>
);
}
}
export default BindingView;
import React, { PureComponent } from 'react';
import { Select, Spin } from 'antd';
import { connect } from 'dva';
import styles from './GeographicView.less';
const { Option } = Select;
const nullSlectItem = {
label: '',
key: '',
};
@connect(({ geographic }) => {
const { province, isLoading, city } = geographic;
return {
province,
city,
isLoading,
};
})
class GeographicView extends PureComponent {
componentDidMount = () => {
const { dispatch } = this.props;
dispatch({
type: 'geographic/fetchProvince',
});
};
componentDidUpdate(props) {
const { dispatch, value } = this.props;
if (!props.value && !!value && !!value.province) {
dispatch({
type: 'geographic/fetchCity',
payload: value.province.key,
});
}
}
getProvinceOption() {
const { province } = this.props;
return this.getOption(province);
}
getCityOption = () => {
const { city } = this.props;
return this.getOption(city);
};
getOption = list => {
if (!list || list.length < 1) {
return (
<Option key={0} value={0}>
没有找到选项
</Option>
);
}
return list.map(item => (
<Option key={item.id} value={item.id}>
{item.name}
</Option>
));
};
selectProvinceItem = item => {
const { dispatch, onChange } = this.props;
dispatch({
type: 'geographic/fetchCity',
payload: item.key,
});
onChange({
province: item,
city: nullSlectItem,
});
};
selectCityItem = item => {
const { value, onChange } = this.props;
onChange({
province: value.province,
city: item,
});
};
conversionObject() {
const { value } = this.props;
if (!value) {
return {
province: nullSlectItem,
city: nullSlectItem,
};
}
const { province, city } = value;
return {
province: province || nullSlectItem,
city: city || nullSlectItem,
};
}
render() {
const { province, city } = this.conversionObject();
const { isLoading } = this.props;
return (
<Spin spinning={isLoading} wrapperClassName={styles.row}>
<Select
className={styles.item}
value={province}
labelInValue
showSearch
onSelect={this.selectProvinceItem}
>
{this.getProvinceOption()}
</Select>
<Select
className={styles.item}
value={city}
labelInValue
showSearch
onSelect={this.selectCityItem}
>
{this.getCityOption()}
</Select>
</Spin>
);
}
}
export default GeographicView;
@import '~antd/lib/style/themes/default.less';
.row {
.item {
max-width: 220px;
width: 50%;
}
.item:first-child {
margin-right: 8px;
width: ~'calc(50% - 8px)';
}
}
@media screen and (max-width: @screen-sm) {
.item:first-child {
margin: 0;
margin-bottom: 8px;
}
}
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: <FormattedMessage id="app.settings.menuMap.basic" defaultMessage="Basic Settings" />,
security: (
<FormattedMessage id="app.settings.menuMap.security" defaultMessage="Security Settings" />
),
binding: (
<FormattedMessage id="app.settings.menuMap.binding" defaultMessage="Account Binding" />
),
notification: (
<FormattedMessage
id="app.settings.menuMap.notification"
defaultMessage="New Message Notification"
/>
),
};
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 => <Item key={item}>{menuMap[item]}</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 (
<GridContent>
<div
className={styles.main}
ref={ref => {
this.main = ref;
}}
>
<div className={styles.leftmenu}>
<Menu mode={mode} selectedKeys={[selectKey]} onClick={this.selectKey}>
{this.getmenu()}
</Menu>
</div>
<div className={styles.right}>
<div className={styles.title}>{this.getRightTitle()}</div>
{children}
</div>
</div>
</GridContent>
);
}
}
export default Info;
@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;
}
}
}
import React, { Component, Fragment } from 'react';
import { formatMessage } from 'umi/locale';
import { Switch, List } from 'antd';
class NotificationView extends Component {
getData = () => {
const Action = (
<Switch
checkedChildren={formatMessage({ id: 'app.settings.open' })}
unCheckedChildren={formatMessage({ id: 'app.settings.close' })}
defaultChecked
/>
);
return [
{
title: formatMessage({ id: 'app.settings.notification.password' }, {}),
description: formatMessage({ id: 'app.settings.notification.password-description' }, {}),
actions: [Action],
},
{
title: formatMessage({ id: 'app.settings.notification.messages' }, {}),
description: formatMessage({ id: 'app.settings.notification.messages-description' }, {}),
actions: [Action],
},
{
title: formatMessage({ id: 'app.settings.notification.todo' }, {}),
description: formatMessage({ id: 'app.settings.notification.todo-description' }, {}),
actions: [Action],
},
];
};
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description} />
</List.Item>
)}
/>
</Fragment>
);
}
}
export default NotificationView;
import React, { Fragment, PureComponent } from 'react';
import { Input } from 'antd';
import styles from './PhoneView.less';
class PhoneView extends PureComponent {
render() {
const { value, onChange } = this.props;
let values = ['', ''];
if (value) {
values = value.split('-');
}
return (
<Fragment>
<Input
className={styles.area_code}
value={values[0]}
onChange={e => {
onChange(`${e.target.value}-${values[1]}`);
}}
/>
<Input
className={styles.phone_number}
onChange={e => {
onChange(`${values[0]}-${e.target.value}`);
}}
value={values[1]}
/>
</Fragment>
);
}
}
export default PhoneView;
@import '~antd/lib/style/themes/default.less';
.area_code {
max-width: 128px;
margin-right: 8px;
width: 30%;
}
.phone_number {
max-width: 312px;
width: ~'calc(70% - 8px)';
}
import React, { Component, Fragment } from 'react';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { List } from 'antd';
// import { getTimeDistance } from '@/utils/utils';
const passwordStrength = {
strong: (
<font className="strong">
<FormattedMessage id="app.settings.security.strong" defaultMessage="Strong" />
</font>
),
medium: (
<font className="medium">
<FormattedMessage id="app.settings.security.medium" defaultMessage="Medium" />
</font>
),
weak: (
<font className="weak">
<FormattedMessage id="app.settings.security.weak" defaultMessage="Weak" />
Weak
</font>
),
};
class SecurityView extends Component {
getData = () => [
{
title: formatMessage({ id: 'app.settings.security.password' }, {}),
description: (
<Fragment>
{formatMessage({ id: 'app.settings.security.password-description' })}
{passwordStrength.strong}
</Fragment>
),
actions: [
<a>
<FormattedMessage id="app.settings.security.modify" defaultMessage="Modify" />
</a>,
],
},
{
title: formatMessage({ id: 'app.settings.security.phone' }, {}),
description: `${formatMessage(
{ id: 'app.settings.security.phone-description' },
{}
)}:138****8293`,
actions: [
<a>
<FormattedMessage id="app.settings.security.modify" defaultMessage="Modify" />
</a>,
],
},
{
title: formatMessage({ id: 'app.settings.security.question' }, {}),
description: formatMessage({ id: 'app.settings.security.question-description' }, {}),
actions: [
<a>
<FormattedMessage id="app.settings.security.set" defaultMessage="Set" />
</a>,
],
},
{
title: formatMessage({ id: 'app.settings.security.email' }, {}),
description: `${formatMessage(
{ id: 'app.settings.security.email-description' },
{}
)}:ant***sign.com`,
actions: [
<a>
<FormattedMessage id="app.settings.security.modify" defaultMessage="Modify" />
</a>,
],
},
{
title: formatMessage({ id: 'app.settings.security.mfa' }, {}),
description: formatMessage({ id: 'app.settings.security.mfa-description' }, {}),
actions: [
<a>
<FormattedMessage id="app.settings.security.bind" defaultMessage="Bind" />
</a>,
],
},
];
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description} />
</List.Item>
)}
/>
</Fragment>
);
}
}
export default SecurityView;
import { queryProvince, queryCity } from '@/services/geographic';
export default {
namespace: 'geographic',
state: {
province: [],
city: [],
isLoading: false,
},
effects: {
*fetchProvince(_, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryProvince);
yield put({
type: 'setProvince',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
*fetchCity({ payload }, { call, put }) {
yield put({
type: 'changeLoading',
payload: true,
});
const response = yield call(queryCity, payload);
yield put({
type: 'setCity',
payload: response,
});
yield put({
type: 'changeLoading',
payload: false,
});
},
},
reducers: {
setProvince(state, action) {
return {
...state,
province: action.payload,
};
},
setCity(state, action) {
return {
...state,
city: action.payload,
};
},
changeLoading(state, action) {
return {
...state,
isLoading: action.payload,
};
},
},
};
import React from 'react';
export default () => <p>hello pro!</p>;
import React, { Component, Suspense } from 'react';
import { connect } from 'dva';
import { Row, Col, Icon, Menu, Dropdown } from 'antd';
import GridContent from '@/components/PageHeaderWrapper/GridContent';
import { getTimeDistance } from '@/utils/utils';
import styles from './Analysis.less';
import PageLoading from '@/components/PageLoading';
const IntroduceRow = React.lazy(() => import('./IntroduceRow'));
const SalesCard = React.lazy(() => import('./SalesCard'));
const TopSearch = React.lazy(() => import('./TopSearch'));
const ProportionSales = React.lazy(() => import('./ProportionSales'));
const OfflineData = React.lazy(() => import('./OfflineData'));
@connect(({ chart, loading }) => ({
chart,
loading: loading.effects['chart/fetch'],
}))
class Analysis extends Component {
state = {
salesType: 'all',
currentTabKey: '',
rangePickerValue: getTimeDistance('year'),
};
componentDidMount() {
const { dispatch } = this.props;
this.reqRef = requestAnimationFrame(() => {
dispatch({
type: 'chart/fetch',
});
});
}
componentWillUnmount() {
const { dispatch } = this.props;
dispatch({
type: 'chart/clear',
});
cancelAnimationFrame(this.reqRef);
clearTimeout(this.timeoutId);
}
handleChangeSalesType = e => {
this.setState({
salesType: e.target.value,
});
};
handleTabChange = key => {
this.setState({
currentTabKey: key,
});
};
handleRangePickerChange = rangePickerValue => {
const { dispatch } = this.props;
this.setState({
rangePickerValue,
});
dispatch({
type: 'chart/fetchSalesData',
});
};
selectDate = type => {
const { dispatch } = this.props;
this.setState({
rangePickerValue: getTimeDistance(type),
});
dispatch({
type: 'chart/fetchSalesData',
});
};
isActive = type => {
const { rangePickerValue } = this.state;
const value = getTimeDistance(type);
if (!rangePickerValue[0] || !rangePickerValue[1]) {
return '';
}
if (
rangePickerValue[0].isSame(value[0], 'day') &&
rangePickerValue[1].isSame(value[1], 'day')
) {
return styles.currentDate;
}
return '';
};
render() {
const { rangePickerValue, salesType, currentTabKey } = this.state;
const { chart, loading } = this.props;
const {
visitData,
visitData2,
salesData,
searchData,
offlineData,
offlineChartData,
salesTypeData,
salesTypeDataOnline,
salesTypeDataOffline,
} = chart;
let salesPieData;
if (salesType === 'all') {
salesPieData = salesTypeData;
} else {
salesPieData = salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline;
}
const menu = (
<Menu>
<Menu.Item>操作一</Menu.Item>
<Menu.Item>操作二</Menu.Item>
</Menu>
);
const dropdownGroup = (
<span className={styles.iconGroup}>
<Dropdown overlay={menu} placement="bottomRight">
<Icon type="ellipsis" />
</Dropdown>
</span>
);
const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
return (
<GridContent>
<Suspense fallback={<PageLoading />}>
<IntroduceRow loading={loading} visitData={visitData} />
</Suspense>
<Suspense fallback={null}>
<SalesCard
rangePickerValue={rangePickerValue}
salesData={salesData}
isActive={this.isActive}
handleRangePickerChange={this.handleRangePickerChange}
loading={loading}
selectDate={this.selectDate}
/>
</Suspense>
<div className={styles.twoColLayout}>
<Row gutter={24}>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<TopSearch
loading={loading}
visitData2={visitData2}
selectDate={this.selectDate}
searchData={searchData}
dropdownGroup={dropdownGroup}
/>
</Suspense>
</Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<ProportionSales
dropdownGroup={dropdownGroup}
salesType={salesType}
loading={loading}
salesPieData={salesPieData}
handleChangeSalesType={this.handleChangeSalesType}
/>
</Suspense>
</Col>
</Row>
</div>
<Suspense fallback={null}>
<OfflineData
activeKey={activeKey}
loading={loading}
offlineData={offlineData}
offlineChartData={offlineChartData}
handleTabChange={this.handleTabChange}
/>
</Suspense>
</GridContent>
);
}
}
export default Analysis;
@import '~antd/lib/style/themes/default.less';
@import '~@/utils/utils.less';
.iconGroup {
i {
transition: color 0.32s;
color: @text-color-secondary;
cursor: pointer;
margin-left: 16px;
&:hover {
color: @text-color;
}
}
}
.rankingList {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
.clearfix();
margin-top: 16px;
display: flex;
align-items: center;
span {
color: @text-color;
font-size: 14px;
line-height: 22px;
}
.rankingItemNumber {
background-color: @background-color-base;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 16px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
margin-top: 1.5px;
&.active {
background-color: #314659;
color: #fff;
}
}
.rankingItemTitle {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 8px;
}
}
}
.salesExtra {
display: inline-block;
margin-right: 24px;
a {
color: @text-color;
margin-left: 24px;
&:hover {
color: @primary-color;
}
&.currentDate {
color: @primary-color;
}
}
}
.salesCard {
.salesBar {
padding: 0 0 32px 32px;
}
.salesRank {
padding: 0 32px 32px 72px;
}
:global {
.ant-tabs-bar {
padding-left: 16px;
.ant-tabs-nav .ant-tabs-tab {
padding-top: 16px;
padding-bottom: 14px;
line-height: 24px;
}
}
.ant-tabs-extra-content {
padding-right: 24px;
line-height: 55px;
}
.ant-card-head {
position: relative;
}
.ant-card-head-title {
align-items: normal;
}
}
}
.salesCardExtra {
height: inherit;
}
.salesTypeRadio {
position: absolute;
right: 54px;
bottom: 12px;
}
.offlineCard {
:global {
.ant-tabs-ink-bar {
bottom: auto;
}
.ant-tabs-bar {
border-bottom: none;
}
.ant-tabs-nav-container-scrolling {
padding-left: 40px;
padding-right: 40px;
}
.ant-tabs-tab-prev-icon:before {
position: relative;
left: 6px;
}
.ant-tabs-tab-next-icon:before {
position: relative;
right: 6px;
}
.ant-tabs-tab-active h4 {
color: @primary-color;
}
}
}
.twoColLayout {
.salesCard {
height: calc(100% - 24px);
}
div[class^='ant-col']:last-child {
right: 0\9;
height: 100%\9;
position: absolute\9;
}
:global {
.ant-row {
display: flex;
display: block\9;
flex-flow: row wrap;
position: relative\9;
}
}
}
.trendText {
margin-left: 8px;
color: @heading-color;
}
@media screen and (max-width: @screen-lg) {
.salesExtra {
display: none;
}
.rankingList {
li {
span:first-child {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-md) {
.rankingTitle {
margin-top: 16px;
}
.salesCard .salesBar {
padding: 16px;
}
}
@media screen and (max-width: @screen-sm) {
.salesExtraWrap {
display: none;
}
.salesCard {
:global {
.ant-tabs-content {
padding-top: 30px;
}
}
}
}
import React, { memo } from 'react';
import { Row, Col, Icon, Tooltip } from 'antd';
import { FormattedMessage } from 'umi/locale';
import styles from './Analysis.less';
import { ChartCard, MiniArea, MiniBar, MiniProgress, Field } from 'ant-design-pro/lib/Charts';
import Trend from 'ant-design-pro/lib/Trend';
import numeral from 'numeral';
import Yuan from '@/utils/Yuan';
const topColResponsiveProps = {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 6,
style: { marginBottom: 24 },
};
const IntroduceRow = memo(({ loading, visitData }) => (
<Row gutter={24}>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
title={<FormattedMessage id="app.analysis.total-sales" defaultMessage="Total Sales" />}
action={
<Tooltip
title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
>
<Icon type="info-circle-o" />
</Tooltip>
}
loading={loading}
total={() => <Yuan>126560</Yuan>}
footer={
<Field
label={<FormattedMessage id="app.analysis.day-sales" defaultMessage="Daily Sales" />}
value={`¥${numeral(12423).format('0,0')}`}
/>
}
contentHeight={46}
>
<Trend flag="up" style={{ marginRight: 16 }}>
<FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
<FormattedMessage id="app.analysis.day" defaultMessage="Daily Changes" />
<span className={styles.trendText}>11%</span>
</Trend>
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
loading={loading}
title={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
action={
<Tooltip
title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
>
<Icon type="info-circle-o" />
</Tooltip>
}
total={numeral(8846).format('0,0')}
footer={
<Field
label={<FormattedMessage id="app.analysis.day-visits" defaultMessage="Daily Visits" />}
value={numeral(1234).format('0,0')}
/>
}
contentHeight={46}
>
<MiniArea color="#975FE4" data={visitData} />
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
bordered={false}
loading={loading}
title={<FormattedMessage id="app.analysis.payments" defaultMessage="Payments" />}
action={
<Tooltip
title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
>
<Icon type="info-circle-o" />
</Tooltip>
}
total={numeral(6560).format('0,0')}
footer={
<Field
label={
<FormattedMessage
id="app.analysis.conversion-rate"
defaultMessage="Conversion Rate"
/>
}
value="60%"
/>
}
contentHeight={46}
>
<MiniBar data={visitData} />
</ChartCard>
</Col>
<Col {...topColResponsiveProps}>
<ChartCard
loading={loading}
bordered={false}
title={
<FormattedMessage
id="app.analysis.operational-effect"
defaultMessage="Operational Effect"
/>
}
action={
<Tooltip
title={<FormattedMessage id="app.analysis.introduce" defaultMessage="Introduce" />}
>
<Icon type="info-circle-o" />
</Tooltip>
}
total="78%"
footer={
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<Trend flag="up" style={{ marginRight: 16 }}>
<FormattedMessage id="app.analysis.week" defaultMessage="Weekly Changes" />
<span className={styles.trendText}>12%</span>
</Trend>
<Trend flag="down">
<FormattedMessage id="app.analysis.day" defaultMessage="Weekly Changes" />
<span className={styles.trendText}>11%</span>
</Trend>
</div>
}
contentHeight={46}
>
<MiniProgress percent={78} strokeWidth={8} target={80} color="#13C2C2" />
</ChartCard>
</Col>
</Row>
));
export default IntroduceRow;
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { Row, Col, Card, Tooltip } from 'antd';
import { Pie, WaterWave, Gauge, TagCloud } from 'ant-design-pro/lib/Charts';
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
import CountDown from 'ant-design-pro/lib/CountDown';
import ActiveChart from '@/components/ActiveChart';
import numeral from 'numeral';
import GridContent from '@/components/PageHeaderWrapper/GridContent';
import Authorized from '@/utils/Authorized';
import styles from './Monitor.less';
const { Secured } = Authorized;
const targetTime = new Date().getTime() + 3900000;
// use permission as a parameter
const havePermissionAsync = new Promise(resolve => {
// Call resolve on behalf of passed
setTimeout(() => resolve(), 300);
});
@Secured(havePermissionAsync)
@connect(({ monitor, loading }) => ({
monitor,
loading: loading.models.monitor,
}))
class Monitor extends PureComponent {
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'monitor/fetchTags',
});
}
render() {
const { monitor, loading } = this.props;
const { tags } = monitor;
return (
<GridContent>
<Row gutter={24}>
<Col xl={18} lg={24} md={24} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card
title={
<FormattedMessage
id="app.monitor.trading-activity"
defaultMessage="Real-Time Trading Activity"
/>
}
bordered={false}
>
<Row>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle={
<FormattedMessage
id="app.monitor.total-transactions"
defaultMessage="Total transactions today"
/>
}
suffix=""
total={numeral(124543233).format('0,0')}
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle={
<FormattedMessage
id="app.monitor.sales-target"
defaultMessage="Sales target completion rate"
/>
}
total="92%"
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle={
<FormattedMessage
id="app.monitor.remaining-time"
defaultMessage="Remaining time of activity"
/>
}
total={<CountDown target={targetTime} />}
/>
</Col>
<Col md={6} sm={12} xs={24}>
<NumberInfo
subTitle={
<FormattedMessage
id="app.monitor.total-transactions-per-second"
defaultMessage="Total transactions per second"
/>
}
suffix=""
total={numeral(234).format('0,0')}
/>
</Col>
</Row>
<div className={styles.mapChart}>
<Tooltip
title={
<FormattedMessage
id="app.monitor.waiting-for-implementation"
defaultMessage="Waiting for implementation"
/>
}
>
<img
src="https://gw.alipayobjects.com/zos/rmsportal/HBWnDEUXCnGnGrRfrpKa.png"
alt="map"
/>
</Tooltip>
</div>
</Card>
</Col>
<Col xl={6} lg={24} md={24} sm={24} xs={24}>
<Card
title={
<FormattedMessage
id="app.monitor.activity-forecast"
defaultMessage="Activity forecast"
/>
}
style={{ marginBottom: 24 }}
bordered={false}
>
<ActiveChart />
</Card>
<Card
title={<FormattedMessage id="app.monitor.efficiency" defaultMessage="Efficiency" />}
style={{ marginBottom: 24 }}
bodyStyle={{ textAlign: 'center' }}
bordered={false}
>
<Gauge
title={formatMessage({ id: 'app.monitor.ratio', defaultMessage: 'Ratio' })}
height={180}
percent={87}
/>
</Card>
</Col>
</Row>
<Row gutter={24}>
<Col xl={12} lg={24} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card
title={
<FormattedMessage
id="app.monitor.proportion-per-category"
defaultMessage="Proportion Per Category"
/>
}
bordered={false}
className={styles.pieCard}
>
<Row style={{ padding: '16px 0' }}>
<Col span={8}>
<Pie
animate={false}
percent={28}
subTitle={
<FormattedMessage id="app.monitor.fast-food" defaultMessage="Fast food" />
}
total="28%"
height={128}
lineWidth={2}
/>
</Col>
<Col span={8}>
<Pie
animate={false}
color="#5DDECF"
percent={22}
subTitle={
<FormattedMessage
id="app.monitor.western-food"
defaultMessage="Western food"
/>
}
total="22%"
height={128}
lineWidth={2}
/>
</Col>
<Col span={8}>
<Pie
animate={false}
color="#2FC25B"
percent={32}
subTitle={
<FormattedMessage id="app.monitor.hot-pot" defaultMessage="Hot pot" />
}
total="32%"
height={128}
lineWidth={2}
/>
</Col>
</Row>
</Card>
</Col>
<Col xl={6} lg={12} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card
title={
<FormattedMessage
id="app.monitor.popular-searches"
defaultMessage="Popular Searches"
/>
}
loading={loading}
bordered={false}
bodyStyle={{ overflow: 'hidden' }}
>
<TagCloud data={tags} height={161} />
</Card>
</Col>
<Col xl={6} lg={12} sm={24} xs={24} style={{ marginBottom: 24 }}>
<Card
title={
<FormattedMessage
id="app.monitor.resource-surplus"
defaultMessage="Resource Surplus"
/>
}
bodyStyle={{ textAlign: 'center', fontSize: 0 }}
bordered={false}
>
<WaterWave
height={161}
title={
<FormattedMessage id="app.monitor.fund-surplus" defaultMessage="Fund Surplus" />
}
percent={34}
/>
</Card>
</Col>
</Row>
</GridContent>
);
}
}
export default Monitor;
@import '~antd/lib/style/themes/default.less';
@import '~@/utils/utils.less';
.mapChart {
padding-top: 24px;
height: 452px;
text-align: center;
img {
display: inline-block;
max-width: 100%;
max-height: 437px;
}
}
.pieCard :global(.pie-stat) {
font-size: 24px !important;
}
@media screen and (max-width: @screen-lg) {
.mapChart {
height: auto;
}
}
import React, { memo } from 'react';
import { Card, Tabs, Row, Col } from 'antd';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { TimelineChart, Pie } from 'ant-design-pro/lib/Charts';
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
import styles from './Analysis.less';
const CustomTab = ({ data, currentTabKey: currentKey }) => (
<Row gutter={8} style={{ width: 138, margin: '8px 0' }}>
<Col span={12}>
<NumberInfo
title={data.name}
subTitle={
<FormattedMessage id="app.analysis.conversion-rate" defaultMessage="Conversion Rate" />
}
gap={2}
total={`${data.cvr * 100}%`}
theme={currentKey !== data.name && 'light'}
/>
</Col>
<Col span={12} style={{ paddingTop: 36 }}>
<Pie
animate={false}
color={currentKey !== data.name && '#BDE4FF'}
inner={0.55}
tooltip={false}
margin={[0, 0, 0, 0]}
percent={data.cvr * 100}
height={64}
/>
</Col>
</Row>
);
const { TabPane } = Tabs;
const OfflineData = memo(
({ activeKey, loading, offlineData, offlineChartData, handleTabChange }) => (
<Card
loading={loading}
className={styles.offlineCard}
bordered={false}
style={{ marginTop: 32 }}
>
<Tabs activeKey={activeKey} onChange={handleTabChange}>
{offlineData.map(shop => (
<TabPane tab={<CustomTab data={shop} currentTabKey={activeKey} />} key={shop.name}>
<div style={{ padding: '0 24px' }}>
<TimelineChart
height={400}
data={offlineChartData}
titleMap={{
y1: formatMessage({ id: 'app.analysis.traffic' }),
y2: formatMessage({ id: 'app.analysis.payments' }),
}}
/>
</div>
</TabPane>
))}
</Tabs>
</Card>
)
);
export default OfflineData;
import React, { memo } from 'react';
import { Card, Radio } from 'antd';
import { FormattedMessage } from 'umi/locale';
import styles from './Analysis.less';
import { Pie } from 'ant-design-pro/lib/Charts';
import Yuan from '@/utils/Yuan';
const ProportionSales = memo(
({ dropdownGroup, salesType, loading, salesPieData, handleChangeSalesType }) => (
<Card
loading={loading}
className={styles.salesCard}
bordered={false}
title={
<FormattedMessage
id="app.analysis.the-proportion-of-sales"
defaultMessage="The Proportion of Sales"
/>
}
bodyStyle={{ padding: 24 }}
extra={
<div className={styles.salesCardExtra}>
{dropdownGroup}
<div className={styles.salesTypeRadio}>
<Radio.Group value={salesType} onChange={handleChangeSalesType}>
<Radio.Button value="all">
<FormattedMessage id="app.analysis.channel.all" defaultMessage="ALL" />
</Radio.Button>
<Radio.Button value="online">
<FormattedMessage id="app.analysis.channel.online" defaultMessage="Online" />
</Radio.Button>
<Radio.Button value="stores">
<FormattedMessage id="app.analysis.channel.stores" defaultMessage="Stores" />
</Radio.Button>
</Radio.Group>
</div>
</div>
}
style={{ marginTop: 24 }}
>
<h4 style={{ marginTop: 10, marginBottom: 32 }}>
<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />
</h4>
<Pie
hasLegend
subTitle={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
total={() => <Yuan>{salesPieData.reduce((pre, now) => now.y + pre, 0)}</Yuan>}
data={salesPieData}
valueFormat={value => <Yuan>{value}</Yuan>}
height={270}
lineWidth={4}
style={{ padding: '8px 0' }}
/>
</Card>
)
);
export default ProportionSales;
import React, { memo } from 'react';
import { Row, Col, Card, Tabs, DatePicker } from 'antd';
import { FormattedMessage, formatMessage } from 'umi/locale';
import numeral from 'numeral';
import { Bar } from 'ant-design-pro/lib/Charts';
import styles from './Analysis.less';
const { RangePicker } = DatePicker;
const { TabPane } = Tabs;
const rankingListData = [];
for (let i = 0; i < 7; i += 1) {
rankingListData.push({
title: formatMessage({ id: 'app.analysis.test' }, { no: i }),
total: 323234,
});
}
const SalesCard = memo(
({ rangePickerValue, salesData, isActive, handleRangePickerChange, loading, selectDate }) => (
<Card loading={loading} bordered={false} bodyStyle={{ padding: 0 }}>
<div className={styles.salesCard}>
<Tabs
tabBarExtraContent={
<div className={styles.salesExtraWrap}>
<div className={styles.salesExtra}>
<a className={isActive('today')} onClick={() => selectDate('today')}>
<FormattedMessage id="app.analysis.all-day" defaultMessage="All Day" />
</a>
<a className={isActive('week')} onClick={() => selectDate('week')}>
<FormattedMessage id="app.analysis.all-week" defaultMessage="All Week" />
</a>
<a className={isActive('month')} onClick={() => selectDate('month')}>
<FormattedMessage id="app.analysis.all-month" defaultMessage="All Month" />
</a>
<a className={isActive('year')} onClick={() => selectDate('year')}>
<FormattedMessage id="app.analysis.all-year" defaultMessage="All Year" />
</a>
</div>
<RangePicker
value={rangePickerValue}
onChange={handleRangePickerChange}
style={{ width: 256 }}
/>
</div>
}
size="large"
tabBarStyle={{ marginBottom: 24 }}
>
<TabPane
tab={<FormattedMessage id="app.analysis.sales" defaultMessage="Sales" />}
key="sales"
>
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Bar
height={295}
title={
<FormattedMessage
id="app.analysis.sales-trend"
defaultMessage="Sales Trend"
/>
}
data={salesData}
/>
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>
<FormattedMessage
id="app.analysis.sales-ranking"
defaultMessage="Sales Ranking"
/>
</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span
className={`${styles.rankingItemNumber} ${i < 3 ? styles.active : ''}`}
>
{i + 1}
</span>
<span className={styles.rankingItemTitle} title={item.title}>
{item.title}
</span>
<span className={styles.rankingItemValue}>
{numeral(item.total).format('0,0')}
</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
</TabPane>
<TabPane
tab={<FormattedMessage id="app.analysis.visits" defaultMessage="Visits" />}
key="views"
>
<Row>
<Col xl={16} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesBar}>
<Bar
height={292}
title={
<FormattedMessage
id="app.analysis.visits-trend"
defaultMessage="Visits Trend"
/>
}
data={salesData}
/>
</div>
</Col>
<Col xl={8} lg={12} md={12} sm={24} xs={24}>
<div className={styles.salesRank}>
<h4 className={styles.rankingTitle}>
<FormattedMessage
id="app.analysis.visits-ranking"
defaultMessage="Visits Ranking"
/>
</h4>
<ul className={styles.rankingList}>
{rankingListData.map((item, i) => (
<li key={item.title}>
<span
className={`${styles.rankingItemNumber} ${i < 3 ? styles.active : ''}`}
>
{i + 1}
</span>
<span className={styles.rankingItemTitle} title={item.title}>
{item.title}
</span>
<span>{numeral(item.total).format('0,0')}</span>
</li>
))}
</ul>
</div>
</Col>
</Row>
</TabPane>
</Tabs>
</div>
</Card>
)
);
export default SalesCard;
import React, { memo } from 'react';
import { Row, Col, Table, Tooltip, Card, Icon } from 'antd';
import { FormattedMessage } from 'umi/locale';
import Trend from 'ant-design-pro/lib/Trend';
import numeral from 'numeral';
import NumberInfo from 'ant-design-pro/lib/NumberInfo';
import { MiniArea } from 'ant-design-pro/lib/Charts';
import styles from './Analysis.less';
const columns = [
{
title: <FormattedMessage id="app.analysis.table.rank" defaultMessage="Rank" />,
dataIndex: 'index',
key: 'index',
},
{
title: (
<FormattedMessage id="app.analysis.table.search-keyword" defaultMessage="Search keyword" />
),
dataIndex: 'keyword',
key: 'keyword',
render: text => <a href="/">{text}</a>,
},
{
title: <FormattedMessage id="app.analysis.table.users" defaultMessage="Users" />,
dataIndex: 'count',
key: 'count',
sorter: (a, b) => a.count - b.count,
className: styles.alignRight,
},
{
title: <FormattedMessage id="app.analysis.table.weekly-range" defaultMessage="Weekly Range" />,
dataIndex: 'range',
key: 'range',
sorter: (a, b) => a.range - b.range,
render: (text, record) => (
<Trend flag={record.status === 1 ? 'down' : 'up'}>
<span style={{ marginRight: 4 }}>{text}%</span>
</Trend>
),
align: 'right',
},
];
const TopSearch = memo(({ loading, visitData2, searchData, dropdownGroup }) => (
<Card
loading={loading}
bordered={false}
title={
<FormattedMessage id="app.analysis.online-top-search" defaultMessage="Online Top Search" />
}
extra={dropdownGroup}
style={{ marginTop: 24 }}
>
<Row gutter={68}>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle={
<span>
<FormattedMessage id="app.analysis.search-users" defaultMessage="search users" />
<Tooltip
title={<FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />}
>
<Icon style={{ marginLeft: 8 }} type="info-circle-o" />
</Tooltip>
</span>
}
gap={8}
total={numeral(12321).format('0,0')}
status="up"
subTotal={17.1}
/>
<MiniArea line height={45} data={visitData2} />
</Col>
<Col sm={12} xs={24} style={{ marginBottom: 24 }}>
<NumberInfo
subTitle={
<span>
<FormattedMessage
id="app.analysis.per-capita-search"
defaultMessage="Per Capita Search"
/>
<Tooltip
title={<FormattedMessage id="app.analysis.introduce" defaultMessage="introduce" />}
>
<Icon style={{ marginLeft: 8 }} type="info-circle-o" />
</Tooltip>
</span>
}
total={2.7}
status="down"
subTotal={26.2}
gap={8}
/>
<MiniArea line height={45} data={visitData2} />
</Col>
</Row>
<Table
rowKey={record => record.index}
size="small"
columns={columns}
dataSource={searchData}
pagination={{
style: { marginBottom: 0 },
pageSize: 5,
}}
/>
</Card>
));
export default TopSearch;
import React, { PureComponent } from 'react';
import moment from 'moment';
import { connect } from 'dva';
import Link from 'umi/link';
import { Row, Col, Card, List, Avatar } from 'antd';
import { Radar } from 'ant-design-pro/lib/Charts';
import EditableLinkGroup from '@/components/EditableLinkGroup';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './Workplace.less';
const links = [
{
title: '操作一',
href: '',
},
{
title: '操作二',
href: '',
},
{
title: '操作三',
href: '',
},
{
title: '操作四',
href: '',
},
{
title: '操作五',
href: '',
},
{
title: '操作六',
href: '',
},
];
@connect(({ user, project, activities, chart, loading }) => ({
currentUser: user.currentUser,
project,
activities,
chart,
currentUserLoading: loading.effects['user/fetchCurrent'],
projectLoading: loading.effects['project/fetchNotice'],
activitiesLoading: loading.effects['activities/fetchList'],
}))
class Workplace extends PureComponent {
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'user/fetchCurrent',
});
dispatch({
type: 'project/fetchNotice',
});
dispatch({
type: 'activities/fetchList',
});
dispatch({
type: 'chart/fetch',
});
}
componentWillUnmount() {
const { dispatch } = this.props;
dispatch({
type: 'chart/clear',
});
}
renderActivities() {
const {
activities: { list },
} = this.props;
return list.map(item => {
const events = item.template.split(/@\{([^{}]*)\}/gi).map(key => {
if (item[key]) {
return (
<a href={item[key].link} key={item[key].name}>
{item[key].name}
</a>
);
}
return key;
});
return (
<List.Item key={item.id}>
<List.Item.Meta
avatar={<Avatar src={item.user.avatar} />}
title={
<span>
<a className={styles.username}>{item.user.name}</a>
&nbsp;
<span className={styles.event}>{events}</span>
</span>
}
description={
<span className={styles.datetime} title={item.updatedAt}>
{moment(item.updatedAt).fromNow()}
</span>
}
/>
</List.Item>
);
});
}
render() {
const {
currentUser,
currentUserLoading,
project: { notice },
projectLoading,
activitiesLoading,
chart: { radarData },
} = this.props;
const pageHeaderContent =
currentUser && Object.keys(currentUser).length ? (
<div className={styles.pageHeaderContent}>
<div className={styles.avatar}>
<Avatar size="large" src={currentUser.avatar} />
</div>
<div className={styles.content}>
<div className={styles.contentTitle}>
早安
{currentUser.name}
祝你开心每一天
</div>
<div>
{currentUser.title} |{currentUser.group}
</div>
</div>
</div>
) : null;
const extraContent = (
<div className={styles.extraContent}>
<div className={styles.statItem}>
<p>项目数</p>
<p>56</p>
</div>
<div className={styles.statItem}>
<p>团队内排名</p>
<p>
8<span> / 24</span>
</p>
</div>
<div className={styles.statItem}>
<p>项目访问</p>
<p>2,223</p>
</div>
</div>
);
return (
<PageHeaderWrapper
loading={currentUserLoading}
content={pageHeaderContent}
extraContent={extraContent}
>
<Row gutter={24}>
<Col xl={16} lg={24} md={24} sm={24} xs={24}>
<Card
className={styles.projectList}
style={{ marginBottom: 24 }}
title="进行中的项目"
bordered={false}
extra={<Link to="/">全部项目</Link>}
loading={projectLoading}
bodyStyle={{ padding: 0 }}
>
{notice.map(item => (
<Card.Grid className={styles.projectGrid} key={item.id}>
<Card bodyStyle={{ padding: 0 }} bordered={false}>
<Card.Meta
title={
<div className={styles.cardTitle}>
<Avatar size="small" src={item.logo} />
<Link to={item.href}>{item.title}</Link>
</div>
}
description={item.description}
/>
<div className={styles.projectItemContent}>
<Link to={item.memberLink}>{item.member || ''}</Link>
{item.updatedAt && (
<span className={styles.datetime} title={item.updatedAt}>
{moment(item.updatedAt).fromNow()}
</span>
)}
</div>
</Card>
</Card.Grid>
))}
</Card>
<Card
bodyStyle={{ padding: 0 }}
bordered={false}
className={styles.activeCard}
title="动态"
loading={activitiesLoading}
>
<List loading={activitiesLoading} size="large">
<div className={styles.activitiesList}>{this.renderActivities()}</div>
</List>
</Card>
</Col>
<Col xl={8} lg={24} md={24} sm={24} xs={24}>
<Card
style={{ marginBottom: 24 }}
title="快速开始 / 便捷导航"
bordered={false}
bodyStyle={{ padding: 0 }}
>
<EditableLinkGroup onAdd={() => {}} links={links} linkElement={Link} />
</Card>
<Card
style={{ marginBottom: 24 }}
bordered={false}
title="XX 指数"
loading={radarData.length === 0}
>
<div className={styles.chart}>
<Radar hasLegend height={343} data={radarData} />
</div>
</Card>
<Card
bodyStyle={{ paddingTop: 12, paddingBottom: 12 }}
bordered={false}
title="团队"
loading={projectLoading}
>
<div className={styles.members}>
<Row gutter={48}>
{notice.map(item => (
<Col span={12} key={`members-item-${item.id}`}>
<Link to={item.href}>
<Avatar src={item.logo} size="small" />
<span className={styles.member}>{item.member}</span>
</Link>
</Col>
))}
</Row>
</div>
</Card>
</Col>
</Row>
</PageHeaderWrapper>
);
}
}
export default Workplace;
@import '~antd/lib/style/themes/default.less';
@import '~@/utils/utils.less';
.activitiesList {
padding: 0 24px 8px 24px;
.username {
color: @text-color;
}
.event {
font-weight: normal;
}
}
.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;
}
}
}
}
.members {
a {
display: block;
margin: 12px 0;
height: 24px;
color: @text-color;
transition: all 0.3s;
.textOverflow();
.member {
font-size: @font-size-base;
line-height: 24px;
vertical-align: top;
margin-left: 12px;
}
&:hover {
color: @primary-color;
}
}
}
.projectList {
:global {
.ant-card-meta-description {
color: @text-color-secondary;
height: 44px;
line-height: 22px;
overflow: hidden;
}
}
.cardTitle {
font-size: 0;
a {
color: @heading-color;
margin-left: 12px;
line-height: 24px;
height: 24px;
display: inline-block;
vertical-align: top;
font-size: @font-size-base;
&:hover {
color: @primary-color;
}
}
}
.projectGrid {
width: 33.33%;
}
.projectItemContent {
display: flex;
margin-top: 8px;
overflow: hidden;
font-size: 12px;
height: 20px;
line-height: 20px;
.textOverflow();
a {
color: @text-color-secondary;
display: inline-block;
flex: 1 1 0;
.textOverflow();
&:hover {
color: @primary-color;
}
}
.datetime {
color: @disabled-color;
flex: 0 0 auto;
float: right;
}
}
}
.datetime {
color: @disabled-color;
}
@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.members {
margin-bottom: 0;
}
.extraContent {
margin-left: -44px;
.statItem {
padding: 0 16px;
}
}
}
@media screen and (max-width: @screen-lg) {
.activeCard {
margin-bottom: 24px;
}
.members {
margin-bottom: 0;
}
.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;
}
.projectList {
.projectGrid {
width: 50%;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeaderContent {
display: block;
.content {
margin-left: 0;
}
}
.extraContent {
.statItem {
float: none;
}
}
}
@media screen and (max-width: @screen-xs) {
.projectList {
.projectGrid {
width: 100%;
}
}
}
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,
};
},
},
};
import { fakeChartData } from '@/services/api';
export default {
namespace: 'chart',
state: {
visitData: [],
visitData2: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
loading: false,
},
effects: {
*fetch(_, { call, put }) {
const response = yield call(fakeChartData);
yield put({
type: 'save',
payload: response,
});
},
*fetchSalesData(_, { call, put }) {
const response = yield call(fakeChartData);
yield put({
type: 'save',
payload: {
salesData: response.salesData,
},
});
},
},
reducers: {
save(state, { payload }) {
return {
...state,
...payload,
};
},
clear() {
return {
visitData: [],
visitData2: [],
salesData: [],
searchData: [],
offlineData: [],
offlineChartData: [],
salesTypeData: [],
salesTypeDataOnline: [],
salesTypeDataOffline: [],
radarData: [],
};
},
},
};
import { queryTags } from '@/services/api';
export default {
namespace: 'monitor',
state: {
tags: [],
},
effects: {
*fetchTags(_, { call, put }) {
const response = yield call(queryTags);
yield put({
type: 'saveTags',
payload: response.list,
});
},
},
reducers: {
saveTags(state, action) {
return {
...state,
tags: action.payload,
};
},
},
};
import React from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import Exception from 'ant-design-pro/lib/Exception';
const Exception403 = () => (
<Exception
type="403"
desc={formatMessage({ id: 'app.exception.description.403' })}
linkElement={Link}
backText={formatMessage({ id: 'app.exception.back' })}
/>
);
export default Exception403;
import React from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import Exception from 'ant-design-pro/lib/Exception';
const Exception404 = () => (
<Exception
type="404"
desc={formatMessage({ id: 'app.exception.description.404' })}
linkElement={Link}
backText={formatMessage({ id: 'app.exception.back' })}
/>
);
export default Exception404;
import React from 'react';
import { formatMessage } from 'umi/locale';
import Link from 'umi/link';
import Exception from 'ant-design-pro/lib/Exception';
const Exception500 = () => (
<Exception
type="500"
desc={formatMessage({ id: 'app.exception.description.500' })}
linkElement={Link}
backText={formatMessage({ id: 'app.exception.back' })}
/>
);
export default Exception500;
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 (
<Card>
<Spin spinning={isloading} wrapperClassName={styles.trigger}>
<Button type="danger" onClick={() => this.triggerError(401)}>
触发401
</Button>
<Button type="danger" onClick={() => this.triggerError(403)}>
触发403
</Button>
<Button type="danger" onClick={() => this.triggerError(500)}>
触发500
</Button>
<Button type="danger" onClick={() => this.triggerError(404)}>
触发404
</Button>
</Spin>
</Card>
);
}
}
export default TriggerException;
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,
};
},
},
};
.trigger {
background: 'red';
:global(.ant-btn) {
margin-right: 8px;
margin-bottom: 12px;
}
}
import React, { PureComponent } from 'react';
import {
Card,
Button,
Form,
Icon,
Col,
Row,
DatePicker,
TimePicker,
Input,
Select,
Popover,
} from 'antd';
import { connect } from 'dva';
import FooterToolbar from 'ant-design-pro/lib/FooterToolbar';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import TableForm from './TableForm';
import styles from './style.less';
const { Option } = Select;
const { RangePicker } = DatePicker;
const fieldLabels = {
name: '仓库名',
url: '仓库域名',
owner: '仓库管理员',
approver: '审批人',
dateRange: '生效日期',
type: '仓库类型',
name2: '任务名',
url2: '任务描述',
owner2: '执行人',
approver2: '责任人',
dateRange2: '生效日期',
type2: '任务类型',
};
const tableData = [
{
key: '1',
workId: '00001',
name: 'John Brown',
department: 'New York No. 1 Lake Park',
},
{
key: '2',
workId: '00002',
name: 'Jim Green',
department: 'London No. 1 Lake Park',
},
{
key: '3',
workId: '00003',
name: 'Joe Black',
department: 'Sidney No. 1 Lake Park',
},
];
@connect(({ loading }) => ({
submitting: loading.effects['form/submitAdvancedForm'],
}))
@Form.create()
class AdvancedForm extends PureComponent {
state = {
width: '100%',
};
componentDidMount() {
window.addEventListener('resize', this.resizeFooterToolbar, { passive: true });
}
componentWillUnmount() {
window.removeEventListener('resize', this.resizeFooterToolbar);
}
getErrorInfo = () => {
const {
form: { getFieldsError },
} = this.props;
const errors = getFieldsError();
const errorCount = Object.keys(errors).filter(key => errors[key]).length;
if (!errors || errorCount === 0) {
return null;
}
const scrollToField = fieldKey => {
const labelNode = document.querySelector(`label[for="${fieldKey}"]`);
if (labelNode) {
labelNode.scrollIntoView(true);
}
};
const errorList = Object.keys(errors).map(key => {
if (!errors[key]) {
return null;
}
return (
<li key={key} className={styles.errorListItem} onClick={() => scrollToField(key)}>
<Icon type="cross-circle-o" className={styles.errorIcon} />
<div className={styles.errorMessage}>{errors[key][0]}</div>
<div className={styles.errorField}>{fieldLabels[key]}</div>
</li>
);
});
return (
<span className={styles.errorIcon}>
<Popover
title="表单校验信息"
content={errorList}
overlayClassName={styles.errorPopover}
trigger="click"
getPopupContainer={trigger => trigger.parentNode}
>
<Icon type="exclamation-circle" />
</Popover>
{errorCount}
</span>
);
};
resizeFooterToolbar = () => {
requestAnimationFrame(() => {
const sider = document.querySelectorAll('.ant-layout-sider')[0];
if (sider) {
const width = `calc(100% - ${sider.style.width})`;
const { width: stateWidth } = this.state;
if (stateWidth !== width) {
this.setState({ width });
}
}
});
};
validate = () => {
const {
form: { validateFieldsAndScroll },
dispatch,
} = this.props;
validateFieldsAndScroll((error, values) => {
if (!error) {
// submit the values
dispatch({
type: 'form/submitAdvancedForm',
payload: values,
});
}
});
};
render() {
const {
form: { getFieldDecorator },
submitting,
} = this.props;
const { width } = this.state;
return (
<PageHeaderWrapper
title="高级表单"
content="高级表单常见于一次性输入和提交大批量数据的场景。"
wrapperClassName={styles.advancedForm}
>
<Card title="仓库管理" className={styles.card} bordered={false}>
<Form layout="vertical" hideRequiredMark>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.name}>
{getFieldDecorator('name', {
rules: [{ required: true, message: '请输入仓库名称' }],
})(<Input placeholder="请输入仓库名称" />)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.url}>
{getFieldDecorator('url', {
rules: [{ required: true, message: '请选择' }],
})(
<Input
style={{ width: '100%' }}
addonBefore="http://"
addonAfter=".com"
placeholder="请输入"
/>
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.owner}>
{getFieldDecorator('owner', {
rules: [{ required: true, message: '请选择管理员' }],
})(
<Select placeholder="请选择管理员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.approver}>
{getFieldDecorator('approver', {
rules: [{ required: true, message: '请选择审批员' }],
})(
<Select placeholder="请选择审批员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.dateRange}>
{getFieldDecorator('dateRange', {
rules: [{ required: true, message: '请选择生效日期' }],
})(
<RangePicker placeholder={['开始日期', '结束日期']} style={{ width: '100%' }} />
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.type}>
{getFieldDecorator('type', {
rules: [{ required: true, message: '请选择仓库类型' }],
})(
<Select placeholder="请选择仓库类型">
<Option value="private">私密</Option>
<Option value="public">公开</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
</Form>
</Card>
<Card title="任务管理" className={styles.card} bordered={false}>
<Form layout="vertical" hideRequiredMark>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.name2}>
{getFieldDecorator('name2', {
rules: [{ required: true, message: '请输入' }],
})(<Input placeholder="请输入" />)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.url2}>
{getFieldDecorator('url2', {
rules: [{ required: true, message: '请选择' }],
})(<Input placeholder="请输入" />)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.owner2}>
{getFieldDecorator('owner2', {
rules: [{ required: true, message: '请选择管理员' }],
})(
<Select placeholder="请选择管理员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col lg={6} md={12} sm={24}>
<Form.Item label={fieldLabels.approver2}>
{getFieldDecorator('approver2', {
rules: [{ required: true, message: '请选择审批员' }],
})(
<Select placeholder="请选择审批员">
<Option value="xiao">付晓晓</Option>
<Option value="mao">周毛毛</Option>
</Select>
)}
</Form.Item>
</Col>
<Col xl={{ span: 6, offset: 2 }} lg={{ span: 8 }} md={{ span: 12 }} sm={24}>
<Form.Item label={fieldLabels.dateRange2}>
{getFieldDecorator('dateRange2', {
rules: [{ required: true, message: '请输入' }],
})(
<TimePicker
placeholder="提醒时间"
style={{ width: '100%' }}
getPopupContainer={trigger => trigger.parentNode}
/>
)}
</Form.Item>
</Col>
<Col xl={{ span: 8, offset: 2 }} lg={{ span: 10 }} md={{ span: 24 }} sm={24}>
<Form.Item label={fieldLabels.type2}>
{getFieldDecorator('type2', {
rules: [{ required: true, message: '请选择仓库类型' }],
})(
<Select placeholder="请选择仓库类型">
<Option value="private">私密</Option>
<Option value="public">公开</Option>
</Select>
)}
</Form.Item>
</Col>
</Row>
</Form>
</Card>
<Card title="成员管理" bordered={false}>
{getFieldDecorator('members', {
initialValue: tableData,
})(<TableForm />)}
</Card>
<FooterToolbar style={{ width }}>
{this.getErrorInfo()}
<Button type="primary" onClick={this.validate} loading={submitting}>
提交
</Button>
</FooterToolbar>
</PageHeaderWrapper>
);
}
}
export default AdvancedForm;
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { formatMessage, FormattedMessage } from 'umi/locale';
import {
Form,
Input,
DatePicker,
Select,
Button,
Card,
InputNumber,
Radio,
Icon,
Tooltip,
} from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from './style.less';
const FormItem = Form.Item;
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TextArea } = Input;
@connect(({ loading }) => ({
submitting: loading.effects['form/submitRegularForm'],
}))
@Form.create()
class BasicForms extends PureComponent {
handleSubmit = e => {
const { dispatch, form } = this.props;
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
if (!err) {
dispatch({
type: 'form/submitRegularForm',
payload: values,
});
}
});
};
render() {
const { submitting } = this.props;
const {
form: { getFieldDecorator, getFieldValue },
} = this.props;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 7 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 },
md: { span: 10 },
},
};
const submitFormLayout = {
wrapperCol: {
xs: { span: 24, offset: 0 },
sm: { span: 10, offset: 7 },
},
};
return (
<PageHeaderWrapper
title={<FormattedMessage id="app.forms.basic.title" />}
content={<FormattedMessage id="app.forms.basic.description" />}
>
<Card bordered={false}>
<Form onSubmit={this.handleSubmit} hideRequiredMark style={{ marginTop: 8 }}>
<FormItem {...formItemLayout} label={<FormattedMessage id="form.title.label" />}>
{getFieldDecorator('title', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.title.required' }),
},
],
})(<Input placeholder={formatMessage({ id: 'form.title.placeholder' })} />)}
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="form.date.label" />}>
{getFieldDecorator('date', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.date.required' }),
},
],
})(
<RangePicker
style={{ width: '100%' }}
placeholder={[
formatMessage({ id: 'form.date.placeholder.start' }),
formatMessage({ id: 'form.date.placeholder.end' }),
]}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="form.goal.label" />}>
{getFieldDecorator('goal', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.goal.required' }),
},
],
})(
<TextArea
style={{ minHeight: 32 }}
placeholder={formatMessage({ id: 'form.goal.placeholder' })}
rows={4}
/>
)}
</FormItem>
<FormItem {...formItemLayout} label={<FormattedMessage id="form.standard.label" />}>
{getFieldDecorator('standard', {
rules: [
{
required: true,
message: formatMessage({ id: 'validation.standard.required' }),
},
],
})(
<TextArea
style={{ minHeight: 32 }}
placeholder={formatMessage({ id: 'form.standard.placeholder' })}
rows={4}
/>
)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
<FormattedMessage id="form.client.label" />
<em className={styles.optional}>
<FormattedMessage id="form.optional" />
<Tooltip title={<FormattedMessage id="form.client.label.tooltip" />}>
<Icon type="info-circle-o" style={{ marginRight: 4 }} />
</Tooltip>
</em>
</span>
}
>
{getFieldDecorator('client')(
<Input placeholder={formatMessage({ id: 'form.client.placeholder' })} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
<FormattedMessage id="form.invites.label" />
<em className={styles.optional}>
<FormattedMessage id="form.optional" />
</em>
</span>
}
>
{getFieldDecorator('invites')(
<Input placeholder={formatMessage({ id: 'form.invites.placeholder' })} />
)}
</FormItem>
<FormItem
{...formItemLayout}
label={
<span>
<FormattedMessage id="form.weight.label" />
<em className={styles.optional}>
<FormattedMessage id="form.optional" />
</em>
</span>
}
>
{getFieldDecorator('weight')(
<InputNumber
placeholder={formatMessage({ id: 'form.weight.placeholder' })}
min={0}
max={100}
/>
)}
<span className="ant-form-text">%</span>
</FormItem>
<FormItem
{...formItemLayout}
label={<FormattedMessage id="form.public.label" />}
help={<FormattedMessage id="form.public.label.help" />}
>
<div>
{getFieldDecorator('public', {
initialValue: '1',
})(
<Radio.Group>
<Radio value="1">
<FormattedMessage id="form.public.radio.public" />
</Radio>
<Radio value="2">
<FormattedMessage id="form.public.radio.partially-public" />
</Radio>
<Radio value="3">
<FormattedMessage id="form.public.radio.private" />
</Radio>
</Radio.Group>
)}
<FormItem style={{ marginBottom: 0 }}>
{getFieldDecorator('publicUsers')(
<Select
mode="multiple"
placeholder={formatMessage({ id: 'form.publicUsers.placeholder' })}
style={{
margin: '8px 0',
display: getFieldValue('public') === '2' ? 'block' : 'none',
}}
>
<Option value="1">
<FormattedMessage id="form.publicUsers.option.A" />
</Option>
<Option value="2">
<FormattedMessage id="form.publicUsers.option.B" />
</Option>
<Option value="3">
<FormattedMessage id="form.publicUsers.option.C" />
</Option>
</Select>
)}
</FormItem>
</div>
</FormItem>
<FormItem {...submitFormLayout} style={{ marginTop: 32 }}>
<Button type="primary" htmlType="submit" loading={submitting}>
<FormattedMessage id="form.submit" />
</Button>
<Button style={{ marginLeft: 8 }}>
<FormattedMessage id="form.save" />
</Button>
</FormItem>
</Form>
</Card>
</PageHeaderWrapper>
);
}
}
export default BasicForms;
import React, { Fragment } from 'react';
import { connect } from 'dva';
import { Form, Input, Button, Select, Divider } from 'antd';
import router from 'umi/router';
import styles from './style.less';
const { Option } = Select;
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
@connect(({ form }) => ({
data: form.step,
}))
@Form.create()
class Step1 extends React.PureComponent {
render() {
const { form, dispatch, data } = this.props;
const { getFieldDecorator, validateFields } = form;
const onValidateForm = () => {
validateFields((err, values) => {
if (!err) {
dispatch({
type: 'form/saveStepFormData',
payload: values,
});
router.push('/form/step-form/confirm');
}
});
};
return (
<Fragment>
<Form layout="horizontal" className={styles.stepForm} hideRequiredMark>
<Form.Item {...formItemLayout} label="付款账户">
{getFieldDecorator('payAccount', {
initialValue: data.payAccount,
rules: [{ required: true, message: '请选择付款账户' }],
})(
<Select placeholder="test@example.com">
<Option value="ant-design@alipay.com">ant-design@alipay.com</Option>
</Select>
)}
</Form.Item>
<Form.Item {...formItemLayout} label="收款账户">
<Input.Group compact>
<Select defaultValue="alipay" style={{ width: 100 }}>
<Option value="alipay">支付宝</Option>
<Option value="bank">银行账户</Option>
</Select>
{getFieldDecorator('receiverAccount', {
initialValue: data.receiverAccount,
rules: [
{ required: true, message: '请输入收款人账户' },
{ type: 'email', message: '账户名应为邮箱格式' },
],
})(<Input style={{ width: 'calc(100% - 100px)' }} placeholder="test@example.com" />)}
</Input.Group>
</Form.Item>
<Form.Item {...formItemLayout} label="收款人姓名">
{getFieldDecorator('receiverName', {
initialValue: data.receiverName,
rules: [{ required: true, message: '请输入收款人姓名' }],
})(<Input placeholder="请输入收款人姓名" />)}
</Form.Item>
<Form.Item {...formItemLayout} label="转账金额">
{getFieldDecorator('amount', {
initialValue: data.amount,
rules: [
{ required: true, message: '请输入转账金额' },
{
pattern: /^(\d+)((?:\.\d+)?)$/,
message: '请输入合法金额数字',
},
],
})(<Input prefix="" placeholder="请输入金额" />)}
</Form.Item>
<Form.Item
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: {
span: formItemLayout.wrapperCol.span,
offset: formItemLayout.labelCol.span,
},
}}
label=""
>
<Button type="primary" onClick={onValidateForm}>
下一步
</Button>
</Form.Item>
</Form>
<Divider style={{ margin: '40px 0 24px' }} />
<div className={styles.desc}>
<h3>说明</h3>
<h4>转账到支付宝账户</h4>
<p>
如果需要这里可以放一些关于产品的常见问题说明如果需要这里可以放一些关于产品的常见问题说明如果需要这里可以放一些关于产品的常见问题说明
</p>
<h4>转账到银行卡</h4>
<p>
如果需要这里可以放一些关于产品的常见问题说明如果需要这里可以放一些关于产品的常见问题说明如果需要这里可以放一些关于产品的常见问题说明
</p>
</div>
</Fragment>
);
}
}
export default Step1;
import React from 'react';
import { connect } from 'dva';
import { Form, Input, Button, Alert, Divider } from 'antd';
import router from 'umi/router';
import { digitUppercase } from '@/utils/utils';
import styles from './style.less';
const formItemLayout = {
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
};
@connect(({ form, loading }) => ({
submitting: loading.effects['form/submitStepForm'],
data: form.step,
}))
@Form.create()
class Step2 extends React.PureComponent {
render() {
const { form, data, dispatch, submitting } = this.props;
const { getFieldDecorator, validateFields } = form;
const onPrev = () => {
router.push('/form/step-form/info');
};
const onValidateForm = e => {
e.preventDefault();
validateFields((err, values) => {
if (!err) {
dispatch({
type: 'form/submitStepForm',
payload: {
...data,
...values,
},
});
}
});
};
return (
<Form layout="horizontal" className={styles.stepForm}>
<Alert
closable
showIcon
message="确认转账后,资金将直接打入对方账户,无法退回。"
style={{ marginBottom: 24 }}
/>
<Form.Item {...formItemLayout} className={styles.stepFormText} label="付款账户">
{data.payAccount}
</Form.Item>
<Form.Item {...formItemLayout} className={styles.stepFormText} label="收款账户">
{data.receiverAccount}
</Form.Item>
<Form.Item {...formItemLayout} className={styles.stepFormText} label="收款人姓名">
{data.receiverName}
</Form.Item>
<Form.Item {...formItemLayout} className={styles.stepFormText} label="转账金额">
<span className={styles.money}>{data.amount}</span>
<span className={styles.uppercase}>{digitUppercase(data.amount)}</span>
</Form.Item>
<Divider style={{ margin: '24px 0' }} />
<Form.Item {...formItemLayout} label="支付密码" required={false}>
{getFieldDecorator('password', {
initialValue: '123456',
rules: [
{
required: true,
message: '需要支付密码才能进行支付',
},
],
})(<Input type="password" autoComplete="off" style={{ width: '80%' }} />)}
</Form.Item>
<Form.Item
style={{ marginBottom: 8 }}
wrapperCol={{
xs: { span: 24, offset: 0 },
sm: {
span: formItemLayout.wrapperCol.span,
offset: formItemLayout.labelCol.span,
},
}}
label=""
>
<Button type="primary" onClick={onValidateForm} loading={submitting}>
提交
</Button>
<Button onClick={onPrev} style={{ marginLeft: 8 }}>
上一步
</Button>
</Form.Item>
</Form>
);
}
}
export default Step2;
import React, { Fragment } from 'react';
import { connect } from 'dva';
import { Button, Row, Col } from 'antd';
import router from 'umi/router';
import Result from 'ant-design-pro/lib/Result';
import styles from './style.less';
@connect(({ form }) => ({
data: form.step,
}))
class Step3 extends React.PureComponent {
render() {
const { data } = this.props;
const onFinish = () => {
router.push('/form/step-form/info');
};
const information = (
<div className={styles.information}>
<Row>
<Col xs={24} sm={8} className={styles.label}>
付款账户
</Col>
<Col xs={24} sm={16}>
{data.payAccount}
</Col>
</Row>
<Row>
<Col xs={24} sm={8} className={styles.label}>
收款账户
</Col>
<Col xs={24} sm={16}>
{data.receiverAccount}
</Col>
</Row>
<Row>
<Col xs={24} sm={8} className={styles.label}>
收款人姓名
</Col>
<Col xs={24} sm={16}>
{data.receiverName}
</Col>
</Row>
<Row>
<Col xs={24} sm={8} className={styles.label}>
转账金额
</Col>
<Col xs={24} sm={16}>
<span className={styles.money}>{data.amount}</span>
</Col>
</Row>
</div>
);
const actions = (
<Fragment>
<Button type="primary" onClick={onFinish}>
再转一笔
</Button>
<Button>查看账单</Button>
</Fragment>
);
return (
<Result
type="success"
title="操作成功"
description="预计两小时内到账"
extra={information}
actions={actions}
className={styles.result}
/>
);
}
}
export default Step3;
import React, { PureComponent, Fragment } from 'react';
import { Card, Steps } from 'antd';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import styles from '../style.less';
const { Step } = Steps;
export default class StepForm extends PureComponent {
getCurrentStep() {
const { location } = this.props;
const { pathname } = location;
const pathList = pathname.split('/');
switch (pathList[pathList.length - 1]) {
case 'info':
return 0;
case 'confirm':
return 1;
case 'result':
return 2;
default:
return 0;
}
}
render() {
const { location, children } = this.props;
return (
<PageHeaderWrapper
title="分步表单"
tabActiveKey={location.pathname}
content="将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成。"
>
<Card bordered={false}>
<Fragment>
<Steps current={this.getCurrentStep()} className={styles.steps}>
<Step title="填写转账信息" />
<Step title="确认转账信息" />
<Step title="完成" />
</Steps>
{children}
</Fragment>
</Card>
</PageHeaderWrapper>
);
}
}
@import '~antd/lib/style/themes/default.less';
.stepForm {
margin: 40px auto 0;
max-width: 500px;
}
.stepFormText {
margin-bottom: 24px;
:global {
.ant-form-item-label,
.ant-form-item-control {
line-height: 22px;
}
}
}
.result {
margin: 0 auto;
max-width: 560px;
padding: 24px 0 8px;
}
.desc {
padding: 0 56px;
color: @text-color-secondary;
h3 {
font-size: 16px;
margin: 0 0 12px 0;
color: @text-color-secondary;
line-height: 32px;
}
h4 {
margin: 0 0 4px 0;
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
p {
margin-top: 0;
margin-bottom: 12px;
line-height: 22px;
}
}
@media screen and (max-width: @screen-md) {
.desc {
padding: 0;
}
}
.information {
line-height: 22px;
:global {
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.label {
color: @heading-color;
text-align: right;
padding-right: 8px;
@media screen and (max-width: @screen-sm) {
text-align: left;
}
}
}
.money {
font-family: 'Helvetica Neue', sans-serif;
font-weight: 500;
font-size: 20px;
line-height: 14px;
}
.uppercase {
font-size: 12px;
}
import React, { PureComponent, Fragment } from 'react';
import { Table, Button, Input, message, Popconfirm, Divider } from 'antd';
import isEqual from 'lodash/isEqual';
import styles from './style.less';
class TableForm extends PureComponent {
index = 0;
cacheOriginData = {};
constructor(props) {
super(props);
this.state = {
data: props.value,
loading: false,
/* eslint-disable-next-line react/no-unused-state */
value: props.value,
};
}
static getDerivedStateFromProps(nextProps, preState) {
if (isEqual(nextProps.value, preState.value)) {
return null;
}
return {
data: nextProps.value,
value: nextProps.value,
};
}
getRowByKey(key, newData) {
const { data } = this.state;
return (newData || data).filter(item => item.key === key)[0];
}
toggleEditable = (e, key) => {
e.preventDefault();
const { data } = this.state;
const newData = data.map(item => ({ ...item }));
const target = this.getRowByKey(key, newData);
if (target) {
// 进入编辑状态时保存原始数据
if (!target.editable) {
this.cacheOriginData[key] = { ...target };
}
target.editable = !target.editable;
this.setState({ data: newData });
}
};
newMember = () => {
const { data } = this.state;
const newData = data.map(item => ({ ...item }));
newData.push({
key: `NEW_TEMP_ID_${this.index}`,
workId: '',
name: '',
department: '',
editable: true,
isNew: true,
});
this.index += 1;
this.setState({ data: newData });
};
remove(key) {
const { data } = this.state;
const { onChange } = this.props;
const newData = data.filter(item => item.key !== key);
this.setState({ data: newData });
onChange(newData);
}
handleKeyPress(e, key) {
if (e.key === 'Enter') {
this.saveRow(e, key);
}
}
handleFieldChange(e, fieldName, key) {
const { data } = this.state;
const newData = data.map(item => ({ ...item }));
const target = this.getRowByKey(key, newData);
if (target) {
target[fieldName] = e.target.value;
this.setState({ data: newData });
}
}
saveRow(e, key) {
e.persist();
this.setState({
loading: true,
});
setTimeout(() => {
if (this.clickedCancel) {
this.clickedCancel = false;
return;
}
const target = this.getRowByKey(key) || {};
if (!target.workId || !target.name || !target.department) {
message.error('请填写完整成员信息。');
e.target.focus();
this.setState({
loading: false,
});
return;
}
delete target.isNew;
this.toggleEditable(e, key);
const { data } = this.state;
const { onChange } = this.props;
onChange(data);
this.setState({
loading: false,
});
}, 500);
}
cancel(e, key) {
this.clickedCancel = true;
e.preventDefault();
const { data } = this.state;
const newData = data.map(item => ({ ...item }));
const target = this.getRowByKey(key, newData);
if (this.cacheOriginData[key]) {
Object.assign(target, this.cacheOriginData[key]);
delete this.cacheOriginData[key];
}
target.editable = false;
this.setState({ data: newData });
this.clickedCancel = false;
}
render() {
const columns = [
{
title: '成员姓名',
dataIndex: 'name',
key: 'name',
width: '20%',
render: (text, record) => {
if (record.editable) {
return (
<Input
value={text}
autoFocus
onChange={e => this.handleFieldChange(e, 'name', record.key)}
onKeyPress={e => this.handleKeyPress(e, record.key)}
placeholder="成员姓名"
/>
);
}
return text;
},
},
{
title: '工号',
dataIndex: 'workId',
key: 'workId',
width: '20%',
render: (text, record) => {
if (record.editable) {
return (
<Input
value={text}
onChange={e => this.handleFieldChange(e, 'workId', record.key)}
onKeyPress={e => this.handleKeyPress(e, record.key)}
placeholder="工号"
/>
);
}
return text;
},
},
{
title: '所属部门',
dataIndex: 'department',
key: 'department',
width: '40%',
render: (text, record) => {
if (record.editable) {
return (
<Input
value={text}
onChange={e => this.handleFieldChange(e, 'department', record.key)}
onKeyPress={e => this.handleKeyPress(e, record.key)}
placeholder="所属部门"
/>
);
}
return text;
},
},
{
title: '操作',
key: 'action',
render: (text, record) => {
const { loading } = this.state;
if (!!record.editable && loading) {
return null;
}
if (record.editable) {
if (record.isNew) {
return (
<span>
<a onClick={e => this.saveRow(e, record.key)}>添加</a>
<Divider type="vertical" />
<Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
<a>删除</a>
</Popconfirm>
</span>
);
}
return (
<span>
<a onClick={e => this.saveRow(e, record.key)}>保存</a>
<Divider type="vertical" />
<a onClick={e => this.cancel(e, record.key)}>取消</a>
</span>
);
}
return (
<span>
<a onClick={e => this.toggleEditable(e, record.key)}>编辑</a>
<Divider type="vertical" />
<Popconfirm title="是否要删除此行?" onConfirm={() => this.remove(record.key)}>
<a>删除</a>
</Popconfirm>
</span>
);
},
},
];
const { loading, data } = this.state;
return (
<Fragment>
<Table
loading={loading}
columns={columns}
dataSource={data}
pagination={false}
rowClassName={record => (record.editable ? styles.editable : '')}
/>
<Button
style={{ width: '100%', marginTop: 16, marginBottom: 8 }}
type="dashed"
onClick={this.newMember}
icon="plus"
>
新增成员
</Button>
</Fragment>
);
}
}
export default TableForm;
import { routerRedux } from 'dva/router';
import { message } from 'antd';
import { fakeSubmitForm } from '@/services/api';
export default {
namespace: 'form',
state: {
step: {
payAccount: 'ant-design@alipay.com',
receiverAccount: 'test@example.com',
receiverName: 'Alex',
amount: '500',
},
},
effects: {
*submitRegularForm({ payload }, { call }) {
yield call(fakeSubmitForm, payload);
message.success('提交成功');
},
*submitStepForm({ payload }, { call, put }) {
yield call(fakeSubmitForm, payload);
yield put({
type: 'saveStepFormData',
payload,
});
yield put(routerRedux.push('/form/step-form/result'));
},
*submitAdvancedForm({ payload }, { call }) {
yield call(fakeSubmitForm, payload);
message.success('提交成功');
},
},
reducers: {
saveStepFormData(state, { payload }) {
return {
...state,
step: {
...state.step,
...payload,
},
};
},
},
};
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
@import '~antd/lib/style/themes/default.less';
.title {
color: @heading-color;
font-size: 16px;
font-weight: 500;
margin-bottom: 16px;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment