初始化 antd-pro
This commit is contained in:
13
admin-web/src/pages/404.js
Normal file
13
admin-web/src/pages/404.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import Link from 'umi/link';
|
||||
import { formatMessage } from 'umi/locale';
|
||||
import Exception from '@/components/Exception';
|
||||
|
||||
export default () => (
|
||||
<Exception
|
||||
type="404"
|
||||
linkElement={Link}
|
||||
desc={formatMessage({ id: 'app.exception.description.404' })}
|
||||
backText={formatMessage({ id: 'app.exception.back' })}
|
||||
/>
|
||||
);
|
||||
88
admin-web/src/pages/Account/Center/Applications.js
Normal file
88
admin-web/src/pages/Account/Center/Applications.js
Normal file
@@ -0,0 +1,88 @@
|
||||
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;
|
||||
59
admin-web/src/pages/Account/Center/Articles.js
Normal file
59
admin-web/src/pages/Account/Center/Articles.js
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
12
admin-web/src/pages/Account/Center/Articles.less
Normal file
12
admin-web/src/pages/Account/Center/Articles.less
Normal file
@@ -0,0 +1,12 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.articleList {
|
||||
:global {
|
||||
.ant-list-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
a.listItemMetaTitle {
|
||||
color: @heading-color;
|
||||
}
|
||||
216
admin-web/src/pages/Account/Center/Center.js
Normal file
216
admin-web/src/pages/Account/Center/Center.js
Normal file
@@ -0,0 +1,216 @@
|
||||
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;
|
||||
97
admin-web/src/pages/Account/Center/Center.less
Normal file
97
admin-web/src/pages/Account/Center/Center.less
Normal file
@@ -0,0 +1,97 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.avatarHolder {
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
|
||||
& > img {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-bottom: 4px;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
p {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 26px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
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 {
|
||||
margin-bottom: 12px;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
admin-web/src/pages/Account/Center/Projects.js
Normal file
52
admin-web/src/pages/Account/Center/Projects.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { List, Card } from 'antd';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'dva';
|
||||
import AvatarList from '@/components/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;
|
||||
192
admin-web/src/pages/Account/Settings/BaseView.js
Normal file
192
admin-web/src/pages/Account/Settings/BaseView.js
Normal file
@@ -0,0 +1,192 @@
|
||||
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/antfincdn/XAosXuNZyF/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;
|
||||
52
admin-web/src/pages/Account/Settings/BaseView.less
Normal file
52
admin-web/src/pages/Account/Settings/BaseView.less
Normal file
@@ -0,0 +1,52 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.baseView {
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
|
||||
.left {
|
||||
min-width: 224px;
|
||||
max-width: 448px;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-left: 104px;
|
||||
.avatar_title {
|
||||
height: 22px;
|
||||
margin-bottom: 8px;
|
||||
color: @heading-color;
|
||||
font-size: @font-size-base;
|
||||
line-height: 22px;
|
||||
}
|
||||
.avatar {
|
||||
width: 144px;
|
||||
height: 144px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.button_view {
|
||||
width: 144px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xl) {
|
||||
.baseView {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 448px;
|
||||
padding: 20px;
|
||||
.avatar_title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
admin-web/src/pages/Account/Settings/BindingView.js
Normal file
60
admin-web/src/pages/Account/Settings/BindingView.js
Normal file
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
128
admin-web/src/pages/Account/Settings/GeographicView.js
Normal file
128
admin-web/src/pages/Account/Settings/GeographicView.js
Normal file
@@ -0,0 +1,128 @@
|
||||
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;
|
||||
19
admin-web/src/pages/Account/Settings/GeographicView.less
Normal file
19
admin-web/src/pages/Account/Settings/GeographicView.less
Normal file
@@ -0,0 +1,19 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.row {
|
||||
.item {
|
||||
width: 50%;
|
||||
max-width: 220px;
|
||||
}
|
||||
.item:first-child {
|
||||
width: ~'calc(50% - 8px)';
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
.item:first-child {
|
||||
margin: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
125
admin-web/src/pages/Account/Settings/Info.js
Normal file
125
admin-web/src/pages/Account/Settings/Info.js
Normal file
@@ -0,0 +1,125 @@
|
||||
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;
|
||||
97
admin-web/src/pages/Account/Settings/Info.less
Normal file
97
admin-web/src/pages/Account/Settings/Info.less
Normal file
@@ -0,0 +1,97 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
overflow: auto;
|
||||
background-color: @body-background;
|
||||
.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-top: 8px;
|
||||
padding-right: 40px;
|
||||
padding-bottom: 8px;
|
||||
padding-left: 40px;
|
||||
.title {
|
||||
margin-bottom: 12px;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.ant-list-split .ant-list-item:last-child {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
.ant-list-item {
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.ant-list-item-meta {
|
||||
// 账号绑定图标
|
||||
.taobao {
|
||||
display: block;
|
||||
color: #ff4000;
|
||||
font-size: 48px;
|
||||
line-height: 48px;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
.dingding {
|
||||
margin: 2px;
|
||||
padding: 6px;
|
||||
color: #fff;
|
||||
font-size: 32px;
|
||||
line-height: 32px;
|
||||
background-color: #2eabff;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
.alipay {
|
||||
color: #2eabff;
|
||||
font-size: 48px;
|
||||
line-height: 48px;
|
||||
border-radius: @border-radius-base;
|
||||
}
|
||||
}
|
||||
|
||||
// 密码强度
|
||||
font.strong {
|
||||
color: @success-color;
|
||||
}
|
||||
font.medium {
|
||||
color: @warning-color;
|
||||
}
|
||||
font.weak {
|
||||
color: @error-color;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-md) {
|
||||
.main {
|
||||
flex-direction: column;
|
||||
.leftmenu {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
.right {
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
admin-web/src/pages/Account/Settings/NotificationView.js
Normal file
50
admin-web/src/pages/Account/Settings/NotificationView.js
Normal file
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
33
admin-web/src/pages/Account/Settings/PhoneView.js
Normal file
33
admin-web/src/pages/Account/Settings/PhoneView.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
11
admin-web/src/pages/Account/Settings/PhoneView.less
Normal file
11
admin-web/src/pages/Account/Settings/PhoneView.less
Normal file
@@ -0,0 +1,11 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.area_code {
|
||||
width: 30%;
|
||||
max-width: 128px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.phone_number {
|
||||
width: ~'calc(70% - 8px)';
|
||||
max-width: 312px;
|
||||
}
|
||||
102
admin-web/src/pages/Account/Settings/SecurityView.js
Normal file
102
admin-web/src/pages/Account/Settings/SecurityView.js
Normal file
@@ -0,0 +1,102 @@
|
||||
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;
|
||||
65
admin-web/src/pages/Account/Settings/models/geographic.js
Normal file
65
admin-web/src/pages/Account/Settings/models/geographic.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
13
admin-web/src/pages/Authorized.js
Normal file
13
admin-web/src/pages/Authorized.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import RenderAuthorized from '@/components/Authorized';
|
||||
import { getAuthority } from '@/utils/authority';
|
||||
import Redirect from 'umi/redirect';
|
||||
|
||||
const Authority = getAuthority();
|
||||
const Authorized = RenderAuthorized(Authority);
|
||||
|
||||
export default ({ children }) => (
|
||||
<Authorized authority={children.props.route.authority} noMatch={<Redirect to="/user/login" />}>
|
||||
{children}
|
||||
</Authorized>
|
||||
);
|
||||
190
admin-web/src/pages/Dashboard/Analysis.js
Normal file
190
admin-web/src/pages/Dashboard/Analysis.js
Normal file
@@ -0,0 +1,190 @@
|
||||
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';
|
||||
import { AsyncLoadBizCharts } from '@/components/Charts/AsyncLoadBizCharts';
|
||||
|
||||
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 props => (
|
||||
<AsyncLoadBizCharts>
|
||||
<Analysis {...props} />
|
||||
</AsyncLoadBizCharts>
|
||||
);
|
||||
198
admin-web/src/pages/Dashboard/Analysis.less
Normal file
198
admin-web/src/pages/Dashboard/Analysis.less
Normal file
@@ -0,0 +1,198 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.iconGroup {
|
||||
i {
|
||||
margin-left: 16px;
|
||||
color: @text-color-secondary;
|
||||
cursor: pointer;
|
||||
transition: color 0.32s;
|
||||
&:hover {
|
||||
color: @text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rankingList {
|
||||
margin: 25px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
li {
|
||||
.clearfix();
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
span {
|
||||
color: @text-color;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.rankingItemNumber {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 1.5px;
|
||||
margin-right: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
background-color: @background-color-base;
|
||||
border-radius: 20px;
|
||||
&.active {
|
||||
color: #fff;
|
||||
background-color: #314659;
|
||||
}
|
||||
}
|
||||
.rankingItemTitle {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.salesExtra {
|
||||
display: inline-block;
|
||||
margin-right: 24px;
|
||||
a {
|
||||
margin-left: 24px;
|
||||
color: @text-color;
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
&.currentDate {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.salesCard {
|
||||
.salesBar {
|
||||
padding: 0 0 32px 32px;
|
||||
}
|
||||
.salesRank {
|
||||
padding: 0 32px 32px 72px;
|
||||
}
|
||||
:global {
|
||||
.ant-tabs-bar {
|
||||
padding-left: 16px;
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 14px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
.ant-tabs-extra-content {
|
||||
padding-right: 24px;
|
||||
line-height: 55px;
|
||||
}
|
||||
.ant-card-head {
|
||||
position: relative;
|
||||
}
|
||||
.ant-card-head-title {
|
||||
align-items: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.salesCardExtra {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.salesTypeRadio {
|
||||
position: absolute;
|
||||
right: 54px;
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.offlineCard {
|
||||
:global {
|
||||
.ant-tabs-ink-bar {
|
||||
bottom: auto;
|
||||
}
|
||||
.ant-tabs-bar {
|
||||
border-bottom: none;
|
||||
}
|
||||
.ant-tabs-nav-container-scrolling {
|
||||
padding-right: 40px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
.ant-tabs-tab-prev-icon::before {
|
||||
position: relative;
|
||||
left: 6px;
|
||||
}
|
||||
.ant-tabs-tab-next-icon::before {
|
||||
position: relative;
|
||||
right: 6px;
|
||||
}
|
||||
.ant-tabs-tab-active h4 {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.twoColLayout {
|
||||
.salesCard {
|
||||
height: calc(100% - 24px);
|
||||
}
|
||||
div[class^='ant-col']:last-child {
|
||||
position: absolute\9;
|
||||
right: 0\9;
|
||||
height: 100%\9;
|
||||
}
|
||||
:global {
|
||||
.ant-row {
|
||||
position: relative\9;
|
||||
display: flex;
|
||||
display: block\9;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
144
admin-web/src/pages/Dashboard/IntroduceRow.js
Normal file
144
admin-web/src/pages/Dashboard/IntroduceRow.js
Normal file
@@ -0,0 +1,144 @@
|
||||
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 '@/components/Charts';
|
||||
import Trend from '@/components/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;
|
||||
249
admin-web/src/pages/Dashboard/Monitor.js
Normal file
249
admin-web/src/pages/Dashboard/Monitor.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import React, { Component } from 'react';
|
||||
import { AsyncLoadBizCharts } from '@/components/Charts/AsyncLoadBizCharts';
|
||||
import { connect } from 'dva';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import { Row, Col, Card, Tooltip } from 'antd';
|
||||
import { Pie, WaterWave, Gauge, TagCloud } from '@/components/Charts';
|
||||
import NumberInfo from '@/components/NumberInfo';
|
||||
import CountDown from '@/components/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 Component {
|
||||
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/antfincdn/h%24wFbzuuzz/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 props => (
|
||||
<AsyncLoadBizCharts>
|
||||
<Monitor {...props} />
|
||||
</AsyncLoadBizCharts>
|
||||
);
|
||||
23
admin-web/src/pages/Dashboard/Monitor.less
Normal file
23
admin-web/src/pages/Dashboard/Monitor.less
Normal file
@@ -0,0 +1,23 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.mapChart {
|
||||
height: 452px;
|
||||
padding-top: 24px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
65
admin-web/src/pages/Dashboard/OfflineData.js
Normal file
65
admin-web/src/pages/Dashboard/OfflineData.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Card, Tabs, Row, Col } from 'antd';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import styles from './Analysis.less';
|
||||
import { TimelineChart, Pie } from '@/components/Charts';
|
||||
import NumberInfo from '@/components/NumberInfo';
|
||||
|
||||
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;
|
||||
58
admin-web/src/pages/Dashboard/ProportionSales.js
Normal file
58
admin-web/src/pages/Dashboard/ProportionSales.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Card, Radio } from 'antd';
|
||||
import { FormattedMessage } from 'umi/locale';
|
||||
import styles from './Analysis.less';
|
||||
import { Pie } from '@/components/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;
|
||||
150
admin-web/src/pages/Dashboard/SalesCard.js
Normal file
150
admin-web/src/pages/Dashboard/SalesCard.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Row, Col, Card, Tabs, DatePicker } from 'antd';
|
||||
import { FormattedMessage, formatMessage } from 'umi/locale';
|
||||
import numeral from 'numeral';
|
||||
import styles from './Analysis.less';
|
||||
import { Bar } from '@/components/Charts';
|
||||
|
||||
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;
|
||||
111
admin-web/src/pages/Dashboard/TopSearch.js
Normal file
111
admin-web/src/pages/Dashboard/TopSearch.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Row, Col, Table, Tooltip, Card, Icon } from 'antd';
|
||||
import { FormattedMessage } from 'umi/locale';
|
||||
import Trend from '@/components/Trend';
|
||||
import numeral from 'numeral';
|
||||
import styles from './Analysis.less';
|
||||
import NumberInfo from '@/components/NumberInfo';
|
||||
import { MiniArea } from '@/components/Charts';
|
||||
|
||||
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;
|
||||
260
admin-web/src/pages/Dashboard/Workplace.js
Normal file
260
admin-web/src/pages/Dashboard/Workplace.js
Normal file
@@ -0,0 +1,260 @@
|
||||
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 { AsyncLoadBizCharts } from '@/components/Charts/AsyncLoadBizCharts';
|
||||
import { Radar } from '@/components/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>
|
||||
|
||||
<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 props => (
|
||||
<AsyncLoadBizCharts>
|
||||
<Workplace {...props} />
|
||||
</AsyncLoadBizCharts>
|
||||
);
|
||||
228
admin-web/src/pages/Dashboard/Workplace.less
Normal file
228
admin-web/src/pages/Dashboard/Workplace.less
Normal file
@@ -0,0 +1,228 @@
|
||||
@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 {
|
||||
display: block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 72px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
flex: 1 1 auto;
|
||||
margin-left: 24px;
|
||||
color: @text-color-secondary;
|
||||
line-height: 22px;
|
||||
.contentTitle {
|
||||
margin-bottom: 12px;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extraContent {
|
||||
.clearfix();
|
||||
|
||||
float: right;
|
||||
white-space: nowrap;
|
||||
.statItem {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0 32px;
|
||||
> p:first-child {
|
||||
margin-bottom: 4px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
line-height: 22px;
|
||||
}
|
||||
> p {
|
||||
margin: 0;
|
||||
color: @heading-color;
|
||||
font-size: 30px;
|
||||
line-height: 38px;
|
||||
> span {
|
||||
color: @text-color-secondary;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background-color: @border-color-split;
|
||||
content: '';
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.members {
|
||||
a {
|
||||
display: block;
|
||||
height: 24px;
|
||||
margin: 12px 0;
|
||||
color: @text-color;
|
||||
transition: all 0.3s;
|
||||
.textOverflow();
|
||||
.member {
|
||||
margin-left: 12px;
|
||||
font-size: @font-size-base;
|
||||
line-height: 24px;
|
||||
vertical-align: top;
|
||||
}
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.projectList {
|
||||
:global {
|
||||
.ant-card-meta-description {
|
||||
height: 44px;
|
||||
overflow: hidden;
|
||||
color: @text-color-secondary;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
.cardTitle {
|
||||
font-size: 0;
|
||||
a {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
margin-left: 12px;
|
||||
color: @heading-color;
|
||||
font-size: @font-size-base;
|
||||
line-height: 24px;
|
||||
vertical-align: top;
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
.projectGrid {
|
||||
width: 33.33%;
|
||||
}
|
||||
.projectItemContent {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
.textOverflow();
|
||||
a {
|
||||
display: inline-block;
|
||||
flex: 1 1 0;
|
||||
color: @text-color-secondary;
|
||||
.textOverflow();
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
.datetime {
|
||||
flex: 0 0 auto;
|
||||
float: right;
|
||||
color: @disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
admin-web/src/pages/Dashboard/models/activities.js
Normal file
28
admin-web/src/pages/Dashboard/models/activities.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
61
admin-web/src/pages/Dashboard/models/chart.js
Normal file
61
admin-web/src/pages/Dashboard/models/chart.js
Normal file
@@ -0,0 +1,61 @@
|
||||
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: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
28
admin-web/src/pages/Dashboard/models/monitor.js
Normal file
28
admin-web/src/pages/Dashboard/models/monitor.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
15
admin-web/src/pages/Exception/403.js
Normal file
15
admin-web/src/pages/Exception/403.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi/locale';
|
||||
import Link from 'umi/link';
|
||||
import Exception from '@/components/Exception';
|
||||
|
||||
const Exception403 = () => (
|
||||
<Exception
|
||||
type="403"
|
||||
desc={formatMessage({ id: 'app.exception.description.403' })}
|
||||
linkElement={Link}
|
||||
backText={formatMessage({ id: 'app.exception.back' })}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Exception403;
|
||||
15
admin-web/src/pages/Exception/404.js
Normal file
15
admin-web/src/pages/Exception/404.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi/locale';
|
||||
import Link from 'umi/link';
|
||||
import Exception from '@/components/Exception';
|
||||
|
||||
const Exception404 = () => (
|
||||
<Exception
|
||||
type="404"
|
||||
desc={formatMessage({ id: 'app.exception.description.404' })}
|
||||
linkElement={Link}
|
||||
backText={formatMessage({ id: 'app.exception.back' })}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Exception404;
|
||||
15
admin-web/src/pages/Exception/500.js
Normal file
15
admin-web/src/pages/Exception/500.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { formatMessage } from 'umi/locale';
|
||||
import Link from 'umi/link';
|
||||
import Exception from '@/components/Exception';
|
||||
|
||||
const Exception500 = () => (
|
||||
<Exception
|
||||
type="500"
|
||||
desc={formatMessage({ id: 'app.exception.description.500' })}
|
||||
linkElement={Link}
|
||||
backText={formatMessage({ id: 'app.exception.back' })}
|
||||
/>
|
||||
);
|
||||
|
||||
export default Exception500;
|
||||
50
admin-web/src/pages/Exception/TriggerException.js
Normal file
50
admin-web/src/pages/Exception/TriggerException.js
Normal file
@@ -0,0 +1,50 @@
|
||||
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;
|
||||
28
admin-web/src/pages/Exception/models/error.js
Normal file
28
admin-web/src/pages/Exception/models/error.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
7
admin-web/src/pages/Exception/style.less
Normal file
7
admin-web/src/pages/Exception/style.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.trigger {
|
||||
background: 'red';
|
||||
:global(.ant-btn) {
|
||||
margin-right: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
323
admin-web/src/pages/Forms/AdvancedForm.js
Normal file
323
admin-web/src/pages/Forms/AdvancedForm.js
Normal file
@@ -0,0 +1,323 @@
|
||||
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 '@/components/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;
|
||||
247
admin-web/src/pages/Forms/BasicForm.js
Normal file
247
admin-web/src/pages/Forms/BasicForm.js
Normal file
@@ -0,0 +1,247 @@
|
||||
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;
|
||||
115
admin-web/src/pages/Forms/StepForm/Step1.js
Normal file
115
admin-web/src/pages/Forms/StepForm/Step1.js
Normal file
@@ -0,0 +1,115 @@
|
||||
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;
|
||||
99
admin-web/src/pages/Forms/StepForm/Step2.js
Normal file
99
admin-web/src/pages/Forms/StepForm/Step2.js
Normal file
@@ -0,0 +1,99 @@
|
||||
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;
|
||||
74
admin-web/src/pages/Forms/StepForm/Step3.js
Normal file
74
admin-web/src/pages/Forms/StepForm/Step3.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { Button, Row, Col } from 'antd';
|
||||
import router from 'umi/router';
|
||||
import Result from '@/components/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;
|
||||
46
admin-web/src/pages/Forms/StepForm/index.js
Normal file
46
admin-web/src/pages/Forms/StepForm/index.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
78
admin-web/src/pages/Forms/StepForm/style.less
Normal file
78
admin-web/src/pages/Forms/StepForm/style.less
Normal file
@@ -0,0 +1,78 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.stepForm {
|
||||
max-width: 500px;
|
||||
margin: 40px auto 0;
|
||||
}
|
||||
|
||||
.stepFormText {
|
||||
margin-bottom: 24px;
|
||||
:global {
|
||||
.ant-form-item-label,
|
||||
.ant-form-item-control {
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 0 8px;
|
||||
}
|
||||
|
||||
.desc {
|
||||
padding: 0 56px;
|
||||
color: @text-color-secondary;
|
||||
h3 {
|
||||
margin: 0 0 12px 0;
|
||||
color: @text-color-secondary;
|
||||
font-size: 16px;
|
||||
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 {
|
||||
padding-right: 8px;
|
||||
color: @heading-color;
|
||||
text-align: right;
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.money {
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
font-family: 'Helvetica Neue', sans-serif;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
font-size: 12px;
|
||||
}
|
||||
261
admin-web/src/pages/Forms/TableForm.js
Normal file
261
admin-web/src/pages/Forms/TableForm.js
Normal file
@@ -0,0 +1,261 @@
|
||||
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;
|
||||
47
admin-web/src/pages/Forms/models/form.js
Normal file
47
admin-web/src/pages/Forms/models/form.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
90
admin-web/src/pages/Forms/style.less
Normal file
90
admin-web/src/pages/Forms/style.less
Normal file
@@ -0,0 +1,90 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.steps:global(.ant-steps) {
|
||||
max-width: 750px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
|
||||
.errorIcon {
|
||||
margin-right: 24px;
|
||||
color: @error-color;
|
||||
cursor: pointer;
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.errorPopover {
|
||||
:global {
|
||||
.ant-popover-inner-content {
|
||||
min-width: 256px;
|
||||
max-height: 290px;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.errorListItem {
|
||||
padding: 8px 16px;
|
||||
list-style: none;
|
||||
border-bottom: 1px solid @border-color-split;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background: @primary-1;
|
||||
}
|
||||
&:last-child {
|
||||
border: 0;
|
||||
}
|
||||
.errorIcon {
|
||||
float: left;
|
||||
margin-top: 4px;
|
||||
margin-right: 12px;
|
||||
padding-bottom: 22px;
|
||||
color: @error-color;
|
||||
}
|
||||
.errorField {
|
||||
margin-top: 2px;
|
||||
color: @text-color-secondary;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.editable {
|
||||
td {
|
||||
padding-top: 13px !important;
|
||||
padding-bottom: 12.5px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// custom footer for fixed footer toolbar
|
||||
.advancedForm + div {
|
||||
padding-bottom: 64px;
|
||||
}
|
||||
|
||||
.advancedForm {
|
||||
:global {
|
||||
.ant-form .ant-row:last-child .ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.ant-table td {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.optional {
|
||||
color: @text-color-secondary;
|
||||
font-style: normal;
|
||||
}
|
||||
192
admin-web/src/pages/List/Applications.js
Normal file
192
admin-web/src/pages/List/Applications.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import numeral from 'numeral';
|
||||
import { connect } from 'dva';
|
||||
import { FormattedMessage } from 'umi/locale';
|
||||
import { Row, Col, Form, Card, Select, Icon, Avatar, List, Tooltip, Dropdown, Menu } from 'antd';
|
||||
import TagSelect from '@/components/TagSelect';
|
||||
import StandardFormRow from '@/components/StandardFormRow';
|
||||
|
||||
import { formatWan } from '@/utils/utils';
|
||||
|
||||
import styles from './Applications.less';
|
||||
|
||||
const { Option } = Select;
|
||||
const FormItem = Form.Item;
|
||||
|
||||
@connect(({ list, loading }) => ({
|
||||
list,
|
||||
loading: loading.models.list,
|
||||
}))
|
||||
@Form.create({
|
||||
onValuesChange({ dispatch }, changedValues, allValues) {
|
||||
// 表单项变化时请求数据
|
||||
// eslint-disable-next-line
|
||||
console.log(changedValues, allValues);
|
||||
// 模拟查询表单生效
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 8,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
class FilterCardList extends PureComponent {
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 8,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
list: { list },
|
||||
loading,
|
||||
form,
|
||||
} = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
|
||||
const CardInfo = ({ activeUser, newUser }) => (
|
||||
<div className={styles.cardInfo}>
|
||||
<div>
|
||||
<p>活跃用户</p>
|
||||
<p>{activeUser}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>新增用户</p>
|
||||
<p>{newUser}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const formItemLayout = {
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 16 },
|
||||
},
|
||||
};
|
||||
|
||||
const actionsTextMap = {
|
||||
expandText: <FormattedMessage id="component.tagSelect.expand" defaultMessage="Expand" />,
|
||||
collapseText: (
|
||||
<FormattedMessage id="component.tagSelect.collapse" defaultMessage="Collapse" />
|
||||
),
|
||||
selectAllText: <FormattedMessage id="component.tagSelect.all" defaultMessage="All" />,
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.filterCardList}>
|
||||
<Card bordered={false}>
|
||||
<Form layout="inline">
|
||||
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
|
||||
<FormItem>
|
||||
{getFieldDecorator('category')(
|
||||
<TagSelect expandable actionsText={actionsTextMap}>
|
||||
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
|
||||
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
|
||||
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
|
||||
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
|
||||
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
|
||||
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
|
||||
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
|
||||
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
|
||||
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
|
||||
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
|
||||
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
|
||||
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
|
||||
</TagSelect>
|
||||
)}
|
||||
</FormItem>
|
||||
</StandardFormRow>
|
||||
<StandardFormRow title="其它选项" grid last>
|
||||
<Row gutter={16}>
|
||||
<Col lg={8} md={10} sm={10} xs={24}>
|
||||
<FormItem {...formItemLayout} label="作者">
|
||||
{getFieldDecorator('author', {})(
|
||||
<Select placeholder="不限" style={{ maxWidth: 200, width: '100%' }}>
|
||||
<Option value="lisa">王昭君</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col lg={8} md={10} sm={10} xs={24}>
|
||||
<FormItem {...formItemLayout} label="好评度">
|
||||
{getFieldDecorator('rate', {})(
|
||||
<Select placeholder="不限" style={{ maxWidth: 200, width: '100%' }}>
|
||||
<Option value="good">优秀</Option>
|
||||
<Option value="normal">普通</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</StandardFormRow>
|
||||
</Form>
|
||||
</Card>
|
||||
<List
|
||||
rowKey="id"
|
||||
style={{ marginTop: 24 }}
|
||||
grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
|
||||
loading={loading}
|
||||
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={styles.cardItemContent}>
|
||||
<CardInfo
|
||||
activeUser={formatWan(item.activeUser)}
|
||||
newUser={numeral(item.newUser).format('0,0')}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterCardList;
|
||||
44
admin-web/src/pages/List/Applications.less
Normal file
44
admin-web/src/pages/List/Applications.less
Normal file
@@ -0,0 +1,44 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.filterCardList {
|
||||
margin-bottom: -24px;
|
||||
:global {
|
||||
.ant-card-meta-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
// disabled white space
|
||||
.ant-card-meta-avatar {
|
||||
font-size: 0;
|
||||
}
|
||||
.ant-card-actions {
|
||||
background: #f7f9fa;
|
||||
}
|
||||
.ant-list .ant-list-item-content-single {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.cardInfo {
|
||||
.clearfix();
|
||||
|
||||
margin-top: 16px;
|
||||
margin-left: 40px;
|
||||
& > div {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 50%;
|
||||
text-align: left;
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
p:first-child {
|
||||
margin-bottom: 4px;
|
||||
color: @text-color-secondary;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
251
admin-web/src/pages/List/Articles.js
Normal file
251
admin-web/src/pages/List/Articles.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { Form, Card, Select, List, Tag, Icon, Row, Col, Button } from 'antd';
|
||||
import { FormattedMessage } from 'umi/locale';
|
||||
|
||||
import TagSelect from '@/components/TagSelect';
|
||||
import StandardFormRow from '@/components/StandardFormRow';
|
||||
import ArticleListContent from '@/components/ArticleListContent';
|
||||
import styles from './Articles.less';
|
||||
|
||||
const { Option } = Select;
|
||||
const FormItem = Form.Item;
|
||||
|
||||
const pageSize = 5;
|
||||
|
||||
@connect(({ list, loading }) => ({
|
||||
list,
|
||||
loading: loading.models.list,
|
||||
}))
|
||||
@Form.create({
|
||||
onValuesChange({ dispatch }, changedValues, allValues) {
|
||||
// 表单项变化时请求数据
|
||||
// eslint-disable-next-line
|
||||
console.log(changedValues, allValues);
|
||||
// 模拟查询表单生效
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
class SearchList extends Component {
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setOwner = () => {
|
||||
const { form } = this.props;
|
||||
form.setFieldsValue({
|
||||
owner: ['wzj'],
|
||||
});
|
||||
};
|
||||
|
||||
fetchMore = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/appendFetch',
|
||||
payload: {
|
||||
count: pageSize,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
form,
|
||||
list: { list },
|
||||
loading,
|
||||
} = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
|
||||
const owners = [
|
||||
{
|
||||
id: 'wzj',
|
||||
name: '我自己',
|
||||
},
|
||||
{
|
||||
id: 'wjh',
|
||||
name: '吴家豪',
|
||||
},
|
||||
{
|
||||
id: 'zxx',
|
||||
name: '周星星',
|
||||
},
|
||||
{
|
||||
id: 'zly',
|
||||
name: '赵丽颖',
|
||||
},
|
||||
{
|
||||
id: 'ym',
|
||||
name: '姚明',
|
||||
},
|
||||
];
|
||||
|
||||
const IconText = ({ type, text }) => (
|
||||
<span>
|
||||
<Icon type={type} style={{ marginRight: 8 }} />
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
|
||||
const formItemLayout = {
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 24 },
|
||||
md: { span: 12 },
|
||||
},
|
||||
};
|
||||
|
||||
const actionsTextMap = {
|
||||
expandText: <FormattedMessage id="component.tagSelect.expand" defaultMessage="Expand" />,
|
||||
collapseText: (
|
||||
<FormattedMessage id="component.tagSelect.collapse" defaultMessage="Collapse" />
|
||||
),
|
||||
selectAllText: <FormattedMessage id="component.tagSelect.all" defaultMessage="All" />,
|
||||
};
|
||||
|
||||
const loadMore =
|
||||
list.length > 0 ? (
|
||||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||||
<Button onClick={this.fetchMore} style={{ paddingLeft: 48, paddingRight: 48 }}>
|
||||
{loading ? (
|
||||
<span>
|
||||
<Icon type="loading" /> 加载中...
|
||||
</span>
|
||||
) : (
|
||||
'加载更多'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Card bordered={false}>
|
||||
<Form layout="inline">
|
||||
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
|
||||
<FormItem>
|
||||
{getFieldDecorator('category')(
|
||||
<TagSelect expandable actionsText={actionsTextMap}>
|
||||
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
|
||||
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
|
||||
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
|
||||
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
|
||||
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
|
||||
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
|
||||
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
|
||||
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
|
||||
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
|
||||
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
|
||||
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
|
||||
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
|
||||
</TagSelect>
|
||||
)}
|
||||
</FormItem>
|
||||
</StandardFormRow>
|
||||
<StandardFormRow title="owner" grid>
|
||||
<Row>
|
||||
<Col>
|
||||
<FormItem {...formItemLayout}>
|
||||
{getFieldDecorator('owner', {
|
||||
initialValue: ['wjh', 'zxx'],
|
||||
})(
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ maxWidth: 286, width: '100%' }}
|
||||
placeholder="选择 owner"
|
||||
>
|
||||
{owners.map(owner => (
|
||||
<Option key={owner.id} value={owner.id}>
|
||||
{owner.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<a className={styles.selfTrigger} onClick={this.setOwner}>
|
||||
只看自己的
|
||||
</a>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</StandardFormRow>
|
||||
<StandardFormRow title="其它选项" grid last>
|
||||
<Row gutter={16}>
|
||||
<Col xl={8} lg={10} md={12} sm={24} xs={24}>
|
||||
<FormItem {...formItemLayout} label="活跃用户">
|
||||
{getFieldDecorator('user', {})(
|
||||
<Select placeholder="不限" style={{ maxWidth: 200, width: '100%' }}>
|
||||
<Option value="lisa">李三</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col xl={8} lg={10} md={12} sm={24} xs={24}>
|
||||
<FormItem {...formItemLayout} label="好评度">
|
||||
{getFieldDecorator('rate', {})(
|
||||
<Select placeholder="不限" style={{ maxWidth: 200, width: '100%' }}>
|
||||
<Option value="good">优秀</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</StandardFormRow>
|
||||
</Form>
|
||||
</Card>
|
||||
<Card
|
||||
style={{ marginTop: 24 }}
|
||||
bordered={false}
|
||||
bodyStyle={{ padding: '8px 32px 32px 32px' }}
|
||||
>
|
||||
<List
|
||||
size="large"
|
||||
loading={list.length === 0 ? loading : false}
|
||||
rowKey="id"
|
||||
itemLayout="vertical"
|
||||
loadMore={loadMore}
|
||||
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} />,
|
||||
]}
|
||||
extra={<div className={styles.listItemExtra} />}
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchList;
|
||||
31
admin-web/src/pages/List/Articles.less
Normal file
31
admin-web/src/pages/List/Articles.less
Normal file
@@ -0,0 +1,31 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
a.listItemMetaTitle {
|
||||
color: @heading-color;
|
||||
}
|
||||
.listItemExtra {
|
||||
width: 272px;
|
||||
height: 1px;
|
||||
}
|
||||
.selfTrigger {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xs) {
|
||||
.selfTrigger {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: @screen-md) {
|
||||
.selfTrigger {
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: @screen-lg) {
|
||||
.listItemExtra {
|
||||
width: 0;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
340
admin-web/src/pages/List/BasicList.js
Normal file
340
admin-web/src/pages/List/BasicList.js
Normal file
@@ -0,0 +1,340 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'dva';
|
||||
import {
|
||||
List,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Radio,
|
||||
Input,
|
||||
Progress,
|
||||
Button,
|
||||
Icon,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Avatar,
|
||||
Modal,
|
||||
Form,
|
||||
DatePicker,
|
||||
Select,
|
||||
} from 'antd';
|
||||
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
import Result from '@/components/Result';
|
||||
|
||||
import styles from './BasicList.less';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;
|
||||
const SelectOption = Select.Option;
|
||||
const { Search, TextArea } = Input;
|
||||
|
||||
@connect(({ list, loading }) => ({
|
||||
list,
|
||||
loading: loading.models.list,
|
||||
}))
|
||||
@Form.create()
|
||||
class BasicList extends PureComponent {
|
||||
state = { visible: false, done: false };
|
||||
|
||||
formLayout = {
|
||||
labelCol: { span: 7 },
|
||||
wrapperCol: { span: 13 },
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showModal = () => {
|
||||
this.setState({
|
||||
visible: true,
|
||||
current: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
showEditModal = item => {
|
||||
this.setState({
|
||||
visible: true,
|
||||
current: item,
|
||||
});
|
||||
};
|
||||
|
||||
handleDone = () => {
|
||||
setTimeout(() => this.addBtn.blur(), 0);
|
||||
this.setState({
|
||||
done: false,
|
||||
visible: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleCancel = () => {
|
||||
setTimeout(() => this.addBtn.blur(), 0);
|
||||
this.setState({
|
||||
visible: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
const { dispatch, form } = this.props;
|
||||
const { current } = this.state;
|
||||
const id = current ? current.id : '';
|
||||
|
||||
setTimeout(() => this.addBtn.blur(), 0);
|
||||
form.validateFields((err, fieldsValue) => {
|
||||
if (err) return;
|
||||
this.setState({
|
||||
done: true,
|
||||
});
|
||||
dispatch({
|
||||
type: 'list/submit',
|
||||
payload: { id, ...fieldsValue },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deleteItem = id => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/submit',
|
||||
payload: { id },
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
list: { list },
|
||||
loading,
|
||||
} = this.props;
|
||||
const {
|
||||
form: { getFieldDecorator },
|
||||
} = this.props;
|
||||
const { visible, done, current = {} } = this.state;
|
||||
|
||||
const editAndDelete = (key, currentItem) => {
|
||||
if (key === 'edit') this.showEditModal(currentItem);
|
||||
else if (key === 'delete') {
|
||||
Modal.confirm({
|
||||
title: '删除任务',
|
||||
content: '确定删除该任务吗?',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => this.deleteItem(currentItem.id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const modalFooter = done
|
||||
? { footer: null, onCancel: this.handleDone }
|
||||
: { okText: '保存', onOk: this.handleSubmit, onCancel: this.handleCancel };
|
||||
|
||||
const Info = ({ title, value, bordered }) => (
|
||||
<div className={styles.headerInfo}>
|
||||
<span>{title}</span>
|
||||
<p>{value}</p>
|
||||
{bordered && <em />}
|
||||
</div>
|
||||
);
|
||||
|
||||
const extraContent = (
|
||||
<div className={styles.extraContent}>
|
||||
<RadioGroup defaultValue="all">
|
||||
<RadioButton value="all">全部</RadioButton>
|
||||
<RadioButton value="progress">进行中</RadioButton>
|
||||
<RadioButton value="waiting">等待中</RadioButton>
|
||||
</RadioGroup>
|
||||
<Search className={styles.extraContentSearch} placeholder="请输入" onSearch={() => ({})} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const paginationProps = {
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
pageSize: 5,
|
||||
total: 50,
|
||||
};
|
||||
|
||||
const ListContent = ({ data: { owner, createdAt, percent, status } }) => (
|
||||
<div className={styles.listContent}>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>Owner</span>
|
||||
<p>{owner}</p>
|
||||
</div>
|
||||
<div className={styles.listContentItem}>
|
||||
<span>开始时间</span>
|
||||
<p>{moment(createdAt).format('YYYY-MM-DD HH:mm')}</p>
|
||||
</div>
|
||||
<div className={styles.listContentItem}>
|
||||
<Progress percent={percent} status={status} strokeWidth={6} style={{ width: 180 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MoreBtn = props => (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu onClick={({ key }) => editAndDelete(key, props.current)}>
|
||||
<Menu.Item key="edit">编辑</Menu.Item>
|
||||
<Menu.Item key="delete">删除</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<a>
|
||||
更多 <Icon type="down" />
|
||||
</a>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
const getModalContent = () => {
|
||||
if (done) {
|
||||
return (
|
||||
<Result
|
||||
type="success"
|
||||
title="操作成功"
|
||||
description="一系列的信息描述,很短同样也可以带标点。"
|
||||
actions={
|
||||
<Button type="primary" onClick={this.handleDone}>
|
||||
知道了
|
||||
</Button>
|
||||
}
|
||||
className={styles.formResult}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem label="任务名称" {...this.formLayout}>
|
||||
{getFieldDecorator('title', {
|
||||
rules: [{ required: true, message: '请输入任务名称' }],
|
||||
initialValue: current.title,
|
||||
})(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
<FormItem label="开始时间" {...this.formLayout}>
|
||||
{getFieldDecorator('createdAt', {
|
||||
rules: [{ required: true, message: '请选择开始时间' }],
|
||||
initialValue: current.createdAt ? moment(current.createdAt) : null,
|
||||
})(
|
||||
<DatePicker
|
||||
showTime
|
||||
placeholder="请选择"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem label="任务负责人" {...this.formLayout}>
|
||||
{getFieldDecorator('owner', {
|
||||
rules: [{ required: true, message: '请选择任务负责人' }],
|
||||
initialValue: current.owner,
|
||||
})(
|
||||
<Select placeholder="请选择">
|
||||
<SelectOption value="付晓晓">付晓晓</SelectOption>
|
||||
<SelectOption value="周毛毛">周毛毛</SelectOption>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem {...this.formLayout} label="产品描述">
|
||||
{getFieldDecorator('subDescription', {
|
||||
rules: [{ message: '请输入至少五个字符的产品描述!', min: 5 }],
|
||||
initialValue: current.subDescription,
|
||||
})(<TextArea rows={4} placeholder="请输入至少五个字符" />)}
|
||||
</FormItem>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<PageHeaderWrapper>
|
||||
<div className={styles.standardList}>
|
||||
<Card bordered={false}>
|
||||
<Row>
|
||||
<Col sm={8} xs={24}>
|
||||
<Info title="我的待办" value="8个任务" bordered />
|
||||
</Col>
|
||||
<Col sm={8} xs={24}>
|
||||
<Info title="本周任务平均处理时间" value="32分钟" bordered />
|
||||
</Col>
|
||||
<Col sm={8} xs={24}>
|
||||
<Info title="本周完成任务数" value="24个任务" />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className={styles.listCard}
|
||||
bordered={false}
|
||||
title="标准列表"
|
||||
style={{ marginTop: 24 }}
|
||||
bodyStyle={{ padding: '0 32px 40px 32px' }}
|
||||
extra={extraContent}
|
||||
>
|
||||
<Button
|
||||
type="dashed"
|
||||
style={{ width: '100%', marginBottom: 8 }}
|
||||
icon="plus"
|
||||
onClick={this.showModal}
|
||||
ref={component => {
|
||||
/* eslint-disable */
|
||||
this.addBtn = findDOMNode(component);
|
||||
/* eslint-enable */
|
||||
}}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
<List
|
||||
size="large"
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={paginationProps}
|
||||
dataSource={list}
|
||||
renderItem={item => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<a
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
this.showEditModal(item);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>,
|
||||
<MoreBtn current={item} />,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar src={item.logo} shape="square" size="large" />}
|
||||
title={<a href={item.href}>{item.title}</a>}
|
||||
description={item.subDescription}
|
||||
/>
|
||||
<ListContent data={item} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
<Modal
|
||||
title={done ? null : `任务${current.id ? '编辑' : '添加'}`}
|
||||
className={styles.standardListForm}
|
||||
width={640}
|
||||
bodyStyle={done ? { padding: '72px 0' } : { padding: '28px 0 0' }}
|
||||
destroyOnClose
|
||||
visible={visible}
|
||||
{...modalFooter}
|
||||
>
|
||||
{getModalContent()}
|
||||
</Modal>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicList;
|
||||
195
admin-web/src/pages/List/BasicList.less
Normal file
195
admin-web/src/pages/List/BasicList.less
Normal file
@@ -0,0 +1,195 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.standardList {
|
||||
:global {
|
||||
.ant-card-head {
|
||||
border-bottom: none;
|
||||
}
|
||||
.ant-card-head-title {
|
||||
padding: 24px 0;
|
||||
line-height: 32px;
|
||||
}
|
||||
.ant-card-extra {
|
||||
padding: 24px 0;
|
||||
}
|
||||
.ant-list-pagination {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
.ant-avatar-lg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
.headerInfo {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
& > span {
|
||||
display: inline-block;
|
||||
margin-bottom: 4px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
line-height: 22px;
|
||||
}
|
||||
& > p {
|
||||
margin: 0;
|
||||
color: @heading-color;
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
& > em {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 56px;
|
||||
background-color: @border-color-split;
|
||||
}
|
||||
}
|
||||
.listContent {
|
||||
font-size: 0;
|
||||
.listContentItem {
|
||||
display: inline-block;
|
||||
margin-left: 40px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
vertical-align: middle;
|
||||
> span {
|
||||
line-height: 20px;
|
||||
}
|
||||
> p {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.extraContentSearch {
|
||||
width: 272px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xs) {
|
||||
.standardList {
|
||||
:global {
|
||||
.ant-list-item-content {
|
||||
display: block;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
.ant-list-item-action {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.listContent {
|
||||
margin-left: 0;
|
||||
& > div {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
.listCard {
|
||||
:global {
|
||||
.ant-card-head-title {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
.standardList {
|
||||
.extraContentSearch {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
.headerInfo {
|
||||
margin-bottom: 16px;
|
||||
& > em {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-md) {
|
||||
.standardList {
|
||||
.listContent {
|
||||
& > div {
|
||||
display: block;
|
||||
}
|
||||
& > div:last-child {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listCard {
|
||||
:global {
|
||||
.ant-radio-group {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-lg) and (min-width: @screen-md) {
|
||||
.standardList {
|
||||
.listContent {
|
||||
& > div {
|
||||
display: block;
|
||||
}
|
||||
& > div:last-child {
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-xl) {
|
||||
.standardList {
|
||||
.listContent {
|
||||
& > div {
|
||||
margin-left: 24px;
|
||||
}
|
||||
& > div:last-child {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1400px) {
|
||||
.standardList {
|
||||
.listContent {
|
||||
text-align: right;
|
||||
& > div:last-child {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.standardListForm {
|
||||
:global {
|
||||
.ant-form-item {
|
||||
margin-bottom: 12px;
|
||||
&:last-child {
|
||||
margin-bottom: 32px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.formResult {
|
||||
width: 100%;
|
||||
[class^='title'] {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
101
admin-web/src/pages/List/CardList.js
Normal file
101
admin-web/src/pages/List/CardList.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { Card, Button, Icon, List } from 'antd';
|
||||
|
||||
import Ellipsis from '@/components/Ellipsis';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
import styles from './CardList.less';
|
||||
|
||||
@connect(({ list, loading }) => ({
|
||||
list,
|
||||
loading: loading.models.list,
|
||||
}))
|
||||
class CardList extends PureComponent {
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 8,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
list: { list },
|
||||
loading,
|
||||
} = this.props;
|
||||
|
||||
const content = (
|
||||
<div className={styles.pageHeaderContent}>
|
||||
<p>
|
||||
段落示意:蚂蚁金服务设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,
|
||||
提供跨越设计与开发的体验解决方案。
|
||||
</p>
|
||||
<div className={styles.contentLink}>
|
||||
<a>
|
||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg" />{' '}
|
||||
快速开始
|
||||
</a>
|
||||
<a>
|
||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg" />{' '}
|
||||
产品简介
|
||||
</a>
|
||||
<a>
|
||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg" />{' '}
|
||||
产品文档
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const extraContent = (
|
||||
<div className={styles.extraImg}>
|
||||
<img
|
||||
alt="这是一个标题"
|
||||
src="https://gw.alipayobjects.com/zos/rmsportal/RzwpdLnhmvDJToTdfDPe.png"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<PageHeaderWrapper title="卡片列表" content={content} extraContent={extraContent}>
|
||||
<div className={styles.cardList}>
|
||||
<List
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
grid={{ gutter: 24, lg: 3, md: 2, sm: 1, xs: 1 }}
|
||||
dataSource={['', ...list]}
|
||||
renderItem={item =>
|
||||
item ? (
|
||||
<List.Item key={item.id}>
|
||||
<Card hoverable className={styles.card} actions={[<a>操作一</a>, <a>操作二</a>]}>
|
||||
<Card.Meta
|
||||
avatar={<img alt="" className={styles.cardAvatar} src={item.avatar} />}
|
||||
title={<a>{item.title}</a>}
|
||||
description={
|
||||
<Ellipsis className={styles.item} lines={3}>
|
||||
{item.description}
|
||||
</Ellipsis>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</List.Item>
|
||||
) : (
|
||||
<List.Item>
|
||||
<Button type="dashed" className={styles.newButton}>
|
||||
<Icon type="plus" /> 新建产品
|
||||
</Button>
|
||||
</List.Item>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CardList;
|
||||
113
admin-web/src/pages/List/CardList.less
Normal file
113
admin-web/src/pages/List/CardList.less
Normal file
@@ -0,0 +1,113 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.cardList {
|
||||
margin-bottom: -24px;
|
||||
|
||||
.card {
|
||||
:global {
|
||||
.ant-card-meta-title {
|
||||
margin-bottom: 12px;
|
||||
& > a {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
color: @heading-color;
|
||||
}
|
||||
}
|
||||
.ant-card-actions {
|
||||
background: #f7f9fa;
|
||||
}
|
||||
.ant-card-body:hover {
|
||||
.ant-card-meta-title > a {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.item {
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-list .ant-list-item-content-single {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extraImg {
|
||||
width: 195px;
|
||||
margin-top: -60px;
|
||||
text-align: center;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.newButton {
|
||||
width: 100%;
|
||||
height: 188px;
|
||||
color: @text-color-secondary;
|
||||
background-color: #fff;
|
||||
border-color: @border-color-base;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.cardAvatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 48px;
|
||||
}
|
||||
|
||||
.cardDescription {
|
||||
.textOverflowMulti();
|
||||
}
|
||||
|
||||
.pageHeaderContent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contentLink {
|
||||
margin-top: 16px;
|
||||
a {
|
||||
margin-right: 32px;
|
||||
img {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-lg) {
|
||||
.contentLink {
|
||||
a {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: @screen-md) {
|
||||
.extraImg {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
.pageHeaderContent {
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
.contentLink {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
left: 0;
|
||||
width: 1000px;
|
||||
a {
|
||||
margin-right: 16px;
|
||||
}
|
||||
img {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
admin-web/src/pages/List/List.js
Normal file
80
admin-web/src/pages/List/List.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { Component } from 'react';
|
||||
import router from 'umi/router';
|
||||
import { connect } from 'dva';
|
||||
import { Input } from 'antd';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
@connect()
|
||||
class SearchList extends Component {
|
||||
handleTabChange = 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;
|
||||
}
|
||||
};
|
||||
|
||||
handleFormSubmit = value => {
|
||||
// eslint-disable-next-line
|
||||
console.log(value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const tabList = [
|
||||
{
|
||||
key: 'articles',
|
||||
tab: '文章',
|
||||
},
|
||||
{
|
||||
key: 'projects',
|
||||
tab: '项目',
|
||||
},
|
||||
{
|
||||
key: 'applications',
|
||||
tab: '应用',
|
||||
},
|
||||
];
|
||||
|
||||
const mainSearch = (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Input.Search
|
||||
placeholder="请输入"
|
||||
enterButton="搜索"
|
||||
size="large"
|
||||
onSearch={this.handleFormSubmit}
|
||||
style={{ width: 522 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const { match, children, location } = this.props;
|
||||
|
||||
return (
|
||||
<PageHeaderWrapper
|
||||
title="搜索列表"
|
||||
content={mainSearch}
|
||||
tabList={tabList}
|
||||
tabActiveKey={location.pathname.replace(`${match.path}/`, '')}
|
||||
onTabChange={this.handleTabChange}
|
||||
>
|
||||
{children}
|
||||
{/* <Switch>
|
||||
{routes.map(item => (
|
||||
<Route key={item.key} path={item.path} component={item.component} exact={item.exact} />
|
||||
))}
|
||||
</Switch> */}
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchList;
|
||||
163
admin-web/src/pages/List/Projects.js
Normal file
163
admin-web/src/pages/List/Projects.js
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import moment from 'moment';
|
||||
import { connect } from 'dva';
|
||||
import { Row, Col, Form, Card, Select, List } from 'antd';
|
||||
import { FormattedMessage } from 'umi/locale';
|
||||
|
||||
import TagSelect from '@/components/TagSelect';
|
||||
import AvatarList from '@/components/AvatarList';
|
||||
import Ellipsis from '@/components/Ellipsis';
|
||||
import StandardFormRow from '@/components/StandardFormRow';
|
||||
|
||||
import styles from './Projects.less';
|
||||
|
||||
const { Option } = Select;
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/* eslint react/no-array-index-key: 0 */
|
||||
|
||||
@connect(({ list, loading }) => ({
|
||||
list,
|
||||
loading: loading.models.list,
|
||||
}))
|
||||
@Form.create({
|
||||
onValuesChange({ dispatch }, changedValues, allValues) {
|
||||
// 表单项变化时请求数据
|
||||
// eslint-disable-next-line
|
||||
console.log(changedValues, allValues);
|
||||
// 模拟查询表单生效
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 8,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
class CoverCardList extends PureComponent {
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'list/fetch',
|
||||
payload: {
|
||||
count: 8,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
list: { list = [] },
|
||||
loading,
|
||||
form,
|
||||
} = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
|
||||
const cardList = list ? (
|
||||
<List
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
grid={{ gutter: 24, xl: 4, lg: 3, md: 3, sm: 2, xs: 1 }}
|
||||
dataSource={list}
|
||||
renderItem={item => (
|
||||
<List.Item>
|
||||
<Card
|
||||
className={styles.card}
|
||||
hoverable
|
||||
cover={<img alt={item.title} src={item.cover} />}
|
||||
>
|
||||
<Card.Meta
|
||||
title={<a>{item.title}</a>}
|
||||
description={<Ellipsis lines={2}>{item.subDescription}</Ellipsis>}
|
||||
/>
|
||||
<div className={styles.cardItemContent}>
|
||||
<span>{moment(item.updatedAt).fromNow()}</span>
|
||||
<div className={styles.avatarList}>
|
||||
<AvatarList size="mini">
|
||||
{item.members.map((member, i) => (
|
||||
<AvatarList.Item
|
||||
key={`${item.id}-avatar-${i}`}
|
||||
src={member.avatar}
|
||||
tips={member.name}
|
||||
/>
|
||||
))}
|
||||
</AvatarList>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const formItemLayout = {
|
||||
wrapperCol: {
|
||||
xs: { span: 24 },
|
||||
sm: { span: 16 },
|
||||
},
|
||||
};
|
||||
|
||||
const actionsTextMap = {
|
||||
expandText: <FormattedMessage id="component.tagSelect.expand" defaultMessage="Expand" />,
|
||||
collapseText: (
|
||||
<FormattedMessage id="component.tagSelect.collapse" defaultMessage="Collapse" />
|
||||
),
|
||||
selectAllText: <FormattedMessage id="component.tagSelect.all" defaultMessage="All" />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.coverCardList}>
|
||||
<Card bordered={false}>
|
||||
<Form layout="inline">
|
||||
<StandardFormRow title="所属类目" block style={{ paddingBottom: 11 }}>
|
||||
<FormItem>
|
||||
{getFieldDecorator('category')(
|
||||
<TagSelect expandable actionsText={actionsTextMap}>
|
||||
<TagSelect.Option value="cat1">类目一</TagSelect.Option>
|
||||
<TagSelect.Option value="cat2">类目二</TagSelect.Option>
|
||||
<TagSelect.Option value="cat3">类目三</TagSelect.Option>
|
||||
<TagSelect.Option value="cat4">类目四</TagSelect.Option>
|
||||
<TagSelect.Option value="cat5">类目五</TagSelect.Option>
|
||||
<TagSelect.Option value="cat6">类目六</TagSelect.Option>
|
||||
<TagSelect.Option value="cat7">类目七</TagSelect.Option>
|
||||
<TagSelect.Option value="cat8">类目八</TagSelect.Option>
|
||||
<TagSelect.Option value="cat9">类目九</TagSelect.Option>
|
||||
<TagSelect.Option value="cat10">类目十</TagSelect.Option>
|
||||
<TagSelect.Option value="cat11">类目十一</TagSelect.Option>
|
||||
<TagSelect.Option value="cat12">类目十二</TagSelect.Option>
|
||||
</TagSelect>
|
||||
)}
|
||||
</FormItem>
|
||||
</StandardFormRow>
|
||||
<StandardFormRow title="其它选项" grid last>
|
||||
<Row gutter={16}>
|
||||
<Col lg={8} md={10} sm={10} xs={24}>
|
||||
<FormItem {...formItemLayout} label="作者">
|
||||
{getFieldDecorator('author', {})(
|
||||
<Select placeholder="不限" style={{ maxWidth: 200, width: '100%' }}>
|
||||
<Option value="lisa">王昭君</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col lg={8} md={10} sm={10} xs={24}>
|
||||
<FormItem {...formItemLayout} label="好评度">
|
||||
{getFieldDecorator('rate', {})(
|
||||
<Select placeholder="不限" style={{ maxWidth: 200, width: '100%' }}>
|
||||
<Option value="good">优秀</Option>
|
||||
<Option value="normal">普通</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
</StandardFormRow>
|
||||
</Form>
|
||||
</Card>
|
||||
<div className={styles.cardList}>{cardList}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CoverCardList;
|
||||
57
admin-web/src/pages/List/Projects.less
Normal file
57
admin-web/src/pages/List/Projects.less
Normal file
@@ -0,0 +1,57 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.coverCardList {
|
||||
margin-bottom: -24px;
|
||||
|
||||
.card {
|
||||
:global {
|
||||
.ant-card-meta-title {
|
||||
margin-bottom: 4px;
|
||||
& > a {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
color: @heading-color;
|
||||
}
|
||||
}
|
||||
.ant-card-meta-description {
|
||||
height: 44px;
|
||||
overflow: hidden;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:global {
|
||||
.ant-card-meta-title > a {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cardItemContent {
|
||||
display: flex;
|
||||
height: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: -4px;
|
||||
line-height: 20px;
|
||||
& > span {
|
||||
flex: 1;
|
||||
color: @text-color-secondary;
|
||||
font-size: 12px;
|
||||
}
|
||||
.avatarList {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
.cardList {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
:global {
|
||||
.ant-list .ant-list-item-content-single {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
691
admin-web/src/pages/List/TableList.js
Normal file
691
admin-web/src/pages/List/TableList.js
Normal file
@@ -0,0 +1,691 @@
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import moment from 'moment';
|
||||
import router from 'umi/router';
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Icon,
|
||||
Button,
|
||||
Dropdown,
|
||||
Menu,
|
||||
InputNumber,
|
||||
DatePicker,
|
||||
Modal,
|
||||
message,
|
||||
Badge,
|
||||
Divider,
|
||||
Steps,
|
||||
Radio,
|
||||
} from 'antd';
|
||||
import StandardTable from '@/components/StandardTable';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
import styles from './TableList.less';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { Step } = Steps;
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
const RadioGroup = Radio.Group;
|
||||
const getValue = obj =>
|
||||
Object.keys(obj)
|
||||
.map(key => obj[key])
|
||||
.join(',');
|
||||
const statusMap = ['default', 'processing', 'success', 'error'];
|
||||
const status = ['关闭', '运行中', '已上线', '异常'];
|
||||
|
||||
const CreateForm = Form.create()(props => {
|
||||
const { modalVisible, form, handleAdd, handleModalVisible } = props;
|
||||
const okHandle = () => {
|
||||
form.validateFields((err, fieldsValue) => {
|
||||
if (err) return;
|
||||
form.resetFields();
|
||||
handleAdd(fieldsValue);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
destroyOnClose
|
||||
title="新建规则"
|
||||
visible={modalVisible}
|
||||
onOk={okHandle}
|
||||
onCancel={() => handleModalVisible()}
|
||||
>
|
||||
<FormItem labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="描述">
|
||||
{form.getFieldDecorator('desc', {
|
||||
rules: [{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }],
|
||||
})(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
@Form.create()
|
||||
class UpdateForm extends PureComponent {
|
||||
static defaultProps = {
|
||||
handleUpdate: () => {},
|
||||
handleUpdateModalVisible: () => {},
|
||||
values: {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
formVals: {
|
||||
name: props.values.name,
|
||||
desc: props.values.desc,
|
||||
key: props.values.key,
|
||||
target: '0',
|
||||
template: '0',
|
||||
type: '1',
|
||||
time: '',
|
||||
frequency: 'month',
|
||||
},
|
||||
currentStep: 0,
|
||||
};
|
||||
|
||||
this.formLayout = {
|
||||
labelCol: { span: 7 },
|
||||
wrapperCol: { span: 13 },
|
||||
};
|
||||
}
|
||||
|
||||
handleNext = currentStep => {
|
||||
const { form, handleUpdate } = this.props;
|
||||
const { formVals: oldValue } = this.state;
|
||||
form.validateFields((err, fieldsValue) => {
|
||||
if (err) return;
|
||||
const formVals = { ...oldValue, ...fieldsValue };
|
||||
this.setState(
|
||||
{
|
||||
formVals,
|
||||
},
|
||||
() => {
|
||||
if (currentStep < 2) {
|
||||
this.forward();
|
||||
} else {
|
||||
handleUpdate(formVals);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
backward = () => {
|
||||
const { currentStep } = this.state;
|
||||
this.setState({
|
||||
currentStep: currentStep - 1,
|
||||
});
|
||||
};
|
||||
|
||||
forward = () => {
|
||||
const { currentStep } = this.state;
|
||||
this.setState({
|
||||
currentStep: currentStep + 1,
|
||||
});
|
||||
};
|
||||
|
||||
renderContent = (currentStep, formVals) => {
|
||||
const { form } = this.props;
|
||||
if (currentStep === 1) {
|
||||
return [
|
||||
<FormItem key="target" {...this.formLayout} label="监控对象">
|
||||
{form.getFieldDecorator('target', {
|
||||
initialValue: formVals.target,
|
||||
})(
|
||||
<Select style={{ width: '100%' }}>
|
||||
<Option value="0">表一</Option>
|
||||
<Option value="1">表二</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>,
|
||||
<FormItem key="template" {...this.formLayout} label="规则模板">
|
||||
{form.getFieldDecorator('template', {
|
||||
initialValue: formVals.template,
|
||||
})(
|
||||
<Select style={{ width: '100%' }}>
|
||||
<Option value="0">规则模板一</Option>
|
||||
<Option value="1">规则模板二</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>,
|
||||
<FormItem key="type" {...this.formLayout} label="规则类型">
|
||||
{form.getFieldDecorator('type', {
|
||||
initialValue: formVals.type,
|
||||
})(
|
||||
<RadioGroup>
|
||||
<Radio value="0">强</Radio>
|
||||
<Radio value="1">弱</Radio>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormItem>,
|
||||
];
|
||||
}
|
||||
if (currentStep === 2) {
|
||||
return [
|
||||
<FormItem key="time" {...this.formLayout} label="开始时间">
|
||||
{form.getFieldDecorator('time', {
|
||||
rules: [{ required: true, message: '请选择开始时间!' }],
|
||||
})(
|
||||
<DatePicker
|
||||
style={{ width: '100%' }}
|
||||
showTime
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择开始时间"
|
||||
/>
|
||||
)}
|
||||
</FormItem>,
|
||||
<FormItem key="frequency" {...this.formLayout} label="调度周期">
|
||||
{form.getFieldDecorator('frequency', {
|
||||
initialValue: formVals.frequency,
|
||||
})(
|
||||
<Select style={{ width: '100%' }}>
|
||||
<Option value="month">月</Option>
|
||||
<Option value="week">周</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>,
|
||||
];
|
||||
}
|
||||
return [
|
||||
<FormItem key="name" {...this.formLayout} label="规则名称">
|
||||
{form.getFieldDecorator('name', {
|
||||
rules: [{ required: true, message: '请输入规则名称!' }],
|
||||
initialValue: formVals.name,
|
||||
})(<Input placeholder="请输入" />)}
|
||||
</FormItem>,
|
||||
<FormItem key="desc" {...this.formLayout} label="规则描述">
|
||||
{form.getFieldDecorator('desc', {
|
||||
rules: [{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 }],
|
||||
initialValue: formVals.desc,
|
||||
})(<TextArea rows={4} placeholder="请输入至少五个字符" />)}
|
||||
</FormItem>,
|
||||
];
|
||||
};
|
||||
|
||||
renderFooter = currentStep => {
|
||||
const { handleUpdateModalVisible, values } = this.props;
|
||||
if (currentStep === 1) {
|
||||
return [
|
||||
<Button key="back" style={{ float: 'left' }} onClick={this.backward}>
|
||||
上一步
|
||||
</Button>,
|
||||
<Button key="cancel" onClick={() => handleUpdateModalVisible(false, values)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="forward" type="primary" onClick={() => this.handleNext(currentStep)}>
|
||||
下一步
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
if (currentStep === 2) {
|
||||
return [
|
||||
<Button key="back" style={{ float: 'left' }} onClick={this.backward}>
|
||||
上一步
|
||||
</Button>,
|
||||
<Button key="cancel" onClick={() => handleUpdateModalVisible(false, values)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={() => this.handleNext(currentStep)}>
|
||||
完成
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
return [
|
||||
<Button key="cancel" onClick={() => handleUpdateModalVisible(false, values)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="forward" type="primary" onClick={() => this.handleNext(currentStep)}>
|
||||
下一步
|
||||
</Button>,
|
||||
];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { updateModalVisible, handleUpdateModalVisible, values } = this.props;
|
||||
const { currentStep, formVals } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
width={640}
|
||||
bodyStyle={{ padding: '32px 40px 48px' }}
|
||||
destroyOnClose
|
||||
title="规则配置"
|
||||
visible={updateModalVisible}
|
||||
footer={this.renderFooter(currentStep)}
|
||||
onCancel={() => handleUpdateModalVisible(false, values)}
|
||||
afterClose={() => handleUpdateModalVisible()}
|
||||
>
|
||||
<Steps style={{ marginBottom: 28 }} size="small" current={currentStep}>
|
||||
<Step title="基本信息" />
|
||||
<Step title="配置规则属性" />
|
||||
<Step title="设定调度周期" />
|
||||
</Steps>
|
||||
{this.renderContent(currentStep, formVals)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint react/no-multi-comp:0 */
|
||||
@connect(({ rule, loading }) => ({
|
||||
rule,
|
||||
loading: loading.models.rule,
|
||||
}))
|
||||
@Form.create()
|
||||
class TableList extends PureComponent {
|
||||
state = {
|
||||
modalVisible: false,
|
||||
updateModalVisible: false,
|
||||
expandForm: false,
|
||||
selectedRows: [],
|
||||
formValues: {},
|
||||
stepFormValues: {},
|
||||
};
|
||||
|
||||
columns = [
|
||||
{
|
||||
title: '规则名称',
|
||||
dataIndex: 'name',
|
||||
render: text => <a onClick={() => this.previewItem(text)}>{text}</a>,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'desc',
|
||||
},
|
||||
{
|
||||
title: '服务调用次数',
|
||||
dataIndex: 'callNo',
|
||||
sorter: true,
|
||||
render: val => `${val} 万`,
|
||||
// mark to display a total number
|
||||
needTotal: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
filters: [
|
||||
{
|
||||
text: status[0],
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
text: status[1],
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
text: status[2],
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
text: status[3],
|
||||
value: 3,
|
||||
},
|
||||
],
|
||||
render(val) {
|
||||
return <Badge status={statusMap[val]} text={status[val]} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '上次调度时间',
|
||||
dataIndex: 'updatedAt',
|
||||
sorter: true,
|
||||
render: val => <span>{moment(val).format('YYYY-MM-DD HH:mm:ss')}</span>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
render: (text, record) => (
|
||||
<Fragment>
|
||||
<a onClick={() => this.handleUpdateModalVisible(true, record)}>配置</a>
|
||||
<Divider type="vertical" />
|
||||
<a href="">订阅警报</a>
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'rule/fetch',
|
||||
});
|
||||
}
|
||||
|
||||
handleStandardTableChange = (pagination, filtersArg, sorter) => {
|
||||
const { dispatch } = this.props;
|
||||
const { formValues } = this.state;
|
||||
|
||||
const filters = Object.keys(filtersArg).reduce((obj, key) => {
|
||||
const newObj = { ...obj };
|
||||
newObj[key] = getValue(filtersArg[key]);
|
||||
return newObj;
|
||||
}, {});
|
||||
|
||||
const params = {
|
||||
currentPage: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...formValues,
|
||||
...filters,
|
||||
};
|
||||
if (sorter.field) {
|
||||
params.sorter = `${sorter.field}_${sorter.order}`;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'rule/fetch',
|
||||
payload: params,
|
||||
});
|
||||
};
|
||||
|
||||
previewItem = id => {
|
||||
router.push(`/profile/basic/${id}`);
|
||||
};
|
||||
|
||||
handleFormReset = () => {
|
||||
const { form, dispatch } = this.props;
|
||||
form.resetFields();
|
||||
this.setState({
|
||||
formValues: {},
|
||||
});
|
||||
dispatch({
|
||||
type: 'rule/fetch',
|
||||
payload: {},
|
||||
});
|
||||
};
|
||||
|
||||
toggleForm = () => {
|
||||
const { expandForm } = this.state;
|
||||
this.setState({
|
||||
expandForm: !expandForm,
|
||||
});
|
||||
};
|
||||
|
||||
handleMenuClick = e => {
|
||||
const { dispatch } = this.props;
|
||||
const { selectedRows } = this.state;
|
||||
|
||||
if (selectedRows.length === 0) return;
|
||||
switch (e.key) {
|
||||
case 'remove':
|
||||
dispatch({
|
||||
type: 'rule/remove',
|
||||
payload: {
|
||||
key: selectedRows.map(row => row.key),
|
||||
},
|
||||
callback: () => {
|
||||
this.setState({
|
||||
selectedRows: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleSelectRows = rows => {
|
||||
this.setState({
|
||||
selectedRows: rows,
|
||||
});
|
||||
};
|
||||
|
||||
handleSearch = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const { dispatch, form } = this.props;
|
||||
|
||||
form.validateFields((err, fieldsValue) => {
|
||||
if (err) return;
|
||||
|
||||
const values = {
|
||||
...fieldsValue,
|
||||
updatedAt: fieldsValue.updatedAt && fieldsValue.updatedAt.valueOf(),
|
||||
};
|
||||
|
||||
this.setState({
|
||||
formValues: values,
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: 'rule/fetch',
|
||||
payload: values,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleModalVisible = flag => {
|
||||
this.setState({
|
||||
modalVisible: !!flag,
|
||||
});
|
||||
};
|
||||
|
||||
handleUpdateModalVisible = (flag, record) => {
|
||||
this.setState({
|
||||
updateModalVisible: !!flag,
|
||||
stepFormValues: record || {},
|
||||
});
|
||||
};
|
||||
|
||||
handleAdd = fields => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'rule/add',
|
||||
payload: {
|
||||
desc: fields.desc,
|
||||
},
|
||||
});
|
||||
|
||||
message.success('添加成功');
|
||||
this.handleModalVisible();
|
||||
};
|
||||
|
||||
handleUpdate = fields => {
|
||||
const { dispatch } = this.props;
|
||||
const { formValues } = this.state;
|
||||
dispatch({
|
||||
type: 'rule/update',
|
||||
payload: {
|
||||
query: formValues,
|
||||
body: {
|
||||
name: fields.name,
|
||||
desc: fields.desc,
|
||||
key: fields.key,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
message.success('配置成功');
|
||||
this.handleUpdateModalVisible();
|
||||
};
|
||||
|
||||
renderSimpleForm() {
|
||||
const {
|
||||
form: { getFieldDecorator },
|
||||
} = this.props;
|
||||
return (
|
||||
<Form onSubmit={this.handleSearch} layout="inline">
|
||||
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="规则名称">
|
||||
{getFieldDecorator('name')(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="使用状态">
|
||||
{getFieldDecorator('status')(
|
||||
<Select placeholder="请选择" style={{ width: '100%' }}>
|
||||
<Option value="0">关闭</Option>
|
||||
<Option value="1">运行中</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<span className={styles.submitButtons}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
|
||||
重置
|
||||
</Button>
|
||||
<a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
|
||||
展开 <Icon type="down" />
|
||||
</a>
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderAdvancedForm() {
|
||||
const {
|
||||
form: { getFieldDecorator },
|
||||
} = this.props;
|
||||
return (
|
||||
<Form onSubmit={this.handleSearch} layout="inline">
|
||||
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="规则名称">
|
||||
{getFieldDecorator('name')(<Input placeholder="请输入" />)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="使用状态">
|
||||
{getFieldDecorator('status')(
|
||||
<Select placeholder="请选择" style={{ width: '100%' }}>
|
||||
<Option value="0">关闭</Option>
|
||||
<Option value="1">运行中</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="调用次数">
|
||||
{getFieldDecorator('number')(<InputNumber style={{ width: '100%' }} />)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={{ md: 8, lg: 24, xl: 48 }}>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="更新日期">
|
||||
{getFieldDecorator('date')(
|
||||
<DatePicker style={{ width: '100%' }} placeholder="请输入更新日期" />
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="使用状态">
|
||||
{getFieldDecorator('status3')(
|
||||
<Select placeholder="请选择" style={{ width: '100%' }}>
|
||||
<Option value="0">关闭</Option>
|
||||
<Option value="1">运行中</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col md={8} sm={24}>
|
||||
<FormItem label="使用状态">
|
||||
{getFieldDecorator('status4')(
|
||||
<Select placeholder="请选择" style={{ width: '100%' }}>
|
||||
<Option value="0">关闭</Option>
|
||||
<Option value="1">运行中</Option>
|
||||
</Select>
|
||||
)}
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
|
||||
重置
|
||||
</Button>
|
||||
<a style={{ marginLeft: 8 }} onClick={this.toggleForm}>
|
||||
收起 <Icon type="up" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { expandForm } = this.state;
|
||||
return expandForm ? this.renderAdvancedForm() : this.renderSimpleForm();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
rule: { data },
|
||||
loading,
|
||||
} = this.props;
|
||||
const { selectedRows, modalVisible, updateModalVisible, stepFormValues } = this.state;
|
||||
const menu = (
|
||||
<Menu onClick={this.handleMenuClick} selectedKeys={[]}>
|
||||
<Menu.Item key="remove">删除</Menu.Item>
|
||||
<Menu.Item key="approval">批量审批</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const parentMethods = {
|
||||
handleAdd: this.handleAdd,
|
||||
handleModalVisible: this.handleModalVisible,
|
||||
};
|
||||
const updateMethods = {
|
||||
handleUpdateModalVisible: this.handleUpdateModalVisible,
|
||||
handleUpdate: this.handleUpdate,
|
||||
};
|
||||
return (
|
||||
<PageHeaderWrapper title="查询表格">
|
||||
<Card bordered={false}>
|
||||
<div className={styles.tableList}>
|
||||
<div className={styles.tableListForm}>{this.renderForm()}</div>
|
||||
<div className={styles.tableListOperator}>
|
||||
<Button icon="plus" type="primary" onClick={() => this.handleModalVisible(true)}>
|
||||
新建
|
||||
</Button>
|
||||
{selectedRows.length > 0 && (
|
||||
<span>
|
||||
<Button>批量操作</Button>
|
||||
<Dropdown overlay={menu}>
|
||||
<Button>
|
||||
更多操作 <Icon type="down" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<StandardTable
|
||||
selectedRows={selectedRows}
|
||||
loading={loading}
|
||||
data={data}
|
||||
columns={this.columns}
|
||||
onSelectRow={this.handleSelectRows}
|
||||
onChange={this.handleStandardTableChange}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<CreateForm {...parentMethods} modalVisible={modalVisible} />
|
||||
{stepFormValues && Object.keys(stepFormValues).length ? (
|
||||
<UpdateForm
|
||||
{...updateMethods}
|
||||
updateModalVisible={updateModalVisible}
|
||||
values={stepFormValues}
|
||||
/>
|
||||
) : null}
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableList;
|
||||
49
admin-web/src/pages/List/TableList.less
Normal file
49
admin-web/src/pages/List/TableList.less
Normal file
@@ -0,0 +1,49 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
@import '~@/utils/utils.less';
|
||||
|
||||
.tableList {
|
||||
.tableListOperator {
|
||||
margin-bottom: 16px;
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tableListForm {
|
||||
:global {
|
||||
.ant-form-item {
|
||||
display: flex;
|
||||
margin-right: 0;
|
||||
margin-bottom: 24px;
|
||||
> .ant-form-item-label {
|
||||
width: auto;
|
||||
padding-right: 8px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.ant-form-item-control {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
.ant-form-item-control-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.submitButtons {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-lg) {
|
||||
.tableListForm :global(.ant-form-item) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-md) {
|
||||
.tableListForm :global(.ant-form-item) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
55
admin-web/src/pages/List/models/rule.js
Normal file
55
admin-web/src/pages/List/models/rule.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { queryRule, removeRule, addRule, updateRule } from '@/services/api';
|
||||
|
||||
export default {
|
||||
namespace: 'rule',
|
||||
|
||||
state: {
|
||||
data: {
|
||||
list: [],
|
||||
pagination: {},
|
||||
},
|
||||
},
|
||||
|
||||
effects: {
|
||||
*fetch({ payload }, { call, put }) {
|
||||
const response = yield call(queryRule, payload);
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: response,
|
||||
});
|
||||
},
|
||||
*add({ payload, callback }, { call, put }) {
|
||||
const response = yield call(addRule, payload);
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: response,
|
||||
});
|
||||
if (callback) callback();
|
||||
},
|
||||
*remove({ payload, callback }, { call, put }) {
|
||||
const response = yield call(removeRule, payload);
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: response,
|
||||
});
|
||||
if (callback) callback();
|
||||
},
|
||||
*update({ payload, callback }, { call, put }) {
|
||||
const response = yield call(updateRule, payload);
|
||||
yield put({
|
||||
type: 'save',
|
||||
payload: response,
|
||||
});
|
||||
if (callback) callback();
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
save(state, action) {
|
||||
return {
|
||||
...state,
|
||||
data: action.payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
353
admin-web/src/pages/Profile/AdvancedProfile.js
Normal file
353
admin-web/src/pages/Profile/AdvancedProfile.js
Normal file
@@ -0,0 +1,353 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import Debounce from 'lodash-decorators/debounce';
|
||||
import Bind from 'lodash-decorators/bind';
|
||||
import { connect } from 'dva';
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
Dropdown,
|
||||
Icon,
|
||||
Row,
|
||||
Col,
|
||||
Steps,
|
||||
Card,
|
||||
Popover,
|
||||
Badge,
|
||||
Table,
|
||||
Tooltip,
|
||||
Divider,
|
||||
} from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import DescriptionList from '@/components/DescriptionList';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
import styles from './AdvancedProfile.less';
|
||||
|
||||
const { Step } = Steps;
|
||||
const { Description } = DescriptionList;
|
||||
const ButtonGroup = Button.Group;
|
||||
|
||||
const getWindowWidth = () => window.innerWidth || document.documentElement.clientWidth;
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item key="1">选项一</Menu.Item>
|
||||
<Menu.Item key="2">选项二</Menu.Item>
|
||||
<Menu.Item key="3">选项三</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const action = (
|
||||
<Fragment>
|
||||
<ButtonGroup>
|
||||
<Button>操作</Button>
|
||||
<Button>操作</Button>
|
||||
<Dropdown overlay={menu} placement="bottomRight">
|
||||
<Button>
|
||||
<Icon type="ellipsis" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
<Button type="primary">主操作</Button>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const extra = (
|
||||
<Row>
|
||||
<Col xs={24} sm={12}>
|
||||
<div className={styles.textSecondary}>状态</div>
|
||||
<div className={styles.heading}>待审批</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={12}>
|
||||
<div className={styles.textSecondary}>订单金额</div>
|
||||
<div className={styles.heading}>¥ 568.08</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const description = (
|
||||
<DescriptionList className={styles.headerList} size="small" col="2">
|
||||
<Description term="创建人">曲丽丽</Description>
|
||||
<Description term="订购产品">XX 服务</Description>
|
||||
<Description term="创建时间">2017-07-07</Description>
|
||||
<Description term="关联单据">
|
||||
<a href="">12421</a>
|
||||
</Description>
|
||||
<Description term="生效日期">2017-07-07 ~ 2017-08-08</Description>
|
||||
<Description term="备注">请于两个工作日内确认</Description>
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
const tabList = [
|
||||
{
|
||||
key: 'detail',
|
||||
tab: '详情',
|
||||
},
|
||||
{
|
||||
key: 'rule',
|
||||
tab: '规则',
|
||||
},
|
||||
];
|
||||
|
||||
const desc1 = (
|
||||
<div className={classNames(styles.textSecondary, styles.stepDescription)}>
|
||||
<Fragment>
|
||||
曲丽丽
|
||||
<Icon type="dingding-o" style={{ marginLeft: 8 }} />
|
||||
</Fragment>
|
||||
<div>2016-12-12 12:32</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const desc2 = (
|
||||
<div className={styles.stepDescription}>
|
||||
<Fragment>
|
||||
周毛毛
|
||||
<Icon type="dingding-o" style={{ color: '#00A0E9', marginLeft: 8 }} />
|
||||
</Fragment>
|
||||
<div>
|
||||
<a href="">催一下</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const popoverContent = (
|
||||
<div style={{ width: 160 }}>
|
||||
吴加号
|
||||
<span className={styles.textSecondary} style={{ float: 'right' }}>
|
||||
<Badge status="default" text={<span style={{ color: 'rgba(0, 0, 0, 0.45)' }}>未响应</span>} />
|
||||
</span>
|
||||
<div className={styles.textSecondary} style={{ marginTop: 4 }}>
|
||||
耗时:2小时25分钟
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const customDot = (dot, { status }) =>
|
||||
status === 'process' ? (
|
||||
<Popover placement="topLeft" arrowPointAtCenter content={popoverContent}>
|
||||
{dot}
|
||||
</Popover>
|
||||
) : (
|
||||
dot
|
||||
);
|
||||
|
||||
const operationTabList = [
|
||||
{
|
||||
key: 'tab1',
|
||||
tab: '操作日志一',
|
||||
},
|
||||
{
|
||||
key: 'tab2',
|
||||
tab: '操作日志二',
|
||||
},
|
||||
{
|
||||
key: 'tab3',
|
||||
tab: '操作日志三',
|
||||
},
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
},
|
||||
{
|
||||
title: '操作人',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '执行结果',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: text =>
|
||||
text === 'agree' ? (
|
||||
<Badge status="success" text="成功" />
|
||||
) : (
|
||||
<Badge status="error" text="驳回" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作时间',
|
||||
dataIndex: 'updatedAt',
|
||||
key: 'updatedAt',
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
dataIndex: 'memo',
|
||||
key: 'memo',
|
||||
},
|
||||
];
|
||||
|
||||
@connect(({ profile, loading }) => ({
|
||||
profile,
|
||||
loading: loading.effects['profile/fetchAdvanced'],
|
||||
}))
|
||||
class AdvancedProfile extends Component {
|
||||
state = {
|
||||
operationkey: 'tab1',
|
||||
stepDirection: 'horizontal',
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'profile/fetchAdvanced',
|
||||
});
|
||||
|
||||
this.setStepDirection();
|
||||
window.addEventListener('resize', this.setStepDirection, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.setStepDirection);
|
||||
this.setStepDirection.cancel();
|
||||
}
|
||||
|
||||
onOperationTabChange = key => {
|
||||
this.setState({ operationkey: key });
|
||||
};
|
||||
|
||||
@Bind()
|
||||
@Debounce(200)
|
||||
setStepDirection() {
|
||||
const { stepDirection } = this.state;
|
||||
const w = getWindowWidth();
|
||||
if (stepDirection !== 'vertical' && w <= 576) {
|
||||
this.setState({
|
||||
stepDirection: 'vertical',
|
||||
});
|
||||
} else if (stepDirection !== 'horizontal' && w > 576) {
|
||||
this.setState({
|
||||
stepDirection: 'horizontal',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stepDirection, operationkey } = this.state;
|
||||
const { profile, loading } = this.props;
|
||||
const { advancedOperation1, advancedOperation2, advancedOperation3 } = profile;
|
||||
const contentList = {
|
||||
tab1: (
|
||||
<Table
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
dataSource={advancedOperation1}
|
||||
columns={columns}
|
||||
/>
|
||||
),
|
||||
tab2: (
|
||||
<Table
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
dataSource={advancedOperation2}
|
||||
columns={columns}
|
||||
/>
|
||||
),
|
||||
tab3: (
|
||||
<Table
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
dataSource={advancedOperation3}
|
||||
columns={columns}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeaderWrapper
|
||||
title="单号:234231029431"
|
||||
logo={
|
||||
<img alt="" src="https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png" />
|
||||
}
|
||||
action={action}
|
||||
content={description}
|
||||
extraContent={extra}
|
||||
tabList={tabList}
|
||||
>
|
||||
<Card title="流程进度" style={{ marginBottom: 24 }} bordered={false}>
|
||||
<Steps direction={stepDirection} progressDot={customDot} current={1}>
|
||||
<Step title="创建项目" description={desc1} />
|
||||
<Step title="部门初审" description={desc2} />
|
||||
<Step title="财务复核" />
|
||||
<Step title="完成" />
|
||||
</Steps>
|
||||
</Card>
|
||||
<Card title="用户信息" style={{ marginBottom: 24 }} bordered={false}>
|
||||
<DescriptionList style={{ marginBottom: 24 }}>
|
||||
<Description term="用户姓名">付小小</Description>
|
||||
<Description term="会员卡号">32943898021309809423</Description>
|
||||
<Description term="身份证">3321944288191034921</Description>
|
||||
<Description term="联系方式">18112345678</Description>
|
||||
<Description term="联系地址">
|
||||
曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口
|
||||
</Description>
|
||||
</DescriptionList>
|
||||
<DescriptionList style={{ marginBottom: 24 }} title="信息组">
|
||||
<Description term="某某数据">725</Description>
|
||||
<Description term="该数据更新时间">2017-08-08</Description>
|
||||
<Description> </Description>
|
||||
<Description
|
||||
term={
|
||||
<span>
|
||||
某某数据
|
||||
<Tooltip title="数据说明">
|
||||
<Icon
|
||||
style={{ color: 'rgba(0, 0, 0, 0.43)', marginLeft: 4 }}
|
||||
type="info-circle-o"
|
||||
/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
725
|
||||
</Description>
|
||||
<Description term="该数据更新时间">2017-08-08</Description>
|
||||
</DescriptionList>
|
||||
<h4 style={{ marginBottom: 16 }}>信息组</h4>
|
||||
<Card type="inner" title="多层级信息组">
|
||||
<DescriptionList size="small" style={{ marginBottom: 16 }} title="组名称">
|
||||
<Description term="负责人">林东东</Description>
|
||||
<Description term="角色码">1234567</Description>
|
||||
<Description term="所属部门">XX公司 - YY部</Description>
|
||||
<Description term="过期时间">2017-08-08</Description>
|
||||
<Description term="描述">
|
||||
这段描述很长很长很长很长很长很长很长很长很长很长很长很长很长很长...
|
||||
</Description>
|
||||
</DescriptionList>
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
<DescriptionList size="small" style={{ marginBottom: 16 }} title="组名称" col="1">
|
||||
<Description term="学名">
|
||||
Citrullus lanatus (Thunb.) Matsum. et
|
||||
Nakai一年生蔓生藤本;茎、枝粗壮,具明显的棱。卷须较粗..
|
||||
</Description>
|
||||
</DescriptionList>
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
<DescriptionList size="small" title="组名称">
|
||||
<Description term="负责人">付小小</Description>
|
||||
<Description term="角色码">1234568</Description>
|
||||
</DescriptionList>
|
||||
</Card>
|
||||
</Card>
|
||||
<Card title="用户近半年来电记录" style={{ marginBottom: 24 }} bordered={false}>
|
||||
<div className={styles.noData}>
|
||||
<Icon type="frown-o" />
|
||||
暂无数据
|
||||
</div>
|
||||
</Card>
|
||||
<Card
|
||||
className={styles.tabsCard}
|
||||
bordered={false}
|
||||
tabList={operationTabList}
|
||||
onTabChange={this.onOperationTabChange}
|
||||
>
|
||||
{contentList[operationkey]}
|
||||
</Card>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdvancedProfile;
|
||||
54
admin-web/src/pages/Profile/AdvancedProfile.less
Normal file
54
admin-web/src/pages/Profile/AdvancedProfile.less
Normal file
@@ -0,0 +1,54 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.headerList {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tabsCard {
|
||||
:global {
|
||||
.ant-card-head {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noData {
|
||||
color: @disabled-color;
|
||||
font-size: 16px;
|
||||
line-height: 64px;
|
||||
text-align: center;
|
||||
i {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
margin-right: 16px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: @heading-color;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stepDescription {
|
||||
position: relative;
|
||||
left: 38px;
|
||||
padding-top: 8px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
|
||||
> div {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.textSecondary {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
.stepDescription {
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
188
admin-web/src/pages/Profile/BasicProfile.js
Normal file
188
admin-web/src/pages/Profile/BasicProfile.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { Card, Badge, Table, Divider } from 'antd';
|
||||
import DescriptionList from '@/components/DescriptionList';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
import styles from './BasicProfile.less';
|
||||
|
||||
const { Description } = DescriptionList;
|
||||
|
||||
const progressColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
},
|
||||
{
|
||||
title: '当前进度',
|
||||
dataIndex: 'rate',
|
||||
key: 'rate',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: text =>
|
||||
text === 'success' ? (
|
||||
<Badge status="success" text="成功" />
|
||||
) : (
|
||||
<Badge status="processing" text="进行中" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作员ID',
|
||||
dataIndex: 'operator',
|
||||
key: 'operator',
|
||||
},
|
||||
{
|
||||
title: '耗时',
|
||||
dataIndex: 'cost',
|
||||
key: 'cost',
|
||||
},
|
||||
];
|
||||
|
||||
@connect(({ profile, loading }) => ({
|
||||
profile,
|
||||
loading: loading.effects['profile/fetchBasic'],
|
||||
}))
|
||||
class BasicProfile extends Component {
|
||||
componentDidMount() {
|
||||
const { dispatch, match } = this.props;
|
||||
const { params } = match;
|
||||
|
||||
dispatch({
|
||||
type: 'profile/fetchBasic',
|
||||
payload: params.id || '1000000000',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { profile = {}, loading } = this.props;
|
||||
const { basicGoods = [], basicProgress = [], userInfo = {}, application = {} } = profile;
|
||||
let goodsData = [];
|
||||
if (basicGoods.length) {
|
||||
let num = 0;
|
||||
let amount = 0;
|
||||
basicGoods.forEach(item => {
|
||||
num += Number(item.num);
|
||||
amount += Number(item.amount);
|
||||
});
|
||||
goodsData = basicGoods.concat({
|
||||
id: '总计',
|
||||
num,
|
||||
amount,
|
||||
});
|
||||
}
|
||||
const renderContent = (value, row, index) => {
|
||||
const obj = {
|
||||
children: value,
|
||||
props: {},
|
||||
};
|
||||
if (index === basicGoods.length) {
|
||||
obj.props.colSpan = 0;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
const goodsColumns = [
|
||||
{
|
||||
title: '商品编号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
render: (text, row, index) => {
|
||||
if (index < basicGoods.length) {
|
||||
return <a href="">{text}</a>;
|
||||
}
|
||||
return {
|
||||
children: <span style={{ fontWeight: 600 }}>总计</span>,
|
||||
props: {
|
||||
colSpan: 4,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '商品名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: renderContent,
|
||||
},
|
||||
{
|
||||
title: '商品条码',
|
||||
dataIndex: 'barcode',
|
||||
key: 'barcode',
|
||||
render: renderContent,
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'right',
|
||||
render: renderContent,
|
||||
},
|
||||
{
|
||||
title: '数量(件)',
|
||||
dataIndex: 'num',
|
||||
key: 'num',
|
||||
align: 'right',
|
||||
render: (text, row, index) => {
|
||||
if (index < basicGoods.length) {
|
||||
return text;
|
||||
}
|
||||
return <span style={{ fontWeight: 600 }}>{text}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
align: 'right',
|
||||
render: (text, row, index) => {
|
||||
if (index < basicGoods.length) {
|
||||
return text;
|
||||
}
|
||||
return <span style={{ fontWeight: 600 }}>{text}</span>;
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<PageHeaderWrapper title="基础详情页" loading={loading}>
|
||||
<Card bordered={false}>
|
||||
<DescriptionList size="large" title="退款申请" style={{ marginBottom: 32 }}>
|
||||
<Description term="取货单号">{application.id}</Description>
|
||||
<Description term="状态">{application.status}</Description>
|
||||
<Description term="销售单号">{application.orderNo}</Description>
|
||||
<Description term="子订单">{application.childOrderNo}</Description>
|
||||
</DescriptionList>
|
||||
<Divider style={{ marginBottom: 32 }} />
|
||||
<DescriptionList size="large" title="用户信息" style={{ marginBottom: 32 }}>
|
||||
<Description term="用户姓名">{userInfo.name}</Description>
|
||||
<Description term="联系电话">{userInfo.tel}</Description>
|
||||
<Description term="常用快递">{userInfo.delivery}</Description>
|
||||
<Description term="取货地址">{userInfo.addr}</Description>
|
||||
<Description term="备注">{userInfo.remark}</Description>
|
||||
</DescriptionList>
|
||||
<Divider style={{ marginBottom: 32 }} />
|
||||
<div className={styles.title}>退货商品</div>
|
||||
<Table
|
||||
style={{ marginBottom: 24 }}
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
dataSource={goodsData}
|
||||
columns={goodsColumns}
|
||||
rowKey="id"
|
||||
/>
|
||||
<div className={styles.title}>退货进度</div>
|
||||
<Table
|
||||
style={{ marginBottom: 16 }}
|
||||
pagination={false}
|
||||
loading={loading}
|
||||
dataSource={basicProgress}
|
||||
columns={progressColumns}
|
||||
/>
|
||||
</Card>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BasicProfile;
|
||||
8
admin-web/src/pages/Profile/BasicProfile.less
Normal file
8
admin-web/src/pages/Profile/BasicProfile.less
Normal file
@@ -0,0 +1,8 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
color: @heading-color;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
38
admin-web/src/pages/Profile/models/profile.js
Normal file
38
admin-web/src/pages/Profile/models/profile.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { queryBasicProfile, queryAdvancedProfile } from '@/services/api';
|
||||
|
||||
export default {
|
||||
namespace: 'profile',
|
||||
|
||||
state: {
|
||||
basicGoods: [],
|
||||
advancedOperation1: [],
|
||||
advancedOperation2: [],
|
||||
advancedOperation3: [],
|
||||
},
|
||||
|
||||
effects: {
|
||||
*fetchBasic({ payload }, { call, put }) {
|
||||
const response = yield call(queryBasicProfile, payload);
|
||||
yield put({
|
||||
type: 'show',
|
||||
payload: response,
|
||||
});
|
||||
},
|
||||
*fetchAdvanced(_, { call, put }) {
|
||||
const response = yield call(queryAdvancedProfile);
|
||||
yield put({
|
||||
type: 'show',
|
||||
payload: response,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
show(state, { payload }) {
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
66
admin-web/src/pages/Result/Error.js
Normal file
66
admin-web/src/pages/Result/Error.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import { Button, Icon, Card } from 'antd';
|
||||
import Result from '@/components/Result';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
const extra = (
|
||||
<Fragment>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
fontWeight: '500',
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="app.result.error.hint-title"
|
||||
defaultMessage="The content you submitted has the following error:"
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />
|
||||
<FormattedMessage
|
||||
id="app.result.error.hint-text1"
|
||||
defaultMessage="Your account has been frozen"
|
||||
/>
|
||||
<a style={{ marginLeft: 16 }}>
|
||||
<FormattedMessage id="app.result.error.hint-btn1" defaultMessage="Thaw immediately" />
|
||||
<Icon type="right" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<Icon style={{ color: '#f5222d', marginRight: 8 }} type="close-circle-o" />
|
||||
<FormattedMessage
|
||||
id="app.result.error.hint-text2"
|
||||
defaultMessage="Your account is not yet eligible to apply"
|
||||
/>
|
||||
<a style={{ marginLeft: 16 }}>
|
||||
<FormattedMessage id="app.result.error.hint-btn2" defaultMessage="Upgrade immediately" />
|
||||
<Icon type="right" />
|
||||
</a>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const actions = (
|
||||
<Button type="primary">
|
||||
<FormattedMessage id="app.result.error.btn-text" defaultMessage="Return to modify" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
export default () => (
|
||||
<PageHeaderWrapper>
|
||||
<Card bordered={false}>
|
||||
<Result
|
||||
type="error"
|
||||
title={formatMessage({ id: 'app.result.error.title' })}
|
||||
description={formatMessage({ id: 'app.result.error.description' })}
|
||||
extra={extra}
|
||||
actions={actions}
|
||||
style={{ marginTop: 48, marginBottom: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
144
admin-web/src/pages/Result/Success.js
Normal file
144
admin-web/src/pages/Result/Success.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import { Button, Row, Col, Icon, Steps, Card } from 'antd';
|
||||
import Result from '@/components/Result';
|
||||
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
|
||||
|
||||
const { Step } = Steps;
|
||||
|
||||
const desc1 = (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: 'rgba(0, 0, 0, 0.45)',
|
||||
position: 'relative',
|
||||
left: 42,
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<div style={{ margin: '8px 0 4px' }}>
|
||||
<FormattedMessage id="app.result.success.step1-operator" defaultMessage="Qu Lili" />
|
||||
<Icon style={{ marginLeft: 8 }} type="dingding-o" />
|
||||
</div>
|
||||
<div>2016-12-12 12:32</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const desc2 = (
|
||||
<div style={{ fontSize: 12, position: 'relative', left: 42, textAlign: 'left' }}>
|
||||
<div style={{ margin: '8px 0 4px' }}>
|
||||
<FormattedMessage id="app.result.success.step2-operator" defaultMessage="Zhou Maomao" />
|
||||
<Icon type="dingding-o" style={{ color: '#00A0E9', marginLeft: 8 }} />
|
||||
</div>
|
||||
<div>
|
||||
<a href="">
|
||||
<FormattedMessage id="app.result.success.step2-extra" defaultMessage="Urge" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const extra = (
|
||||
<Fragment>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: 'rgba(0, 0, 0, 0.85)',
|
||||
fontWeight: '500',
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<FormattedMessage id="app.result.success.operate-title" defaultMessage="Project Name" />
|
||||
</div>
|
||||
<Row style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>
|
||||
<FormattedMessage id="app.result.success.operate-id" defaultMessage="Project ID:" />
|
||||
</span>
|
||||
23421
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={12} lg={12} xl={6}>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>
|
||||
<FormattedMessage id="app.result.success.principal" defaultMessage="Principal:" />
|
||||
</span>
|
||||
<FormattedMessage id="app.result.success.step1-operator" defaultMessage="Qu Lili" />
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={24} lg={24} xl={12}>
|
||||
<span style={{ color: 'rgba(0, 0, 0, 0.85)' }}>
|
||||
<FormattedMessage
|
||||
id="app.result.success.operate-time"
|
||||
defaultMessage="Effective time:"
|
||||
/>
|
||||
</span>
|
||||
2016-12-12 ~ 2017-12-12
|
||||
</Col>
|
||||
</Row>
|
||||
<Steps style={{ marginLeft: -42, width: 'calc(100% + 84px)' }} progressDot current={1}>
|
||||
<Step
|
||||
title={
|
||||
<span style={{ fontSize: 14 }}>
|
||||
<FormattedMessage id="app.result.success.step1-title" defaultMessage="Create project" />
|
||||
</span>
|
||||
}
|
||||
description={desc1}
|
||||
/>
|
||||
<Step
|
||||
title={
|
||||
<span style={{ fontSize: 14 }}>
|
||||
<FormattedMessage
|
||||
id="app.result.success.step2-title"
|
||||
defaultMessage="Departmental preliminary review"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
description={desc2}
|
||||
/>
|
||||
<Step
|
||||
title={
|
||||
<span style={{ fontSize: 14 }}>
|
||||
<FormattedMessage
|
||||
id="app.result.success.step3-title"
|
||||
defaultMessage="Financial review"
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Step
|
||||
title={
|
||||
<span style={{ fontSize: 14 }}>
|
||||
<FormattedMessage id="app.result.success.step4-title" defaultMessage="Finish" />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</Steps>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
const actions = (
|
||||
<Fragment>
|
||||
<Button type="primary">
|
||||
<FormattedMessage id="app.result.success.btn-return" defaultMessage="Back to list" />
|
||||
</Button>
|
||||
<Button>
|
||||
<FormattedMessage id="app.result.success.btn-project" defaultMessage="View project" />
|
||||
</Button>
|
||||
<Button>
|
||||
<FormattedMessage id="app.result.success.btn-print" defaultMessage="Print" />
|
||||
</Button>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
export default () => (
|
||||
<PageHeaderWrapper>
|
||||
<Card bordered={false}>
|
||||
<Result
|
||||
type="success"
|
||||
title={formatMessage({ id: 'app.result.success.title' })}
|
||||
description={formatMessage({ id: 'app.result.success.description' })}
|
||||
extra={extra}
|
||||
actions={actions}
|
||||
style={{ marginTop: 48, marginBottom: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
</PageHeaderWrapper>
|
||||
);
|
||||
9
admin-web/src/pages/Result/Success.test.js
Normal file
9
admin-web/src/pages/Result/Success.test.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import Success from './Success';
|
||||
|
||||
it('renders with Result', () => {
|
||||
const wrapper = shallow(<Success />);
|
||||
expect(wrapper.find('Result').length).toBe(1);
|
||||
expect(wrapper.find('Result').prop('type')).toBe('success');
|
||||
});
|
||||
171
admin-web/src/pages/User/Login.js
Normal file
171
admin-web/src/pages/User/Login.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import Link from 'umi/link';
|
||||
import { Checkbox, Alert, Icon } from 'antd';
|
||||
import Login from '@/components/Login';
|
||||
import styles from './Login.less';
|
||||
|
||||
const { Tab, UserName, Password, Mobile, Captcha, Submit } = Login;
|
||||
|
||||
@connect(({ login, loading }) => ({
|
||||
login,
|
||||
submitting: loading.effects['login/login'],
|
||||
}))
|
||||
class LoginPage extends Component {
|
||||
state = {
|
||||
type: 'account',
|
||||
autoLogin: true,
|
||||
};
|
||||
|
||||
onTabChange = type => {
|
||||
this.setState({ type });
|
||||
};
|
||||
|
||||
onGetCaptcha = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.loginForm.validateFields(['mobile'], {}, (err, values) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'login/getCaptcha',
|
||||
payload: values.mobile,
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
handleSubmit = (err, values) => {
|
||||
const { type } = this.state;
|
||||
if (!err) {
|
||||
const { dispatch } = this.props;
|
||||
dispatch({
|
||||
type: 'login/login',
|
||||
payload: {
|
||||
...values,
|
||||
type,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
changeAutoLogin = e => {
|
||||
this.setState({
|
||||
autoLogin: e.target.checked,
|
||||
});
|
||||
};
|
||||
|
||||
renderMessage = content => (
|
||||
<Alert style={{ marginBottom: 24 }} message={content} type="error" showIcon />
|
||||
);
|
||||
|
||||
render() {
|
||||
const { login, submitting } = this.props;
|
||||
const { type, autoLogin } = this.state;
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<Login
|
||||
defaultActiveKey={type}
|
||||
onTabChange={this.onTabChange}
|
||||
onSubmit={this.handleSubmit}
|
||||
ref={form => {
|
||||
this.loginForm = form;
|
||||
}}
|
||||
>
|
||||
<Tab key="account" tab={formatMessage({ id: 'app.login.tab-login-credentials' })}>
|
||||
{login.status === 'error' &&
|
||||
login.type === 'account' &&
|
||||
!submitting &&
|
||||
this.renderMessage(formatMessage({ id: 'app.login.message-invalid-credentials' }))}
|
||||
<UserName
|
||||
name="userName"
|
||||
placeholder={`${formatMessage({ id: 'app.login.userName' })}: admin or user`}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.userName.required' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Password
|
||||
name="password"
|
||||
placeholder={`${formatMessage({ id: 'app.login.password' })}: ant.design`}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.password.required' }),
|
||||
},
|
||||
]}
|
||||
onPressEnter={e => {
|
||||
e.preventDefault();
|
||||
this.loginForm.validateFields(this.handleSubmit);
|
||||
}}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab key="mobile" tab={formatMessage({ id: 'app.login.tab-login-mobile' })}>
|
||||
{login.status === 'error' &&
|
||||
login.type === 'mobile' &&
|
||||
!submitting &&
|
||||
this.renderMessage(
|
||||
formatMessage({ id: 'app.login.message-invalid-verification-code' })
|
||||
)}
|
||||
<Mobile
|
||||
name="mobile"
|
||||
placeholder={formatMessage({ id: 'form.phone-number.placeholder' })}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.phone-number.required' }),
|
||||
},
|
||||
{
|
||||
pattern: /^1\d{10}$/,
|
||||
message: formatMessage({ id: 'validation.phone-number.wrong-format' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Captcha
|
||||
name="captcha"
|
||||
placeholder={formatMessage({ id: 'form.verification-code.placeholder' })}
|
||||
countDown={120}
|
||||
onGetCaptcha={this.onGetCaptcha}
|
||||
getCaptchaButtonText={formatMessage({ id: 'form.get-captcha' })}
|
||||
getCaptchaSecondText={formatMessage({ id: 'form.captcha.second' })}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.verification-code.required' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Tab>
|
||||
<div>
|
||||
<Checkbox checked={autoLogin} onChange={this.changeAutoLogin}>
|
||||
<FormattedMessage id="app.login.remember-me" />
|
||||
</Checkbox>
|
||||
<a style={{ float: 'right' }} href="">
|
||||
<FormattedMessage id="app.login.forgot-password" />
|
||||
</a>
|
||||
</div>
|
||||
<Submit loading={submitting}>
|
||||
<FormattedMessage id="app.login.login" />
|
||||
</Submit>
|
||||
<div className={styles.other}>
|
||||
<FormattedMessage id="app.login.sign-in-with" />
|
||||
<Icon type="alipay-circle" className={styles.icon} theme="outlined" />
|
||||
<Icon type="taobao-circle" className={styles.icon} theme="outlined" />
|
||||
<Icon type="weibo-circle" className={styles.icon} theme="outlined" />
|
||||
<Link className={styles.register} to="/user/register">
|
||||
<FormattedMessage id="app.login.signup" />
|
||||
</Link>
|
||||
</div>
|
||||
</Login>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginPage;
|
||||
32
admin-web/src/pages/User/Login.less
Normal file
32
admin-web/src/pages/User/Login.less
Normal file
@@ -0,0 +1,32 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.main {
|
||||
width: 388px;
|
||||
margin: 0 auto;
|
||||
@media screen and (max-width: @screen-sm) {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 16px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.other {
|
||||
margin-top: 24px;
|
||||
line-height: 22px;
|
||||
text-align: left;
|
||||
|
||||
.register {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
334
admin-web/src/pages/User/Register.js
Normal file
334
admin-web/src/pages/User/Register.js
Normal file
@@ -0,0 +1,334 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'dva';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import Link from 'umi/link';
|
||||
import router from 'umi/router';
|
||||
import { Form, Input, Button, Select, Row, Col, Popover, Progress } from 'antd';
|
||||
import styles from './Register.less';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { Option } = Select;
|
||||
const InputGroup = Input.Group;
|
||||
|
||||
const passwordStatusMap = {
|
||||
ok: (
|
||||
<div className={styles.success}>
|
||||
<FormattedMessage id="validation.password.strength.strong" />
|
||||
</div>
|
||||
),
|
||||
pass: (
|
||||
<div className={styles.warning}>
|
||||
<FormattedMessage id="validation.password.strength.medium" />
|
||||
</div>
|
||||
),
|
||||
poor: (
|
||||
<div className={styles.error}>
|
||||
<FormattedMessage id="validation.password.strength.short" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
const passwordProgressMap = {
|
||||
ok: 'success',
|
||||
pass: 'normal',
|
||||
poor: 'exception',
|
||||
};
|
||||
|
||||
@connect(({ register, loading }) => ({
|
||||
register,
|
||||
submitting: loading.effects['register/submit'],
|
||||
}))
|
||||
@Form.create()
|
||||
class Register extends Component {
|
||||
state = {
|
||||
count: 0,
|
||||
confirmDirty: false,
|
||||
visible: false,
|
||||
help: '',
|
||||
prefix: '86',
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
const { form, register } = this.props;
|
||||
const account = form.getFieldValue('mail');
|
||||
if (register.status === 'ok') {
|
||||
router.push({
|
||||
pathname: '/user/register-result',
|
||||
state: {
|
||||
account,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
onGetCaptcha = () => {
|
||||
let count = 59;
|
||||
this.setState({ count });
|
||||
this.interval = setInterval(() => {
|
||||
count -= 1;
|
||||
this.setState({ count });
|
||||
if (count === 0) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
getPasswordStatus = () => {
|
||||
const { form } = this.props;
|
||||
const value = form.getFieldValue('password');
|
||||
if (value && value.length > 9) {
|
||||
return 'ok';
|
||||
}
|
||||
if (value && value.length > 5) {
|
||||
return 'pass';
|
||||
}
|
||||
return 'poor';
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
const { form, dispatch } = this.props;
|
||||
form.validateFields({ force: true }, (err, values) => {
|
||||
if (!err) {
|
||||
const { prefix } = this.state;
|
||||
dispatch({
|
||||
type: 'register/submit',
|
||||
payload: {
|
||||
...values,
|
||||
prefix,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleConfirmBlur = e => {
|
||||
const { value } = e.target;
|
||||
const { confirmDirty } = this.state;
|
||||
this.setState({ confirmDirty: confirmDirty || !!value });
|
||||
};
|
||||
|
||||
checkConfirm = (rule, value, callback) => {
|
||||
const { form } = this.props;
|
||||
if (value && value !== form.getFieldValue('password')) {
|
||||
callback(formatMessage({ id: 'validation.password.twice' }));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
checkPassword = (rule, value, callback) => {
|
||||
const { visible, confirmDirty } = this.state;
|
||||
if (!value) {
|
||||
this.setState({
|
||||
help: formatMessage({ id: 'validation.password.required' }),
|
||||
visible: !!value,
|
||||
});
|
||||
callback('error');
|
||||
} else {
|
||||
this.setState({
|
||||
help: '',
|
||||
});
|
||||
if (!visible) {
|
||||
this.setState({
|
||||
visible: !!value,
|
||||
});
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback('error');
|
||||
} else {
|
||||
const { form } = this.props;
|
||||
if (value && confirmDirty) {
|
||||
form.validateFields(['confirm'], { force: true });
|
||||
}
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
changePrefix = value => {
|
||||
this.setState({
|
||||
prefix: value,
|
||||
});
|
||||
};
|
||||
|
||||
renderPasswordProgress = () => {
|
||||
const { form } = this.props;
|
||||
const value = form.getFieldValue('password');
|
||||
const passwordStatus = this.getPasswordStatus();
|
||||
return value && value.length ? (
|
||||
<div className={styles[`progress-${passwordStatus}`]}>
|
||||
<Progress
|
||||
status={passwordProgressMap[passwordStatus]}
|
||||
className={styles.progress}
|
||||
strokeWidth={6}
|
||||
percent={value.length * 10 > 100 ? 100 : value.length * 10}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { form, submitting } = this.props;
|
||||
const { getFieldDecorator } = form;
|
||||
const { count, prefix, help, visible } = this.state;
|
||||
return (
|
||||
<div className={styles.main}>
|
||||
<h3>
|
||||
<FormattedMessage id="app.register.register" />
|
||||
</h3>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem>
|
||||
{getFieldDecorator('mail', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.email.required' }),
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: formatMessage({ id: 'validation.email.wrong-format' }),
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Input size="large" placeholder={formatMessage({ id: 'form.email.placeholder' })} />
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem help={help}>
|
||||
<Popover
|
||||
getPopupContainer={node => node.parentNode}
|
||||
content={
|
||||
<div style={{ padding: '4px 0' }}>
|
||||
{passwordStatusMap[this.getPasswordStatus()]}
|
||||
{this.renderPasswordProgress()}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<FormattedMessage id="validation.password.strength.msg" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
overlayStyle={{ width: 240 }}
|
||||
placement="right"
|
||||
visible={visible}
|
||||
>
|
||||
{getFieldDecorator('password', {
|
||||
rules: [
|
||||
{
|
||||
validator: this.checkPassword,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Input
|
||||
size="large"
|
||||
type="password"
|
||||
placeholder={formatMessage({ id: 'form.password.placeholder' })}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
{getFieldDecorator('confirm', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.confirm-password.required' }),
|
||||
},
|
||||
{
|
||||
validator: this.checkConfirm,
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Input
|
||||
size="large"
|
||||
type="password"
|
||||
placeholder={formatMessage({ id: 'form.confirm-password.placeholder' })}
|
||||
/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<InputGroup compact>
|
||||
<Select
|
||||
size="large"
|
||||
value={prefix}
|
||||
onChange={this.changePrefix}
|
||||
style={{ width: '20%' }}
|
||||
>
|
||||
<Option value="86">+86</Option>
|
||||
<Option value="87">+87</Option>
|
||||
</Select>
|
||||
{getFieldDecorator('mobile', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.phone-number.required' }),
|
||||
},
|
||||
{
|
||||
pattern: /^\d{11}$/,
|
||||
message: formatMessage({ id: 'validation.phone-number.wrong-format' }),
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Input
|
||||
size="large"
|
||||
style={{ width: '80%' }}
|
||||
placeholder={formatMessage({ id: 'form.phone-number.placeholder' })}
|
||||
/>
|
||||
)}
|
||||
</InputGroup>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Row gutter={8}>
|
||||
<Col span={16}>
|
||||
{getFieldDecorator('captcha', {
|
||||
rules: [
|
||||
{
|
||||
required: true,
|
||||
message: formatMessage({ id: 'validation.verification-code.required' }),
|
||||
},
|
||||
],
|
||||
})(
|
||||
<Input
|
||||
size="large"
|
||||
placeholder={formatMessage({ id: 'form.verification-code.placeholder' })}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button
|
||||
size="large"
|
||||
disabled={count}
|
||||
className={styles.getCaptcha}
|
||||
onClick={this.onGetCaptcha}
|
||||
>
|
||||
{count
|
||||
? `${count} s`
|
||||
: formatMessage({ id: 'app.register.get-verification-code' })}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button
|
||||
size="large"
|
||||
loading={submitting}
|
||||
className={styles.submit}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
>
|
||||
<FormattedMessage id="app.register.register" />
|
||||
</Button>
|
||||
<Link className={styles.login} to="/User/Login">
|
||||
<FormattedMessage id="app.register.sign-in" />
|
||||
</Link>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Register;
|
||||
57
admin-web/src/pages/User/Register.less
Normal file
57
admin-web/src/pages/User/Register.less
Normal file
@@ -0,0 +1,57 @@
|
||||
@import '~antd/lib/style/themes/default.less';
|
||||
|
||||
.main {
|
||||
width: 388px;
|
||||
margin: 0 auto;
|
||||
|
||||
:global {
|
||||
.ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.getCaptcha {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.submit {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.login {
|
||||
float: right;
|
||||
line-height: @btn-height-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.success,
|
||||
.warning,
|
||||
.error {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: @success-color;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: @warning-color;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
.progress-pass > .progress {
|
||||
:global {
|
||||
.ant-progress-bg {
|
||||
background-color: @warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
admin-web/src/pages/User/RegisterResult.js
Normal file
41
admin-web/src/pages/User/RegisterResult.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { formatMessage, FormattedMessage } from 'umi/locale';
|
||||
import { Button } from 'antd';
|
||||
import Link from 'umi/link';
|
||||
import Result from '@/components/Result';
|
||||
import styles from './RegisterResult.less';
|
||||
|
||||
const actions = (
|
||||
<div className={styles.actions}>
|
||||
<a href="">
|
||||
<Button size="large" type="primary">
|
||||
<FormattedMessage id="app.register-result.view-mailbox" />
|
||||
</Button>
|
||||
</a>
|
||||
<Link to="/">
|
||||
<Button size="large">
|
||||
<FormattedMessage id="app.register-result.back-home" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RegisterResult = ({ location }) => (
|
||||
<Result
|
||||
className={styles.registerResult}
|
||||
type="success"
|
||||
title={
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage
|
||||
id="app.register-result.msg"
|
||||
values={{ email: location.state ? location.state.account : 'AntDesign@example.com' }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
description={formatMessage({ id: 'app.register-result.activation-email' })}
|
||||
actions={actions}
|
||||
style={{ marginTop: 56 }}
|
||||
/>
|
||||
);
|
||||
|
||||
export default RegisterResult;
|
||||
18
admin-web/src/pages/User/RegisterResult.less
Normal file
18
admin-web/src/pages/User/RegisterResult.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.registerResult {
|
||||
:global {
|
||||
.anticon {
|
||||
font-size: 64px;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
margin-top: 32px;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
.actions {
|
||||
margin-top: 40px;
|
||||
a + a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
admin-web/src/pages/User/models/register.js
Normal file
32
admin-web/src/pages/User/models/register.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { fakeRegister } from '@/services/api';
|
||||
import { setAuthority } from '@/utils/authority';
|
||||
import { reloadAuthorized } from '@/utils/Authorized';
|
||||
|
||||
export default {
|
||||
namespace: 'register',
|
||||
|
||||
state: {
|
||||
status: undefined,
|
||||
},
|
||||
|
||||
effects: {
|
||||
*submit({ payload }, { call, put }) {
|
||||
const response = yield call(fakeRegister, payload);
|
||||
yield put({
|
||||
type: 'registerHandle',
|
||||
payload: response,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
reducers: {
|
||||
registerHandle(state, { payload }) {
|
||||
setAuthority('user');
|
||||
reloadAuthorized();
|
||||
return {
|
||||
...state,
|
||||
status: payload.status,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
17
admin-web/src/pages/document.ejs
Normal file
17
admin-web/src/pages/document.ejs
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
/>
|
||||
<title>Ant Design Pro</title>
|
||||
<link rel="icon" href="/favicon.png" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Sorry, we need js to run correctly!</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user