Commit dbbaccad authored by duanledexianxianxian's avatar duanledexianxianxian 😁

add blocks

parent a04eaed7
import { IConfig, IPlugin } from 'umi-types';
import defaultSettings from './defaultSettings'; // https://umijs.org/config/
import defaultSettings from './defaultSettings';
// https://umijs.org/config/
import os from 'os';
import slash from 'slash2';
import webpackPlugin from './plugin.config';
const { pwa, primaryColor } = defaultSettings;
// preview.pro.ant.design only do not use in your production ;
const { pwa, primaryColor } = defaultSettings; // preview.pro.ant.design only do not use in your production ;
// preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。
const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, TEST, NODE_ENV } = process.env;
......@@ -104,15 +100,232 @@ export default {
routes: [
{
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
component: '../layouts/BlankLayout',
routes: [
{
path: '/user',
component: '../layouts/UserLayout',
routes: [
{
path: '/user',
redirect: '/user/login',
},
{
name: 'login',
path: '/user/login',
component: './user/login',
},
{
name: 'register-result',
path: '/user/register-result',
component: './user/register-result',
},
{
name: 'register',
path: '/user/register',
component: './user/register',
},
],
},
{
path: '/',
name: 'welcome',
icon: 'smile',
component: './Welcome',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'],
routes: [
{
path: '/dashboard',
name: 'dashboard',
icon: 'dashboard',
routes: [
{
name: 'analysis',
path: '/dashboard/analysis',
component: './dashboard/analysis',
},
{
name: 'monitor',
path: '/dashboard/monitor',
component: './dashboard/monitor',
},
{
name: 'workplace',
path: '/dashboard/workplace',
component: './dashboard/workplace',
},
],
},
{
path: '/form',
icon: 'form',
name: 'form',
routes: [
{
name: 'basic-form',
path: '/form/basic-form',
component: './form/basic-form',
},
{
name: 'step-form',
path: '/form/step-form',
component: './form/step-form',
},
{
name: 'advanced-form',
path: '/form/advanced-form',
component: './form/advanced-form',
},
],
},
{
path: '/list',
icon: 'table',
name: 'list',
routes: [
{
path: '/list/search',
name: 'search-list',
component: './list/search',
routes: [
{
path: '/list/search',
redirect: '/list/search/articles',
},
{
name: 'articles',
path: '/list/search/articles',
component: './list/search/articles',
},
{
name: 'projects',
path: '/list/search/projects',
component: './list/search/projects',
},
{
name: 'applications',
path: '/list/search/applications',
component: './list/search/applications',
},
],
},
{
name: 'table-list',
path: '/list/table-list',
component: './list/table-list',
},
{
name: 'basic-list',
path: '/list/basic-list',
component: './list/basic-list',
},
{
name: 'card-list',
path: '/list/card-list',
component: './list/card-list',
},
],
},
{
path: '/profile',
name: 'profile',
icon: 'profile',
routes: [
{
name: 'basic',
path: '/profile/basic',
component: './profile/basic',
},
{
name: 'advanced',
path: '/profile/advanced',
component: './profile/advanced',
},
],
},
{
name: 'result',
icon: 'check-circle-o',
path: '/result',
routes: [
{
name: 'success',
path: '/result/success',
component: './result/success',
},
{
name: 'fail',
path: '/result/fail',
component: './result/fail',
},
],
},
{
name: 'exception',
icon: 'warning',
path: '/exception',
routes: [
{
name: '403',
path: '/exception/403',
component: './exception/403',
},
{
name: '404',
path: '/exception/404',
component: './exception/404',
},
{
name: '500',
path: '/exception/500',
component: './exception/500',
},
],
},
{
name: 'account',
icon: 'user',
path: '/account',
routes: [
{
name: 'center',
path: '/account/center',
component: './account/center',
},
{
name: 'settings',
path: '/account/settings',
component: './account/settings',
},
],
},
{
name: 'editor',
icon: 'highlight',
path: '/editor',
routes: [
{
name: 'flow',
path: '/editor/flow',
component: './editor/flow',
},
{
name: 'mind',
path: '/editor/mind',
component: './editor/mind',
},
{
name: 'koni',
path: '/editor/koni',
component: './editor/koni',
},
],
},
{
path: '/',
redirect: '/dashboard/analysis',
authority: ['admin', 'user'],
},
],
},
],
},
......@@ -141,7 +354,7 @@ export default {
resourcePath: string;
},
localIdentName: string,
localName: string,
localName: string
) => {
if (
context.resourcePath.includes('node_modules') ||
......
......@@ -34,7 +34,6 @@
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-staged"
}
},
"lint-staged": {
......@@ -54,13 +53,22 @@
"dependencies": {
"@ant-design/pro-layout": "^4.5.0",
"@antv/data-set": "^0.10.2",
"@types/lodash.debounce": "^4.0.6",
"@types/react-router": "^5.0.2",
"antd": "^3.19.1",
"bizcharts": "^3.5.3-beta.0",
"bizcharts-plugin-slider": "^2.1.1-beta.1",
"classnames": "^2.2.6",
"dva": "^2.4.1",
"gg-editor": "^2.0.2",
"hash.js": "^1.1.5",
"lodash": "^4.17.11",
"lodash-decorators": "^6.0.1",
"lodash.debounce": "^4.0.8",
"memoize-one": "^5.0.4",
"moment": "^2.24.0",
"numeral": "^2.0.6",
"nzh": "^1.0.3",
"omit.js": "^1.0.2",
"path-to-regexp": "^3.0.0",
"prop-types": "^15.7.2",
......@@ -71,10 +79,13 @@
"react-copy-to-clipboard": "^5.0.1",
"react-document-title": "^2.0.3",
"react-dom": "^16.8.6",
"react-fittext": "^1.0.0",
"react-media": "^1.9.2",
"react-media-hook2": "^1.0.5",
"react-router": "^4.3.1",
"redux": "^4.0.1",
"umi": "^2.7.2",
"umi-plugin-block-dev": "^1.0.0",
"umi-plugin-ga": "^1.1.3",
"umi-plugin-pro-block": "^1.3.2",
"umi-plugin-react": "^1.8.2",
......@@ -85,11 +96,12 @@
"@types/classnames": "^2.2.7",
"@types/history": "^4.7.2",
"@types/jest": "^24.0.13",
"@types/lodash": "^4.14.133",
"@types/lodash": "^4.14.134",
"@types/qs": "^6.5.3",
"@types/react": "^16.8.19",
"@types/react-document-title": "^2.0.3",
"@types/react-dom": "^16.8.4",
"@types/numeral":"^0.0.25",
"@umijs/fabric": "^1.0.4",
"babel-eslint": "^10.0.1",
"chalk": "^2.4.2",
......@@ -158,4 +170,4 @@
"create-umi"
]
}
}
\ No newline at end of file
}
import { ConnectProps, ConnectState } from '@/models/connect';
import { Icon, Tooltip } from 'antd';
import Avatar from './AvatarDropdown';
import HeaderSearch from '../HeaderSearch';
import React from 'react';
......@@ -8,6 +7,7 @@ import SelectLang from '../SelectLang';
import { connect } from 'dva';
import { formatMessage } from 'umi-plugin-react/locale';
import styles from './index.less';
import NoticeIconView from './NoticeIconView';
export type SiderTheme = 'light' | 'dark';
export interface GlobalHeaderRightProps extends ConnectProps {
......@@ -62,7 +62,8 @@ const GlobalHeaderRight: React.SFC<GlobalHeaderRightProps> = props => {
<Icon type="question-circle-o" />
</a>
</Tooltip>
<Avatar />
<NoticeIconView />
<Avatar menu />
<SelectLang className={styles.action} />
</div>
);
......
......@@ -3,15 +3,14 @@
* You can view component api by:
* https://github.com/ant-design/ant-design-pro-layout
*/
import { ConnectProps, ConnectState } from '@/models/connect';
import ProLayout, {
MenuDataItem,
BasicLayoutProps as ProLayoutProps,
Settings,
SettingDrawer,
} from '@ant-design/pro-layout';
import React, { useState } from 'react';
import Authorized from '@/utils/Authorized';
import Link from 'umi/link';
import RightContent from '@/components/GlobalHeader/RightContent';
......@@ -31,16 +30,13 @@ export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
[path: string]: MenuDataItem;
};
};
/**
* use Authorized check all menu item
*/
const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] =>
menuList.map(item => {
const localItem = {
...item,
children: item.children ? menuDataRender(item.children) : [],
};
const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] };
return Authorized.check(item.authority, localItem, null) as MenuDataItem;
});
......@@ -48,6 +44,7 @@ const footerRender: BasicLayoutProps['footerRender'] = (_, defaultDom) => {
if (!isAntDesignPro()) {
return defaultDom;
}
return (
<>
{defaultDom}
......@@ -85,10 +82,10 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
});
}
});
/**
* init variables
*/
const handleMenuCollapse = (payload: boolean): void =>
dispatch &&
dispatch({
......@@ -97,31 +94,42 @@ const BasicLayout: React.FC<BasicLayoutProps> = props => {
});
return (
<ProLayout
logo={logo}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => (
<Link to={menuItemProps.path}>{defaultDom}</Link>
)}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
defaultMessage: 'Home',
}),
},
...routers,
]}
footerRender={footerRender}
menuDataRender={menuDataRender}
formatMessage={formatMessage}
rightContentRender={rightProps => <RightContent {...rightProps} />}
{...props}
{...settings}
>
{children}
</ProLayout>
<>
<ProLayout
logo={logo}
onCollapse={handleMenuCollapse}
menuItemRender={(menuItemProps, defaultDom) => (
<Link to={menuItemProps.path}>{defaultDom}</Link>
)}
breadcrumbRender={(routers = []) => [
{
path: '/',
breadcrumbName: formatMessage({
id: 'menu.home',
defaultMessage: 'Home',
}),
},
...routers,
]}
footerRender={footerRender}
menuDataRender={menuDataRender}
formatMessage={formatMessage}
rightContentRender={rightProps => <RightContent {...rightProps} />}
{...props}
{...settings}
>
{children}
</ProLayout>
<SettingDrawer
settings={settings}
onSettingChange={config =>
dispatch({
type: 'settings/changeSetting',
payload: config,
})
}
/>
</>
);
};
......
import React from 'react';
import CopyBlock from '@/components/CopyBlock';
const Layout: React.FC = ({ children }) => <div>{children}</div>;
const Layout: React.FC = ({ children }) => (
<>
<div>{children}</div>
<CopyBlock id={Date.now()} />
</>
);
export default Layout;
@import '~antd/es/style/themes/default.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;
overflow: hidden;
color: @text-color;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
transition: color 0.3s;
&:hover {
color: @primary-color;
}
}
}
.tabsCard {
:global {
.ant-card-head {
padding: 0 16px;
}
}
}
import { ListItemDataType } from './data.d';
const titles = [
'Alipay',
'Angular',
'Ant Design',
'Ant Design Pro',
'Bootstrap',
'React',
'Vue',
'Webpack',
];
const avatars = [
'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png', // Alipay
'https://gw.alipayobjects.com/zos/rmsportal/zOsKZmFRdUtvpqCImOVY.png', // Angular
'https://gw.alipayobjects.com/zos/rmsportal/dURIMkkrRFpPgTuzkwnB.png', // Ant Design
'https://gw.alipayobjects.com/zos/rmsportal/sfjbOqnsXXJgNCjCzDBL.png', // Ant Design Pro
'https://gw.alipayobjects.com/zos/rmsportal/siCrBXXhmvTQGWPNLBow.png', // Bootstrap
'https://gw.alipayobjects.com/zos/rmsportal/kZzEzemZyKLKFsojXItE.png', // React
'https://gw.alipayobjects.com/zos/rmsportal/ComBAopevLwENQdKWiIn.png', // Vue
'https://gw.alipayobjects.com/zos/rmsportal/nxkuOJlFJuAUhzlMTCEe.png', // Webpack
];
const covers = [
'https://gw.alipayobjects.com/zos/rmsportal/uMfMFlvUuceEyPpotzlq.png',
'https://gw.alipayobjects.com/zos/rmsportal/iZBVOIhGJiAnhplqjvZW.png',
'https://gw.alipayobjects.com/zos/rmsportal/iXjVmWVHbCJAyqvDxdtx.png',
'https://gw.alipayobjects.com/zos/rmsportal/gLaIAoVWTtLbBWZNYEMg.png',
];
const desc = [
'那是一种内在的东西, 他们到达不了,也无法触及的',
'希望是一个好东西,也许是最好的,好东西是不会消亡的',
'生命就像一盒巧克力,结果往往出人意料',
'城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
'那时候我只会想自己想要什么,从不想自己拥有什么',
];
const user = [
'付小小',
'曲丽丽',
'林东东',
'周星星',
'吴加好',
'朱偏右',
'鱼酱',
'乐哥',
'谭小仪',
'仲尼',
];
function fakeList(count: number): ListItemDataType[] {
const list = [];
for (let i = 0; i < count; i += 1) {
list.push({
id: `fake-list-${i}`,
owner: user[i % 10],
title: titles[i % 8],
avatar: avatars[i % 8],
cover: parseInt(`${i / 4}`, 10) % 2 === 0 ? covers[i % 4] : covers[3 - (i % 4)],
status: ['active', 'exception', 'normal'][i % 3] as
| 'normal'
| 'exception'
| 'active'
| 'success',
percent: Math.ceil(Math.random() * 50) + 50,
logo: avatars[i % 8],
href: 'https://ant.design',
updatedAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
createdAt: new Date(new Date().getTime() - 1000 * 60 * 60 * 2 * i).getTime(),
subDescription: desc[i % 5],
description:
'在中台产品的研发过程中,会出现不同的设计规范和实现方式,但其中往往存在很多类似的页面和组件,这些类似的组件会被抽离成一套标准规范。',
activeUser: Math.ceil(Math.random() * 100000) + 100000,
newUser: Math.ceil(Math.random() * 1000) + 1000,
star: Math.ceil(Math.random() * 100) + 100,
like: Math.ceil(Math.random() * 100) + 100,
message: Math.ceil(Math.random() * 10) + 10,
content:
'段落示意:蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 ant.design,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。',
members: [
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ZiESqWwCXBRQoaPONSJe.png',
name: '曲丽丽',
id: 'member1',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/tBOxZPlITHqwlGjsJWaF.png',
name: '王昭君',
id: 'member2',
},
{
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/sBxjgqiuHMGRkIjqlQCd.png',
name: '董娜娜',
id: 'member3',
},
],
});
}
return list;
}
function getFakeList(req: { query: any }, res: { json: (arg0: ListItemDataType[]) => void }) {
const params = req.query;
const count = params.count * 1 || 5;
const result = fakeList(count);
return res.json(result);
}
export default {
'GET /api/fake_list': getFakeList,
// 支持值为 Object 和 Array
'GET /api/currentUser': {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/antfincdn/XAosXuNZyF/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notice: [
{
id: 'xxx1',
title: titles[0],
logo: avatars[0],
description: '那是一种内在的东西,他们到达不了,也无法触及的',
updatedAt: new Date(),
member: '科学搬砖组',
href: '',
memberLink: '',
},
{
id: 'xxx2',
title: titles[1],
logo: avatars[1],
description: '希望是一个好东西,也许是最好的,好东西是不会消亡的',
updatedAt: new Date('2017-07-24'),
member: '全组都是吴彦祖',
href: '',
memberLink: '',
},
{
id: 'xxx3',
title: titles[2],
logo: avatars[2],
description: '城镇中有那么多的酒馆,她却偏偏走进了我的酒馆',
updatedAt: new Date(),
member: '中二少女团',
href: '',
memberLink: '',
},
{
id: 'xxx4',
title: titles[3],
logo: avatars[3],
description: '那时候我只会想自己想要什么,从不想自己拥有什么',
updatedAt: new Date('2017-07-23'),
member: '程序员日常',
href: '',
memberLink: '',
},
{
id: 'xxx5',
title: titles[4],
logo: avatars[4],
description: '凛冬将至',
updatedAt: new Date('2017-07-23'),
member: '高逼格设计天团',
href: '',
memberLink: '',
},
{
id: 'xxx6',
title: titles[5],
logo: avatars[5],
description: '生命就像一盒巧克力,结果往往出人意料',
updatedAt: new Date('2017-07-23'),
member: '骗你来学计算机',
href: '',
memberLink: '',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
};
@import '~antd/es/style/themes/default.less';
.filterCardList {
margin-bottom: -24px;
:global {
.ant-card-meta-content {
margin-top: 0;
}
// disabled white space
.ant-card-meta-avatar {
font-size: 0;
}
.ant-list .ant-list-item-content-single {
max-width: 100%;
}
}
.cardInfo {
margin-top: 16px;
margin-left: 40px;
zoom: 1;
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
clear: both;
height: 0;
font-size: 0;
visibility: hidden;
}
& > 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;
}
}
}
}
import { Avatar, Card, Dropdown, Icon, List, Menu, Tooltip } from 'antd';
import React, { Component } from 'react';
import { connect } from 'dva';
import numeral from 'numeral';
import { ModalState } from '../../model';
import stylesApplications from './index.less';
export function formatWan(val: number) {
const v = val * 1;
if (!v || Number.isNaN(v)) return '';
let result: React.ReactNode = val;
if (val > 10000) {
result = (
<span>
{Math.floor(val / 10000)}
<span
style={{
position: 'relative',
top: -2,
fontSize: 14,
fontStyle: 'normal',
marginLeft: 2,
}}
>
</span>
</span>
);
}
return result;
}
@connect(({ accountCenter }: { accountCenter: ModalState }) => ({
list: accountCenter.list,
}))
class Applications extends Component<Partial<ModalState>> {
render() {
const { 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: React.SFC<{
activeUser: React.ReactNode;
newUser: React.ReactNode;
}> = ({ 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 key="download" title="下载">
<Icon type="download" />
</Tooltip>,
<Tooltip title="编辑" key="edit">
<Icon type="edit" />
</Tooltip>,
<Tooltip title="分享" key="share">
<Icon type="share-alt" />
</Tooltip>,
<Dropdown overlay={itemMenu} key="ellipsis">
<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 Applications;
@import '~antd/es/style/themes/default.less';
.listContent {
.description {
max-width: 720px;
line-height: 22px;
}
.extra {
margin-top: 16px;
color: @text-color-secondary;
line-height: 22px;
& > :global(.ant-avatar) {
position: relative;
top: 1px;
width: 20px;
height: 20px;
margin-right: 8px;
vertical-align: top;
}
& > em {
margin-left: 16px;
color: @disabled-color;
font-style: normal;
}
}
}
@media screen and (max-width: @screen-xs) {
.listContent {
.extra {
& > em {
display: block;
margin-top: 8px;
margin-left: 0;
}
}
}
}
import { Avatar } from 'antd';
import React from 'react';
import moment from 'moment';
import styles from './index.less';
export interface ApplicationsProps {
data: {
content?: string;
updatedAt?: any;
avatar?: string;
owner?: string;
href?: string;
};
}
const ArticleListContent: React.SFC<ApplicationsProps> = ({
data: { content, updatedAt, avatar, owner, href },
}) => (
<div className={styles.listContent}>
<div className={styles.description}>{content}</div>
<div className={styles.extra}>
<Avatar src={avatar} size="small" />
<a href={href}>{owner}</a> 发布在 <a href={href}>{href}</a>
<em>{moment(updatedAt).format('YYYY-MM-DD HH:mm')}</em>
</div>
</div>
);
export default ArticleListContent;
@import '~antd/es/style/themes/default.less';
.articleList {
:global {
.ant-list-item:first-child {
padding-top: 0;
}
}
}
a.listItemMetaTitle {
color: @heading-color;
}
import { Icon, List, Tag } from 'antd';
import React, { Component } from 'react';
import { connect } from 'dva';
import ArticleListContent from '../ArticleListContent';
import { ListItemDataType } from '../../data.d';
import { ModalState } from '../../model';
import styles from './index.less';
@connect(({ accountCenter }: { accountCenter: ModalState }) => ({
list: accountCenter.list,
}))
class Articles extends Component<Partial<ModalState>> {
render() {
const { list } = this.props;
const IconText: React.SFC<{
type: string;
text: React.ReactNode;
}> = ({ type, text }) => (
<span>
<Icon type={type} style={{ marginRight: 8 }} />
{text}
</span>
);
return (
<List<ListItemDataType>
size="large"
className={styles.articleList}
rowKey="id"
itemLayout="vertical"
dataSource={list}
renderItem={item => (
<List.Item
key={item.id}
actions={[
<IconText key="star" type="star-o" text={item.star} />,
<IconText key="like" type="like-o" text={item.like} />,
<IconText key="message" 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 Articles;
@import '~antd/es/style/themes/default.less';
.avatarList {
display: inline-block;
ul {
display: inline-block;
margin-left: 8px;
font-size: 0;
}
}
.avatarItem {
display: inline-block;
width: @avatar-size-base;
height: @avatar-size-base;
margin-left: -8px;
font-size: @font-size-base;
:global {
.ant-avatar {
border: 1px solid #fff;
}
}
}
.avatarItemLarge {
width: @avatar-size-lg;
height: @avatar-size-lg;
}
.avatarItemSmall {
width: @avatar-size-sm;
height: @avatar-size-sm;
}
.avatarItemMini {
width: 20px;
height: 20px;
:global {
.ant-avatar {
width: 20px;
height: 20px;
line-height: 20px;
.ant-avatar-string {
font-size: 12px;
line-height: 18px;
}
}
}
}
import { Avatar, Tooltip } from 'antd';
import React from 'react';
import classNames from 'classnames';
import styles from './index.less';
export declare type SizeType = number | 'small' | 'default' | 'large';
export interface AvatarItemProps {
tips: React.ReactNode;
src: string;
size?: SizeType;
style?: React.CSSProperties;
onClick?: () => void;
}
export interface AvatarListProps {
Item?: React.ReactElement<AvatarItemProps>;
size?: SizeType;
maxLength?: number;
excessItemsStyle?: React.CSSProperties;
style?: React.CSSProperties;
children: React.ReactElement<AvatarItemProps> | React.ReactElement<AvatarItemProps>[];
}
const avatarSizeToClassName = (size?: SizeType | 'mini') =>
classNames(styles.avatarItem, {
[styles.avatarItemLarge]: size === 'large',
[styles.avatarItemSmall]: size === 'small',
[styles.avatarItemMini]: size === 'mini',
});
const Item: React.SFC<AvatarItemProps> = ({ src, size, tips, onClick = () => {} }) => {
const cls = avatarSizeToClassName(size);
return (
<li className={cls} onClick={onClick}>
{tips ? (
<Tooltip title={tips}>
<Avatar src={src} size={size} style={{ cursor: 'pointer' }} />
</Tooltip>
) : (
<Avatar src={src} size={size} />
)}
</li>
);
};
const AvatarList: React.SFC<AvatarListProps> & { Item: typeof Item } = ({
children,
size,
maxLength = 5,
excessItemsStyle,
...other
}) => {
const numOfChildren = React.Children.count(children);
const numToShow = maxLength >= numOfChildren ? numOfChildren : maxLength;
const childrenArray = React.Children.toArray(children) as React.ReactElement<AvatarItemProps>[];
const childrenWithProps = childrenArray.slice(0, numToShow).map(child =>
React.cloneElement(child, {
size,
}),
);
if (numToShow < numOfChildren) {
const cls = avatarSizeToClassName(size);
childrenWithProps.push(
<li key="exceed" className={cls}>
<Avatar size={size} style={excessItemsStyle}>{`+${numOfChildren - maxLength}`}</Avatar>
</li>,
);
}
return (
<div {...other} className={styles.avatarList}>
<ul> {childrenWithProps} </ul>
</div>
);
};
AvatarList.Item = Item;
export default AvatarList;
@import '~antd/es/style/themes/default.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%;
}
}
}
import { Card, List } from 'antd';
import React, { Component } from 'react';
import { connect } from 'dva';
import moment from 'moment';
import AvatarList from '../AvatarList';
import { ListItemDataType } from '../../data.d';
import { ModalState } from '../../model';
import styles from './index.less';
@connect(({ accountCenter }: { accountCenter: ModalState }) => ({
list: accountCenter.list,
}))
class Projects extends Component<Partial<ModalState>> {
render() {
const { list } = this.props;
return (
<List<ListItemDataType>
className={styles.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={styles.card}
hoverable
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta title={<a>{item.title}</a>} description={item.subDescription} />
<div className={styles.cardItemContent}>
<span>{moment(item.updatedAt).fromNow()}</span>
<div className={styles.avatarList}>
<AvatarList size="small">
{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 Projects;
export interface TagType {
key: string;
label: string;
}
export interface ProvinceType {
label: string;
key: string;
}
export interface CityType {
label: string;
key: string;
}
export interface GeographicType {
province: ProvinceType;
city: CityType;
}
export interface NoticeType {
id: string;
title: string;
logo: string;
description: string;
updatedAt: string;
member: string;
href: string;
memberLink: string;
}
export interface CurrentUser {
name: string;
avatar: string;
userid: string;
notice: NoticeType[];
email: string;
signature: string;
title: string;
group: string;
tags: TagType[];
notifyCount: number;
unreadCount: number;
country: string;
geographic: GeographicType;
address: string;
phone: string;
}
export interface Member {
avatar: string;
name: string;
id: string;
}
export interface ListItemDataType {
id: string;
owner: string;
title: string;
avatar: string;
cover: string;
status: 'normal' | 'exception' | 'active' | 'success';
percent: number;
logo: string;
href: string;
body?: any;
updatedAt: number;
createdAt: number;
subDescription: string;
description: string;
activeUser: number;
newUser: number;
star: number;
like: number;
message: number;
content: string;
members: Member[];
}
import { Avatar, Card, Col, Divider, Icon, Input, Row, Tag } from 'antd';
import React, { PureComponent } from 'react';
import { Dispatch } from 'redux';
import { GridContent } from '@ant-design/pro-layout';
import Link from 'umi/link';
import { RouteChildrenProps } from 'react-router';
import { connect } from 'dva';
import { ModalState } from './model';
import Projects from './components/Projects';
import Articles from './components/Articles';
import Applications from './components/Applications';
import { CurrentUser, TagType } from './data.d';
import styles from './Center.less';
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>
),
},
];
interface accountCenterProps extends RouteChildrenProps {
dispatch: Dispatch<any>;
currentUser: CurrentUser;
currentUserLoading: boolean;
}
interface accountCenterState {
newTags: TagType[];
tabKey: 'articles' | 'applications' | 'projects';
inputVisible: boolean;
inputValue: string;
}
@connect(
({
loading,
accountCenter,
}: {
loading: { effects: { [key: string]: boolean } };
accountCenter: ModalState;
}) => ({
currentUser: accountCenter.currentUser,
currentUserLoading: loading.effects['accountCenter/fetchCurrent'],
}),
)
class Center extends PureComponent<
accountCenterProps,
accountCenterState
> {
// static getDerivedStateFromProps(
// props: accountCenterProps,
// state: accountCenterState,
// ) {
// const { match, location } = props;
// const { tabKey } = state;
// const path = match && match.path;
// const urlTabKey = location.pathname.replace(`${path}/`, '');
// if (urlTabKey && urlTabKey !== '/' && tabKey !== urlTabKey) {
// return {
// tabKey: urlTabKey,
// };
// }
// return null;
// }
state: accountCenterState = {
newTags: [],
inputVisible: false,
inputValue: '',
tabKey: 'articles',
};
public input: Input | null | undefined = undefined;
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'accountCenter/fetchCurrent',
});
dispatch({
type: 'accountCenter/fetch',
});
}
onTabChange = (key: string) => {
// If you need to sync state to url
// const { match } = this.props;
// router.push(`${match.url}/${key}`);
this.setState({
tabKey: key as accountCenterState['tabKey'],
});
};
showInput = () => {
this.setState({ inputVisible: true }, () => this.input && this.input.focus());
};
saveInputRef = (input: Input | null) => {
this.input = input;
};
handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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: '',
});
};
renderChildrenByTabKey = (tabKey: accountCenterState['tabKey']) => {
if (tabKey === 'projects') {
return <Projects />;
}
if (tabKey === 'applications') {
return <Applications />;
}
if (tabKey === 'articles') {
return <Articles />;
}
return null;
};
render() {
const { newTags, inputVisible, inputValue, tabKey } = this.state;
const { currentUser, currentUserLoading } = this.props;
const dataLoading = currentUserLoading || !(currentUser && Object.keys(currentUser).length);
return (
<GridContent>
<Row gutter={24}>
<Col lg={7} md={24}>
<Card bordered={false} style={{ marginBottom: 24 }} loading={dataLoading}>
{!dataLoading ? (
<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={ref => this.saveInputRef(ref)}
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>
<Row gutter={36}>
{currentUser.notice &&
currentUser.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>
</div>
</div>
) : null}
</Card>
</Col>
<Col lg={17} md={24}>
<Card
className={styles.tabsCard}
bordered={false}
tabList={operationTabList}
activeTabKey={tabKey}
onTabChange={this.onTabChange}
>
{this.renderChildrenByTabKey(tabKey)}
</Card>
</Col>
</Row>
</GridContent>
);
}
}
export default Center;
import { AnyAction, Reducer } from 'redux';
import { EffectsCommandMap } from 'dva';
import { CurrentUser, ListItemDataType } from './data.d';
import { queryCurrent, queryFakeList } from './service';
export interface ModalState {
currentUser: Partial<CurrentUser>;
list: ListItemDataType[];
}
export type Effect = (
action: AnyAction,
effects: EffectsCommandMap & { select: <T>(func: (state: ModalState) => T) => T },
) => void;
export interface ModelType {
namespace: string;
state: ModalState;
effects: {
fetchCurrent: Effect;
fetch: Effect;
};
reducers: {
saveCurrentUser: Reducer<ModalState>;
queryList: Reducer<ModalState>;
};
}
const Model: ModelType = {
namespace: 'accountCenter',
state: {
currentUser: {},
list: [],
},
effects: {
*fetchCurrent(_, { call, put }) {
const response = yield call(queryCurrent);
yield put({
type: 'saveCurrentUser',
payload: response,
});
},
*fetch({ payload }, { call, put }) {
const response = yield call(queryFakeList, payload);
yield put({
type: 'queryList',
payload: Array.isArray(response) ? response : [],
});
},
},
reducers: {
saveCurrentUser(state, action) {
return {
...(state as ModalState),
currentUser: action.payload || {},
};
},
queryList(state, action) {
return {
...(state as ModalState),
list: action.payload,
};
},
},
};
export default Model;
import request from 'umi-request';
export async function queryCurrent() {
return request('/api/currentUser');
}
export async function queryFakeList(params: { count: number }) {
return request('/api/fake_list', {
params,
});
}
import city from './geographic/city.json';
import province from './geographic/province.json';
function getProvince(req: any, res: { json: (arg0: { name: string; id: string }[]) => void }) {
return res.json(province);
}
function getCity(
req: { params: { province: string | number } },
res: { json: (arg: any) => void },
) {
return res.json(city[req.params.province]);
}
// 代码中会兼容本地 service mock 以及部署站点的静态数据
export default {
// 支持值为 Object 和 Array
'GET /api/currentUser': {
name: 'Serati Ma',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
userid: '00000001',
email: 'antdesign@alipay.com',
signature: '海纳百川,有容乃大',
title: '交互专家',
group: '蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED',
tags: [
{
key: '0',
label: '很有想法的',
},
{
key: '1',
label: '专注设计',
},
{
key: '2',
label: '辣~',
},
{
key: '3',
label: '大长腿',
},
{
key: '4',
label: '川妹子',
},
{
key: '5',
label: '海纳百川',
},
],
notifyCount: 12,
unreadCount: 11,
country: 'China',
geographic: {
province: {
label: '浙江省',
key: '330000',
},
city: {
label: '杭州市',
key: '330100',
},
},
address: '西湖区工专路 77 号',
phone: '0752-268888888',
},
'GET /api/geographic/province': getProvince,
'GET /api/geographic/city/:province': getCity,
};
@import '~antd/es/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;
}
}
}
}
@import '~antd/es/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;
}
}
import React, { Component } from 'react';
import { Select, Spin } from 'antd';
import { Dispatch } from 'redux';
import { connect } from 'dva';
import { CityType, ProvinceType } from '../data.d';
import styles from './GeographicView.less';
const { Option } = Select;
interface SelectItem {
label: string;
key: string;
}
const nullSelectItem: SelectItem = {
label: '',
key: '',
};
interface GeographicViewProps {
dispatch?: Dispatch<any>;
province?: ProvinceType[];
city?: CityType[];
value?: {
province: SelectItem;
city: SelectItem;
};
loading?: boolean;
onChange?: (value: { province: SelectItem; city: SelectItem }) => void;
}
@connect(
({
accountSettings,
loading,
}: {
accountSettings: {
province: ProvinceType[];
city: CityType[];
};
loading: any;
}) => {
const { province, city } = accountSettings;
return {
province,
city,
loading: loading.models.accountSettings,
};
},
)
class GeographicView extends Component<GeographicViewProps> {
componentDidMount = () => {
const { dispatch } = this.props;
if (dispatch) {
dispatch({
type: 'accountSettings/fetchProvince',
});
}
};
componentDidUpdate(props: GeographicViewProps) {
const { dispatch, value } = this.props;
if (!props.value && !!value && !!value.province) {
if (dispatch) {
dispatch({
type: 'accountSettings/fetchCity',
payload: value.province.key,
});
}
}
}
getProvinceOption() {
const { province } = this.props;
if (province) {
return this.getOption(province);
}
return [];
}
getCityOption = () => {
const { city } = this.props;
if (city) {
return this.getOption(city);
}
return [];
};
getOption = (list: CityType[] | ProvinceType[]) => {
if (!list || list.length < 1) {
return (
<Option key={0} value={0}>
没有找到选项
</Option>
);
}
return (list as CityType[]).map(item => (
<Option key={item.key} value={item.key}>
{item.label}
</Option>
));
};
selectProvinceItem = (item: SelectItem) => {
const { dispatch, onChange } = this.props;
if (dispatch) {
dispatch({
type: 'accountSettings/fetchCity',
payload: item.key,
});
}
if (onChange) {
onChange({
province: item,
city: nullSelectItem,
});
}
};
selectCityItem = (item: SelectItem) => {
const { value, onChange } = this.props;
if (value && onChange) {
onChange({
province: value.province,
city: item,
});
}
};
conversionObject() {
const { value } = this.props;
if (!value) {
return {
province: nullSelectItem,
city: nullSelectItem,
};
}
const { province, city } = value;
return {
province: province || nullSelectItem,
city: city || nullSelectItem,
};
}
render() {
const { province, city } = this.conversionObject();
const { loading } = this.props;
return (
<Spin spinning={loading} wrapperClassName={styles.row}>
<Select
className={styles.item}
value={province}
labelInValue
showSearch
onSelect={this.selectProvinceItem}
>
{this.getProvinceOption()}
</Select>
<Select
className={styles.item}
value={city}
labelInValue
showSearch
onSelect={this.selectCityItem}
>
{this.getCityOption()}
</Select>
</Spin>
);
}
}
export default GeographicView;
@import '~antd/es/style/themes/default.less';
.area_code {
width: 30%;
max-width: 128px;
margin-right: 8px;
}
.phone_number {
width: ~'calc(70% - 8px)';
max-width: 312px;
}
import React, { Fragment, PureComponent } from 'react';
import { Input } from 'antd';
import styles from './PhoneView.less';
interface PhoneViewProps {
value?: string;
onChange?: (value: string) => void;
}
class PhoneView extends PureComponent<PhoneViewProps> {
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 => {
if (onChange) {
onChange(`${e.target.value}-${values[1]}`);
}
}}
/>
<Input
className={styles.phone_number}
onChange={e => {
if (onChange) {
onChange(`${values[0]}-${e.target.value}`);
}
}}
value={values[1]}
/>
</Fragment>
);
}
}
export default PhoneView;
import { Button, Form, Input, Select, Upload, message } from 'antd';
import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
import React, { Component, Fragment } from 'react';
import { FormComponentProps } from 'antd/es/form';
import { connect } from 'dva';
import { CurrentUser } from '../data.d';
import GeographicView from './GeographicView';
import PhoneView from './PhoneView';
import styles from './BaseView.less';
const FormItem = Form.Item;
const { Option } = Select;
// 头像组件 方便以后独立,增加裁剪之类的功能
const AvatarView = ({ avatar }: { avatar: string }) => (
<Fragment>
<div className={styles.avatar_title}>
<FormattedMessage id="account-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="account-settings.basic.change-avatar" defaultMessage="Change avatar" />
</Button>
</div>
</Upload>
</Fragment>
);
interface SelectItem {
label: string;
key: string;
}
const validatorGeographic = (
_: any,
value: {
province: SelectItem;
city: SelectItem;
},
callback: (message?: string) => void,
) => {
const { province, city } = value;
if (!province.key) {
callback('Please input your province!');
}
if (!city.key) {
callback('Please input your city!');
}
callback();
};
const validatorPhone = (rule: any, value: string, callback: (message?: string) => void) => {
const values = value.split('-');
if (!values[0]) {
callback('Please input your area code!');
}
if (!values[1]) {
callback('Please input your phone number!');
}
callback();
};
interface BaseViewProps extends FormComponentProps {
currentUser?: CurrentUser;
}
@connect(({ accountSettings }: { accountSettings: { currentUser: CurrentUser } }) => ({
currentUser: accountSettings.currentUser,
}))
class BaseView extends Component<BaseViewProps> {
view: HTMLDivElement | undefined = undefined;
componentDidMount() {
this.setBaseInfo();
}
setBaseInfo = () => {
const { currentUser, form } = this.props;
if (currentUser) {
Object.keys(form.getFieldsValue()).forEach(key => {
const obj = {};
obj[key] = currentUser[key] || null;
form.setFieldsValue(obj);
});
}
};
getAvatarURL() {
const { currentUser } = this.props;
if (currentUser) {
if (currentUser.avatar) {
return currentUser.avatar;
}
const url = 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png';
return url;
}
return '';
}
getViewDom = (ref: HTMLDivElement) => {
this.view = ref;
};
handlerSubmit = (event: React.MouseEvent) => {
event.preventDefault();
const { form } = this.props;
form.validateFields(err => {
if (!err) {
message.success(formatMessage({ id: 'account-settings.basic.update.success' }));
}
});
};
render() {
const {
form: { getFieldDecorator },
} = this.props;
return (
<div className={styles.baseView} ref={this.getViewDom}>
<div className={styles.left}>
<Form layout="vertical" hideRequiredMark>
<FormItem label={formatMessage({ id: 'account-settings.basic.email' })}>
{getFieldDecorator('email', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.email-message' }, {}),
},
],
})(<Input />)}
</FormItem>
<FormItem label={formatMessage({ id: 'account-settings.basic.nickname' })}>
{getFieldDecorator('name', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.nickname-message' }, {}),
},
],
})(<Input />)}
</FormItem>
<FormItem label={formatMessage({ id: 'account-settings.basic.profile' })}>
{getFieldDecorator('profile', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.profile-message' }, {}),
},
],
})(
<Input.TextArea
placeholder={formatMessage({ id: 'account-settings.basic.profile-placeholder' })}
rows={4}
/>,
)}
</FormItem>
<FormItem label={formatMessage({ id: 'account-settings.basic.country' })}>
{getFieldDecorator('country', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.country-message' }, {}),
},
],
})(
<Select style={{ maxWidth: 220 }}>
<Option value="China">中国</Option>
</Select>,
)}
</FormItem>
<FormItem label={formatMessage({ id: 'account-settings.basic.geographic' })}>
{getFieldDecorator('geographic', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.geographic-message' }, {}),
},
{
validator: validatorGeographic,
},
],
})(<GeographicView />)}
</FormItem>
<FormItem label={formatMessage({ id: 'account-settings.basic.address' })}>
{getFieldDecorator('address', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.address-message' }, {}),
},
],
})(<Input />)}
</FormItem>
<FormItem label={formatMessage({ id: 'account-settings.basic.phone' })}>
{getFieldDecorator('phone', {
rules: [
{
required: true,
message: formatMessage({ id: 'account-settings.basic.phone-message' }, {}),
},
{ validator: validatorPhone },
],
})(<PhoneView />)}
</FormItem>
<Button type="primary" onClick={this.handlerSubmit}>
<FormattedMessage id="account-settings.basic.update" defaultMessage="Update Information" />
</Button>
</Form>
</div>
<div className={styles.right}>
<AvatarView avatar={this.getAvatarURL()} />
</div>
</div>
);
}
}
export default Form.create<BaseViewProps>()(BaseView);
import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
import { Icon, List } from 'antd';
import React, { Component, Fragment } from 'react';
class BindingView extends Component {
getData = () => [
{
title: formatMessage({ id: 'account-settings.binding.taobao' }, {}),
description: formatMessage({ id: 'account-settings.binding.taobao-description' }, {}),
actions: [
<a key="Bind">
<FormattedMessage id="account-settings.binding.bind" defaultMessage="Bind" />
</a>,
],
avatar: <Icon type="taobao" className="taobao" />,
},
{
title: formatMessage({ id: 'account-settings.binding.alipay' }, {}),
description: formatMessage({ id: 'account-settings.binding.alipay-description' }, {}),
actions: [
<a key="Bind">
<FormattedMessage id="account-settings.binding.bind" defaultMessage="Bind" />
</a>,
],
avatar: <Icon type="alipay" className="alipay" />,
},
{
title: formatMessage({ id: 'account-settings.binding.dingding' }, {}),
description: formatMessage({ id: 'account-settings.binding.dingding-description' }, {}),
actions: [
<a key="Bind">
<FormattedMessage id="account-settings.binding.bind" defaultMessage="Bind" />
</a>,
],
avatar: <Icon type="dingding" className="dingding" />,
},
];
render() {
return (
<Fragment>
<List
itemLayout="horizontal"
dataSource={this.getData()}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta
avatar={item.avatar}
title={item.title}
description={item.description}
/>
</List.Item>
)}
/>
</Fragment>
);
}
}
export default BindingView;
import { List, Switch } from 'antd';
import React, { Component, Fragment } from 'react';
import { formatMessage } from 'umi-plugin-react/locale';
type Unpacked<T> = T extends (infer U)[] ? U : T;
class NotificationView extends Component {
getData = () => {
const Action = (
<Switch
checkedChildren={formatMessage({ id: 'account-settings.settings.open' })}
unCheckedChildren={formatMessage({ id: 'account-settings.settings.close' })}
defaultChecked
/>
);
return [
{
title: formatMessage({ id: 'account-settings.notification.password' }, {}),
description: formatMessage({ id: 'account-settings.notification.password-description' }, {}),
actions: [Action],
},
{
title: formatMessage({ id: 'account-settings.notification.messages' }, {}),
description: formatMessage({ id: 'account-settings.notification.messages-description' }, {}),
actions: [Action],
},
{
title: formatMessage({ id: 'account-settings.notification.todo' }, {}),
description: formatMessage({ id: 'account-settings.notification.todo-description' }, {}),
actions: [Action],
},
];
};
render() {
const data = this.getData();
return (
<Fragment>
<List<Unpacked<typeof data>>
itemLayout="horizontal"
dataSource={data}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description} />
</List.Item>
)}
/>
</Fragment>
);
}
}
export default NotificationView;
import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale';
import React, { Component, Fragment } from 'react';
import { List } from 'antd';
type Unpacked<T> = T extends (infer U)[] ? U : T;
const passwordStrength = {
strong: (
<span className="strong">
<FormattedMessage id="account-settings.security.strong" defaultMessage="Strong" />
</span>
),
medium: (
<span className="medium">
<FormattedMessage id="account-settings.security.medium" defaultMessage="Medium" />
</span>
),
weak: (
<span className="weak">
<FormattedMessage id="account-settings.security.weak" defaultMessage="Weak" />
Weak
</span>
),
};
class SecurityView extends Component {
getData = () => [
{
title: formatMessage({ id: 'account-settings.security.password' }, {}),
description: (
<Fragment>
{formatMessage({ id: 'account-settings.security.password-description' })}
{passwordStrength.strong}
</Fragment>
),
actions: [
<a key="Modify">
<FormattedMessage id="account-settings.security.modify" defaultMessage="Modify" />
</a>,
],
},
{
title: formatMessage({ id: 'account-settings.security.phone' }, {}),
description: `${formatMessage(
{ id: 'account-settings.security.phone-description' },
{},
)}:138****8293`,
actions: [
<a key="Modify">
<FormattedMessage id="account-settings.security.modify" defaultMessage="Modify" />
</a>,
],
},
{
title: formatMessage({ id: 'account-settings.security.question' }, {}),
description: formatMessage({ id: 'account-settings.security.question-description' }, {}),
actions: [
<a key="Set">
<FormattedMessage id="account-settings.security.set" defaultMessage="Set" />
</a>,
],
},
{
title: formatMessage({ id: 'account-settings.security.email' }, {}),
description: `${formatMessage(
{ id: 'account-settings.security.email-description' },
{},
)}:ant***sign.com`,
actions: [
<a key="Modify">
<FormattedMessage id="account-settings.security.modify" defaultMessage="Modify" />
</a>,
],
},
{
title: formatMessage({ id: 'account-settings.security.mfa' }, {}),
description: formatMessage({ id: 'account-settings.security.mfa-description' }, {}),
actions: [
<a key="bind">
<FormattedMessage id="account-settings.security.bind" defaultMessage="Bind" />
</a>,
],
},
];
render() {
const data = this.getData();
return (
<Fragment>
<List<Unpacked<typeof data>>
itemLayout="horizontal"
dataSource={data}
renderItem={item => (
<List.Item actions={item.actions}>
<List.Item.Meta title={item.title} description={item.description} />
</List.Item>
)}
/>
</Fragment>
);
}
}
export default SecurityView;
export interface TagType {
key: string;
label: string;
}
export interface ProvinceType {
label: string;
key: string;
}
export interface CityType {
label: string;
key: string;
}
export interface GeographicType {
province: ProvinceType;
city: CityType;
}
export interface NoticeType {
id: string;
title: string;
logo: string;
description: string;
updatedAt: string;
member: string;
href: string;
memberLink: string;
}
export interface CurrentUser {
name: string;
avatar: string;
userid: string;
notice: NoticeType[];
email: string;
signature: string;
title: string;
group: string;
tags: TagType[];
notifyCount: number;
unreadCount: number;
country: string;
geographic: GeographicType;
address: string;
phone: string;
}
This diff is collapsed.
[
{
"name": "北京市",
"id": "110000"
},
{
"name": "天津市",
"id": "120000"
},
{
"name": "河北省",
"id": "130000"
},
{
"name": "山西省",
"id": "140000"
},
{
"name": "内蒙古自治区",
"id": "150000"
},
{
"name": "辽宁省",
"id": "210000"
},
{
"name": "吉林省",
"id": "220000"
},
{
"name": "黑龙江省",
"id": "230000"
},
{
"name": "上海市",
"id": "310000"
},
{
"name": "江苏省",
"id": "320000"
},
{
"name": "浙江省",
"id": "330000"
},
{
"name": "安徽省",
"id": "340000"
},
{
"name": "福建省",
"id": "350000"
},
{
"name": "江西省",
"id": "360000"
},
{
"name": "山东省",
"id": "370000"
},
{
"name": "河南省",
"id": "410000"
},
{
"name": "湖北省",
"id": "420000"
},
{
"name": "湖南省",
"id": "430000"
},
{
"name": "广东省",
"id": "440000"
},
{
"name": "广西壮族自治区",
"id": "450000"
},
{
"name": "海南省",
"id": "460000"
},
{
"name": "重庆市",
"id": "500000"
},
{
"name": "四川省",
"id": "510000"
},
{
"name": "贵州省",
"id": "520000"
},
{
"name": "云南省",
"id": "530000"
},
{
"name": "西藏自治区",
"id": "540000"
},
{
"name": "陕西省",
"id": "610000"
},
{
"name": "甘肃省",
"id": "620000"
},
{
"name": "青海省",
"id": "630000"
},
{
"name": "宁夏回族自治区",
"id": "640000"
},
{
"name": "新疆维吾尔自治区",
"id": "650000"
},
{
"name": "台湾省",
"id": "710000"
},
{
"name": "香港特别行政区",
"id": "810000"
},
{
"name": "澳门特别行政区",
"id": "820000"
}
]
import React, { Component } from 'react';
import { Dispatch } from 'redux';
import { FormattedMessage } from 'umi-plugin-react/locale';
import { GridContent } from '@ant-design/pro-layout';
import { Menu } from 'antd';
import { connect } from 'dva';
import BaseView from './components/base';
import BindingView from './components/binding';
import { CurrentUser } from './data.d';
import NotificationView from './components/notification';
import SecurityView from './components/security';
import styles from './style.less';
const { Item } = Menu;
interface SettingsProps {
dispatch: Dispatch<any>;
currentUser: CurrentUser;
}
type SettingsStateKeys = 'base' | 'security' | 'binding' | 'notification';
interface SettingsState {
mode: 'inline' | 'horizontal';
menuMap: {
[key: string]: React.ReactNode;
};
selectKey: SettingsStateKeys;
}
@connect(({ accountSettings }: { accountSettings: { currentUser: CurrentUser } }) => ({
currentUser: accountSettings.currentUser,
}))
class Settings extends Component<
SettingsProps,
SettingsState
> {
main: HTMLDivElement | undefined = undefined;
constructor(props: SettingsProps) {
super(props);
const menuMap = {
base: <FormattedMessage id="account-settings.menuMap.basic" defaultMessage="Basic Settings" />,
security: (
<FormattedMessage id="account-settings.menuMap.security" defaultMessage="Security Settings" />
),
binding: (
<FormattedMessage id="account-settings.menuMap.binding" defaultMessage="Account Binding" />
),
notification: (
<FormattedMessage
id="account-settings.menuMap.notification"
defaultMessage="New Message Notification"
/>
),
};
this.state = {
mode: 'inline',
menuMap,
selectKey: 'base',
};
}
componentDidMount() {
const { dispatch } = this.props;
dispatch({
type: 'accountSettings/fetchCurrent',
});
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: SettingsStateKeys) => {
this.setState({
selectKey: key,
});
};
resize = () => {
if (!this.main) {
return;
}
requestAnimationFrame(() => {
if (!this.main) {
return;
}
let mode: 'inline' | 'horizontal' = '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,
});
});
};
renderChildren = () => {
const { selectKey } = this.state;
switch (selectKey) {
case 'base':
return <BaseView />;
case 'security':
return <SecurityView />;
case 'binding':
return <BindingView />;
case 'notification':
return <NotificationView />;
default:
break;
}
return null;
};
render() {
const { currentUser } = this.props;
if (!currentUser.userid) {
return '';
}
const { mode, selectKey } = this.state;
return (
<GridContent>
<div
className={styles.main}
ref={ref => {
if (ref) {
this.main = ref;
}
}}
>
<div className={styles.leftMenu}>
<Menu
mode={mode}
selectedKeys={[selectKey]}
onClick={({ key }) => this.selectKey(key as SettingsStateKeys)}
>
{this.getMenu()}
</Menu>
</div>
<div className={styles.right}>
<div className={styles.title}>{this.getRightTitle()}</div>
{this.renderChildren()}
</div>
</div>
</GridContent>
);
}
}
export default Settings;
export default {
'account-settings.menuMap.basic': 'Basic Settings',
'account-settings.menuMap.security': 'Security Settings',
'account-settings.menuMap.binding': 'Account Binding',
'account-settings.menuMap.notification': 'New Message Notification',
'account-settings.basic.avatar': 'Avatar',
'account-settings.basic.change-avatar': 'Change avatar',
'account-settings.basic.email': 'Email',
'account-settings.basic.email-message': 'Please input your email!',
'account-settings.basic.nickname': 'Nickname',
'account-settings.basic.nickname-message': 'Please input your Nickname!',
'account-settings.basic.profile': 'Personal profile',
'account-settings.basic.profile-message': 'Please input your personal profile!',
'account-settings.basic.profile-placeholder': 'Brief introduction to yourself',
'account-settings.basic.country': 'Country/Region',
'account-settings.basic.country-message': 'Please input your country!',
'account-settings.basic.geographic': 'Province or city',
'account-settings.basic.geographic-message': 'Please input your geographic info!',
'account-settings.basic.address': 'Street Address',
'account-settings.basic.address-message': 'Please input your address!',
'account-settings.basic.phone': 'Phone Number',
'account-settings.basic.phone-message': 'Please input your phone!',
'account-settings.basic.update': 'Update Information',
'account-settings.basic.update.success': 'Update basic information successfully',
'account-settings.security.strong': 'Strong',
'account-settings.security.medium': 'Medium',
'account-settings.security.weak': 'Weak',
'account-settings.security.password': 'Account Password',
'account-settings.security.password-description': 'Current password strength:',
'account-settings.security.phone': 'Security Phone',
'account-settings.security.phone-description': 'Bound phone:',
'account-settings.security.question': 'Security Question',
'account-settings.security.question-description':
'The security question is not set, and the security policy can effectively protect the account security',
'account-settings.security.email': 'Backup Email',
'account-settings.security.email-description': 'Bound Email:',
'account-settings.security.mfa': 'MFA Device',
'account-settings.security.mfa-description':
'Unbound MFA device, after binding, can be confirmed twice',
'account-settings.security.modify': 'Modify',
'account-settings.security.set': 'Set',
'account-settings.security.bind': 'Bind',
'account-settings.binding.taobao': 'Binding Taobao',
'account-settings.binding.taobao-description': 'Currently unbound Taobao account',
'account-settings.binding.alipay': 'Binding Alipay',
'account-settings.binding.alipay-description': 'Currently unbound Alipay account',
'account-settings.binding.dingding': 'Binding DingTalk',
'account-settings.binding.dingding-description': 'Currently unbound DingTalk account',
'account-settings.binding.bind': 'Bind',
'account-settings.notification.password': 'Account Password',
'account-settings.notification.password-description':
'Messages from other users will be notified in the form of a station letter',
'account-settings.notification.messages': 'System Messages',
'account-settings.notification.messages-description':
'System messages will be notified in the form of a station letter',
'account-settings.notification.todo': 'To-do Notification',
'account-settings.notification.todo-description':
'The to-do list will be notified in the form of a letter from the station',
'account-settings.settings.open': 'Open',
'account-settings.settings.close': 'Close',
};
export default {
'account-settings.menuMap.basic': '基本设置',
'account-settings.menuMap.security': '安全设置',
'account-settings.menuMap.binding': '账号绑定',
'account-settings.menuMap.notification': '新消息通知',
'account-settings.basic.avatar': '头像',
'account-settings.basic.change-avatar': '更换头像',
'account-settings.basic.email': '邮箱',
'account-settings.basic.email-message': '请输入您的邮箱!',
'account-settings.basic.nickname': '昵称',
'account-settings.basic.nickname-message': '请输入您的昵称!',
'account-settings.basic.profile': '个人简介',
'account-settings.basic.profile-message': '请输入个人简介!',
'account-settings.basic.profile-placeholder': '个人简介',
'account-settings.basic.country': '国家/地区',
'account-settings.basic.country-message': '请输入您的国家或地区!',
'account-settings.basic.geographic': '所在省市',
'account-settings.basic.geographic-message': '请输入您的所在省市!',
'account-settings.basic.address': '街道地址',
'account-settings.basic.address-message': '请输入您的街道地址!',
'account-settings.basic.phone': '联系电话',
'account-settings.basic.phone-message': '请输入您的联系电话!',
'account-settings.basic.update': '更新基本信息',
'account-settings.basic.update.success': '更新基本信息成功',
'account-settings.security.strong': '',
'account-settings.security.medium': '',
'account-settings.security.weak': '',
'account-settings.security.password': '账户密码',
'account-settings.security.password-description': '当前密码强度:',
'account-settings.security.phone': '密保手机',
'account-settings.security.phone-description': '已绑定手机:',
'account-settings.security.question': '密保问题',
'account-settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
'account-settings.security.email': '备用邮箱',
'account-settings.security.email-description': '已绑定邮箱:',
'account-settings.security.mfa': 'MFA 设备',
'account-settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
'account-settings.security.modify': '修改',
'account-settings.security.set': '设置',
'account-settings.security.bind': '绑定',
'account-settings.binding.taobao': '绑定淘宝',
'account-settings.binding.taobao-description': '当前未绑定淘宝账号',
'account-settings.binding.alipay': '绑定支付宝',
'account-settings.binding.alipay-description': '当前未绑定支付宝账号',
'account-settings.binding.dingding': '绑定钉钉',
'account-settings.binding.dingding-description': '当前未绑定钉钉账号',
'account-settings.binding.bind': '绑定',
'account-settings.notification.password': '账户密码',
'account-settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
'account-settings.notification.messages': '系统消息',
'account-settings.notification.messages-description': '系统消息将以站内信的形式通知',
'account-settings.notification.todo': '待办任务',
'account-settings.notification.todo-description': '待办任务将以站内信的形式通知',
'account-settings.settings.open': '',
'account-settings.settings.close': '',
};
export default {
'account-settings.menuMap.basic': '基本設置',
'account-settings.menuMap.security': '安全設置',
'account-settings.menuMap.binding': '賬號綁定',
'account-settings.menuMap.notification': '新消息通知',
'account-settings.basic.avatar': '頭像',
'account-settings.basic.change-avatar': '更換頭像',
'account-settings.basic.email': '郵箱',
'account-settings.basic.email-message': '請輸入您的郵箱!',
'account-settings.basic.nickname': '昵稱',
'account-settings.basic.nickname-message': '請輸入您的昵稱!',
'account-settings.basic.profile': '個人簡介',
'account-settings.basic.profile-message': '請輸入個人簡介!',
'account-settings.basic.profile-placeholder': '個人簡介',
'account-settings.basic.country': '國家/地區',
'account-settings.basic.country-message': '請輸入您的國家或地區!',
'account-settings.basic.geographic': '所在省市',
'account-settings.basic.geographic-message': '請輸入您的所在省市!',
'account-settings.basic.address': '街道地址',
'account-settings.basic.address-message': '請輸入您的街道地址!',
'account-settings.basic.phone': '聯系電話',
'account-settings.basic.phone-message': '請輸入您的聯系電話!',
'account-settings.basic.update': '更新基本信息',
'account-settings.basic.update.success': '更新基本信息成功',
'account-settings.security.strong': '',
'account-settings.security.medium': '',
'account-settings.security.weak': '',
'account-settings.security.password': '賬戶密碼',
'account-settings.security.password-description': '當前密碼強度:',
'account-settings.security.phone': '密保手機',
'account-settings.security.phone-description': '已綁定手機:',
'account-settings.security.question': '密保問題',
'account-settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全',
'account-settings.security.email': '備用郵箱',
'account-settings.security.email-description': '已綁定郵箱:',
'account-settings.security.mfa': 'MFA 設備',
'account-settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認',
'account-settings.security.modify': '修改',
'account-settings.security.set': '設置',
'account-settings.security.bind': '綁定',
'account-settings.binding.taobao': '綁定淘寶',
'account-settings.binding.taobao-description': '當前未綁定淘寶賬號',
'account-settings.binding.alipay': '綁定支付寶',
'account-settings.binding.alipay-description': '當前未綁定支付寶賬號',
'account-settings.binding.dingding': '綁定釘釘',
'account-settings.binding.dingding-description': '當前未綁定釘釘賬號',
'account-settings.binding.bind': '綁定',
'account-settings.notification.password': '賬戶密碼',
'account-settings.notification.password-description': '其他用戶的消息將以站內信的形式通知',
'account-settings.notification.messages': '系統消息',
'account-settings.notification.messages-description': '系統消息將以站內信的形式通知',
'account-settings.notification.todo': '待辦任務',
'account-settings.notification.todo-description': '待辦任務將以站內信的形式通知',
'account-settings.settings.open': '',
'account-settings.settings.close': '',
};
This diff is collapsed.
import request from 'umi-request';
export async function queryCurrent() {
return request('/api/currentUser');
}
export async function queryProvince() {
return request('/api/geographic/province');
}
export async function queryCity(province: string) {
return request(`/api/geographic/city/${province}`);
}
export async function query() {
return request('/api/users');
}
@import '~antd/es/style/themes/default.less';
.main {
display: flex;
width: 100%;
height: 100%;
padding-top: 16px;
padding-bottom: 16px;
overflow: auto;
background-color: @menu-bg;
.leftMenu {
width: 224px;
border-right: @border-width-base @border-style-base @border-color-split;
:global {
.ant-menu-inline {
border: none;
}
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
font-weight: bold;
}
}
}
.right {
flex: 1;
padding-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 @border-color-split;
}
.ant-list-item {
padding-top: 14px;
padding-bottom: 14px;
}
}
}
:global {
.ant-list-item-meta {
// 账号绑定图标
.taobao {
display: block;
color: #ff4000;
font-size: 48px;
line-height: 48px;
border-radius: @border-radius-base;
}
.dingding {
margin: 2px;
padding: 6px;
color: #fff;
font-size: 32px;
line-height: 32px;
background-color: #2eabff;
border-radius: @border-radius-base;
}
.alipay {
color: #2eabff;
font-size: 48px;
line-height: 48px;
border-radius: @border-radius-base;
}
}
// 密码强度
font.strong {
color: @success-color;
}
font.medium {
color: @warning-color;
}
font.weak {
color: @error-color;
}
}
@media screen and (max-width: @screen-md) {
.main {
flex-direction: column;
.leftMenu {
width: 100%;
border: none;
}
.right {
padding: 40px;
}
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import * as BizChart from 'bizcharts';
export = BizChart;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment