diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 00000000..7e3649ac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.eslintrc b/.eslintrc new file mode 100755 index 00000000..a655538f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,40 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "rules": { + "generator-star-spacing": [0], + "consistent-return": [0], + "react/forbid-prop-types": [0], + "react/jsx-filename-extension": [1, { "extensions": [".js"] }], + "global-require": [1], + "import/prefer-default-export": [0], + "react/jsx-no-bind": [0], + "react/prop-types": [0], + "react/prefer-stateless-function": [0], + "no-else-return": [0], + "no-restricted-syntax": [0], + "import/no-extraneous-dependencies": [0], + "no-use-before-define": [0], + "jsx-a11y/no-static-element-interactions": [0], + "jsx-a11y/no-noninteractive-element-interactions": [0], + "no-nested-ternary": [0], + "arrow-body-style": [0], + "import/extensions": [0], + "no-bitwise": [0], + "no-cond-assign": [0], + "import/no-unresolved": [0], + "comma-dangle": ["error", { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "ignore" + }], + "require-yield": [1] + }, + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..aa6e4c4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/dist + +# misc +.DS_Store +npm-debug.log* diff --git a/.roadhogrc b/.roadhogrc new file mode 100755 index 00000000..b693be04 --- /dev/null +++ b/.roadhogrc @@ -0,0 +1,26 @@ +{ + "entry": "src/index.js", + "env": { + "development": { + "extraBabelPlugins": [ + "dva-hmr", + "transform-runtime", + "transform-decorators-legacy", + ["import", { "libraryName": "antd", "style": true }] + ] + }, + "production": { + "extraBabelPlugins": [ + "transform-runtime", + "transform-decorators-legacy", + ["import", { "libraryName": "antd", "style": true }] + ] + } + }, + "theme": { + "font-size-base": "14px", + "badge-font-size": "12px", + "btn-font-size-lg": "@font-size-base", + "layout-body-background": "#f5f5f5" + } +} diff --git a/.roadhogrc.mock.js b/.roadhogrc.mock.js new file mode 100644 index 00000000..d086fa8d --- /dev/null +++ b/.roadhogrc.mock.js @@ -0,0 +1,79 @@ +import mockjs from 'mockjs'; +import { getRule, postRule } from './mock/rule'; +import { getActivities, getNotice, getFakeList } from './mock/api'; +import { getFakeChartData } from './mock/chart'; +import { imgMap } from './mock/utils'; +import { getProfileData } from './mock/profile'; +import { getNotices } from './mock/notices'; +import { format, delay } from 'roadhog-api-doc'; + +// 代码中会兼容本地 service mock 以及部署站点的静态数据 + +const proxy = { + // 支持值为 Object 和 Array + 'GET /api/currentUser': { + $desc: "获取当前用户接口", + $params: { + pageSize: { + desc: '分页', + exp: 2, + }, + }, + $body: { + name: 'momo.zxy', + avatar: imgMap.user, + userid: '00000001', + notifyCount: 12, + }, + }, + // GET POST 可省略 + 'GET /api/users': [{ + key: '1', + name: 'John Brown', + age: 32, + address: 'New York No. 1 Lake Park', + }, { + key: '2', + name: 'Jim Green', + age: 42, + address: 'London No. 1 Lake Park', + }, { + key: '3', + name: 'Joe Black', + age: 32, + address: 'Sidney No. 1 Lake Park', + }], + 'GET /api/project/notice': getNotice, + 'GET /api/activities': getActivities, + 'GET /api/rule': getRule, + 'POST /api/rule': { + $params: { + pageSize: { + desc: '分页', + exp: 2, + }, + }, + $body: postRule, + }, + 'POST /api/forms': (req, res) => { + res.send('Ok'); + }, + 'GET /api/tags': mockjs.mock({ + 'list|100': [{ name: '@city', 'value|1-100': 50, 'type|0-2': 1 }] + }), + 'GET /api/fake_list': getFakeList, + 'GET /api/fake_chart_data': getFakeChartData, + 'GET /api/profile': getProfileData, + 'POST /api/login/account': (req, res) => { + res.send({ status: 'error', type: 'account' }); + }, + 'POST /api/login/mobile': (req, res) => { + res.send({ status: 'ok', type: 'mobile' }); + }, + 'POST /api/register': (req, res) => { + res.send({ status: 'ok' }); + }, + 'GET /api/notices': getNotices, +}; + +export default delay(proxy, 1000); diff --git a/mock/.gitkeep b/mock/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mock/api.js b/mock/api.js new file mode 100644 index 00000000..d652f5bd --- /dev/null +++ b/mock/api.js @@ -0,0 +1,205 @@ +import { imgMap, getUrlParams } from './utils'; + +export function fakeList(count) { + const titles = [ + '凤蝶', + 'AntDesignPro', + 'DesignLab', + 'Basement', + 'AntDesign', + '云雀', + '体验云', + 'AntDesignMobile', + ]; + const avatars = [ + 'https://gw.alipayobjects.com/zos/rmsportal/hYjIZrUoBfNxOAYBVDfc.png', // 凤蝶 + 'https://gw.alipayobjects.com/zos/rmsportal/HHWPIzPLCLYmVuPivyiA.png', // 云雀 + 'https://gw.alipayobjects.com/zos/rmsportal/irqByKtOdKfDojxIWTXF.png', // Basement + 'https://gw.alipayobjects.com/zos/rmsportal/VcmdbCBcwPTGYgbYeMzX.png', // DesignLab + ]; + const covers = [ + 'https://gw.alipayobjects.com/zos/rmsportal/JiqGstEfoWAOHiTxclqi.png', + 'https://gw.alipayobjects.com/zos/rmsportal/xMPpMvGSIXusgtgUPAdw.png', + 'https://gw.alipayobjects.com/zos/rmsportal/hQReiajgtqzIVFjLXjHp.png', + 'https://gw.alipayobjects.com/zos/rmsportal/nczfTaXEzhSpvgZZjBev.png', + ]; + + const list = []; + for (let i = 0; i < count; i += 1) { + list.push({ + id: `fake-list-${i}`, + owner: '曲丽丽', + title: titles[i % 8], + avatar: avatars[i % 4], + cover: covers[i % 4], + status: ['active', 'exception', 'normal'][i % 3], + percent: Math.ceil(Math.random() * 50) + 50, + logo: ['https://gw.alipayobjects.com/zos/rmsportal/KoJjkdbuTFxzJmmjuDVR.png', 'https://gw.alipayobjects.com/zos/rmsportal/UxGORCvEXJEsxOfEKZiA.png'][i % 2], + href: 'https://ant.design', + updatedAt: new Date(new Date().getTime() - (1000 * 60 * 60 * 2 * i)), + createdAt: new Date(new Date().getTime() - (1000 * 60 * 60 * 2 * i)), + subDescription: '一句话描述一句话描述', + 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: '段落示意:蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。', + members: [ + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + { + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WPOxPBHGyqsgKPsFtVlJ.png', + name: '王昭君', + }, + ], + }); + } + + return list; +} + +export function getFakeList(req, res, u) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + url = req.url; + } + + const params = getUrlParams(url); + + const count = (params.count * 1) || 20; + + const result = fakeList(count); + + if (res && res.json) { + res.json(result); + } else { + return result; + } +} + +export const getNotice = [ + { + id: 'xxx1', + title: '消息列表体验优化', + logo: imgMap.b, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date(), + member: '蜂鸟项目组', + }, + { + id: 'xxx2', + title: 'XX 平台', + logo: imgMap.c, + description: '这是一条描述信息', + updatedAt: new Date('2017-07-24 11:00:00'), + member: '凤蝶精英小分队', + }, + { + id: 'xxx3', + title: '消息列表体验优化', + logo: imgMap.a, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date(), + member: '蜂鸟项目组', + }, + { + id: 'xxx4', + title: '文档中心1', + logo: imgMap.a, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date('2017-07-23 06:23:00'), + member: '成都超级小分队', + }, + { + id: 'xxx5', + title: '文档中心2', + logo: imgMap.b, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date('2017-07-23 06:23:00'), + member: '成都超级小分队', + }, + { + id: 'xxx6', + title: '智能运营中心', + logo: imgMap.c, + description: '这是一条描述信息这是一条描述信息', + updatedAt: new Date('2017-07-23 06:23:00'), + member: '成都超级小分队', + }, +]; + +export const getActivities = [ + { + id: 'trend-1', + updatedAt: new Date(), + user: { + name: '林东东', + avatar: imgMap.a, + }, + action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)', + }, + { + id: 'trend-2', + updatedAt: new Date(), + user: { + name: '林嘻嘻', + avatar: imgMap.c, + }, + action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)', + }, + { + id: 'trend-3', + updatedAt: new Date(), + user: { + name: '林囡囡', + avatar: imgMap.b, + }, + action: '在 [凤蝶精英小分队](http://github.com/) 新建项目 [六月迭代](http://github.com/)', + }, + { + id: 'trend-4', + updatedAt: new Date(), + user: { + name: '林贝贝', + avatar: imgMap.c, + }, + action: '在 [5 月日常迭代](http://github.com/) 更新至已发布状态', + }, + { + id: 'trend-5', + updatedAt: new Date(), + user: { + name: '林忠忠', + avatar: imgMap.a, + }, + action: '在 [工程效能](http://github.com/) 发布了 [留言](http://github.com/)', + }, + { + id: 'trend-6', + updatedAt: new Date(), + user: { + name: '林呜呜', + avatar: imgMap.d, + }, + action: '在 [云雀](http://github.com/) 新建项目 [品牌迭代](http://github.com/)', + }, +]; + + +export default { + getNotice, + getActivities, + getFakeList, +}; diff --git a/mock/chart.js b/mock/chart.js new file mode 100644 index 00000000..d28c5521 --- /dev/null +++ b/mock/chart.js @@ -0,0 +1,184 @@ +import moment from 'moment'; + +// mock data +const visitData = []; +const beginDay = new Date().getTime(); +for (let i = 0; i < 20; i += 1) { + visitData.push({ + x: moment(new Date(beginDay + (1000 * 60 * 60 * 24 * i))).format('YYYY-MM-DD'), + y: Math.floor(Math.random() * 100) + 10, + }); +} +const salesData = []; +for (let i = 0; i < 12; i += 1) { + salesData.push({ + x: `${i + 1}月`, + y: Math.floor(Math.random() * 1000) + 200, + }); +} +const searchData = []; +for (let i = 0; i < 50; i += 1) { + searchData.push({ + index: i + 1, + keyword: `搜索关键词-${i}`, + count: Math.floor(Math.random() * 1000), + range: Math.floor(Math.random() * 100), + status: Math.floor((Math.random() * 10) % 2), + }); +} +const salesTypeData = [ + { + x: '家用电器', + y: 4544, + }, + { + x: '食用酒水', + y: 3321, + }, + { + x: '个护健康', + y: 3113, + }, + { + x: '服饰箱包', + y: 2341, + }, + { + x: '母婴产品', + y: 1231, + }, + { + x: '其他', + y: 1231, + }, +]; + +const salesTypeDataOnline = [ + { + x: '家用电器', + y: 244, + }, + { + x: '食用酒水', + y: 321, + }, + { + x: '个护健康', + y: 311, + }, + { + x: '服饰箱包', + y: 41, + }, + { + x: '母婴产品', + y: 121, + }, + { + x: '其他', + y: 111, + }, +]; + +const salesTypeDataOffline = [ + { + x: '家用电器', + y: 99, + }, + { + x: '个护健康', + y: 188, + }, + { + x: '服饰箱包', + y: 344, + }, + { + x: '母婴产品', + y: 255, + }, + { + x: '其他', + y: 65, + }, +]; + +const offlineData = []; +for (let i = 0; i < 10; i += 1) { + offlineData.push({ + name: `门店${i}`, + cvr: Math.ceil(Math.random() * 9) / 10, + }); +} +const offlineChartData = []; +for (let i = 0; i < 20; i += 1) { + offlineChartData.push({ + x: (new Date().getTime()) + (1000 * 60 * 30 * i), + y1: Math.floor(Math.random() * 100) + 10, + y2: Math.floor(Math.random() * 100) + 10, + }); +} + +const radarOriginData = [ + { + name: '个人', + ref: 10, + koubei: 8, + output: 4, + contribute: 5, + hot: 7, + }, + { + name: '团队', + ref: 3, + koubei: 9, + output: 6, + contribute: 3, + hot: 1, + }, + { + name: '部门', + ref: 4, + koubei: 1, + output: 6, + contribute: 5, + hot: 7, + }, +]; + +// +const radarData = []; +const radarTitleMap = { + ref: '引用', + koubei: '口碑', + output: '产量', + contribute: '贡献', + hot: '热度', +}; +radarOriginData.forEach((item) => { + Object.keys(item).forEach((key) => { + if (key !== 'name') { + radarData.push({ + name: item.name, + label: radarTitleMap[key], + value: item[key], + }); + } + }); +}); + +export const getFakeChartData = { + visitData, + salesData, + searchData, + offlineData, + offlineChartData, + salesTypeData, + salesTypeDataOnline, + salesTypeDataOffline, + radarData, +}; + +export default { + getFakeChartData, +}; diff --git a/mock/notices.js b/mock/notices.js new file mode 100644 index 00000000..782fac4f --- /dev/null +++ b/mock/notices.js @@ -0,0 +1,85 @@ +export default { + getNotices(req, res) { + res.json([{ + id: '000000001', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', + title: '你收到了 14 份新周报', + datetime: '2017-08-09', + type: '通知', + }, { + id: '000000002', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', + title: '你推荐的 曲妮妮 已通过第三轮面试', + datetime: '2017-08-08', + type: '通知', + }, { + id: '000000003', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', + title: '这种模板可以区分多种通知类型', + datetime: '2017-08-07', + read: true, + type: '通知', + }, { + id: '000000004', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', + title: '左侧图标用于区分不同的类型', + datetime: '2017-08-07', + type: '通知', + }, { + id: '000000005', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', + title: '内容不要超过两行字,超出时自动截断', + datetime: '2017-08-07', + type: '通知', + }, { + id: '000000006', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '曲丽丽 评论了你', + description: '描述信息描述信息描述信息', + datetime: '2017-08-07', + type: '消息', + }, { + id: '000000007', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '朱偏右 回复了你', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: '2017-08-07', + type: '消息', + }, { + id: '000000008', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '标题', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: '2017-08-07', + type: '消息', + }, { + id: '000000009', + title: '任务名称', + description: '任务需要在 2017-01-12 20:00 前启动', + extra: '马上到期', + status: 'urgent', + type: '待办', + }, { + id: '000000010', + title: '第三方紧急代码变更', + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', + extra: '马上到期', + status: 'urgent', + type: '待办', + }, { + id: '000000011', + title: '信息安全考试', + description: '指派竹尔于 2017-01-09 前完成更新并发布', + extra: '已耗时 8 天', + status: 'doing', + type: '待办', + }, { + id: '000000012', + title: 'ABCD 版本发布', + description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', + extra: '进行中', + status: 'processing', + type: '待办', + }]); + }, +}; diff --git a/mock/profile.js b/mock/profile.js new file mode 100644 index 00000000..d2e6083d --- /dev/null +++ b/mock/profile.js @@ -0,0 +1,74 @@ +const operation1 = [ + { + key: 'op1', + type: '订购关系生效', + name: '曲丽丽', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, + { + key: 'op2', + type: '财务复审', + name: '付小小', + status: 'reject', + updatedAt: '2017-10-03 19:23:12', + memo: '不通过原因', + }, + { + key: 'op3', + type: '部门初审', + name: '周毛毛', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, + { + key: 'op4', + type: '提交订单', + name: '林东东', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '很棒', + }, + { + key: 'op5', + type: '创建订单', + name: '汗牙牙', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +const operation2 = [ + { + key: 'op1', + type: '订购关系生效', + name: '曲丽丽', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +const operation3 = [ + { + key: 'op1', + type: '创建订单', + name: '汗牙牙', + status: 'agree', + updatedAt: '2017-10-03 19:23:12', + memo: '-', + }, +]; + +export const getProfileData = { + operation1, + operation2, + operation3, +}; + +export default { + getProfileData, +}; diff --git a/mock/rule.js b/mock/rule.js new file mode 100644 index 00000000..13a4f353 --- /dev/null +++ b/mock/rule.js @@ -0,0 +1,128 @@ +import { getUrlParams } from './utils'; + +// mock tableListDataSource +let tableListDataSource = []; +for (let i = 0; i < 46; i += 1) { + tableListDataSource.push({ + key: i, + href: 'https://ant.design', + avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2], + no: `TradeCode ${i}`, + title: `一个任务名称 ${i}`, + owner: '曲丽丽', + description: '这是一段描述', + callNo: Math.floor(Math.random() * 1000), + status: Math.floor(Math.random() * 10) % 2, + updatedAt: new Date(`2017-07-${Math.floor(i / 2) + 1} ${Math.floor(i / 2) + 1}:00:00`), + createdAt: new Date(`2017-07-${Math.floor(i / 2) + 1} ${Math.floor(i / 2) + 1}:00:00`), + progress: Math.ceil(Math.random() * 100), + }); +} + +export function getRule(req, res, u) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + url = req.url; + } + + const params = getUrlParams(url); + + let dataSource = [...tableListDataSource]; + + if (params.sorter) { + const s = params.sorter.split('_'); + dataSource = dataSource.sort((prev, next) => { + if (s[1] === 'descend') { + return next[s[0]] - prev[s[0]]; + } + return prev[s[0]] - next[s[0]]; + }); + } + + if (params.status) { + const s = params.status.split(','); + if (s.length === 1) { + dataSource = dataSource.filter(data => parseInt(data.status, 10) === parseInt(s[0], 10)); + } + } + + if (params.no) { + dataSource = dataSource.filter(data => data.no.indexOf(params.no) > -1); + } + + let pageSize = 10; + if (params.pageSize) { + pageSize = params.pageSize * 1; + } + + const result = { + list: dataSource, + pagination: { + total: dataSource.length, + pageSize, + current: parseInt(params.currentPage, 10) || 1, + }, + }; + + if (res && res.json) { + res.json(result); + } else { + return result; + } +} + +export function postRule(req, res, u, b) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + url = req.url; + } + + const body = (b && b.body) || req.body; + const method = body.method; + + switch (method) { + /* eslint no-case-declarations:0 */ + case 'delete': + const no = body.no; + tableListDataSource = tableListDataSource.filter(item => no.indexOf(item.no) === -1); + break; + case 'post': + const description = body.description; + const i = Math.ceil(Math.random() * 10000); + tableListDataSource.unshift({ + key: i, + href: 'https://ant.design', + avatar: ['https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png'][i % 2], + no: `TradeCode ${i}`, + title: `一个任务名称 ${i}`, + owner: '曲丽丽', + description, + callNo: Math.floor(Math.random() * 1000), + status: Math.floor(Math.random() * 10) % 2, + updatedAt: new Date(), + createdAt: new Date(), + progress: Math.ceil(Math.random() * 100), + }); + break; + default: + break; + } + + const result = { + list: tableListDataSource, + pagination: { + total: tableListDataSource.length, + }, + }; + + if (res && res.json) { + res.json(result); + } else { + return result; + } +} + +export default { + getRule, + postRule, +}; diff --git a/mock/utils.js b/mock/utils.js new file mode 100644 index 00000000..6e1c72fe --- /dev/null +++ b/mock/utils.js @@ -0,0 +1,45 @@ +export const imgMap = { + user: 'https://gw.alipayobjects.com/zos/rmsportal/YdMCpIJULitXfqHCFPbF.png', + a: 'https://gw.alipayobjects.com/zos/rmsportal/ZrkcSjizAKNWwJTwcadT.png', + b: 'https://gw.alipayobjects.com/zos/rmsportal/KYlwHMeomKQbhJDRUVvt.png', + c: 'https://gw.alipayobjects.com/zos/rmsportal/gabvleTstEvzkbQRfjxu.png', + d: 'https://gw.alipayobjects.com/zos/rmsportal/jvpNzacxUYLlNsHTtrAD.png', +}; + +// refers: https://www.sitepoint.com/get-url-parameters-with-javascript/ +export function getUrlParams(url) { + const d = decodeURIComponent; + let queryString = url ? url.split('?')[1] : window.location.search.slice(1); + const obj = {}; + if (queryString) { + queryString = queryString.split('#')[0]; + const arr = queryString.split('&'); + for (let i = 0; i < arr.length; i += 1) { + const a = arr[i].split('='); + let paramNum; + const paramName = a[0].replace(/\[\d*\]/, (v) => { + paramNum = v.slice(1, -1); + return ''; + }); + const paramValue = typeof (a[1]) === 'undefined' ? true : a[1]; + if (obj[paramName]) { + if (typeof obj[paramName] === 'string') { + obj[paramName] = d([obj[paramName]]); + } + if (typeof paramNum === 'undefined') { + obj[paramName].push(d(paramValue)); + } else { + obj[paramName][paramNum] = d(paramValue); + } + } else { + obj[paramName] = d(paramValue); + } + } + } + return obj; +} + +export default { + getUrlParams, + imgMap, +}; diff --git a/package.json b/package.json new file mode 100755 index 00000000..834aa128 --- /dev/null +++ b/package.json @@ -0,0 +1,47 @@ +{ + "name": "ant-design-admin", + "private": true, + "scripts": { + "start": "roadhog server", + "build": "roadhog build", + "lint": "eslint --ext .js src test", + "precommit": "npm run lint" + }, + "dependencies": { + "antd": "next", + "dva": "^1.2.1", + "g-cloud": "^1.0.2-beta", + "g2": "^2.3.8", + "g2-plugin-slider": "^1.2.1", + "lodash": "^4.17.4", + "marked": "^0.3.6", + "numeral": "^2.0.6", + "prop-types": "^15.5.10", + "qs": "^6.5.0", + "react": "^15.4.0", + "react-document-title": "^2.0.3", + "react-dom": "^15.4.0", + "react-redux": "4.x || 5.x", + "react-router": "2.x || 3.x" + }, + "devDependencies": { + "babel-eslint": "^7.1.1", + "babel-plugin-dva-hmr": "^0.3.2", + "babel-plugin-import": "^1.2.1", + "babel-plugin-transform-decorators-legacy": "^1.3.4", + "babel-plugin-transform-runtime": "^6.9.0", + "babel-runtime": "^6.9.2", + "eslint": "^3.0.0", + "eslint-config-airbnb": "latest", + "eslint-plugin-babel": "^4.0.0", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-jsx-a11y": "^5.0.1", + "eslint-plugin-react": "^7.0.1", + "expect": "^1.20.2", + "husky": "^0.13.4", + "mockjs": "^1.0.1-beta3", + "redbox-react": "^1.3.2", + "roadhog": "^1.0.2", + "roadhog-api-doc": "^0.1.0" + } +} diff --git a/public/index.html b/public/index.html new file mode 100755 index 00000000..60ed560f --- /dev/null +++ b/public/index.html @@ -0,0 +1,13 @@ + + + + + + Ant Design Pro + + + +
+ + + diff --git a/src/assets/yay.jpg b/src/assets/yay.jpg new file mode 100644 index 00000000..e72bd8ff Binary files /dev/null and b/src/assets/yay.jpg differ diff --git a/src/common/nav.js b/src/common/nav.js new file mode 100644 index 00000000..263190c1 --- /dev/null +++ b/src/common/nav.js @@ -0,0 +1,202 @@ +import BasicLayout from '../layouts/BasicLayout'; +import UserLayout from '../layouts/UserLayout'; + +import Analysis from '../routes/Dashboard/Analysis'; +import Monitor from '../routes/Dashboard/Monitor'; +import Workplace from '../routes/Dashboard/Workplace'; + +import TableList from '../routes/List/TableList'; +import CoverCardList from '../routes/List/CoverCardList'; +import CardList from '../routes/List/CardList'; +import FilterCardList from '../routes/List/FilterCardList'; +import SearchList from '../routes/List/SearchList'; +import BasicList from '../routes/List/BasicList'; + +import Profile from '../routes/Profile'; +import BasicForm from '../routes/Forms/BasicForm'; +import AdvancedForm from '../routes/Forms/AdvancedForm'; +import StepForm from '../routes/Forms/StepForm'; +import Step2 from '../routes/Forms/StepForm/Step2'; +import Step3 from '../routes/Forms/StepForm/Step3'; + +import Exception403 from '../routes/Exception/403'; +import Exception404 from '../routes/Exception/404'; +import Exception500 from '../routes/Exception/500'; + +import Success from '../routes/Result/Success'; +import Error from '../routes/Result/Error'; + +import Login from '../routes/User/Login'; +import Register from '../routes/User/Register'; +import RegisterResult from '../routes/User/RegisterResult'; + +function userAdapter(userData) { + userData.children.forEach((item) => { + if (item.children) { + userAdapter(item); + } else { + const userItem = item; + userItem.target = '_blank'; + userItem.noRoute = true; + } + }); + return userData; +} + +export const user = [{ + name: '帐户', + icon: 'setting', + path: 'user', + children: [{ + name: '登录', + path: 'login', + component: Login, + icon: 'setting', + }, { + name: '注册', + path: 'register', + component: Register, + icon: 'setting', + }, { + name: '注册结果', + path: 'register-result', + component: RegisterResult, + icon: 'setting', + }], +}]; + +export const menus = [{ + name: 'Dashboard', + icon: 'setting', + path: 'dashboard', + children: [{ + name: '分析页', + path: 'analysis', + component: Analysis, + icon: 'setting', + }, { + name: '监控页', + path: 'monitor', + component: Monitor, + icon: 'setting', + }, { + name: '工作台', + path: 'workplace', + component: Workplace, + icon: 'setting', + }], +}, { + name: '表单页', + path: 'form', + icon: 'setting', + children: [{ + name: '基础表单', + path: 'basic-form', + component: BasicForm, + icon: 'setting', + }, { + name: '分步表单', + path: 'step-form', + component: StepForm, + icon: 'setting', + children: [{ + path: 'confirm', + component: Step2, + }, { + path: 'result', + component: Step3, + }], + }, { + name: '高级表单', + path: 'advanced-form', + component: AdvancedForm, + icon: 'setting', + }], +}, { + name: '列表页', + path: 'list', + icon: 'setting', + children: [{ + name: '标准表格(表格查询)', + path: 'table-list', + component: TableList, + icon: 'setting', + }, { + name: '标准列表', + path: 'basic-list', + component: BasicList, + icon: 'setting', + }, { + name: '卡片列表', + path: 'card-list', + component: CardList, + icon: 'setting', + }, { + name: '卡片列表(封面)', + path: 'cover-card-list', + component: CoverCardList, + icon: 'setting', + }, { + name: '带筛选卡片列表', + path: 'filter-card-list', + component: FilterCardList, + icon: 'setting', + }, { + name: '搜索列表', + path: 'search', + component: SearchList, + icon: 'setting', + }], +}, { + name: '详情页', + path: 'profile', + component: Profile, + icon: 'setting', +}, { + name: '结果', + path: 'result', + icon: 'setting', + children: [{ + name: '成功', + path: 'success', + component: Success, + icon: 'setting', + }, { + name: '失败', + path: 'fail', + component: Error, + icon: 'setting', + }], +}, { + name: '错误', + path: 'error', + icon: 'setting', + children: [{ + name: '403', + path: '403', + component: Exception403, + icon: 'setting', + }, { + name: '404', + path: '404', + component: Exception404, + icon: 'setting', + }, { + name: '500', + path: '500', + component: Exception500, + icon: 'setting', + }], +}, userAdapter(JSON.parse(JSON.stringify(user[0])))]; + + +export default [{ + component: BasicLayout, + name: '首页', + children: menus, + path: '', +}, { + component: UserLayout, + name: '账户', + children: user, +}]; diff --git a/src/components/ActivitiesItem/index.js b/src/components/ActivitiesItem/index.js new file mode 100644 index 00000000..8f74749b --- /dev/null +++ b/src/components/ActivitiesItem/index.js @@ -0,0 +1,31 @@ +import React from 'react'; +import moment from 'moment'; +import marked from 'marked'; +import { Avatar } from 'antd'; + +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +export default ({ data: { user, updatedAt, action } }) => ( +
+
+ { + user.link && + + + } + { + !user.link && {user.title} + } +
+
+
+ {user.name} +
+
+

{moment(updatedAt).fromNow()}

+
+
+); diff --git a/src/components/ActivitiesItem/index.less b/src/components/ActivitiesItem/index.less new file mode 100644 index 00000000..0c95640f --- /dev/null +++ b/src/components/ActivitiesItem/index.less @@ -0,0 +1,41 @@ +@import "~antd/lib/style/themes/default.less"; + +.activitiesItem { + padding: 24px 24px 0 24px; + position: relative; + + .avatar { + position: absolute; + top: 24px; + left: 24px; + img { + display: block; + border-radius: 32px; + width: 32px; + height: 32px; + } + } + .content { + border-bottom: 1px solid @border-color-split; + padding-left: 48px; + padding-bottom: 24px; + font-size: @font-size-base; + a { + color: @primary-color; + } + & > div { + line-height: 22px; + .name { + margin-right: 4px; + font-weight: 500; + } + div, p { + display: inline-block; + } + } + & > p { + margin-top: 4px; + line-height: 22px; + } + } +} diff --git a/src/components/AvatarList/index.js b/src/components/AvatarList/index.js new file mode 100644 index 00000000..7ae18577 --- /dev/null +++ b/src/components/AvatarList/index.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { Tooltip, Avatar } from 'antd'; +import classNames from 'classnames'; + +import styles from './index.less'; + +const AvatarList = ({ children, size, ...other }) => { + const childrenWithProps = React.Children.map(children, child => + React.cloneElement(child, { + size, + }) + ); + + return ( +
+ +
+ ); +}; + +const Item = ({ src, size, tips, onClick = (() => {}) }) => { + const cls = classNames(styles.avatarItem, { + [styles.avatarItemLarge]: size === 'large', + [styles.avatarItemSmall]: size === 'small', + }); + + return ( +
  • + { + tips ? + + + + : + + } +
  • + ); +}; + +AvatarList.Item = Item; + +export default AvatarList; diff --git a/src/components/AvatarList/index.less b/src/components/AvatarList/index.less new file mode 100644 index 00000000..ba720535 --- /dev/null +++ b/src/components/AvatarList/index.less @@ -0,0 +1,29 @@ +@import "~antd/lib/style/themes/default.less"; + +.avatarList { + display: inline-block; + ul { + display: inline-block; + margin-left: 8px; + font-size: 0; + } +} + +.avatarItem { + display: inline-block; + overflow: hidden; + font-size: @font-size-base; + margin-left: -8px; + width: @avatar-size-base; + height: @avatar-size-base; +} + +.avatarItemLarge { + width: @avatar-size-lg; + height: @avatar-size-lg; +} + +.avatarItemSmall { + width: @avatar-size-sm; + height: @avatar-size-sm; +} diff --git a/src/components/Charts/Bar/index.js b/src/components/Charts/Bar/index.js new file mode 100644 index 00000000..537c4fc4 --- /dev/null +++ b/src/components/Charts/Bar/index.js @@ -0,0 +1,86 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import styles from '../index.less'; + +class Bar extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#33abfb', margin = [32, 0, 32, 40] } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const Frame = G2.Frame; + const frame = new Frame(data); + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height - 22, + legend: null, + plotCfg: { + margin, + }, + }); + + chart.axis('x', { + title: false, + }); + chart.axis('y', { + title: false, + line: false, + tickLine: false, + }); + + chart.source(frame, { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.interval().position('x*y').color(color); + chart.render(); + } + + render() { + const { height, title } = this.props; + + return ( +
    +
    + { title &&

    {title}

    } +
    +
    +
    + ); + } +} + +export default Bar; diff --git a/src/components/Charts/ChartCard/index.js b/src/components/Charts/ChartCard/index.js new file mode 100644 index 00000000..40faf796 --- /dev/null +++ b/src/components/Charts/ChartCard/index.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Card } from 'antd'; + +import styles from './index.less'; + +const ChartCard = ({ contentHeight, title, action, total, footer, children, ...rest }) => ( + +
    +
    + {title} + {action} +
    + { + // eslint-disable-next-line + total &&

    + } +

    +
    + {children} +
    +
    + { + footer &&
    + {footer} +
    + } +
    +
    +); + +export default ChartCard; diff --git a/src/components/Charts/ChartCard/index.less b/src/components/Charts/ChartCard/index.less new file mode 100644 index 00000000..d204e63a --- /dev/null +++ b/src/components/Charts/ChartCard/index.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.chartCard { + position: relative; + .meta { + color: @text-color-secondary; + font-size: @font-size-base; + position: relative; + line-height: 22px; + height: 22px; + } + .action { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + } + .total { + .textOverflow(); + color: @heading-color; + margin-top: 8px; + font-size: 30px; + line-height: 38px; + height: 38px; + } + .content { + position: relative; + width: 100%; + } + .contentFixed { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + } + .footer { + border-top: 1px solid @border-color-split; + padding-top: 8px; + margin-top: 11px; + & > * { + position: relative; + } + } +} diff --git a/src/components/Charts/Field/index.js b/src/components/Charts/Field/index.js new file mode 100644 index 00000000..ed525c56 --- /dev/null +++ b/src/components/Charts/Field/index.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import styles from './index.less'; + +const Field = ({ label, value, ...rest }) => ( +

    + {label} + {value} +

    +); + +export default Field; diff --git a/src/components/Charts/Field/index.less b/src/components/Charts/Field/index.less new file mode 100644 index 00000000..63894f9d --- /dev/null +++ b/src/components/Charts/Field/index.less @@ -0,0 +1,17 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.field { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + span { + font-size: @font-size-base; + line-height: 22px; + } + span:last-child { + font-weight: 600; + margin-left: 8px; + } +} + diff --git a/src/components/Charts/Gauge/index.js b/src/components/Charts/Gauge/index.js new file mode 100644 index 00000000..0c53540e --- /dev/null +++ b/src/components/Charts/Gauge/index.js @@ -0,0 +1,195 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; + +const Shape = G2.Shape; + +/* eslint no-underscore-dangle: 0 */ +class Gauge extends PureComponent { + componentDidMount() { + this.renderChart(); + } + + componentWillReceiveProps(nextProps) { + this.renderChart(nextProps); + } + + handleRef = (n) => { + this.node = n; + } + + initChart(nextProps) { + const { title, color = '#00b1f8' } = nextProps || this.props; + + Shape.registShape('point', 'dashBoard', { + drawShape(cfg, group) { + const originPoint = cfg.points[0]; + const point = this.parsePoint({ x: originPoint.x, y: 0.4 }); + + const center = this.parsePoint({ + x: 0, + y: 0, + }); + + const shape = group.addShape('polygon', { + attrs: { + points: [ + [center.x, center.y], + [point.x + 8, point.y], + [point.x + 8, point.y - 2], + [center.x, center.y - 2], + ], + radius: 2, + lineWidth: 2, + arrow: false, + fill: color, + }, + }); + + group.addShape('Marker', { + attrs: { + symbol: 'circle', + lineWidth: 2, + fill: color, + radius: 8, + x: center.x, + y: center.y, + }, + }); + group.addShape('Marker', { + attrs: { + symbol: 'circle', + lineWidth: 2, + fill: '#fff', + radius: 5, + x: center.x, + y: center.y, + }, + }); + + const origin = cfg.origin; + group.addShape('text', { + attrs: { + x: center.x, + y: center.y + 80, + text: `${origin._origin.value}%`, + textAlign: 'center', + fontSize: 24, + fill: 'rgba(0, 0, 0, 0.85)', + }, + }); + group.addShape('text', { + attrs: { + x: center.x, + y: center.y + 45, + text: title, + textAlign: 'center', + fontSize: 14, + fill: 'rgba(0, 0, 0, 0.43)', + }, + }); + + return shape; + }, + }); + } + + renderChart(nextProps) { + const { height, color = '#00b1f8', bgColor = '#d3f3fe', title, percent } = nextProps || this.props; + const data = [{ name: title, value: percent }]; + + if (this.chart) { + this.chart.clear(); + } + if (this.node) { + this.node.innerHTML = ''; + } + + this.initChart(nextProps); + + const chart = new G2.Chart({ + container: this.node, + forceFit: true, + height, + animate: false, + plotCfg: { + margin: [10, 0, 30, 0], + }, + }); + + chart.source(data); + + chart.tooltip(false); + + chart.coord('gauge', { + startAngle: -1.2 * Math.PI, + endAngle: 0.20 * Math.PI, + }); + chart.col('value', { + type: 'linear', + nice: true, + min: 0, + max: 100, + tickCount: 6, + subTick: false, + }); + chart.axis('value', { + tickLine: { + stroke: color, + }, + labelOffset: -12, + formatter(val) { + switch (val * 1) { + case 20: + return '差'; + case 40: + return '中'; + case 60: + return '良'; + case 80: + return '优'; + default: + return ''; + } + }, + }); + chart.point().position('value').shape('dashBoard'); + draw(data); + + /* eslint no-shadow: 0 */ + function draw(data) { + const val = data[0].value; + const lineWidth = 18; + chart.guide().clear(); + + chart.guide().arc(() => { + return [0, 0.95]; + }, () => { + return [val, 0.95]; + }, { + stroke: color, + lineWidth, + }); + + chart.guide().arc(() => { + return [val, 0.95]; + }, (arg) => { + return [arg.max, 0.95]; + }, { + stroke: bgColor, + lineWidth, + }); + + chart.changeData(data); + } + + this.chart = chart; + } + + render() { + return ( +
    + ); + } +} + +export default Gauge; diff --git a/src/components/Charts/Icon/index.js b/src/components/Charts/Icon/index.js new file mode 100644 index 00000000..d03a3e38 --- /dev/null +++ b/src/components/Charts/Icon/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { Icon } from 'antd'; + +const IconUp = ({ color }) => ( + +); + +const IconDown = ({ color }) => ( + +); + +export default { + IconUp, + IconDown, +}; diff --git a/src/components/Charts/MiniArea/index.js b/src/components/Charts/MiniArea/index.js new file mode 100644 index 00000000..5807c352 --- /dev/null +++ b/src/components/Charts/MiniArea/index.js @@ -0,0 +1,95 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import styles from '../index.less'; + +class MiniArea extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#33abfb', line, xAxis, yAxis } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height + 54, + plotCfg: { + margin: [36, 0, 30, 0], + }, + legend: null, + }); + + if (!xAxis && !yAxis) { + chart.axis(false); + } + + if (xAxis) { + chart.axis('x', xAxis); + } else { + chart.axis('x', false); + } + + if (yAxis) { + chart.axis('y', yAxis); + } else { + chart.axis('y', false); + } + + chart.source(data, { + x: { + type: 'cat', + range: [0, 1], + ...xAxis, + }, + y: { + min: 0, + ...yAxis, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.area().position('x*y').color(color).shape('smooth'); + if (line) { + chart.line().position('x*y').color(color).shape('smooth'); + } + chart.render(); + } + + render() { + const { height } = this.props; + + return ( +
    +
    +
    +
    +
    + ); + } +} + +export default MiniArea; diff --git a/src/components/Charts/MiniBar/index.js b/src/components/Charts/MiniBar/index.js new file mode 100644 index 00000000..d0553a7f --- /dev/null +++ b/src/components/Charts/MiniBar/index.js @@ -0,0 +1,78 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import styles from '../index.less'; + +class MiniBar extends PureComponent { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 0, fit = true, color = '#33ABFB' } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const Frame = G2.Frame; + const frame = new Frame(data); + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height + 54, + plotCfg: { + margin: [36, 0, 30, 0], + }, + legend: null, + }); + + chart.axis(false); + + chart.source(frame, { + x: { + type: 'cat', + }, + y: { + min: 0, + }, + }); + + chart.tooltip({ + title: null, + crosshairs: false, + map: { + name: 'x', + }, + }); + chart.interval().position('x*y').color(color); + chart.render(); + } + + render() { + const { height } = this.props; + + return ( +
    +
    +
    +
    +
    + ); + } +} + +export default MiniBar; diff --git a/src/components/Charts/MiniProgress/index.js b/src/components/Charts/MiniProgress/index.js new file mode 100644 index 00000000..63037a1c --- /dev/null +++ b/src/components/Charts/MiniProgress/index.js @@ -0,0 +1,27 @@ +import React from 'react'; + +import styles from './index.less'; + +const MiniProgress = ({ target, color, strokeWidth, percent }) => ( +
    +
    + + +
    +
    +
    +
    +
    +); + +export default MiniProgress; diff --git a/src/components/Charts/MiniProgress/index.less b/src/components/Charts/MiniProgress/index.less new file mode 100644 index 00000000..94ed47a0 --- /dev/null +++ b/src/components/Charts/MiniProgress/index.less @@ -0,0 +1,37 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.miniProgress { + padding: 5px 0; + position: relative; + width: 100%; + .progressWrap { + background-color: @background-color-base; + position: relative; + } + .progress { + transition: all .4s cubic-bezier(.08, .82, .17, 1) 0s; + border-radius: 1px 0 0 1px; + background-color: @primary-color; + width: 0; + height: 100%; + } + .target { + position: absolute; + top: 0; + bottom: 0; + span { + border-radius: 100px; + position: absolute; + top: 0; + left: 0; + height: 4px; + width: 2px; + } + span:last-child { + top: auto; + bottom: 0; + } + } +} + diff --git a/src/components/Charts/NumberInfo/index.js b/src/components/Charts/NumberInfo/index.js new file mode 100644 index 00000000..4971f998 --- /dev/null +++ b/src/components/Charts/NumberInfo/index.js @@ -0,0 +1,32 @@ +import React from 'react'; +import { Icon } from 'antd'; +import classNames from 'classnames'; + +import styles from './index.less'; + +export default ({ theme, title, subTitle, total, subTotal, status, ...rest }) => ( +
    + { + title &&

    {title}

    + } +
    {subTitle}
    +
    + {total} + { + (status || subTotal) && + { + status && + } + {subTotal} + + } +
    +
    +); diff --git a/src/components/Charts/NumberInfo/index.less b/src/components/Charts/NumberInfo/index.less new file mode 100644 index 00000000..c1dfcba8 --- /dev/null +++ b/src/components/Charts/NumberInfo/index.less @@ -0,0 +1,46 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.numberInfo { + h4 { + color: @heading-color; + margin-bottom: 16px; + } + h6 { + color: @text-color-secondary; + font-size: @font-size-base; + height: 22px; + line-height: 22px; + .textOverflow(); + } + & > div { + margin-top: 8px; + font-size: 0; + .textOverflow(); + & > span { + color: @heading-color; + display: inline-block; + line-height: 32px; + height: 32px; + font-size: 24px; + margin-right: 32px; + } + .subTotal { + color: @text-color-secondary; + font-size: @font-size-base; + vertical-align: top; + i { + font-size: 12px; + transform: scale(0.82); + margin-right: 4px; + } + } + } +} +.numberInfolight { + & > div { + & > span { + color: @text-color; + } + } +} diff --git a/src/components/Charts/Pie/index.js b/src/components/Charts/Pie/index.js new file mode 100644 index 00000000..7228268a --- /dev/null +++ b/src/components/Charts/Pie/index.js @@ -0,0 +1,224 @@ +import React, { Component } from 'react'; +import G2 from 'g2'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +class Pie extends Component { + state = { + legendData: [], + left: undefined, + } + + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + this.renderChart(nextProps.data); + } + + handleRef = (n) => { + this.node = n; + } + handleTotalRef = (n) => { + this.totalNode = n; + } + + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const legendData = this.state.legendData; + legendData[i] = newItem; + + if (this.chart) { + const filterItem = legendData.filter(l => l.checked).map(l => l.x); + this.chart.filter('x', filterItem); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + } + + renderChart(data) { + const { + title, height = 0, + hasLegend, fit = true, + margin, percent, color, + inner = 0.75, + animate = true, + } = this.props; + + let selected = this.props.selected || true; + let tooltip = this.props.tooltips || true; + + let formatColor; + if (percent) { + selected = false; + tooltip = false; + formatColor = (value) => { + if (value === '占比') { + return color || '#0096fa'; + } else { + return '#e9e9e9'; + } + }; + + /* eslint no-param-reassign: */ + data = [ + { + x: '占比', + y: parseFloat(percent), + }, + { + x: '反比', + y: 100 - parseFloat(percent), + }, + ]; + } + + if (!data || (data && data.length < 1)) { + return; + } + + let m = margin; + if (!margin) { + if (hasLegend) { + m = [24, 240, 24, 0]; + } else if (percent) { + m = [0, 0, 0, 0]; + } else { + m = [24, 0, 24, 0]; + } + } + + const h = title ? (height + m[0] + m[2] + (-46)) : (height + m[0] + m[2]); + + // clean + this.node.innerHTML = ''; + + const Stat = G2.Stat; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: h, + plotCfg: { + margin: m, + }, + animate, + }); + + if (!tooltip) { + chart.tooltip(false); + } else { + chart.tooltip({ + title: null, + }); + } + + chart.axis(false); + chart.legend(false); + + chart.source(data, { + x: { + type: 'cat', + range: [0, 1], + }, + y: { + min: 0, + }, + }); + + chart.coord('theta', { + inner, + }); + + chart.intervalStack().position(Stat.summary.percent('y')).color('x', formatColor).selected(selected); + chart.render(); + + this.chart = chart; + + let legendData = []; + if (hasLegend) { + const geom = chart.getGeoms()[0]; // 获取所有的图形 + const items = geom.getData(); // 获取图形对应的数据 + legendData = items.map((item) => { + /* eslint no-underscore-dangle:0 */ + const origin = item._origin; + origin.color = item.color; + origin.checked = true; + return origin; + }); + } + + this.setState({ + legendData, + }, () => { + let left = 0; + if (this.totalNode) { + left = -((this.totalNode.offsetWidth / 2) + ((margin || m)[1] / 2)); + } + this.setState({ + left, + }); + }); + } + + render() { + const { height, title, valueFormat, subTitle, total, hasLegend } = this.props; + const { legendData, left } = this.state; + const mt = -(((legendData.length * 38) - 16) / 2); + + return ( +
    +
    + { title &&

    {title}

    } +
    +
    + { + (subTitle || total) &&
    + { + subTitle &&

    {subTitle}

    + } + { + // eslint-disable-next-line + total &&

    + } +

    + } + { + hasLegend &&
      + { + legendData.map((item, i) => ( +
    • this.handleLegendClick(item, i)}> + + {item.x} + + {`${(item['..percent'] * 100).toFixed(2)}%`} + +
    • + )) + } +
    + } +
    +
    +
    + ); + } +} + +export default Pie; diff --git a/src/components/Charts/Pie/index.less b/src/components/Charts/Pie/index.less new file mode 100644 index 00000000..37392d19 --- /dev/null +++ b/src/components/Charts/Pie/index.less @@ -0,0 +1,70 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.pie { + .content { + position: relative; + } + .legend { + position: absolute; + top: 50%; + right: 0; + min-width: 200px; + li { + cursor: pointer; + margin-bottom: 16px; + height: 22px; + line-height: 22px; + } + } + .dot { + border-radius: 8px; + display: inline-block; + margin-right: 8px; + position: relative; + top: -1px; + height: 8px; + width: 8px; + } + .line { + background-color: @border-color-split; + display: inline-block; + margin-right: 8px; + width: 1px; + height: 16px; + } + .legendTitle { + color: @text-color; + margin-right: 8px; + } + .percent { + color: @text-color-secondary; + } + .value { + position: absolute; + right: 0; + } + .total { + opacity: 0; + position: absolute; + left: 50%; + top: 50%; + margin-top: -34px; + text-align: center; + height: 62px; + & > h4 { + color: @text-color-secondary; + font-size: 14px; + line-height: 22px; + height: 22px; + margin-bottom: 8px; + } + & > p { + color: @heading-color; + display: block; + font-size: 24px; + height: 32px; + line-height: 32px; + } + } +} diff --git a/src/components/Charts/Radar/index.js b/src/components/Charts/Radar/index.js new file mode 100644 index 00000000..1ac14375 --- /dev/null +++ b/src/components/Charts/Radar/index.js @@ -0,0 +1,155 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import { Row, Col } from 'antd'; +import styles from './index.less'; + +/* eslint react/no-danger:0 */ +class Radar extends PureComponent { + state = { + legendData: [], + } + + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + handleRef = (n) => { + this.node = n; + } + + handleLegendClick = (item, i) => { + const newItem = item; + newItem.checked = !newItem.checked; + + const legendData = this.state.legendData; + legendData[i] = newItem; + + if (this.chart) { + const filterItem = legendData.filter(l => l.checked).map(l => l.name); + this.chart.filter('name', filterItem); + this.chart.repaint(); + } + + this.setState({ + legendData, + }); + } + + renderChart(data) { + const { height = 0, + hasLegend = true, + fit = true, + tickCount = 4, + margin = [16, 0, 16, 0] } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: fit, + height: height - 22, + plotCfg: { + margin, + }, + }); + + this.chart = chart; + + chart.source(data, { + value: { + min: 0, + tickCount, + }, + }); + + chart.coord('polar'); + chart.legend(false); + + chart.axis('label', { + line: null, + }); + + chart.axis('value', { + grid: { + type: 'polygon', + }, + }); + + chart.line().position('label*value').color('name'); + chart.point().position('label*value').color('name').shape('circle'); + + chart.render(); + + if (hasLegend) { + const geom = chart.getGeoms()[0]; // 获取所有的图形 + const items = geom.getData(); // 获取图形对应的数据 + const legendData = items.map((item) => { + /* eslint no-underscore-dangle:0 */ + const origin = item._origin; + const result = { + name: origin[0].name, + color: item.color, + checked: true, + value: origin.reduce((p, n) => p + n.value, 0), + }; + + return result; + }); + + this.setState({ + legendData, + }); + } + } + + render() { + const { height, title, hasLegend } = this.props; + const { legendData } = this.state; + + return ( +
    +
    + { title &&

    {title}

    } +
    + { + hasLegend && + { + legendData.map((item, i) => ( + this.handleLegendClick(item, i)} + > +
    +

    + + {item.name} +

    +
    {item.value}
    + { + i !== (legendData.length - 1) &&
    + } +
    + + )) + } + + } +
    +
    + ); + } +} + +export default Radar; diff --git a/src/components/Charts/Radar/index.less b/src/components/Charts/Radar/index.less new file mode 100644 index 00000000..5e334680 --- /dev/null +++ b/src/components/Charts/Radar/index.less @@ -0,0 +1,38 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.radar { + .legend { + margin-top: 16px; + .legendItem { + position: relative; + text-align: center; + p { + cursor: pointer; + } + h6 { + color: @heading-color; + font-size: 24px; + line-height: 32px; + margin-top: 2px; + } + .split { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + height: 40px; + width: 1px; + } + } + .dot { + border-radius: 8px; + display: inline-block; + margin-right: 8px; + position: relative; + top: -1px; + height: 8px; + width: 8px; + } + } +} diff --git a/src/components/Charts/Trend/index.js b/src/components/Charts/Trend/index.js new file mode 100644 index 00000000..91c318b8 --- /dev/null +++ b/src/components/Charts/Trend/index.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Icon } from 'antd'; + +import styles from './index.less'; + +const Item = ({ title, flag, children, ...rest }) => ( +
    + {title} + { flag && } + {children} +
    +); + +const Trend = ({ colorType, children, ...rest }) => ( +
    + {children} +
    +); + +Trend.Item = Item; + +export default Trend; diff --git a/src/components/Charts/Trend/index.less b/src/components/Charts/Trend/index.less new file mode 100644 index 00000000..818fa9e6 --- /dev/null +++ b/src/components/Charts/Trend/index.less @@ -0,0 +1,49 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.trend { + font-size: 0; + height: 22px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .trendItem { + display: inline-block; + margin-right: 16px; + color: @text-color; + font-size: @font-size-base; + line-height: 22px; + height: 22px; + .title { + margin-right: 4px; + } + .value { + color: @text-color; + font-weight: 600; + } + .up, .down { + color: #00a854; + margin-right: 4px; + position: relative; + top: 1px; + i { + font-size: 12px; + transform: scale(0.83); + } + } + .down { + color: #f04134; + top: -1px; + } + } + .trendItem:last-child { + margin-right: 0; + } +} + +.trendgray { + .trend(); + .trendItem { + color: @text-color-secondary; + } +} diff --git a/src/components/Charts/WaterWave/index.js b/src/components/Charts/WaterWave/index.js new file mode 100644 index 00000000..f2dab658 --- /dev/null +++ b/src/components/Charts/WaterWave/index.js @@ -0,0 +1,189 @@ +import React, { PureComponent } from 'react'; +import styles from './index.less'; + +/* eslint no-return-assign: 0 */ +// riddle: https://riddle.alibaba-inc.com/riddles/2d9a4b90 + +class WaterWave extends PureComponent { + static defaultProps = { + height: 160, + } + state = { + radio: 1, + } + + componentDidMount() { + this.renderChart(); + this.resize(); + + window.addEventListener('resize', () => { + this.resize(); + }); + } + + resize() { + const { height } = this.props; + const realWidth = this.root.parentNode.offsetWidth; + if (realWidth < this.props.height) { + const radio = realWidth / height; + this.setState({ + radio, + }); + } else { + this.setState({ + radio: 1, + }); + } + } + + renderChart() { + const { percent, color = '#19AFFA' } = this.props; + const data = percent / 100; + + if (!this.node || !data) { + return; + } + + const canvas = this.node; + const ctx = canvas.getContext('2d'); + + const canvasWidth = canvas.width; + const canvasHeight = canvas.height; + const radius = canvasWidth / 2; + const lineWidth = 2; + const cR = radius - (lineWidth); + + ctx.beginPath(); + ctx.lineWidth = lineWidth; + + const axisLength = canvasWidth - (lineWidth); + const unit = axisLength / 8; + const range = 0.2; // 振幅 + let currRange = range; + const xOffset = lineWidth; + let sp = 0; // 周期偏移量 + let currData = 0; + const waveupsp = 0.005; // 水波上涨速度 + + let arcStack = []; + const bR = radius - (lineWidth); + const circleOffset = -(Math.PI / 2); + let circleLock = true; + + for (let i = circleOffset; i < circleOffset + (2 * Math.PI); i += 1 / (8 * Math.PI)) { + arcStack.push([ + radius + (bR * Math.cos(i)), + radius + (bR * Math.sin(i)), + ]); + } + + const cStartPoint = arcStack.shift(); + ctx.strokeStyle = color; + ctx.moveTo(cStartPoint[0], cStartPoint[1]); + + function drawSin() { + ctx.beginPath(); + ctx.save(); + + const sinStack = []; + for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) { + const x = sp + ((xOffset + i) / unit); + const y = Math.sin(x) * currRange; + const dx = i; + const dy = ((2 * cR * (1 - currData)) + (radius - cR)) - (unit * y); + + ctx.lineTo(dx, dy); + sinStack.push([dx, dy]); + } + + const startPoint = sinStack.shift(); + + ctx.lineTo(xOffset + axisLength, canvasHeight); + ctx.lineTo(xOffset, canvasHeight); + ctx.lineTo(startPoint[0], startPoint[1]); + ctx.fillStyle = color; + ctx.fill(); + ctx.restore(); + } + + function render() { + ctx.clearRect(0, 0, canvasWidth, canvasHeight); + if (circleLock) { + if (arcStack.length) { + const temp = arcStack.shift(); + ctx.lineTo(temp[0], temp[1]); + ctx.stroke(); + } else { + circleLock = false; + ctx.lineTo(cStartPoint[0], cStartPoint[1]); + ctx.stroke(); + arcStack = null; + + ctx.globalCompositeOperation = 'destination-over'; + ctx.beginPath(); + ctx.lineWidth = lineWidth; + ctx.arc(radius, radius, bR, 0, 2 * Math.PI, 1); + + ctx.beginPath(); + ctx.save(); + ctx.arc(radius, radius, radius - (3 * lineWidth), 0, 2 * Math.PI, 1); + + ctx.restore(); + ctx.clip(); + ctx.fillStyle = '#108ee9'; + } + } else { + if (data >= 0.85) { + if (currRange > range / 4) { + const t = range * 0.01; + currRange -= t; + } + } else if (data <= 0.1) { + if (currRange < range * 1.5) { + const t = range * 0.01; + currRange += t; + } + } else { + if (currRange <= range) { + const t = range * 0.01; + currRange += t; + } + if (currRange >= range) { + const t = range * 0.01; + currRange -= t; + } + } + if ((data - currData) > 0) { + currData += waveupsp; + } + if ((data - currData) < 0) { + currData -= waveupsp; + } + + sp += 0.07; + drawSin(); + } + requestAnimationFrame(render); + } + + render(); + } + + render() { + const { radio } = this.state; + const { percent, title, height } = this.props; + return ( +
    (this.root = n)} style={{ transform: `scale(${radio})` }}> + (this.node = n)} width={height} height={height} /> +
    + { + title && {title} + } +

    {percent}%

    +
    +
    + ); + } +} + +export default WaterWave; diff --git a/src/components/Charts/WaterWave/index.less b/src/components/Charts/WaterWave/index.less new file mode 100644 index 00000000..5248ed07 --- /dev/null +++ b/src/components/Charts/WaterWave/index.less @@ -0,0 +1,25 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../../utils/utils.less"; + +.waterWave { + display: inline-block; + position: relative; + transform-origin: left; + .text { + position: absolute; + left: 0; + top: 32px; + text-align: center; + width: 100%; + span { + color: @text-color-secondary; + font-size: 14px; + line-height: 22px; + } + h4 { + color: @heading-color; + line-height: 32px; + font-size: 24px; + } + } +} diff --git a/src/components/Charts/index.js b/src/components/Charts/index.js new file mode 100644 index 00000000..5967d6eb --- /dev/null +++ b/src/components/Charts/index.js @@ -0,0 +1,34 @@ +import numeral from 'numeral'; +import ChartCard from './ChartCard'; +import Bar from './Bar'; +import Pie from './Pie'; +import Radar from './Radar'; +import Gauge from './Gauge'; +import MiniArea from './MiniArea'; +import MiniBar from './MiniBar'; +import MiniProgress from './MiniProgress'; +import Trend from './Trend'; +import Field from './Field'; +import NumberInfo from './NumberInfo'; +import WaterWave from './WaterWave'; +import { IconUp, IconDown } from './Icon'; + +const yuan = val => `¥ ${numeral(val).format('0,0')}`; + +export default { + IconUp, + IconDown, + yuan, + Bar, + Pie, + Gauge, + Radar, + MiniBar, + MiniArea, + MiniProgress, + ChartCard, + Trend, + Field, + NumberInfo, + WaterWave, +}; diff --git a/src/components/Charts/index.less b/src/components/Charts/index.less new file mode 100644 index 00000000..cc4387d0 --- /dev/null +++ b/src/components/Charts/index.less @@ -0,0 +1,9 @@ +.miniChart { + position: relative; + width: 100%; + & > div { + position: absolute; + bottom: -34px; + width: 100%; + } +} diff --git a/src/components/Countdown/index.js b/src/components/Countdown/index.js new file mode 100644 index 00000000..6b364a75 --- /dev/null +++ b/src/components/Countdown/index.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react'; + +function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +class Countdown extends Component { + constructor(props) { + super(props); + + const { targetTime, lastTime } = this.initTime(props); + + this.state = { + targetTime, + lastTime, + }; + } + + componentDidMount() { + this.tick(); + } + + componentWillReceiveProps(nextProps) { + if (this.props.target !== nextProps.target) { + const { targetTime, lastTime } = this.initTime(nextProps); + this.setState({ + lastTime, + targetTime, + }); + } + } + + componentWillUnmount() { + clearTimeout(this.timer); + } + + timer = 0; + interval = 1000; + initTime = (props) => { + let lastTime = 0; + let targetTime = 0; + try { + if (Object.prototype.toString.call(props.target) === '[object Date]') { + targetTime = props.target.getTime(); + } else { + targetTime = new Date(props.target).getTime(); + } + } catch (e) { + throw new Error('invalid target prop', e); + } + + lastTime = targetTime - new Date().getTime(); + + return { + lastTime, + targetTime, + }; + } + // defaultFormat = time => ( + // {moment(time).format('hh:mm:ss')} + // ); + defaultFormat = (time) => { + const hours = 60 * 60 * 1000; + const minutes = 60 * 1000; + + const h = fixedZero(Math.floor(time / hours)); + const m = fixedZero(Math.floor((time - (h * hours)) / minutes)); + const s = fixedZero(Math.floor((time - (h * hours) - (m * minutes)) / 1000)); + return ( + {h}:{m}:{s} + ); + } + tick = () => { + const { onEnd } = this.props; + let { lastTime } = this.state; + + this.timer = setTimeout(() => { + if (lastTime < this.interval) { + clearTimeout(this.timer); + this.setState({ + lastTime: 0, + }); + + if (onEnd) { + onEnd(); + } + } else { + lastTime -= this.interval; + this.setState({ + lastTime, + }); + + this.tick(); + } + }, this.interval); + } + + render() { + const { format = this.defaultFormat } = this.props; + const { lastTime } = this.state; + + const result = format(lastTime); + + return result; + } +} + +export default Countdown; diff --git a/src/components/DescriptionList/Description.js b/src/components/DescriptionList/Description.js new file mode 100644 index 00000000..2aae62b2 --- /dev/null +++ b/src/components/DescriptionList/Description.js @@ -0,0 +1,17 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Col } from 'antd'; +import styles from './index.less'; +import responsive from './responsive'; + +const Description = ({ term, column, className, children, ...restProps }) => { + const clsString = classNames(styles.description, className); + return ( + + {term &&
    {term}
    } + {children &&
    {children}
    } + + ); +}; + +export default Description; diff --git a/src/components/DescriptionList/DescriptionList.js b/src/components/DescriptionList/DescriptionList.js new file mode 100644 index 00000000..25a5faf3 --- /dev/null +++ b/src/components/DescriptionList/DescriptionList.js @@ -0,0 +1,18 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Row } from 'antd'; +import styles from './index.less'; + +export default ({ className, title, col = 3, layout = 'horizontal', gutter = 32, + children, ...restProps }) => { + const clsString = classNames(styles.descriptionList, styles[layout], className); + const column = col > 4 ? 4 : col; + return ( +
    + {title ?
    {title}
    : null} + + {React.Children.map(children, child => React.cloneElement(child, { column }))} + +
    + ); +}; diff --git a/src/components/DescriptionList/demo/basic.md b/src/components/DescriptionList/demo/basic.md new file mode 100644 index 00000000..9e59c015 --- /dev/null +++ b/src/components/DescriptionList/demo/basic.md @@ -0,0 +1,35 @@ +--- +order: 0 +title: Basic +--- + +基本描述列表。 + +````jsx +import { DescriptionList } from 'ant-design-pro'; + +const { Description } = DescriptionList; + +ReactDOM.render( + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + +, mountNode); +```` diff --git a/src/components/DescriptionList/demo/vertical.md b/src/components/DescriptionList/demo/vertical.md new file mode 100644 index 00000000..c45c9f87 --- /dev/null +++ b/src/components/DescriptionList/demo/vertical.md @@ -0,0 +1,35 @@ +--- +order: 1 +title: Vertical +--- + +垂直布局。 + +````jsx +import { DescriptionList } from 'ant-design-pro'; + +const { Description } = DescriptionList; + +ReactDOM.render( + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + + A free, open source, cross-platform, + graphical web browser developed by the + Mozilla Corporation and hundreds of + volunteers. + + +, mountNode); +```` diff --git a/src/components/DescriptionList/index.js b/src/components/DescriptionList/index.js new file mode 100644 index 00000000..357f479f --- /dev/null +++ b/src/components/DescriptionList/index.js @@ -0,0 +1,5 @@ +import DescriptionList from './DescriptionList'; +import Description from './Description'; + +DescriptionList.Description = Description; +export default DescriptionList; diff --git a/src/components/DescriptionList/index.less b/src/components/DescriptionList/index.less new file mode 100644 index 00000000..f7dc3203 --- /dev/null +++ b/src/components/DescriptionList/index.less @@ -0,0 +1,50 @@ +@import "~antd/lib/style/themes/default.less"; + +.descriptionList { + // offset the padding-bottom of last row + :global { + .ant-row { + margin-bottom: -16px; + overflow: hidden; + } + } + + .title { + color: @heading-color; + font-weight: 600; + margin-bottom: 16px; + } + + .term { + padding-bottom: 16px; + margin-right: 8px; + color: @heading-color; + white-space: nowrap; + display: table-cell; + + &:after { + content: ":"; + margin: 0 8px 0 2px; + position: relative; + top: -.5px; + } + } + + .detail { + padding-bottom: 16px; + color: @text-color; + display: table-cell; + } + + &.vertical { + + .term { + padding-bottom: 8px; + display: block; + } + + .detail { + display: block; + } + } +} diff --git a/src/components/DescriptionList/index.md b/src/components/DescriptionList/index.md new file mode 100644 index 00000000..7d63e2ad --- /dev/null +++ b/src/components/DescriptionList/index.md @@ -0,0 +1,29 @@ +--- +category: Components +type: General +title: DescriptionList +subtitle: 描述列表 +cols: 1 +--- + +描述列表用来展示一系列文本信息。 + +## API + +### DescriptionList + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| layout | 布局方式 | Enum{'horizontal', 'vertical'} | 'horizontal' | +| col | 指定信息分几列展示 | number(0 < col <= 4) | 3 | +| title | 列表标题 | ReactNode | - | +| gutter | 列表项间距,单位为 `px` | number | 32 | + +### DescriptionList.Description + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| term | 列表项标题 | ReactNode | - | + + + diff --git a/src/components/DescriptionList/responsive.js b/src/components/DescriptionList/responsive.js new file mode 100644 index 00000000..a5aa73f7 --- /dev/null +++ b/src/components/DescriptionList/responsive.js @@ -0,0 +1,6 @@ +export default { + 1: { xs: 24 }, + 2: { xs: 24, sm: 12 }, + 3: { xs: 24, sm: 12, md: 8 }, + 4: { xs: 24, sm: 12, md: 6 }, +}; diff --git a/src/components/EditableLinkGroup/index.js b/src/components/EditableLinkGroup/index.js new file mode 100644 index 00000000..68c0c42f --- /dev/null +++ b/src/components/EditableLinkGroup/index.js @@ -0,0 +1,46 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'dva/router'; +import { Button, Icon } from 'antd'; +import styles from './index.less'; + +// TODO: 添加逻辑 + +class EditableLinkGroup extends PureComponent { + static defaultProps = { + links: [], + onAdd: () => { + }, + } + state = { + links: this.props.links, + }; + + handleOnClick() { + const { onAdd } = this.props; + onAdd(); + } + + render() { + const { links } = this.state; + return ( +
    + { + links.map(link => {link.title}) + } + { + + } +
    + ); + } +} + +EditableLinkGroup.propTypes = { + links: PropTypes.array, + onAdd: PropTypes.func, +}; + +export default EditableLinkGroup; diff --git a/src/components/EditableLinkGroup/index.less b/src/components/EditableLinkGroup/index.less new file mode 100644 index 00000000..16a3e18a --- /dev/null +++ b/src/components/EditableLinkGroup/index.less @@ -0,0 +1,29 @@ +@import "~antd/lib/style/themes/default.less"; + +.linkGroup { + padding: 20px 0 8px 24px; + font-size: 0; + & > a { + color: @text-color; + display: inline-block; + font-size: @font-size-base; + margin-bottom: 13px; + margin-right: 32px; + &:hover { + color: @primary-color; + } + } + & > button { + border-color: @primary-color; + color: @primary-color; + i { + position: relative; + top: -1px; + } + span { + margin-left: 0 !important; + position: relative; + top: -1px; + } + } +} diff --git a/src/components/Exception/demo/403.md b/src/components/Exception/demo/403.md new file mode 100644 index 00000000..9cafa41c --- /dev/null +++ b/src/components/Exception/demo/403.md @@ -0,0 +1,21 @@ +--- +order: 2 +title: 403 +--- + +403 页面,配合自定义操作。 + +````jsx +import { Exception } from 'ant-design-pro'; +import { Button } from 'antd'; + +const actions = ( +
    + + +
    +); +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/demo/404.md b/src/components/Exception/demo/404.md new file mode 100644 index 00000000..cd693592 --- /dev/null +++ b/src/components/Exception/demo/404.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: 404 +--- + +404 页面。 + +````jsx +import { Exception } from 'ant-design-pro'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/demo/500.md b/src/components/Exception/demo/500.md new file mode 100644 index 00000000..b94e10ef --- /dev/null +++ b/src/components/Exception/demo/500.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: 500 +--- + +500 页面。 + +````jsx +import { Exception } from 'ant-design-pro'; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Exception/index.js b/src/components/Exception/index.js new file mode 100644 index 00000000..39f903f8 --- /dev/null +++ b/src/components/Exception/index.js @@ -0,0 +1,26 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Button } from 'antd'; +import { Link } from 'react-router'; +import config from './typeConfig'; +import styles from './index.less'; + + +export default ({ className, type, title, desc, img, actions }) => { + const pageType = type in config ? type : '404'; + const clsString = classNames(styles.exception, className); + return ( +
    +
    + +
    +
    +

    {title || config[pageType].title}

    +
    {desc || config[pageType].desc}
    +
    + {actions || } +
    +
    +
    + ); +}; diff --git a/src/components/Exception/index.less b/src/components/Exception/index.less new file mode 100644 index 00000000..37b639f9 --- /dev/null +++ b/src/components/Exception/index.less @@ -0,0 +1,37 @@ +@import "~antd/lib/style/themes/default.less"; + +.exception { + display: flex; + align-items: center; + height: 100%; + + .imgBlock { + flex: 0 0 62.5%; + width: 62.5%; + text-align: right; + padding-right: 152px; + } + + .content { + flex: auto; + + h1 { + color: @text-color; + font-size: 68px; + line-height: 68px; + margin-bottom: 16px; + } + + .desc { + color: @text-color-secondary; + font-size: 20px; + margin-bottom: 16px; + } + + .actions { + button:not(:last-child) { + margin-right: 8px; + } + } + } +} diff --git a/src/components/Exception/index.md b/src/components/Exception/index.md new file mode 100644 index 00000000..9ac52653 --- /dev/null +++ b/src/components/Exception/index.md @@ -0,0 +1,19 @@ +--- +category: Components +type: General +title: Exception +subtitle: 异常 +cols: 1 +--- + +异常页用于对页面特定的异常状态进行反馈。通常,它包含对错误状态的阐述,并向用户提供建议或操作,避免用户感到迷失和困惑。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|-------------|------------------------------------------|-------------|-------| +| type | 页面类型,若配置,则自带对应类型默认的 `title`,`desc`,`img`,此默认设置可以被 `title`,`desc`,`img` 覆盖 | Enum {'403', '404', '500'} | - | +| title | 标题 | ReactNode | - | +| desc | 补充描述 | ReactNode | - | +| img | 背景图片地址 | string | - | +| actions | 建议操作,配置此属性时默认的『返回首页』按钮不生效 | ReactNode | - | diff --git a/src/components/Exception/typeConfig.js b/src/components/Exception/typeConfig.js new file mode 100644 index 00000000..a1f018c8 --- /dev/null +++ b/src/components/Exception/typeConfig.js @@ -0,0 +1,19 @@ +const config = { + 403: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/byTGXmzwJVwgotvxHQsU.svg', + title: '403', + desc: '对不起,你没有权限', + }, + 404: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/GdXXOjtMMzaPfCziUVYt.svg', + title: '404', + desc: '你要找的页面不存在', + }, + 500: { + img: 'https://gw.alipayobjects.com/zos/rmsportal/OpTUNDbQGfEWLubSrJap.svg', + title: '500', + desc: '服务器错误,我们正在维修', + }, +}; + +export default config; diff --git a/src/components/FooterToolbar/index.js b/src/components/FooterToolbar/index.js new file mode 100644 index 00000000..81a8257f --- /dev/null +++ b/src/components/FooterToolbar/index.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default class FooterToolbar extends Component { + static contextTypes = { + layoutCollapsed: PropTypes.bool, + }; + state = { + width: '', + }; + componentDidMount() { + this.syncWidth(); + } + componentWillReceiveProps() { + this.syncWidth(); + } + syncWidth() { + const sider = document.querySelectorAll('.ant-layout-sider')[0]; + if (sider) { + this.setState({ + width: `calc(100% - ${sider.style.width})`, + }); + } + } + render() { + const { children, style, className, extra, ...restProps } = this.props; + return ( +
    +
    {extra}
    +
    {children}
    +
    + ); + } +} diff --git a/src/components/FooterToolbar/index.less b/src/components/FooterToolbar/index.less new file mode 100644 index 00000000..b544c1a2 --- /dev/null +++ b/src/components/FooterToolbar/index.less @@ -0,0 +1,32 @@ +@import "~antd/lib/style/themes/default.less"; + +.toolbar { + position: fixed; + width: 100%; + bottom: 0; + right: 0; + height: 56px; + line-height: 56px; + box-shadow: @shadow-1-up; + background: #fff; + padding: 0 28px; + transition: all .3s; + + &:after { + content: ""; + display: block; + clear: both; + } + + .left { + float: left; + } + + .right { + float: right; + } + + button + button { + margin-left: 8px; + } +} diff --git a/src/components/FooterToolbar/index.md b/src/components/FooterToolbar/index.md new file mode 100644 index 00000000..b202bd1e --- /dev/null +++ b/src/components/FooterToolbar/index.md @@ -0,0 +1,9 @@ +--- +category: Components +type: General +title: FooterToolbar +subtitle: 底部固定工具栏 +cols: 1 +--- + +## API diff --git a/src/components/GlobalFooter/demo/basic.md b/src/components/GlobalFooter/demo/basic.md new file mode 100644 index 00000000..2f0d82fe --- /dev/null +++ b/src/components/GlobalFooter/demo/basic.md @@ -0,0 +1,29 @@ +--- +order: 0 +title: Basic +--- + +基本页脚。 + +````jsx +import { GlobalFooter } from 'ant-design-pro'; +import { Icon } from 'antd'; + +const links = [{ + title: '帮助', + href: '', +}, { + title: '隐私', + href: '', +}, { + title: '条款', + href: '', + blankTarget: true, +}]; + +const copyright =
    Copyright 2017 蚂蚁金服体验技术部出品
    ; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/GlobalFooter/index.js b/src/components/GlobalFooter/index.js new file mode 100644 index 00000000..ed668319 --- /dev/null +++ b/src/components/GlobalFooter/index.js @@ -0,0 +1,18 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default ({ className, links, copyright }) => { + const clsString = classNames(styles.globalFooter, className); + return ( +
    + { + links && +
    + {links.map(link => {link.title})} +
    + } + {copyright &&
    {copyright}
    } +
    + ); +}; diff --git a/src/components/GlobalFooter/index.less b/src/components/GlobalFooter/index.less new file mode 100644 index 00000000..90461690 --- /dev/null +++ b/src/components/GlobalFooter/index.less @@ -0,0 +1,23 @@ +@import "~antd/lib/style/themes/default.less"; + +.globalFooter { + padding: 32px 28px 16px; + text-align: center; + + .links { + margin-bottom: 8px; + + a { + color: @text-color-secondary; + + &:not(:last-child) { + margin-right: 40px; + } + } + } + + .copyright { + color: @text-color-secondary; + font-size: @font-size-base; + } +} diff --git a/src/components/GlobalFooter/index.md b/src/components/GlobalFooter/index.md new file mode 100644 index 00000000..6f3e43a1 --- /dev/null +++ b/src/components/GlobalFooter/index.md @@ -0,0 +1,16 @@ +--- +category: Components +type: General +title: GlobalFooter +subtitle: 全局页脚 +cols: 1 +--- + +页脚属于全局导航的一部分,作为对顶部导航的补充,通过传递数据控制展示内容。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| links | 链接数据 | array<{ title: ReactNode, href: string, blankTarget?: boolean }> | - | +| copyright | 版权信息 | ReactNode | - | diff --git a/src/components/HeaderSearch/index.js b/src/components/HeaderSearch/index.js new file mode 100644 index 00000000..de6c44fd --- /dev/null +++ b/src/components/HeaderSearch/index.js @@ -0,0 +1,65 @@ +import React, { PureComponent } from 'react'; +import { Input, Icon, AutoComplete } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default class HeaderSearch extends PureComponent { + static defaultProps = { + defaultActiveFirstOption: false, + }; + state = { + searchMode: false, + value: '', + }; + componentWillUnmount() { + clearTimeout(this.timeout); + } + onKeyDown = (e) => { + if (e.key === 'Enter') { + this.timeout = setTimeout(() => { + this.props.onPressEnter(this.state.value); // Fix duplicate onPressEnter + }, 0); + } + } + onChange = (value) => { + this.setState({ value }); + } + enterSearchMode = () => { + this.setState({ searchMode: true }, () => { + if (this.state.searchMode) { + this.input.refs.input.focus(); + } + }); + } + leaveSearchMode = () => { + this.setState({ + searchMode: false, + value: '', + }); + } + render() { + const { className, placeholder, ...restProps } = this.props; + const inputClass = classNames(styles.input, { + [styles.show]: this.state.searchMode, + }); + return ( + + + + { this.input = node; }} + onKeyDown={this.onKeyDown} + onBlur={this.leaveSearchMode} + /> + + + ); + } +} diff --git a/src/components/HeaderSearch/index.less b/src/components/HeaderSearch/index.less new file mode 100644 index 00000000..11538746 --- /dev/null +++ b/src/components/HeaderSearch/index.less @@ -0,0 +1,27 @@ +.input { + transition: all .3s; + width: 0; + background: transparent; + border-radius: 0; + :global(.ant-select-selection) { + background: transparent; + } + input { + border: 0; + padding-left: 0; + padding-right: 0; + color: #fff; + &::placeholder { + color: rgba(255, 255, 255, .5); + } + } + &, + &:hover, + &:focus { + border-bottom: 1px solid #fff; + } + &.show { + width: 210px; + margin-left: 8px; + } +} diff --git a/src/components/MapChart/index.js b/src/components/MapChart/index.js new file mode 100644 index 00000000..190724ec --- /dev/null +++ b/src/components/MapChart/index.js @@ -0,0 +1,32 @@ +import React, { Component } from 'react'; +import { Tooltip } from 'antd'; + +import styles from './index.less'; + +/* eslint no-return-assign: 0 */ +class MapChart extends Component { + getRect() { + // 0.4657 = 708 / 1520 (img origin size) + const width = this.root.offsetWidth; + const height = width * 0.4657; + return { + width, + height, + }; + } + + render() { + return ( +
    (this.root = n)}> + +
    (this.root = n)}> + map +
    (this.node = n)} /> +
    + +
    + ); + } +} + +export default MapChart; diff --git a/src/components/MapChart/index.less b/src/components/MapChart/index.less new file mode 100644 index 00000000..11bb7cb5 --- /dev/null +++ b/src/components/MapChart/index.less @@ -0,0 +1,10 @@ +.mapChart { + background-color: #fff; + position: relative; + .canvas { + width: 100%; + & > img { + width: 100%; + } + } +} diff --git a/src/components/NoticeIcon/NoticeList.js b/src/components/NoticeIcon/NoticeList.js new file mode 100644 index 00000000..1d09b3ed --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { Avatar, Icon } from 'antd'; +import classNames from 'classnames'; +import styles from './NoticeList.less'; + +export default function NoticeList({ data = [], onClick, onClear, title, locale }) { + if (data.length === 0) { + return ( +
    + + {locale.emptyText} +
    + ); + } + return ( +
    +
      + {data.map((item, i) => { + const itemCls = classNames(styles.item, { + [styles.read]: item.read, + }); + return ( +
    • onClick(item)}> +
      + {item.avatar ? : null} +
      +

      {item.title}

      +
      + {item.description} +
      +
      {item.datetime}
      +
      {item.extra}
      +
      +
      +
    • + ); + })} +
    +
    + {locale.clear}{title} +
    +
    + ); +} diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less new file mode 100644 index 00000000..1a7b0452 --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.less @@ -0,0 +1,89 @@ +@import "~antd/lib/style/themes/default.less"; + +.list { + max-height: 400px; + overflow: auto; + .item { + transition: all .3s; + overflow: hidden; + cursor: pointer; + + .wrapper { + margin: 0 32px; + padding: 12px 0; + border-bottom: 1px solid @border-color-split; + } + &.read { + opacity: .4; + } + &:last-child .wrapper { + border-bottom: 0; + } + &:hover { + background: @primary-1; + } + .content { + position: relative; + overflow: hidden; + } + .avatar { + margin-right: 16px; + float: left; + margin-top: 4px; + background: #fff; + } + .title { + font-weight: normal; + color: @text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .description { + color: @text-color-secondary; + font-size: 12px; + margin-top: 8px; + } + .datetime { + color: @text-color-secondary; + font-size: 12px; + margin-top: 4px; + } + .extra { + position: absolute; + right: 0; + top: 0; + color: @text-color-secondary; + font-size: 12px; + } + } +} + +.notFound { + text-align: center; + height: 120px; + line-height: 120px; + font-size: 14px; + color: @text-color-secondary; + > i { + font-size: 16px; + margin-right: 8px; + vertical-align: middle; + margin-top: -1px; + } +} + +.clear { + height: 46px; + line-height: 46px; + text-align: center; + color: @text-color-secondary; + border-radius: 0 0 @border-radius-base @border-radius-base; + border-top: 1px solid @border-color-split; + transition: all .3s; + cursor: pointer; + + &:hover { + color: @text-color; + } +} diff --git a/src/components/NoticeIcon/demo/basic.md b/src/components/NoticeIcon/demo/basic.md new file mode 100644 index 00000000..c61a91f7 --- /dev/null +++ b/src/components/NoticeIcon/demo/basic.md @@ -0,0 +1,12 @@ +--- +order: 1 +title: 通知图标 +--- + +通常用在全局导航上。 + +````jsx +import { NoticeIcon } from 'ant-design-pro'; + +ReactDOM.render(, mountNode); +```` diff --git a/src/components/NoticeIcon/demo/popover.md b/src/components/NoticeIcon/demo/popover.md new file mode 100644 index 00000000..071b2eac --- /dev/null +++ b/src/components/NoticeIcon/demo/popover.md @@ -0,0 +1,41 @@ +--- +order: 2 +title: 带浮层卡片 +--- + +点击展开通知卡片,展现多种类型的通知。 + +````jsx +import { NoticeIcon } from 'ant-design-pro'; +import moment from 'moment'; + +const data = [{ + key: '1', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '曲丽丽 评论了你', + description: '描述信息描述信息描述信息', + datetime: moment('2017-08-07').fromNow(), +}, { + key: '2', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '朱偏右 回复了你', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: moment('2017-08-07').fromNow(), +}, { + key: '3', + avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', + title: '标题', + description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', + datetime: moment('2017-08-07').fromNow(), +}]; + +ReactDOM.render( +
    + + + + + +
    +, mountNode); +```` diff --git a/src/components/NoticeIcon/index.js b/src/components/NoticeIcon/index.js new file mode 100644 index 00000000..63b2f8d5 --- /dev/null +++ b/src/components/NoticeIcon/index.js @@ -0,0 +1,93 @@ +import React, { PureComponent } from 'react'; +import { Popover, Icon, Tabs, Badge, Spin } from 'antd'; +import classNames from 'classnames'; +import List from './NoticeList'; +import styles from './index.less'; + +const { TabPane } = Tabs; + +export default class NoticeIcon extends PureComponent { + static defaultProps = { + onItemClick: () => {}, + onPopupVisibleChange: () => {}, + onTabChange: () => {}, + onClear: () => {}, + loading: false, + locale: { + emptyText: '暂无数据', + clear: '清空', + }, + }; + static Tab = TabPane; + constructor(props) { + super(props); + this.state = {}; + if (props.children && props.children[0]) { + this.state.tabType = props.children[0].props.title; + } + } + onItemClick = (item, tabProps) => { + const { onItemClick } = this.props; + onItemClick(item, tabProps); + } + onTabChange = (tabType) => { + this.setState({ tabType }); + this.props.onTabChange(tabType); + } + getNotificationBox() { + const { children, loading, locale } = this.props; + if (!children) { + return null; + } + const panes = children.map((child) => { + const title = child.props.list && child.props.list.length > 0 + ? `${child.props.title} (${child.props.list.length})` : child.props.title; + return ( + + this.onItemClick(item, child.props)} + onClear={() => this.props.onClear(child.props.title)} + title={child.props.title} + locale={locale} + /> + + ); + }); + return ( + + + {panes} + + + ); + } + render() { + const { className, count, popupAlign } = this.props; + const noticeButtonClass = classNames(className, styles.noticeButton); + const notificationBox = this.getNotificationBox(); + const trigger = ( + + + + + + ); + if (!notificationBox) { + return trigger; + } + return ( + + {trigger} + + ); + } +} diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less new file mode 100644 index 00000000..5e2ff1b3 --- /dev/null +++ b/src/components/NoticeIcon/index.less @@ -0,0 +1,36 @@ +@import "~antd/lib/style/themes/default.less"; + +.popover { + width: 336px; + :global(.ant-popover-inner-content) { + padding: 0; + } +} + +.noticeButton { + cursor: pointer; + display: inline-block; + transition: all .3s; +} + +.icon { + font-size: 20px; +} + +.tabs { + :global { + .ant-tabs-nav-container { + font-size: 14px; + } + .ant-tabs-nav-scroll { + text-align: center; + } + .ant-tabs-bar { + margin-bottom: 0; + } + .ant-tabs-nav .ant-tabs-tab { + padding-top: 16px; + padding-bottom: 16px; + } + } +} diff --git a/src/components/NoticeIcon/index.md b/src/components/NoticeIcon/index.md new file mode 100644 index 00000000..f0b1b120 --- /dev/null +++ b/src/components/NoticeIcon/index.md @@ -0,0 +1,39 @@ +--- +category: Components +type: General +title: NoticeIcon +subtitle: 通知菜单 +cols: 1 +--- + +用在顶部导航上,作为整个产品统一的通知中心。 + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +count | 图标上的消息总数 | number | - +loading | 弹出卡片加载状态 | boolean | false +onClear | 点击清空按钮的回调 | function(tabTitle) | - +onItemClick | 点击列表项的回调 | function(item, tabProps) | - +onTabChange | 切换页签的回调 | function(tabTitle) | - +popupAlign | 弹出卡片的位置配置 | Object [alignConfig](https://github.com/yiminghe/dom-align#alignconfig-object-details) | - +onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | - +locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }` + +### NoticeIcon.Tab + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +title | 消息分类的页签标题 | string | - +data | 列表数据,格式参照下表 | Array | `[]` + +### Tab data + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +avatar | 头像图片链接 | string | - +title | 标题 | ReactNode | - +description | 描述信息 | ReactNode | - +datetime | 时间戳 | ReactNode | - +extra | 额外信息,在列表项右上角 | ReactNode | - diff --git a/src/components/PageHeader/demo/image.md b/src/components/PageHeader/demo/image.md new file mode 100644 index 00000000..b5d0dc95 --- /dev/null +++ b/src/components/PageHeader/demo/image.md @@ -0,0 +1,71 @@ +--- +order: 2 +title: With Image +--- + +带图片的页头。 + +````jsx +import { PageHeader } from 'ant-design-pro'; + +const content = ( +
    +

    段落示意:蚂蚁金服务设计平台-design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态, +提供跨越设计与开发的体验解决方案。

    + +
    +); + +const extra = ( +
    + +
    +); + +const breadcrumbList = [{ + title: '一级菜单', + href: '/', +}, { + title: '二级菜单', + href: '/', +}, { + title: '三级菜单', +}]; + +ReactDOM.render( +
    + +
    +, mountNode); +```` + + diff --git a/src/components/PageHeader/demo/simple.md b/src/components/PageHeader/demo/simple.md new file mode 100644 index 00000000..bff44117 --- /dev/null +++ b/src/components/PageHeader/demo/simple.md @@ -0,0 +1,26 @@ +--- +order: 3 +title: Simple +--- + +简单的页头。 + +````jsx +import { PageHeader } from 'ant-design-pro'; + +const breadcrumbList = [{ + title: '一级菜单', + href: '/', +}, { + title: '二级菜单', + href: '/', +}, { + title: '三级菜单', +}]; + +ReactDOM.render( +
    + +
    +, mountNode); +```` diff --git a/src/components/PageHeader/demo/standard.md b/src/components/PageHeader/demo/standard.md new file mode 100644 index 00000000..e870e986 --- /dev/null +++ b/src/components/PageHeader/demo/standard.md @@ -0,0 +1,81 @@ +--- +order: 1 +title: Standard +--- + +标准页头。 + +````jsx +import { PageHeader } from 'ant-design-pro'; +import { Button, Menu, Dropdown, Icon, Row, Col } from 'antd'; + +const menu = ( + + 选项一 + 选项二 + 选项三 + +); + +const action = ( +
    + + + + + +
    +); + +const extra = ( + + +
    状态
    +
    待审批
    + + +
    订单金额
    +
    ¥ 568.08
    + +
    +); + +const breadcrumbList = [{ + title: '一级菜单', + href: '/', +}, { + title: '二级菜单', + href: '/', +}, { + title: '三级菜单', +}]; + +const tabList = [{ + key: 'detail', + tab: '详情', +}, { + key: 'rule', + tab: '规则', +}]; + +function onTabChange(key) { + console.log(key); +} + +ReactDOM.render( +
    + } + action={action} + content="DescriptionList 占位" + extraContent={extra} + breadcrumbList={breadcrumbList} + tabList={tabList} + onTabChange={onTabChange} + /> +
    +, mountNode); +```` diff --git a/src/components/PageHeader/demo/structure.md b/src/components/PageHeader/demo/structure.md new file mode 100644 index 00000000..0c7f81e6 --- /dev/null +++ b/src/components/PageHeader/demo/structure.md @@ -0,0 +1,67 @@ +--- +order: 0 +title: Structure +--- + +基本结构,可以形成多种组合。 + +````jsx +import { PageHeader } from 'ant-design-pro'; + +const breadcrumbList = [{ + title: '面包屑', +}]; + +const tabList = [{ + key: '1', + tab: '页签一', +}, { + key: '2', + tab: '页签二', +}, { + key: '3', + tab: '页签三', +}]; + +ReactDOM.render( +
    + Title
    } + logo={
    logo
    } + action={
    action
    } + content={
    content
    } + extraContent={
    extraContent
    } + breadcrumbList={breadcrumbList} + tabList={tabList} + /> +
    +, mountNode); +```` + + diff --git a/src/components/PageHeader/index.js b/src/components/PageHeader/index.js new file mode 100644 index 00000000..364b5562 --- /dev/null +++ b/src/components/PageHeader/index.js @@ -0,0 +1,98 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { Breadcrumb, Tabs } from 'antd'; +import { Link } from 'dva/router'; +import classNames from 'classnames'; +import styles from './index.less'; + +const TabPane = Tabs.TabPane; + +function itemRender(route, params, routes, paths) { + const last = routes.indexOf(route) === routes.length - 1; + return (last || !route.component) + ? {route.breadcrumbName} + : {route.breadcrumbName}; +} + +export default class PageHeader extends PureComponent { + static contextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + }; + onChange = (key) => { + if (this.props.onTabChange) { + this.props.onTabChange(key); + } + }; + getBreadcrumbProps = () => { + return { + routes: this.props.routes || this.context.routes, + params: this.props.params || this.context.params, + }; + }; + render() { + const { routes, params } = this.getBreadcrumbProps(); + const { title, logo, action, content, extraContent, + breadcrumbList, tabList, className } = this.props; + const clsString = classNames(styles.pageHeader, className); + let breadcrumb; + if (routes && params) { + breadcrumb = ( + route.breadcrumbName)} + params={params} + itemRender={itemRender} + /> + ); + } else if (breadcrumbList && breadcrumbList.length) { + breadcrumb = ( + + { + breadcrumbList.map(item => ( + + {item.href ? {item.title} : item.title} + ) + ) + } + + ); + } else { + breadcrumb = null; + } + + const tabDefaultValue = tabList && tabList.filter(item => item.default)[0]; + + return ( +
    + {breadcrumb} +
    + {logo &&
    {logo}
    } +
    +
    + {title &&

    {title}

    } + {action &&
    {action}
    } +
    +
    + {content &&
    {content}
    } + {extraContent &&
    {extraContent}
    } +
    +
    +
    + { + tabList && + tabList.length && + + { + tabList.map(item => ) + } + + } +
    + ); + } +} diff --git a/src/components/PageHeader/index.less b/src/components/PageHeader/index.less new file mode 100644 index 00000000..962479e5 --- /dev/null +++ b/src/components/PageHeader/index.less @@ -0,0 +1,95 @@ +@import "~antd/lib/style/themes/default.less"; + +.pageHeader { + background: @component-background; + padding: 18px 28px 0 36px; + border-bottom: @border-width-base @border-style-base @border-color-split; + + .detail { + display: flex; + } + + .row { + display: flex; + } + + .breadcrumb { + margin-bottom: 18px; + } + + .tabs { + margin: 0 0 -17px -8px; + + :global { + .ant-tabs-bar { + border-bottom: @border-width-base @border-style-base @border-color-split; + } + } + } + + .logo { + flex: 0 1 auto; + margin-right: 16px; + padding-top: 1px; + } + + .title { + font-size: 20px; + font-weight: 500; + color: @heading-color; + } + + .action { + margin-left: 56px; + min-width: 266px; + + button:not(:last-child) { + margin-right: 8px; + } + } + + .title, .action, .content, .extraContent, .main { + flex: auto; + } + + .title, .action { + margin-bottom: 16px; + } + + .logo, .content, .extraContent { + margin-bottom: 12px; + } + + .action, .extraContent { + text-align: right; + } + + .extraContent { + margin-left: 88px; + min-width: 242px; + } +} + +@media screen and (max-width: @screen-md) { + .pageHeader { + .extraContent { + margin-left: 44px; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeader { + .extraContent { + margin-left: 24px; + } + } +} + +@media screen and (max-width: @screen-xs) { + .pageHeader { + .extraContent { + margin-left: 8px; + } + } +} diff --git a/src/components/PageHeader/index.md b/src/components/PageHeader/index.md new file mode 100644 index 00000000..4a42c68f --- /dev/null +++ b/src/components/PageHeader/index.md @@ -0,0 +1,26 @@ +--- +category: Components +type: General +title: PageHeader +subtitle: 页头 +cols: 1 +--- + +页头用来声明页面的主题,包含了用户所关注的最重要的信息,使用户可以快速理解当前页面是什么以及它的功能。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| title | title 区域 | ReactNode | - | +| logo | logo区域 | ReactNode | - | +| action | 操作区,位于 title 行的行尾 | ReactNode | - | +| content | 内容区 | ReactNode | - | +| extraContent | 额外内容区,位于content的右侧 | ReactNode | - | +| routes | 面包屑相关属性,router 的路由栈信息 | object[] | - | +| params | 面包屑相关属性,路由的参数 | object | - | +| breadcrumbList | 面包屑数据,配置了 `routes` `params` 时此属性无效 | array<{title: ReactNode, href?: string}> | - | +| tabList | tab 标题列表 | array<{key: string, tab: ReactNode}> | - | +| onTabChange | 切换面板的回调 | (key) => void | - | + +> 面包屑的配置方式有两种,一是结合 `react-router`,通过配置 `routes` 及 `params` 实现,类似 [面包屑 Demo](https://ant.design/components/breadcrumb-cn/#components-breadcrumb-demo-router);二是直接配置 `breadcrumbList`。 你也可以将 `routes` 及 `params` 放到 context 中,`PageHeader` 组件会自动获取。 diff --git a/src/components/RadioText/index.js b/src/components/RadioText/index.js new file mode 100644 index 00000000..6d62dcdc --- /dev/null +++ b/src/components/RadioText/index.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { Radio } from 'antd'; + +import styles from './index.less'; + +const RadioButton = Radio.Button; + +export default props => (
    + +
    ); diff --git a/src/components/RadioText/index.less b/src/components/RadioText/index.less new file mode 100644 index 00000000..333cd16f --- /dev/null +++ b/src/components/RadioText/index.less @@ -0,0 +1,12 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.radioText { + display: inline; + :global { + .ant-radio-button-wrapper { + border: none; + padding: 0 12px; + } + } +} diff --git a/src/components/Result/demo/classic.md b/src/components/Result/demo/classic.md new file mode 100644 index 00000000..8fbcf185 --- /dev/null +++ b/src/components/Result/demo/classic.md @@ -0,0 +1,64 @@ +--- +order: 1 +title: Classic +--- + +典型结果页面。 + +````jsx +import { Result } from 'ant-design-pro'; +import { Button, Row, Col, Icon, Steps } from 'antd'; + +const Step = Steps.Step; + +const desc1 = ( +
    +
    曲丽丽
    +
    2016-12-12 12:32
    +
    +); + +const desc2 = ( +
    +
    周毛毛
    + +
    +); + +const extra = ( +
    +
    + 项目名称 +
    + + 项目 ID:23421 + 负责人:曲丽丽 + 生效时间:2016-12-12 ~ 2017-12-12 + + + + + + + +
    +); + +const actions = ( +
    + + + +
    +); + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Result/demo/error.md b/src/components/Result/demo/error.md new file mode 100644 index 00000000..1ab7ad8e --- /dev/null +++ b/src/components/Result/demo/error.md @@ -0,0 +1,39 @@ +--- +order: 2 +title: Failed +--- + +提交失败。 + +````jsx +import { Result } from 'ant-design-pro'; +import { Button, Icon } from 'antd'; + +const extra = ( +
    +
    + 您提交的内容有如下错误: +
    +
    + 您的账户已被冻结 + 立即解冻 +
    +
    + 您的账户还不具备申请资格 + 立即升级 +
    +
    +); + +const actions = ; + +ReactDOM.render( + +, mountNode); +```` diff --git a/src/components/Result/demo/structure.md b/src/components/Result/demo/structure.md new file mode 100644 index 00000000..13f2dab7 --- /dev/null +++ b/src/components/Result/demo/structure.md @@ -0,0 +1,20 @@ +--- +order: 0 +title: Structure +--- + +结构包含 `处理结果`,`补充信息` 以及 `操作建议` 三个部分,其中 `处理结果` 由 `提示图标`,`标题` 和 `结果描述` 组成。 + +````jsx +import { Result } from 'ant-design-pro'; + +ReactDOM.render( + 标题
    } + description={
    结果描述
    } + extra="其他补充信息,自带灰底效果" + actions={
    操作建议,一般放置按钮组
    } + /> +, mountNode); +```` diff --git a/src/components/Result/index.js b/src/components/Result/index.js new file mode 100644 index 00000000..4af5310f --- /dev/null +++ b/src/components/Result/index.js @@ -0,0 +1,21 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Icon } from 'antd'; +import styles from './index.less'; + +export default ({ className, type, title, description, extra, actions, ...restProps }) => { + const iconMap = { + error: , + success: , + }; + const clsString = classNames(styles.result, className); + return ( +
    +
    {iconMap[type]}
    +
    {title}
    + {description &&
    {description}
    } + {extra &&
    {extra}
    } + {actions &&
    {actions}
    } +
    + ); +}; diff --git a/src/components/Result/index.less b/src/components/Result/index.less new file mode 100644 index 00000000..4af83fa1 --- /dev/null +++ b/src/components/Result/index.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; + +.result { + text-align: center; + + .icon { + font-size: 72px; + line-height: 72px; + margin-bottom: 24px; + + & > .success { + color: @success-color; + } + + & > .error { + color: @error-color; + } + } + + .title { + font-size: 24px; + color: @heading-color; + font-weight: 500; + line-height: 32px; + margin-bottom: 16px; + } + + .description { + font-size: 14px; + color: @text-color-secondary; + margin-bottom: 24px; + } + + .extra { + background: rgba(245, 245, 245, 0.5); + padding: 24px 40px; + margin-bottom: 32px; + border-radius: @border-radius-sm; + text-align: left; + } + + .actions button:not(:last-child) { + margin-right: 8px; + } +} diff --git a/src/components/Result/index.md b/src/components/Result/index.md new file mode 100644 index 00000000..8e01c7f8 --- /dev/null +++ b/src/components/Result/index.md @@ -0,0 +1,19 @@ +--- +category: Components +type: General +title: Result +subtitle: 处理结果 +cols: 1 +--- + +结果页用于对用户进行的一系列任务处理结果进行反馈。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | +|----------|------------------------------------------|-------------|-------| +| type | 类型,不同类型自带对应的图标 | Enum {'success', 'error'} | - | +| title | 标题 | ReactNode | - | +| description | 结果描述 | ReactNode | - | +| extra | 补充信息,有默认的灰色背景 | ReactNode | - | +| actions | 操作建议,推荐放置跳转链接,按钮组等 | ReactNode | - | diff --git a/src/components/SearchInput/index.js b/src/components/SearchInput/index.js new file mode 100644 index 00000000..e5e4403c --- /dev/null +++ b/src/components/SearchInput/index.js @@ -0,0 +1,15 @@ +import React from 'react'; +import { Button, Input } from 'antd'; + +import styles from './index.less'; + +export default ({ onSearch = () => ({}), text = '搜索', ...reset }) => ( +
    + {text}} + /> +
    +); diff --git a/src/components/SearchInput/index.less b/src/components/SearchInput/index.less new file mode 100644 index 00000000..7d00f448 --- /dev/null +++ b/src/components/SearchInput/index.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.search { + display: inline-block; + :global { + .ant-input-group-addon { + border: none; + padding: 0; + } + .ant-input-group .ant-input { + width: 522px; + } + } + input { + border-right: none; + height: 40px; + line-height: 40px; + } + button { + border-radius: 0 @border-radius-base @border-radius-base 0; + width: 86px; + height: 40px; + } +} + +@media screen and (max-width: @screen-sm) { + .search { + :global { + .ant-input-group .ant-input { + width: 300px; + } + } + } +} + +@media screen and (max-width: @screen-xs) { + .search { + :global { + .ant-input-group .ant-input { + width: 200px; + } + } + } +} diff --git a/src/components/StandardFormRow/index.js b/src/components/StandardFormRow/index.js new file mode 100644 index 00000000..27ecd79b --- /dev/null +++ b/src/components/StandardFormRow/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import classNames from 'classnames'; +import styles from './index.less'; + +export default ({ title, children, last, block, grid, ...rest }) => { + const cls = classNames(styles.standardFormRow, { + [styles.standardFormRowBlock]: block, + [styles.standardFormRowLast]: last, + [styles.standardFormRowGrid]: grid, + }); + + return ( +
    + { + title &&
    + {title} +
    + } +
    + {children} +
    +
    + ); +}; diff --git a/src/components/StandardFormRow/index.less b/src/components/StandardFormRow/index.less new file mode 100644 index 00000000..9f11453d --- /dev/null +++ b/src/components/StandardFormRow/index.less @@ -0,0 +1,68 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.standardFormRow { + border-bottom: 1px dashed @border-color-split; + padding-bottom: 16px; + margin-bottom: 16px; + display: flex; + :global { + .ant-form-item { + margin-right: 24px; + } + .ant-form-item-label label { + color: @text-color; + margin-right: 16px; + } + } + .label { + color: @heading-color; + font-size: @font-size-base; + margin-right: 24px; + flex: 0 0 auto; + text-align: right; + & > span { + display: inline-block; + height: 32px; + line-height: 32px; + &:after { + content: ':'; + } + } + } + .content { + flex: 1 1 0; + :global { + .ant-form-item:last-child { + margin-right: 0; + } + } + } +} + +.standardFormRowLast { + border: none; + padding-bottom: 0; + margin-bottom: 0; +} + +.standardFormRowBlock { + :global { + .ant-form-item, + div.ant-form-item-control-wrapper { + display: block; + } + } +} + +.standardFormRowGrid { + :global { + .ant-form-item, + div.ant-form-item-control-wrapper { + display: block; + } + .ant-form-item-label { + float: left; + } + } +} diff --git a/src/components/StandardTable/index.js b/src/components/StandardTable/index.js new file mode 100644 index 00000000..4d6be6e8 --- /dev/null +++ b/src/components/StandardTable/index.js @@ -0,0 +1,149 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { Table, Alert, Badge } from 'antd'; +import styles from './index.less'; + +class StandardTable extends PureComponent { + state = { + selectedRowKeys: [], + selectedRows: [], + totalCallNo: 0, + loading: false, + }; + + componentWillReceiveProps(nextProps) { + // clean state + if (nextProps.selectedRows.length === 0) { + this.setState({ + selectedRows: [], + selectedRowKeys: [], + totalCallNo: 0, + }); + } + } + + handleRowSelectChange = (selectedRowKeys, selectedRows) => { + const totalCallNo = selectedRows.reduce((sum, val) => { + return sum + parseFloat(val.callNo, 10); + }, 0); + + if (this.props.onSelectRow) { + this.props.onSelectRow(selectedRows); + } + + this.setState({ selectedRowKeys, selectedRows, totalCallNo }); + } + + handleTableChange = (pagination, filters, sorter) => { + this.props.onChange(pagination, filters, sorter); + } + + cleanSelectedKeys = () => { + this.handleRowSelectChange([], []); + } + + render() { + const { selectedRowKeys, totalCallNo } = this.state; + const { data: { list, pagination }, loading } = this.props; + + const status = ['关闭', '运行中']; + + const columns = [ + { + title: '规则编号', + dataIndex: 'no', + }, + { + title: '描述', + dataIndex: 'description', + }, + { + title: '服务调用次数', + dataIndex: 'callNo', + sorter: true, + render: val => ( +

    + {val} 万 +

    + ), + }, + { + title: '状态', + dataIndex: 'status', + filters: [ + { + text: status[0], + value: 0, + }, + { + text: status[1], + value: 1, + }, + ], + render(val) { + if (val === 0) { + return ; + } else { + return ; + } + }, + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + sorter: true, + render: val => {moment(val).format('YYYY-MM-DD HH:mm:ss')}, + }, + { + title: '操作', + render: () => ( +

    + 配置 + + 订阅警报 +

    + ), + }, + ]; + + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + ...pagination, + }; + + const rowSelection = { + selectedRowKeys, + onChange: this.handleRowSelectChange, + }; + + return ( +
    +
    + + 已选择 {selectedRowKeys.length} 项   + 服务调用总计 {totalCallNo} 万 + 清空 +

    + )} + type="info" + showIcon + /> +
    + record.key} + rowSelection={rowSelection} + dataSource={list} + columns={columns} + pagination={paginationProps} + onChange={this.handleTableChange} + /> + + ); + } +} + +export default StandardTable; diff --git a/src/components/StandardTable/index.less b/src/components/StandardTable/index.less new file mode 100644 index 00000000..34ec184c --- /dev/null +++ b/src/components/StandardTable/index.less @@ -0,0 +1,22 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.standardTable { + :global { + .ant-table-pagination { + margin-bottom: 0; + } + } + + .tableAlert { + margin-bottom: 16px; + } + + .splitLine { + background: @border-color-split; + display: inline-block; + margin: 0 8px; + width: 1px; + height: 12px; + } +} diff --git a/src/components/TagCloud/index.js b/src/components/TagCloud/index.js new file mode 100644 index 00000000..0ade0e71 --- /dev/null +++ b/src/components/TagCloud/index.js @@ -0,0 +1,134 @@ +import React, { PureComponent } from 'react'; +import G2 from 'g2'; +import Cloud from 'g-cloud'; + +/* eslint no-underscore-dangle: 0 */ +/* eslint no-param-reassign: 0 */ +/* eslint no-return-assign: 0 */ + +class TagCloud extends PureComponent { + componentDidMount() { + this.initTagCloud(); + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (this.props.data !== nextProps.data) { + this.renderChart(nextProps.data); + } + } + + initTagCloud = () => { + const Util = G2.Util; + const Shape = G2.Shape; + + function getTextAttrs(cfg) { + const textAttrs = Util.mix(true, {}, { + fillOpacity: cfg.opacity, + fontSize: cfg.size, + rotate: 0, // cfg.origin._origin.rotate, + text: cfg.origin._origin.text, + textAlign: 'center', + fill: cfg.color, + textBaseline: 'Alphabetic', + }, cfg.style); + return textAttrs; + } + + // 给point注册一个词云的shape + Shape.registShape('point', 'cloud', { + drawShape(cfg, container) { + cfg.points = this.parsePoints(cfg.points); + const attrs = getTextAttrs(cfg); + const shape = container.addShape('text', { + attrs: Util.mix(attrs, { + x: cfg.points[0].x, + y: cfg.points[0].y, + }), + }); + return shape; + }, + }); + } + + renderChart(data) { + if (!data || data.length < 1) { + return; + } + + const { height } = this.props; + let width = 0; + if (this.root) { + width = this.root.offsetWidth; + } + + // clean + if (this.node) { + this.node.innerHTML = ''; + } + + data.sort((a, b) => b.value - a.value); + + const max = data[0].value; + const min = data[data.length - 1].value; + + // 构造一个词云布局对象 + const layout = new Cloud({ + words: data, + width, + height, + + // 设定文字大小配置函数(默认为12-40px的随机大小) + size: words => (((words.value - min) / (max - min)) * 10) + 12, + + // 设定文字内容 + text: words => words.name, + }); + + // 执行词云布局函数,并在回调函数中调用G2对结果进行绘制 + layout.exec((texts) => { + const chart = new G2.Chart({ + container: this.node, + width, + height, + plotCfg: { + margin: 0, + }, + }); + + chart.legend(false); + chart.axis(false); + chart.tooltip(false); + + chart.source(texts); + + // 将词云坐标系调整为G2的坐标系 + chart.coord().reflect(); + + chart + .point() + .position('x*y') + .color('text') + .size('size', size => size) + .shape('cloud') + .style({ + fontStyle: texts[0].style, + fontFamily: texts[0].font, + fontWeight: texts[0].weight, + }); + + chart.render(); + }); + } + + render() { + return ( +
    (this.root = n)} style={{ width: '100%' }}> +
    (this.node = n)} /> +
    + ); + } +} + +export default TagCloud; + diff --git a/src/components/TagSelect/index.js b/src/components/TagSelect/index.js new file mode 100644 index 00000000..e0969eba --- /dev/null +++ b/src/components/TagSelect/index.js @@ -0,0 +1,164 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import { Tag, Icon } from 'antd'; + +import styles from './index.less'; + +const CheckableTag = Tag.CheckableTag; + +const TagSelectOption = ({ children, checked, onChange, value }) => ( + onChange(value, state)} + > + {children} + +); +TagSelectOption.defaultProps = { + displayName: 'TagSelectOption', +}; + +const TagSelectExpand = ({ children }) => ( +
    {children}
    +); +TagSelectExpand.defaultProps = { + displayName: 'TagSelectExpand', +}; + +class TagSelect extends PureComponent { + static defaultProps = { + initialValue: [], + }; + + state = { + checkedAll: false, + expand: false, + checkedTags: this.props.initialValue || [], + }; + + onSelectAll = (checked) => { + const { onChange } = this.props; + let checkedTags = []; + let expand = this.state.expand; + + if (checked) { + const tags = this.getAllTags(); + checkedTags = tags.list; + expand = tags.expand; + } + + this.setState({ + checkedAll: checked, + checkedTags, + expand, + }); + + if (onChange) { + onChange(checkedTags); + } + } + + getAllTags() { + let expand = this.state.expand; + const { children } = this.props; + + let checkedTags = children.filter(child => child.props.displayName === 'TagSelectOption').map(child => child.props.value); + const expandChild = children.filter(child => child.props.displayName === 'TagSelectExpand')[0]; + if (expandChild) { + checkedTags = checkedTags.concat( + expandChild.props.children.map(child => child.props.value) + ); + expand = true; + } + return { + list: checkedTags, + expand, + }; + } + + handleTagChange = (value, checked) => { + const { onChange } = this.props; + const { checkedTags } = this.state; + + const index = checkedTags.indexOf(value); + if (checked && index === -1) { + checkedTags.push(value); + } else if (!checked && index > -1) { + checkedTags.splice(index, 1); + } + + const tags = this.getAllTags(); + + let checkedAll = false; + if (tags.list.length === checkedTags.length) { + checkedAll = true; + } + + this.setState({ + checkedAll, + checkedTags, + }); + + if (onChange) { + onChange(checkedTags); + } + } + + handleExpand = () => { + this.setState({ + expand: !this.state.expand, + }); + } + + render() { + const { checkedTags, checkedAll, expand } = this.state; + const { children } = this.props; + + const expandNode = children.filter(child => child.props.displayName === 'TagSelectExpand')[0]; + + const cls = classNames(styles.tagSelect, { + [styles.expandTag]: expandNode, + }); + + return ( +
    + + 全部 + + { + children.filter(child => child.props.displayName === 'TagSelectOption').map(child => React.cloneElement(child, { + key: `tag-select-${child.props.value}`, + checked: checkedTags.indexOf(child.props.value) > -1, + onChange: this.handleTagChange, + })) + } + { + expandNode && + { expand ? '收起' : '展开'} + + } + { + expandNode &&
    + { + expandNode.props.children.map(child => React.cloneElement(child, { + key: `tag-select-${child.props.value}`, + checked: checkedTags.indexOf(child.props.value) > -1, + onChange: this.handleTagChange, + })) + } +
    + } +
    + ); + } +} + +TagSelect.Option = TagSelectOption; +TagSelect.Expand = TagSelectExpand; + +export default TagSelect; diff --git a/src/components/TagSelect/index.less b/src/components/TagSelect/index.less new file mode 100644 index 00000000..c669684b --- /dev/null +++ b/src/components/TagSelect/index.less @@ -0,0 +1,26 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.tagSelect { + user-select: none; + margin-left: -8px; + position: relative; + .expand { + transition: all 0.32s ease; + overflow: hidden; + max-height: 100px; + } + .fold { + .expand(); + max-height: 0; + } + .trigger { + position: absolute; + top: 0; + right: 0; + } +} +.expandTag { + padding-right: 50px; +} + diff --git a/src/components/TimelineChart/index.js b/src/components/TimelineChart/index.js new file mode 100644 index 00000000..c5b185cd --- /dev/null +++ b/src/components/TimelineChart/index.js @@ -0,0 +1,104 @@ +import React, { Component } from 'react'; +import G2 from 'g2'; +import Slider from 'g2-plugin-slider'; +import styles from './index.less'; + +class TimelineChart extends Component { + componentDidMount() { + this.renderChart(this.props.data); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.data !== this.props.data) { + this.renderChart(nextProps.data); + } + } + + sliderId = `timeline-chart-slider-${Math.random() * 1000}` + + handleRef = (n) => { + this.node = n; + } + + renderChart(data) { + const { height = 400, margin = [60, 40, 40, 40], titleMap } = this.props; + + if (!data || (data && data.length < 1)) { + return; + } + + // clean + if (this.sliderId) { + document.getElementById(this.sliderId).innerHTML = ''; + } + this.node.innerHTML = ''; + + const chart = new G2.Chart({ + container: this.node, + forceFit: true, + height, + plotCfg: { + margin, + }, + }); + + chart.axis('x', { + title: false, + }); + chart.axis('y1', { + title: false, + }); + chart.axis('y2', false); + + chart.legend({ + mode: false, + position: 'top', + }); + + chart.source(data, { + x: { + type: 'timeCat', + tickCount: 16, + mask: 'HH:MM', + range: [0, 1], + }, + y1: { + alias: titleMap.y1, + min: 0, + }, + y2: { + alias: titleMap.y2, + min: 0, + }, + }); + + chart.line().position('x*y1').color('#4FAAEB'); + chart.line().position('x*y2').color('#9AD681'); + + /* eslint new-cap:0 */ + const slider = new Slider({ + domId: this.sliderId, + height: 26, + xDim: 'x', + yDim: 'y1', + charts: [chart], + }); + slider.render(); + } + + render() { + const { height, title } = this.props; + + return ( +
    +
    + { title &&

    {title}

    } +
    +
    +
    +
    + ); + } +} + +export default TimelineChart; diff --git a/src/components/TimelineChart/index.less b/src/components/TimelineChart/index.less new file mode 100644 index 00000000..17519756 --- /dev/null +++ b/src/components/TimelineChart/index.less @@ -0,0 +1,3 @@ +.timelineChart { + background: #fff; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000..1a5f9a5b --- /dev/null +++ b/src/index.js @@ -0,0 +1,25 @@ +import dva from 'dva'; +// import { browserHistory } from 'dva/router'; +import 'moment/locale/zh-cn'; +import models from './models'; + +import './index.less'; + +// 1. Initialize +const app = dva({ + // history: browserHistory, +}); + +// 2. Plugins +// app.use({}); + +// 3. Model +models.forEach((m) => { + app.model(m); +}); + +// 4. Router +app.router(require('./router')); + +// 5. Start +app.start('#root'); diff --git a/src/index.less b/src/index.less new file mode 100644 index 00000000..06d5f3e5 --- /dev/null +++ b/src/index.less @@ -0,0 +1,14 @@ +html, body, :global(#root) { + height: 100%; +} + +body { + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +// temporary font size patch +:global(.ant-tag) { + font-size: 12px; +} diff --git a/src/layouts/BasicLayout.js b/src/layouts/BasicLayout.js new file mode 100644 index 00000000..bd3ffa8a --- /dev/null +++ b/src/layouts/BasicLayout.js @@ -0,0 +1,260 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Layout, Menu, Icon, Avatar, Dropdown, Tag, message } from 'antd'; +import DocumentTitle from 'react-document-title'; +import { connect } from 'dva'; +import { Link, routerRedux } from 'dva/router'; +import moment from 'moment'; +import groupBy from 'lodash/groupBy'; +import styles from './BasicLayout.less'; +import HeaderSearch from '../components/HeaderSearch'; +import NoticeIcon from '../components/NoticeIcon'; +import GlobalFooter from '../components/GlobalFooter'; +import { menus } from '../common/nav'; + +const { Header, Sider, Content } = Layout; +const { SubMenu } = Menu; + +class BasicLayout extends React.PureComponent { + static childContextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + } + state = { + mode: 'inline', + }; + getChildContext() { + const { routes, params } = this.props; + return { routes, params }; + } + componentDidMount() { + this.props.dispatch({ + type: 'user/fetchCurrent', + }); + } + onCollapse = (collapsed) => { + this.props.dispatch({ + type: 'global/changeLayoutCollapsed', + payload: collapsed, + }); + } + onMenuClick = ({ key }) => { + if (key === 'logout') { + this.props.dispatch(routerRedux.push('/user/login')); + } + } + getDefaultCollapsedSubMenus() { + const currentMenuSelectedKeys = [...this.getCurrentMenuSelectedKeys()]; + currentMenuSelectedKeys.splice(-1, 1); + return currentMenuSelectedKeys; + } + getCurrentMenuSelectedKeys() { + const { location: { pathname } } = this.props; + const keys = pathname.split('/').slice(1); + if (keys.length === 1 && keys[0] === '') { + return [menus[0].key]; + } + return keys; + } + getNavMenuItems(menusData, parentPath = '') { + return menusData.map((item) => { + if (!item.name) { + return null; + } + const itemPath = `${parentPath}/${item.path || ''}`.replace(/\/+/g, '/'); + if (item.children && item.children.some(child => child.name)) { + return ( + + + {item.name} + + } + key={item.key || item.path} + > + {this.getNavMenuItems(item.children, itemPath)} + + ); + } + return ( + + + + {item.name} + + + ); + }); + } + getPageTitle() { + const { routes } = this.props; + for (let i = routes.length - 1; i >= 0; i -= 1) { + if (routes[i].breadcrumbName) { + return `${routes[i].breadcrumbName} - Ant Design Pro`; + } + } + return 'Ant Design Pro'; + } + getNoticeData() { + const { notices = [] } = this.props; + if (notices.length === 0) { + return {}; + } + const newNotices = notices.map((notice) => { + const newNotice = { ...notice }; + if (newNotice.datetime) { + newNotice.datetime = moment(notice.datetime).fromNow(); + } + // transform id to item key + if (newNotice.id) { + newNotice.key = newNotice.id; + } + if (newNotice.extra && newNotice.status) { + const color = ({ + processing: 'blue', + urgent: 'red', + doing: 'yellow', + })[newNotice.status]; + newNotice.extra = {newNotice.extra}; + } + return newNotice; + }); + return groupBy(newNotices, 'type'); + } + toggle = () => { + const { collapsed } = this.props; + this.props.dispatch({ + type: 'global/changeLayoutCollapsed', + payload: !collapsed, + }); + } + handleNoticeClear = (type) => { + message.success(`清空了${type}`); + this.props.dispatch({ + type: 'global/clearNotices', + payload: type, + }); + } + handleNoticeVisibleChange = (visible) => { + if (visible) { + this.props.dispatch({ + type: 'global/fetchNotices', + }); + } + } + render() { + const { children, currentUser, collapsed, fetchingNotices } = this.props; + + const menu = ( + + 个人中心 + 设置 + + 退出登录 + + ); + + const noticeData = this.getNoticeData(); + + return ( + + + +
    + + logo +

    Ant Design Pro

    + +
    + + {this.getNavMenuItems(menus)} + +
    + +
    + +
    + { + console.log('input', value); // eslint-disable-line + }} + onPressEnter={(value) => { + console.log('enter', value); // eslint-disable-line + }} + /> + { + console.log(item, tabProps); // eslint-disable-line + }} + onClear={this.handleNoticeClear} + onPopupVisibleChange={this.handleNoticeVisibleChange} + loading={fetchingNotices} + popupAlign={{ offset: [20, -16] }} + > + + + + + + + + {currentUser.name} + + +
    +
    + + {children} + Copyright 2017 蚂蚁金服体验技术部出品
    } + /> + + + + + ); + } +} + +export default connect(state => ({ + currentUser: state.user.currentUser, + collapsed: state.global.collapsed, + fetchingNotices: state.global.fetchingNotices, + notices: state.global.notices, +}))(BasicLayout); diff --git a/src/layouts/BasicLayout.less b/src/layouts/BasicLayout.less new file mode 100644 index 00000000..eccda762 --- /dev/null +++ b/src/layouts/BasicLayout.less @@ -0,0 +1,95 @@ +@import "~antd/lib/style/themes/default.less"; + +.header { + background: @primary-color; + padding: 0 16px 0 0; + color: #fff; +} + +.logo { + height: 64px; + position: relative; + line-height: 64px; + padding: 0 24px; + background: @primary-color; + overflow: hidden; + img { + display: inline-block; + vertical-align: middle; + height: 32px; + } + h1 { + color: #fff; + display: inline-block; + vertical-align: middle; + font-size: 22px; + margin-left: 12px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin-top: -3px; + } +} + +:global(.ant-layout-sider-collapsed) .logo > a { + width: 32px; +} + +.trigger { + font-size: 20px; + line-height: 64px; + cursor: pointer; + transition: all .3s; + color: #fff; + padding: 0 28px; + vertical-align: middle; + &:hover { + background: @primary-7; + } +} + +.right { + float: right; + height: 100%; + .action { + cursor: pointer; + padding: 0 12px; + display: inline-block; + transition: all .3s; + height: 100%; + > i { + font-size: 20px; + vertical-align: middle; + } + &:global(.ant-popover-open), + &:hover { + background: @primary-7; + } + } + .search:hover { + background: transparent; + } + .account { + .avatar { + margin: 20px 8px 20px 0; + color: @primary-color; + background: rgba(255, 255, 255, .85); + vertical-align: middle; + } + } +} + +.menu { + :global(.anticon) { + margin-right: 8px; + } + :global(.ant-dropdown-menu-item) { + padding-left: 16px; + padding-right: 16px; + width: 190px; + } +} + +:global { + .ant-layout { + overflow-x: hidden; + } +} diff --git a/src/layouts/BlankLayout.js b/src/layouts/BlankLayout.js new file mode 100644 index 00000000..505270f8 --- /dev/null +++ b/src/layouts/BlankLayout.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default props =>
    ; diff --git a/src/layouts/PageHeaderLayout.js b/src/layouts/PageHeaderLayout.js new file mode 100644 index 00000000..ef1f0f6d --- /dev/null +++ b/src/layouts/PageHeaderLayout.js @@ -0,0 +1,9 @@ +import React from 'react'; +import PageHeader from '../components/PageHeader'; + +export default ({ children, ...restProps }) => ( +
    + + {children ?
    {children}
    : null} +
    +); diff --git a/src/layouts/UserLayout.js b/src/layouts/UserLayout.js new file mode 100644 index 00000000..4ae73853 --- /dev/null +++ b/src/layouts/UserLayout.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import DocumentTitle from 'react-document-title'; +import { Icon } from 'antd'; +import GlobalFooter from '../components/GlobalFooter'; +import styles from './UserLayout.less'; + +const links = [{ + title: '帮助', + href: '', +}, { + title: '隐私', + href: '', +}, { + title: '条款', + href: '', +}]; + +const copyright =
    Copyright 2017 蚂蚁金服体验技术部出品
    ; + +class UserLayout extends React.PureComponent { + static childContextTypes = { + routes: PropTypes.array, + params: PropTypes.object, + } + getChildContext() { + const { routes, params } = this.props; + return { routes, params }; + } + getPageTitle() { + const { routes } = this.props; + for (let i = routes.length - 1; i >= 0; i -= 1) { + if (routes[i].breadcrumbName) { + return `${routes[i].breadcrumbName} - Ant Design Pro`; + } + } + return 'Ant Design Pro'; + } + render() { + const { children } = this.props; + + return ( + +
    +
    +
    + + Ant Design +
    +

    Ant Design 是东半球最具影响力的 Web 设计规范

    +
    + {children} + +
    +
    + ); + } +} + +export default UserLayout; diff --git a/src/layouts/UserLayout.less b/src/layouts/UserLayout.less new file mode 100644 index 00000000..7da9a074 --- /dev/null +++ b/src/layouts/UserLayout.less @@ -0,0 +1,46 @@ +@import "~antd/lib/style/themes/default.less"; + +.container { + background: @background-color-base; + background-image: url('https://gw.alipayobjects.com/zos/rmsportal/bOjjckIwLKuWCswKAghg.svg'); + width: 100%; + min-height: 100%; + background-repeat: no-repeat; + background-position: center; + background-size: 85%; + padding: 110px 0 144px 0; + position: relative; +} + +.top { + text-align: center; +} + +.header { + height: 44px; + line-height: 44px; +} + +.logo { + height: 44px; + vertical-align: top; + margin-right: 12px; +} + +.title { + font-size: 33px; + color: @heading-color; +} + +.desc { + font-size: @font-size-lg; + color: @text-color-secondary; + margin-top: 12px; + margin-bottom: 40px; +} + +.footer { + position: absolute; + width: 100%; + bottom: 0; +} diff --git a/src/models/activities.js b/src/models/activities.js new file mode 100644 index 00000000..c12584eb --- /dev/null +++ b/src/models/activities.js @@ -0,0 +1,43 @@ +import { queryActivities } from '../services/api'; + +export default { + namespace: 'activities', + + state: { + list: [], + loading: true, + }, + + effects: { + *fetchList({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryActivities); + yield put({ + type: 'saveList', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + }, + + reducers: { + saveList(state, action) { + return { + ...state, + list: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/chart.js b/src/models/chart.js new file mode 100644 index 00000000..43e83e8f --- /dev/null +++ b/src/models/chart.js @@ -0,0 +1,64 @@ +import { fakeChartData } from '../services/api'; + +export default { + namespace: 'chart', + + state: { + visitData: [], + salesData: [], + searchData: [], + offlineData: [], + offlineChartData: [], + salesTypeData: [], + salesTypeDataOnline: [], + salesTypeDataOffline: [], + radarData: [], + }, + + effects: { + *fetch({ payload }, { call, put }) { + const response = yield call(fakeChartData); + yield put({ + type: 'save', + payload: response, + }); + }, + *fetchSalesData({ payload }, { call, put }) { + const response = yield call(fakeChartData); + yield put({ + type: 'save', + payload: { + salesData: response.salesData, + }, + }); + }, + }, + + reducers: { + save(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + setter(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + clear() { + return { + visitData: [], + salesData: [], + searchData: [], + offlineData: [], + offlineChartData: [], + salesTypeData: [], + salesTypeDataOnline: [], + salesTypeDataOffline: [], + radarData: [], + }; + }, + }, +}; diff --git a/src/models/form.js b/src/models/form.js new file mode 100644 index 00000000..a3a144a2 --- /dev/null +++ b/src/models/form.js @@ -0,0 +1,88 @@ +import { routerRedux } from 'dva/router'; +import { message } from 'antd'; +import { fakeSubmitForm } from '../services/api'; + +export default { + namespace: 'form', + + state: { + step: { + }, + regularFormSubmitting: false, + stepFormSubmitting: false, + advancedFormSubmitting: false, + }, + + effects: { + *submitRegularForm({ payload }, { call, put }) { + yield put({ + type: 'changeRegularFormSubmitting', + payload: true, + }); + yield call(fakeSubmitForm, payload); + yield put({ + type: 'changeRegularFormSubmitting', + payload: false, + }); + message.success('提交成功'); + }, + *submitStepForm({ payload }, { call, put }) { + yield put({ + type: 'changeStepFormSubmitting', + payload: true, + }); + yield call(fakeSubmitForm, payload); + yield put({ + type: 'saveStepFormData', + payload, + }); + yield put({ + type: 'changeStepFormSubmitting', + payload: false, + }); + yield put(routerRedux.push('/form/step-form/result')); + }, + *submitAdvancedForm({ payload }, { call, put }) { + yield put({ + type: 'changeAdvancedFormSubmitting', + payload: true, + }); + yield call(fakeSubmitForm, payload); + yield put({ + type: 'changeAdvancedFormSubmitting', + payload: false, + }); + message.success('提交成功'); + }, + }, + + reducers: { + saveStepFormData(state, { payload }) { + return { + ...state, + step: { + ...state.step, + ...payload, + }, + }; + }, + changeRegularFormSubmitting(state, { payload }) { + return { + ...state, + regularFormSubmitting: payload, + }; + }, + changeStepFormSubmitting(state, { payload }) { + return { + ...state, + stepFormSubmitting: payload, + }; + }, + changeAdvancedFormSubmitting(state, { payload }) { + return { + ...state, + advancedFormSubmitting: payload, + }; + }, + }, +}; diff --git a/src/models/global.js b/src/models/global.js new file mode 100644 index 00000000..6628c59b --- /dev/null +++ b/src/models/global.js @@ -0,0 +1,53 @@ +import { queryNotices } from '../services/api'; + +export default { + namespace: 'global', + + state: { + collapsed: false, + notices: [], + fetchingNotices: false, + }, + + effects: { + *fetchNotices({ payload }, { call, put }) { + yield put({ + type: 'changeNoticeLoading', + payload: true, + }); + const data = yield call(queryNotices); + yield put({ + type: 'saveNotices', + payload: data, + }); + }, + }, + + reducers: { + changeLayoutCollapsed(state, { payload }) { + return { + ...state, + collapsed: payload, + }; + }, + saveNotices(state, { payload }) { + return { + ...state, + notices: payload, + fetchingNotices: false, + }; + }, + clearNotices(state, { payload }) { + return { + ...state, + notices: state.notices.filter(item => item.type !== payload), + }; + }, + changeNoticeLoading(state, { payload }) { + return { + ...state, + fetchingNotices: payload, + }; + }, + }, +}; diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 00000000..36666147 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,11 @@ +// Use require.context to require reducers automatically +// Ref: https://webpack.github.io/docs/context.html +const context = require.context('./', false, /\.js$/); +const keys = context.keys().filter(item => item !== './index.js'); + +const models = []; +for (let i = 0; i < keys.length; i += 1) { + models.push(context(keys[i])); +} + +export default models; diff --git a/src/models/list.js b/src/models/list.js new file mode 100644 index 00000000..8d74663f --- /dev/null +++ b/src/models/list.js @@ -0,0 +1,46 @@ +import { queryFakeList } from '../services/api'; + +export default { + namespace: 'list', + + state: { + list: [], + loading: true, + }, + + effects: { + *fetch({ payload, callback }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryFakeList, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + if (callback) { + callback(); + } + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + list: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/login.js b/src/models/login.js new file mode 100644 index 00000000..f815f46b --- /dev/null +++ b/src/models/login.js @@ -0,0 +1,58 @@ +import { fakeAccountLogin, fakeMobileLogin } from '../services/api'; + +export default { + namespace: 'login', + + state: { + status: undefined, + }, + + effects: { + *accountSubmit({ payload }, { call, put }) { + yield put({ + type: 'changeSubmitting', + payload: true, + }); + const response = yield call(fakeAccountLogin); + yield put({ + type: 'loginHandle', + payload: response, + }); + yield put({ + type: 'changeSubmitting', + payload: false, + }); + }, + *mobileSubmit({ payload }, { call, put }) { + yield put({ + type: 'changeSubmitting', + payload: true, + }); + const response = yield call(fakeMobileLogin); + yield put({ + type: 'loginHandle', + payload: response, + }); + yield put({ + type: 'changeSubmitting', + payload: false, + }); + }, + }, + + reducers: { + loginHandle(state, { payload }) { + return { + ...state, + status: payload.status, + type: payload.type, + }; + }, + changeSubmitting(state, { payload }) { + return { + ...state, + submitting: payload, + }; + }, + }, +}; diff --git a/src/models/monitor.js b/src/models/monitor.js new file mode 100644 index 00000000..3a7503e2 --- /dev/null +++ b/src/models/monitor.js @@ -0,0 +1,28 @@ +import { queryTags } from '../services/api'; + +export default { + namespace: 'monitor', + + state: { + tags: [], + }, + + effects: { + *fetchTags({ payload }, { call, put }) { + const response = yield call(queryTags); + yield put({ + type: 'saveTags', + payload: response.list, + }); + }, + }, + + reducers: { + saveTags(state, action) { + return { + ...state, + tags: action.payload, + }; + }, + }, +}; diff --git a/src/models/profile.js b/src/models/profile.js new file mode 100644 index 00000000..7c7a9a6d --- /dev/null +++ b/src/models/profile.js @@ -0,0 +1,45 @@ +import { queryProfile } from '../services/api'; + +export default { + namespace: 'profile', + + state: { + operation1: [], + operation2: [], + operation3: [], + loading: true, + }, + + effects: { + *fetch({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryProfile); + yield put({ + type: 'show', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + }, + + reducers: { + show(state, { payload }) { + return { + ...state, + ...payload, + }; + }, + changeLoading(state, { payload }) { + return { + ...state, + loading: payload, + }; + }, + }, +}; diff --git a/src/models/project.js b/src/models/project.js new file mode 100644 index 00000000..f6243dcf --- /dev/null +++ b/src/models/project.js @@ -0,0 +1,43 @@ +import { queryProjectNotice } from '../services/api'; + +export default { + namespace: 'project', + + state: { + notice: [], + loading: true, + }, + + effects: { + *fetchNotice({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryProjectNotice); + yield put({ + type: 'saveNotice', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + }, + + reducers: { + saveNotice(state, action) { + return { + ...state, + notice: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/register.js b/src/models/register.js new file mode 100644 index 00000000..c878692f --- /dev/null +++ b/src/models/register.js @@ -0,0 +1,42 @@ +import { fakeRegister } from '../services/api'; + +export default { + namespace: 'register', + + state: { + status: undefined, + }, + + effects: { + *submit({ payload }, { call, put }) { + yield put({ + type: 'changeSubmitting', + payload: true, + }); + const response = yield call(fakeRegister); + yield put({ + type: 'registerHandle', + payload: response, + }); + yield put({ + type: 'changeSubmitting', + payload: false, + }); + }, + }, + + reducers: { + registerHandle(state, { payload }) { + return { + ...state, + status: payload.status, + }; + }, + changeSubmitting(state, { payload }) { + return { + ...state, + submitting: payload, + }; + }, + }, +}; diff --git a/src/models/rule.js b/src/models/rule.js new file mode 100644 index 00000000..8b36ba3f --- /dev/null +++ b/src/models/rule.js @@ -0,0 +1,80 @@ +import { queryRule, removeRule, addRule } from '../services/api'; + +export default { + namespace: 'rule', + + state: { + data: { + list: [], + pagination: {}, + }, + loading: true, + }, + + effects: { + *fetch({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryRule, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + *add({ payload, callback }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(addRule, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + + if (callback) callback(); + }, + *remove({ payload, callback }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(removeRule, payload); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + + if (callback) callback(); + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + data: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + }, +}; diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 00000000..570be681 --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,57 @@ +import { query as queryUsers, queryCurrent } from '../services/user'; + +export default { + namespace: 'user', + + state: { + list: [], + loading: false, + currentUser: {}, + }, + + effects: { + *fetch({ payload }, { call, put }) { + yield put({ + type: 'changeLoading', + payload: true, + }); + const response = yield call(queryUsers); + yield put({ + type: 'save', + payload: response, + }); + yield put({ + type: 'changeLoading', + payload: false, + }); + }, + *fetchCurrent({ payload }, { call, put }) { + const response = yield call(queryCurrent); + yield put({ + type: 'saveCurrentUser', + payload: response, + }); + }, + }, + + reducers: { + save(state, action) { + return { + ...state, + list: action.payload, + }; + }, + changeLoading(state, action) { + return { + ...state, + loading: action.payload, + }; + }, + saveCurrentUser(state, action) { + return { + ...state, + currentUser: action.payload, + }; + }, + }, +}; diff --git a/src/router.js b/src/router.js new file mode 100644 index 00000000..a31ba0ca --- /dev/null +++ b/src/router.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { Router, Route, Redirect } from 'dva/router'; +import navData from './common/nav'; + +function getRoutes(data, level = 0) { + return data.map((item, i) => { + let children; + if (item.children) { + children = getRoutes(item.children, level + 1); + } + let homePageRedirect; + if (level === 1 && i === 0) { + let indexPath; + // First children router + if (item.children && item.children[0]) { + indexPath = `/${item.path}/${item.children[0].path}`; + } else { + indexPath = item.path; + } + homePageRedirect = ; + } + if (item.noRoute) { + return null; + } + return ( + + {homePageRedirect} + {children} + + ); + }); +} + +function RouterConfig({ history }) { + return ( + + {getRoutes(navData)} + + ); +} + +export default RouterConfig; diff --git a/src/routes/Dashboard.css b/src/routes/Dashboard.css new file mode 100644 index 00000000..3f7685d1 --- /dev/null +++ b/src/routes/Dashboard.css @@ -0,0 +1,5 @@ +.normal { + font-family: Georgia, sans-serif; + margin-top: 3em; + text-align: center; +} diff --git a/src/routes/Dashboard.js b/src/routes/Dashboard.js new file mode 100644 index 00000000..0bb12890 --- /dev/null +++ b/src/routes/Dashboard.js @@ -0,0 +1,100 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card, Table, Icon } from 'antd'; + +const columns = [{ + title: 'Name', + dataIndex: 'name', + key: 'name', +}, { + title: 'Age', + dataIndex: 'age', + key: 'age', +}, { + title: 'Address', + dataIndex: 'address', + key: 'address', +}, { + title: 'Action', + key: 'action', + render: (text, record) => ( + + Action 一 {record.name} + + Delete + + + More actions + + + ), +}]; + +class Dashboard extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'user/fetch', + }); + } + render() { + const { user: { list, loading } } = this.props; + return ( +
    + +
    + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + +

    卡片内容

    +

    卡片内容

    +

    卡片内容

    +
    + + + + + } + > +
    + + + + + ); + } +} + +export default connect(state => ({ + user: state.user, +}))(Dashboard); diff --git a/src/routes/Dashboard/Analysis.js b/src/routes/Dashboard/Analysis.js new file mode 100644 index 00000000..9ea97b18 --- /dev/null +++ b/src/routes/Dashboard/Analysis.js @@ -0,0 +1,388 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Icon, Card, Tabs, Table, Radio, DatePicker, Tooltip } from 'antd'; +import numeral from 'numeral'; + +import { ChartCard, Trend, yuan, MiniArea, MiniBar, MiniProgress, Field, Bar, Pie, NumberInfo, IconUp, IconDown } from '../../components/Charts'; + +import TimelineChart from '../../components/TimelineChart'; +import { getTimeDistance } from '../../utils/utils'; + +import styles from './Analysis.less'; + +const TabPane = Tabs.TabPane; +const { RangePicker } = DatePicker; + +const rankingListData = []; +for (let i = 0; i < 7; i += 1) { + rankingListData.push({ + title: `工专路 ${i} 号店`, + total: 323234, + }); +} + +@connect(state => ({ + chart: state.chart, +})) +export default class Analysis extends Component { + state = { + salesType: 'all', + currentTabKey: '', + rangePickerValue: [], + } + + componentDidMount() { + this.props.dispatch({ + type: 'chart/fetch', + }); + } + + componentWillUnmount() { + const { dispatch } = this.props; + dispatch({ + type: 'chart/clear', + }); + } + + handleChangeSalesType = (e) => { + this.setState({ + salesType: e.target.value, + }); + } + + handleTabChange = (key) => { + this.setState({ + currentTabKey: key, + }); + } + + handleRangePickerChange = (rangePickerValue) => { + this.setState({ + rangePickerValue, + }); + } + + selectDate = (type) => { + this.setState({ + rangePickerValue: getTimeDistance(type), + }); + + this.props.dispatch({ + type: 'chart/fetchSalesData', + }); + } + + render() { + const { rangePickerValue, salesType, currentTabKey } = this.state; + const { chart } = this.props; + const { + visitData, + salesData, + searchData, + offlineData, + offlineChartData, + salesTypeData, + salesTypeDataOnline, + salesTypeDataOffline, + } = chart; + + const salesPieData = salesType === 'all' ? + salesTypeData + : + (salesType === 'online' ? salesTypeDataOnline : salesTypeDataOffline); + + const iconGroup = ( + + + + ); + + const salesExtra = (); + + const columns = [ + { + title: '排名', + dataIndex: 'index', + key: 'index', + }, + { + title: '搜索关键词', + dataIndex: 'keyword', + key: 'keyword', + render: text => {text}, + }, + { + title: '用户数', + dataIndex: 'count', + key: 'count', + sorter: (a, b) => a.count - b.count, + }, + { + title: '周涨幅', + dataIndex: 'range', + key: 'range', + sorter: (a, b) => a.range - b.range, + render: (text, record) => ( + {text}% {record.status === 1 ? : } + ), + }, + ]; + + const CustomTab = ({ data, currentTabKey: currentKey }) => ( + + + + + + + + + ); + + const topColResponsiveProps = { + xs: 24, + sm: 12, + md: 6, + style: { marginBottom: 24 }, + }; + + return ( +
    + +
    + } + total={yuan(126560)} + footer={} + contentHeight={46} + > + + 12.3% + 11% + + + + + } + total={numeral(8846).format('0,0')} + footer={} + contentHeight={46} + > + + + + + } + total={numeral(6560).format('0,0')} + footer={} + contentHeight={46} + > + + + + + } + total="78%" + footer={ + 12.3% + 11% + } + contentHeight={46} + > + + + + + + +
    + + + +
    + + + +

    门店销售额排名

    +
      + { + rankingListData.map((item, i) => ( +
    • + {i + 1} + {item.title} + {numeral(item.total).format('0,0')} +
    • + )) + } +
    + + + + + 访问量没有, 因为偷懒了 + + + + + + + + + + + 搜索用户数量 } + total={numeral(12321).format('0,0')} + status="up" + subTotal={17.1} + /> + + + + + + + +
    record.index} + size="middle" + columns={columns} + dataSource={searchData} + pagination={{ + style: { marginBottom: 0 }, + showSizeChanger: true, + showQuickJumper: true, + pageSize: 5, + }} + /> + + + + + + 全部渠道 + 线上 + 门店 + +
    + now.y + pre, 0))} + data={salesPieData} + valueFormat={val => yuan(val)} + height={294} + /> +
    +
    + + + + + + { + offlineData.map(shop => ( + } + key={shop.name} + > +
    + +
    +
    ) + ) + } +
    +
    + + ); + } +} diff --git a/src/routes/Dashboard/Analysis.less b/src/routes/Dashboard/Analysis.less new file mode 100644 index 00000000..390941ae --- /dev/null +++ b/src/routes/Dashboard/Analysis.less @@ -0,0 +1,100 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.iconGroup { + i { + cursor: pointer; + margin-left: 16px; + } +} +.rankingList { + margin-top: 25px; + li { + .clearfix(); + margin-top: 16px; + span { + color: @text-color; + font-size: 14px; + line-height: 22px; + } + span:first-child { + background-color: @background-color-base; + border-radius: 20px; + display: inline-block; + font-size: 12px; + font-weight: 600; + margin-right: 24px; + height: 20px; + line-height: 20px; + width: 20px; + text-align: center; + } + span.active { + background-color: @primary-color; + color: #fff; + } + span:last-child { + float: right; + } + } +} + +.salesExtra { + display: inline-block; + margin-right: 24px; + a { + color: @text-color; + margin-left: 24px; + &:hover { + color: @primary-color; + } + } +} + +.salesCard { + :global { + .ant-tabs-content { + padding: 0 24px 24px 24px; + } + .ant-tabs-bar { + padding-left: 24px; + .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; + } + } +} + +@media screen and (max-width: @screen-lg) { + .rankingList { + li { + span:first-child { + margin-right: 8px; + } + } + } +} + +@media screen and (max-width: @screen-sm) { + .salesExtra { + display: none; + } + .salesExtraWrap { + position: absolute; + top: 50px; + left: 24px; + } + .salesCard { + :global { + .ant-tabs-content { + padding-top: 30px; + } + } + } +} diff --git a/src/routes/Dashboard/Monitor.js b/src/routes/Dashboard/Monitor.js new file mode 100644 index 00000000..b8b20568 --- /dev/null +++ b/src/routes/Dashboard/Monitor.js @@ -0,0 +1,191 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card } from 'antd'; +import numeral from 'numeral'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import { NumberInfo, MiniArea, Pie, WaterWave, Gauge } from '../../components/Charts'; +import MapChart from '../../components/MapChart'; +import TagCloud from '../../components/TagCloud'; +import Countdown from '../../components/Countdown'; +import { fixedZero } from '../../utils/utils'; + +import styles from './Monitor.less'; + +const activeData = []; +for (let i = 0; i < 24; i += 1) { + activeData.push({ + x: `${fixedZero(i)}:00`, + y: (i * 50) + (Math.floor(Math.random() * 200)), + }); +} + +const MapData = []; +for (let i = 0; i < 50; i += 1) { + MapData.push({ + x: Math.floor(Math.random() * 600), + y: Math.floor(Math.random() * 400), + value: Math.floor(Math.random() * 1000) + 500, + }); +} +const targetTime = new Date().getTime() + 3900000; + +@connect(state => ({ + monitor: state.monitor, +})) +export default class Monitor extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'monitor/fetchTags', + }); + } + + render() { + const { monitor } = this.props; + const { tags } = monitor; + + return ( + + + + + + + + + + + + + } + /> + + + + + +
    + +
    + + +
    + +
    + +
    + +
    + { + activeData && ( +
    +

    {[...activeData].sort()[activeData.length - 1].y + 200} 亿元

    +

    {[...activeData].sort()[Math.floor(activeData.length / 2)].y} 亿元

    +
    + ) + } + { + activeData && ( +
    + 00:00 + {activeData[Math.floor(activeData.length / 2)].x} + {activeData[activeData.length - 1].x} +
    + ) + } +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + } +} diff --git a/src/routes/Dashboard/Monitor.less b/src/routes/Dashboard/Monitor.less new file mode 100644 index 00000000..59a8357e --- /dev/null +++ b/src/routes/Dashboard/Monitor.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.activeChart { + position: relative; +} +.activeChartGrid { + p { + position: absolute; + top: 80px; + } + p:last-child { + top: 115px; + } +} +.activeChartLegend { + position: relative; + font-size: 0; + margin-top: 8px; + height: 20px; + line-height: 20px; + span { + display: inline-block; + font-size: 12px; + text-align: center; + width: 33.33%; + } + span:first-child { + text-align: left; + } + span:last-child { + text-align: right; + } +} + +.mapChart { + padding-top: 46px; + height: 436px; +} + +@media screen and (max-width: @screen-lg) { + .mapChart { + height: auto; + } +} diff --git a/src/routes/Dashboard/Workplace.js b/src/routes/Dashboard/Workplace.js new file mode 100644 index 00000000..c1928a48 --- /dev/null +++ b/src/routes/Dashboard/Workplace.js @@ -0,0 +1,263 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Link } from 'dva/router'; +import { Row, Col, Card, List, Avatar, Alert, Icon } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import EditableLinkGroup from '../../components/EditableLinkGroup'; +import { Radar } from '../../components/Charts'; + +import styles from './Workplace.less'; + +const links = [ + { + title: '操作一', + href: '', + }, + { + title: '操作二', + href: '', + }, + { + title: '操作三', + href: '', + }, + { + title: '操作四', + href: '', + }, + { + title: '操作五', + href: '', + }, + { + title: '操作六', + href: '', + }, +]; + +const members = [ + { + id: 'members-1', + title: '凤蝶精英小分队', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/CRxBvUggxBYzWBTGmkxF.png', + link: '', + }, + { + id: 'members-2', + title: 'Ant Design', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/RBytOnluTcyeyDazAbvs.png', + link: '', + }, + { + id: 'members-3', + title: 'DesignLab', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/HQVJYAXtWHEJvLxQjmPa.png', + link: '', + }, + { + id: 'members-4', + title: 'Basement', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/HQVJYAXtWHEJvLxQjmPa.png', + link: '', + }, + { + id: 'members-5', + title: 'Github', + logo: 'https://gw.alipayobjects.com/zos/rmsportal/RBytOnluTcyeyDazAbvs.png', + link: '', + }, +]; + +@connect(state => ({ + project: state.project, + activities: state.activities, + chart: state.chart, +})) +export default class Workplace extends PureComponent { + componentDidMount() { + const { dispatch } = this.props; + dispatch({ + type: 'project/fetchNotice', + }); + dispatch({ + type: 'activities/fetchList', + }); + dispatch({ + type: 'chart/fetch', + }); + } + + componentWillUnmount() { + const { dispatch } = this.props; + dispatch({ + type: 'chart/clear', + }); + } + + render() { + const { + project: { loading: projectLoading, notice }, + activities: { loading: activitiesLoading, list: activitiesList }, + chart: { radarData }, + } = this.props; + + const pageHeaderContent = ( + + ); + + const pageHeaderTitle = ( +
    +
    + +
    +
    +

    早安, 曲丽丽, 祝你开心每一天

    +

    交互专家 | 蚂蚁金服-某某某事业群-某某平台部-某某技术部-UED

    +
    +
    + ); + + const pageHeaderAction = ( +
    +
    +

    项目数

    +

    56

    + +
    +
    +

    团队内排名

    +

    8 / 24

    + +
    +
    +

    项目访问

    +

    2,223

    +
    +
    + ); + + return ( + + +
    + 全部项目} + loading={projectLoading} + bodyStyle={{ padding: 0 }} + > + { + !projectLoading && notice.length > 0 && notice.map(item => ( + + + } + title={{item.title}} + description={item.description} + /> +
    + {item.member || ''} + { + item.updatedAt && {moment(item.updatedAt).fromNow()} + } +
    +
    +
    + )) + } +
    + + +
    + { + activitiesList.map(item => ( + + } + title={

    {item.user.name} 在 xx 新建了项目 xxxx

    } + description={moment(item.updatedAt).fromNow()} + /> +
    + )) + } +
    +
    +
    + +
    + + {}} + links={links} + /> + + +
    + { + + } +
    +
    + +
    + + { + members.map(item => ( +
    + + {item.title} + {item.title} + + + )) + } + + + + + + + ); + } +} diff --git a/src/routes/Dashboard/Workplace.less b/src/routes/Dashboard/Workplace.less new file mode 100644 index 00000000..11990695 --- /dev/null +++ b/src/routes/Dashboard/Workplace.less @@ -0,0 +1,191 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.activitiesList { + padding: 0 24px 24px 24px; + :global { + .ant-list-item-meta-title:hover { + color: @text-color; + } + } +} + +.pageHeaderTitle { + display: flex; + .titleAvatar { + flex: 0 1 80px; + & > span { + border-radius: 80px; + display: block; + width: 80px; + height: 80px; + } + } + .titleContent { + position: relative; + top: 8px; + margin-left: 32px; + flex: 1 1 auto; + p { + font-weight: normal; + } + & > p:last-child { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + margin-top: 12px; + } + } +} + +.pageHeaderAction { + float: right; + .clearfix(); + & > div { + text-align: right; + padding: 0 24px; + position: relative; + float: left; + & > p:first-child { + color: @text-color-secondary; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 2px; + } + & > p { + color: @text-color; + font-size: 30px; + line-height: 38px; + & > span { + color: @text-color-secondary; + font-size: 20px; + } + } + & > em { + background-color: @border-color-split; + position: absolute; + top: 8px; + right: 0; + width: 1px; + height: 40px; + } + } + & > div:last-child { + padding-right: 0; + } +} + +.members { + a { + display: block; + margin-bottom: 24px; + line-height: 24px; + height: 24px; + .textOverflow(); + img { + border-radius: 24px; + display: inline; + position: relative; + top: -2px; + width: 24px; + height: 24px; + margin-right: 12px; + vertical-align: middle; + } + span { + font-size: @font-size-base; + color: @text-color; + line-height: 24px; + max-width: 100px; + .textOverflow(); + } + &:hover { + span { + color: @primary-color; + } + } + } +} + +.projectList { + :global { + .ant-card-meta-title { + font-size: 14px; + a { + color: @heading-color; + &:hover { + color: @primary-color; + } + } + } + .ant-card-meta-description { + font-size: 12px; + min-height: 36px; + } + } + .projectGrid { + width: 33.33%; + } + .projectItemContent { + display: flex; + padding-left: 48px; + margin-top: 12px; + overflow: hidden; + font-size: 12px; + height: 20px; + line-height: 20px; + .textOverflow(); + a { + color: @text-color-secondary; + display: inline-block; + flex: 1 1 0; + .textOverflow(); + &:hover { + color: @primary-color; + } + } + span { + color: @text-color-secondary; + flex: 0 0 auto; + float: right; + } + } +} + +@media screen and (max-width: @screen-xl) and (min-width: @screen-lg) { + .pageHeaderAction { + margin-left: -44px; + & > div { + padding: 0 16px; + } + } +} + +@media screen and (max-width: @screen-lg) { + .pageHeaderAction { + margin-left: -64px; + & > div { + padding: 0 16px; + text-align: left; + & > em { + display: none; + } + } + } +} + +@media screen and (max-width: @screen-md) { + .projectList { + .projectGrid { + width: 50%; + } + } +} + +@media screen and (max-width: @screen-xs) { + .projectList { + .projectGrid { + width: 100%; + } + } +} diff --git a/src/routes/Exception/403.js b/src/routes/Exception/403.js new file mode 100644 index 00000000..ea0d2307 --- /dev/null +++ b/src/routes/Exception/403.js @@ -0,0 +1,4 @@ +import React from 'react'; +import Exception from '../../components/Exception'; + +export default () => ; diff --git a/src/routes/Exception/404.js b/src/routes/Exception/404.js new file mode 100644 index 00000000..e114e04c --- /dev/null +++ b/src/routes/Exception/404.js @@ -0,0 +1,4 @@ +import React from 'react'; +import Exception from '../../components/Exception'; + +export default () => ; diff --git a/src/routes/Exception/500.js b/src/routes/Exception/500.js new file mode 100644 index 00000000..7fa97a08 --- /dev/null +++ b/src/routes/Exception/500.js @@ -0,0 +1,4 @@ +import React from 'react'; +import Exception from '../../components/Exception'; + +export default () => ; diff --git a/src/routes/Forms/AdvancedForm.js b/src/routes/Forms/AdvancedForm.js new file mode 100644 index 00000000..a83f7bb3 --- /dev/null +++ b/src/routes/Forms/AdvancedForm.js @@ -0,0 +1,267 @@ +import React from 'react'; +import { Card, Button, Form, Icon, Col, Row, DatePicker, TimePicker, Input, Select, Popover } from 'antd'; +import { connect } from 'dva'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import FooterToolbar from '../../components/FooterToolbar'; +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', +}]; + +function AdvancedForm({ form, dispatch, submitting }) { + const { getFieldDecorator, validateFieldsAndScroll, getFieldsError } = form; + const validate = () => { + validateFieldsAndScroll((error, values) => { + if (!error) { + // submit the values + dispatch({ + type: 'form/submitAdvancedForm', + payload: values, + }); + } + }); + }; + const errors = getFieldsError(); + const getErrorInfo = () => { + 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 ( +
  • scrollToField(key)}> + +
    {errors[key][0]}
    +
    {fieldLabels[key]}
    +
  • + ); + }); + return ( + + trigger.parentNode} + > + + + {errorCount} + + ); + }; + return ( + + +
    + +
    + + {getFieldDecorator('name', { + rules: [{ required: true, message: '请输入仓库名称' }], + })( + + )} + + + + + {getFieldDecorator('url', { + rules: [{ required: true, message: '请选择' }], + })( + + )} + + + + + {getFieldDecorator('owner', { + rules: [{ required: true, message: '请选择管理员' }], + })( + + )} + + + + + + + {getFieldDecorator('approver', { + rules: [{ required: true, message: '请选择审批员' }], + })( + + )} + + + + + {getFieldDecorator('dateRange', { + rules: [{ required: true, message: '请选择生效日期' }], + })( + + )} + + + + + {getFieldDecorator('type', { + rules: [{ required: true, message: '请选择仓库类型' }], + })( + + )} + + + + + + +
    + +
    + + {getFieldDecorator('name2', { + rules: [{ required: true, message: '请输入' }], + })( + + )} + + + + + {getFieldDecorator('url2', { + rules: [{ required: true, message: '请选择' }], + })( + + )} + + + + + {getFieldDecorator('owner2', { + rules: [{ required: true, message: '请选择管理员' }], + })( + + )} + + + + + + + {getFieldDecorator('approver2', { + rules: [{ required: true, message: '请选择审批员' }], + })( + + )} + + + + + {getFieldDecorator('dateRange2', { + rules: [{ required: true, message: '请输入' }], + })( + + )} + + + + + {getFieldDecorator('type2', { + rules: [{ required: true, message: '请选择仓库类型' }], + })( + + )} + + + + + + + {getFieldDecorator('members', { + initialValue: tableData, + })()} + + + {getErrorInfo()} + + + + + ); +} + +export default connect(state => ({ + collapsed: state.global.collapsed, + submitting: state.form.advancedFormSubmitting, +}))(Form.create()(AdvancedForm)); diff --git a/src/routes/Forms/BasicForm.js b/src/routes/Forms/BasicForm.js new file mode 100644 index 00000000..c6ed6df5 --- /dev/null +++ b/src/routes/Forms/BasicForm.js @@ -0,0 +1,145 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Form, Input, DatePicker, Select, Button, Card } from 'antd'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +const FormItem = Form.Item; +const Option = Select.Option; +const { RangePicker } = DatePicker; + +@connect(state => ({ + submitting: state.form.regularFormSubmitting, +})) +@Form.create() +export default class BasicForms extends PureComponent { + handleSubmit = (e) => { + e.preventDefault(); + this.props.form.validateFieldsAndScroll((err, values) => { + if (!err) { + this.props.dispatch({ + type: 'form/submitRegularForm', + payload: values, + }); + } + }); + } + render() { + const { submitting } = this.props; + const { getFieldDecorator } = this.props.form; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 3 }, + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 12 }, + md: { span: 10 }, + }, + }; + + const submitFormLayout = { + wrapperCol: { + xs: { span: 24, offset: 0 }, + sm: { span: 10, offset: 3 }, + }, + }; + + return ( + + +
    + + {getFieldDecorator('appType', { + rules: [{ + required: true, message: '应用类型', + }], + })( + + )} + + + {getFieldDecorator('productName', { + rules: [{ + required: true, message: '请输入产品名', + }], + })( + + )} + + + {getFieldDecorator('appName', { + rules: [ + { required: true, message: '请输入应用名' }, + { pattern: /^[a-zA-Z0-9-]+$/, message: '只能输入英文、数字、中划线' }, + ], + })( + + )} + + + {getFieldDecorator('appChineseName', { + rules: [ + { required: true, message: '请输入应用中文名' }, + { pattern: /^[\u4e00-\u9fa5]+$/, message: '请输入中文' }, + ], + })( + + )} + + + {getFieldDecorator('dateRange', { + rules: [{ type: 'array', required: true, message: '请选择生效日期' }], + })( + + )} + + + {getFieldDecorator('domain', { + rules: [{ required: true, message: '请输入域名' }], + })( + + )} + + + + + +
    +
    + ); + } +} diff --git a/src/routes/Forms/StepForm/Step1.js b/src/routes/Forms/StepForm/Step1.js new file mode 100644 index 00000000..d4982d6b --- /dev/null +++ b/src/routes/Forms/StepForm/Step1.js @@ -0,0 +1,101 @@ +import React from 'react'; +import { Form, Input, Button, Select, Divider } from 'antd'; +import { routerRedux } from 'dva/router'; +import styles from './style.less'; + +const Option = Select.Option; + +export default ({ formItemLayout, form, dispatch }) => { + const { getFieldDecorator, validateFields } = form; + const onValidateForm = () => { + validateFields((err, values) => { + if (!err) { + dispatch({ + type: 'form/saveStepFormData', + payload: values, + }); + dispatch(routerRedux.push('/form/step-form/confirm')); + } + }); + }; + return ( +
    +
    + + {getFieldDecorator('payAccount', { + initialValue: 'ant-design@alipay.com', + rules: [{ required: true, message: '请选择付款账户' }], + })( + + )} + + + + + {getFieldDecorator('receiverAccount', { + initialValue: 'test@example.com', + rules: [ + { required: true, message: '请输入收款人账户' }, + { type: 'email', message: '账户名应为邮箱格式' }, + ], + })( + + )} + + + + {getFieldDecorator('receiverName', { + initialValue: 'Alex', + rules: [{ required: true, message: '请输入收款人姓名' }], + })( + + )} + + + {getFieldDecorator('amount', { + initialValue: '500', + rules: [ + { required: true, message: '请输入转账金额' }, + { pattern: /^(\d+)((?:\.\d+)?)$/, message: '请输入合法金额数字' }, + ], + })( + + )} + + + + + + +
    +

    说明

    +

    转账到支付宝账户

    +

    如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。

    +

    转账到银行卡

    +

    如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。

    +
    +
    + ); +}; diff --git a/src/routes/Forms/StepForm/Step2.js b/src/routes/Forms/StepForm/Step2.js new file mode 100644 index 00000000..4a43d0b2 --- /dev/null +++ b/src/routes/Forms/StepForm/Step2.js @@ -0,0 +1,89 @@ +import React from 'react'; +import { Form, Input, Button, Alert, Divider } from 'antd'; +import { routerRedux } from 'dva/router'; +import styles from './style.less'; + +export default ({ formItemLayout, form, data, dispatch, submitting }) => { + const { getFieldDecorator, validateFields } = form; + const onPrev = () => { + dispatch(routerRedux.push('/form/step-form')); + }; + const onValidateForm = (e) => { + e.preventDefault(); + validateFields((err, values) => { + if (!err) { + dispatch({ + type: 'form/submitStepForm', + payload: { + ...data, + ...values, + }, + }); + } + }); + }; + return ( +
    + + + {data.payAccount} + + + {data.receiverAccount} + + + {data.receiverName} + + + {data.amount} 元 + + + + {getFieldDecorator('password', { + initialValue: '123456', + rules: [{ + required: true, message: '需要支付密码才能进行支付', + }], + })( + + )} + + + + + + + + ); +}; diff --git a/src/routes/Forms/StepForm/Step3.js b/src/routes/Forms/StepForm/Step3.js new file mode 100644 index 00000000..4e8f55dc --- /dev/null +++ b/src/routes/Forms/StepForm/Step3.js @@ -0,0 +1,51 @@ +import React from 'react'; +import { Button, Row, Col } from 'antd'; +import { routerRedux } from 'dva/router'; +import Result from '../../../components/Result'; +import styles from './style.less'; + +export default ({ dispatch, data }) => { + const onFinish = () => { + dispatch(routerRedux.push('/form/step-form')); + }; + const information = ( +
    + +
    付款账户: + {data.payAccount} + + + 收款账户: + {data.receiverAccount} + + + 收款人姓名: + {data.receiverName} + + + 转账金额: + {data.amount} 元 + + + ); + const actions = ( +
    + + +
    + ); + return ( + + ); +}; diff --git a/src/routes/Forms/StepForm/index.js b/src/routes/Forms/StepForm/index.js new file mode 100644 index 00000000..85a0a20c --- /dev/null +++ b/src/routes/Forms/StepForm/index.js @@ -0,0 +1,63 @@ +import React, { cloneElement, PureComponent } from 'react'; +import { connect } from 'dva'; +import { Card, Steps, Form } from 'antd'; +import PageHeaderLayout from '../../../layouts/PageHeaderLayout'; +import Step1 from './Step1'; +import styles from '../style.less'; + +const Step = Steps.Step; + +@Form.create() +class StepForm extends PureComponent { + getCurrentStep() { + const { routes } = this.props; + switch (routes[routes.length - 1].path) { + case 'step-form': return 0; + case 'confirm': return 1; + case 'result': return 2; + default: return 0; + } + } + render() { + const { form, stepFormData, submitting, dispatch, children } = this.props; + const formItemLayout = { + labelCol: { + span: 5, + }, + wrapperCol: { + span: 19, + }, + }; + return ( + + +
    + + + + + + {children ? cloneElement(children, { + form, + formItemLayout, + data: stepFormData, + submitting, + dispatch, + }) : ( + + )} +
    +
    +
    + ); + } +} + +export default connect(state => ({ + stepFormData: state.form.step, + submitting: state.form.stepFormSubmitting, +}))(StepForm); diff --git a/src/routes/Forms/StepForm/style.less b/src/routes/Forms/StepForm/style.less new file mode 100644 index 00000000..16850fa5 --- /dev/null +++ b/src/routes/Forms/StepForm/style.less @@ -0,0 +1,59 @@ +@import "~antd/lib/style/themes/default.less"; + +.stepForm { + margin: 40px auto; + max-width: 500px; +} + +.stepFormText { + :global { + .ant-form-item-label, + .ant-form-item-control { + line-height: 22px; + } + } +} + +.result { + margin: 0 auto; + max-width: 520px; + padding: 32px 0; +} + +.desc { + h3 { + font-size: 14px; + margin: 8px 0; + color: @text-color-secondary; + } + h4 { + margin: 2px 0; + color: @text-color-secondary; + } + p { + margin-bottom: 16px; + } + padding: 0 34px; + color: @text-color-secondary; + font-size: 12px; +} + +.information { + line-height: 22px; + :global { + .ant-row:not(:last-child) { + margin-bottom: 24px; + } + } + .label { + font-weight: 500; + text-align: right; + padding-right: 8px; + } +} + +.money { + font-weight: 500; + font-size: 20px; + line-height: 22px; +} diff --git a/src/routes/Forms/TableForm.js b/src/routes/Forms/TableForm.js new file mode 100644 index 00000000..5be2baae --- /dev/null +++ b/src/routes/Forms/TableForm.js @@ -0,0 +1,200 @@ +import React, { PureComponent } from 'react'; +import { Table, Button, Input, message } from 'antd'; +import styles from './style.less'; + +export default class TableForm extends PureComponent { + constructor(props) { + super(props); + + this.state = { + data: props.value, + }; + } + componentWillReceiveProps(nextProps) { + if ('value' in nextProps) { + this.setState({ + data: nextProps.value, + }); + } + } + getRowByKey(key) { + return this.state.data.filter(item => item.key === key)[0]; + } + index = 0; + cacheOriginData = {}; + handleSubmit = (e) => { + e.preventDefault(); + this.props.form.validateFieldsAndScroll((err, values) => { + if (!err) { + this.props.dispatch({ + type: 'form/submit', + payload: values, + }); + } + }); + } + toggleEditable(e, key) { + e.preventDefault(); + const target = this.getRowByKey(key); + if (target) { + // 进入编辑状态时保存原始数据 + if (!target.editable) { + this.cacheOriginData[key] = { ...target }; + } + target.editable = !target.editable; + this.setState({ data: [...this.state.data] }); + } + } + remove(e, key) { + e.preventDefault(); + const newData = this.state.data.filter(item => item.key !== key); + this.setState({ data: newData }); + this.props.onChange(newData); + } + newMember = () => { + const newData = [...this.state.data]; + newData.push({ + key: `NEW_TEMP_ID_${this.index}`, + workId: '', + name: '', + department: '', + editable: true, + }); + this.index += 1; + this.setState({ data: newData }); + } + handleFieldChange(e, fieldName, key) { + const newData = [...this.state.data]; + const target = this.getRowByKey(key); + if (target) { + target[fieldName] = e.target.value; + this.setState({ data: newData }); + } + } + saveRow(e, key) { + const target = this.getRowByKey(key); + if (!target.workId || !target.name || !target.department) { + message.error('请填写完整成员信息。'); + return; + } + this.toggleEditable(e, key); + this.props.onChange(this.state.data); + } + cancel(e, key) { + e.preventDefault(); + const target = this.getRowByKey(key); + if (this.cacheOriginData[key]) { + Object.assign(target, this.cacheOriginData[key]); + target.editable = false; + delete this.cacheOriginData[key]; + } + this.setState({ data: [...this.state.data] }); + } + render() { + const columns = [{ + title: '成员姓名', + dataIndex: 'name', + key: 'name', + width: '20%', + render: (text, record) => { + if (record.editable) { + return ( + this.handleFieldChange(e, 'name', record.key)} + placeholder="成员姓名" + /> + ); + } + return text; + }, + }, { + title: '工号', + dataIndex: 'workId', + key: 'workId', + width: '20%', + render: (text, record) => { + if (record.editable) { + return ( + this.handleFieldChange(e, 'workId', record.key)} + placeholder="工号" + /> + ); + } + return text; + }, + }, { + title: '所属部门', + dataIndex: 'department', + key: 'department', + width: '40%', + render: (text, record) => { + if (record.editable) { + return ( + this.handleFieldChange(e, 'department', record.key)} + placeholder="所属部门" + /> + ); + } + return text; + }, + }, { + title: '操作', + key: 'action', + render: (text, record) => { + if (record.editable) { + if (record.key.indexOf('NEW_TEMP_ID_') >= 0) { + return ( + + this.saveRow(e, record.key)}>保存 + + this.remove(e, record.key)}>删除 + + ); + } + return ( + + this.saveRow(e, record.key)}>保存 + + this.cancel(e, record.key)}>取消 + + ); + } + return ( + + this.toggleEditable(e, record.key)}>编辑 + + this.remove(e, record.key)}>删除 + + ); + }, + }]; + + return ( +
    +
    { + return record.editable ? styles.editable : ''; + }} + /> + + + ); + } +} diff --git a/src/routes/Forms/style.less b/src/routes/Forms/style.less new file mode 100644 index 00000000..18f256b2 --- /dev/null +++ b/src/routes/Forms/style.less @@ -0,0 +1,78 @@ +@import "~antd/lib/style/themes/default.less"; + +.card { + margin-bottom: 24px; +} + +.heading { + font-size: 14px; + line-height: 22px; + margin: 0 0 16px 0; +} + +.steps { + max-width: 750px; + margin: 16px auto; +} + +.divider { + border: 0; + border-top: 1px solid @border-color-split; + height: 1px; + margin: 0 0 24px 0; +} + +.errorIcon { + cursor: pointer; + color: @error-color; + margin-right: 24px; + i { + margin-right: 4px; + } +} + +.errorPopover { + :global { + .ant-popover-inner-content { + padding: 0; + max-height: 400px; + overflow: auto; + min-width: 240px; + } + } +} + +.errorListItem { + list-style: none; + border-bottom: 1px solid @border-color-split; + padding: 8px 24px; + cursor: pointer; + transition: all .3s; + &:hover { + background: @primary-1; + } + &:last-child { + border: 0; + } + .errorIcon { + color: @error-color; + float: left; + margin-top: 4px; + margin-right: 8px; + padding-bottom: 22px; + } + .errorField { + font-size: 12px; + color: @text-color-secondary; + margin-top: 4px; + } +} + +// 避免表格编辑模式切换时抖动 +.editable { + td { + transition: none !important; + padding-top: 12.5px !important; + padding-bottom: 12.5px !important; + } +} diff --git a/src/routes/List/BasicList.js b/src/routes/List/BasicList.js new file mode 100644 index 00000000..90063e51 --- /dev/null +++ b/src/routes/List/BasicList.js @@ -0,0 +1,145 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { List, Card, Row, Col, Radio, Input, Progress, Button, Icon, Dropdown, Menu, Avatar } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './BasicList.less'; + +const RadioButton = Radio.Button; +const RadioGroup = Radio.Group; +const Search = Input.Search; + +@connect(state => ({ + list: state.list, +})) +export default class BasicList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 5, + }, + }); + } + + render() { + const { list: { list, loading } } = this.props; + + const Info = ({ title, value, bordered }) => ( +
    + {title} +

    {value}

    + {bordered && } +
    + ); + + const extraContent = ( +
    + + 全部 + 进行中 + 等待中 + + ({})} + /> +
    + ); + + const paginationProps = { + showSizeChanger: true, + showQuickJumper: true, + pageSize: 5, + total: 50, + }; + + const ListContent = ({ data: { owner, createdAt, percent, status } }) => ( +
    +
    + Owner +

    {owner}

    +
    +
    + 开始时间 +

    {moment(createdAt).format('YYYY-MM-DD hh:mm')}

    +
    +
    + +
    +
    + ); + + const menu = ( + + + 编辑 + + + 删除 + + + ); + + const MoreBtn = () => ( + + + 更多 + + + ); + + return ( + +
    + + +
    + + + + + + + + + + + + + + + { + list && list.map(item => ( + 编辑, ]} + > + } + title={{item.title}} + description={item.subDescription} + /> + + + )) + } + + + + + ); + } +} diff --git a/src/routes/List/BasicList.less b/src/routes/List/BasicList.less new file mode 100644 index 00000000..e530c2ff --- /dev/null +++ b/src/routes/List/BasicList.less @@ -0,0 +1,103 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.standardList { + :global { + .ant-list-pagination { + text-align: right; + } + } + .headerInfo { + position: relative; + text-align: center; + & > span { + color: @text-color-secondary; + display: inline-block; + font-size: @font-size-base; + line-height: 22px; + margin-bottom: 4px; + } + & > p { + color: @heading-color; + font-size: 24px; + line-height: 32px; + } + & > em { + background-color: @border-color-split; + position: absolute; + height: 56px; + width: 1px; + top: 0; + right: 0; + } + } + .listContent { + margin-left: 24px; + font-size: 0; + & > div { + color: @text-color-secondary; + display: inline-block; + font-size: @font-size-base; + margin-left: 32px; + & > span { + line-height: 20px; + } + & > p { + margin-top: 4px; + line-height: 22px; + } + } + & > div:last-child { + position: relative; + top: -16px; + width: 188px; + } + } + .extraContentSearch { + margin-left: 16px; + width: 272px; + } +} + +@media screen and (max-width: @screen-sm) { + .standardList { + .extraContentSearch { + margin-left: 0; + width: 100%; + } + .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%; + } + } + } +} + +@media screen and (max-width: @screen-lg) and (min-width: @screen-md) { + .standardList { + .listContent { + & > div { + display: block; + } + & > div:last-child { + top: 0; + width: 100%; + } + } + } +} diff --git a/src/routes/List/CardList.js b/src/routes/List/CardList.js new file mode 100644 index 00000000..f406fca4 --- /dev/null +++ b/src/routes/List/CardList.js @@ -0,0 +1,91 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Row, Col, Card, Avatar, Spin, Button, Icon } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './CardList.less'; + +@connect(state => ({ + list: state.list, +})) +export default class CardList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + + render() { + const { list: { list, loading } } = this.props; + + const content = ( +
    +

    段落示意:蚂蚁金服务设计平台-design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态, + 提供跨越设计与开发的体验解决方案。

    + +
    + ); + + const extraContent = ( +
    + 这是一个标题 +
    + ); + + return ( + +
    + { + loading ? + + : + +
    + + + { + list && list.map(item => ( + + 操作一, 操作二]} + > + } + title={item.title} + description={( +

    + {item.description} +

    + )} + /> +
    + + )) + } + + } + + + ); + } +} diff --git a/src/routes/List/CardList.less b/src/routes/List/CardList.less new file mode 100644 index 00000000..664da006 --- /dev/null +++ b/src/routes/List/CardList.less @@ -0,0 +1,71 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.cardList { + :global { + .ant-card-meta-content { + margin-top: 0; + } + } +} + +.extraImg { + margin-top: -60px; + text-align: center; +} + +.newButton { + background-color: transparent; + border-color: @border-color-base; + color: @text-color-secondary; + width: 100%; + height: 178px; + &:hover { + background-color: transparent; + } +} + +.cardDescription { + .textOverflowMulti(); +} + +.pageHeaderContent { + position: relative; +} + +.contentLink { + margin-top: 16px; + a { + margin-right: 32px; + } + img { + vertical-align: middle; + margin-right: 8px; + } +} + +@media screen and (max-width: @screen-lg) { + .contentLink { + a { + margin-right: 16px; + } + } +} + +@media screen and (max-width: @screen-sm) { + .pageHeaderContent { + padding-bottom: 30px; + } + .contentLink { + position: absolute; + left: 0; + bottom: -4px; + width: 1000px; + a { + margin-right: 16px; + } + img { + margin-right: 4px; + } + } +} diff --git a/src/routes/List/CoverCardList.js b/src/routes/List/CoverCardList.js new file mode 100644 index 00000000..00cb75f0 --- /dev/null +++ b/src/routes/List/CoverCardList.js @@ -0,0 +1,206 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Row, Col, Form, Card, Select, Spin } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import StandardFormRow from '../../components/StandardFormRow'; +import TagSelect from '../../components/TagSelect'; +import AvatarList from '../../components/AvatarList'; +import SearchInput from '../../components/SearchInput'; + +import styles from './CoverCardList.less'; + +const Option = Select.Option; +const FormItem = Form.Item; +const TagOption = TagSelect.Option; +const TagExpand = TagSelect.Expand; + +/* eslint react/no-array-index-key: 0 */ +@Form.create() +@connect(state => ({ + list: state.list, +})) +export default class CoverCardList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + + handleFormSubmit = () => { + const { form, dispatch } = this.props; + // setTimeout 用于保证获取表单值是在所有表单字段更新完毕的时候 + setTimeout(() => { + form.validateFields((err) => { + if (!err) { + // eslint-disable-next-line + dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + }); + }, 0); + } + + render() { + const { list: { list = [], loading }, form } = this.props; + const { getFieldDecorator } = form; + + const cardList = list ? ( + + { + list.map(item => ( +
    + } + > + +
    + {moment(item.updatedAt).fromNow()} +
    + + { + item.members.map((member, i) => ( + + )) + } + +
    +
    +
    + + )) + } + + ) : null; + + const tabList = [ + { + key: 'docs', + tab: '文章', + }, + { + key: 'app', + tab: '应用', + }, + { + key: 'project', + tab: '项目', + }, + ]; + + const pageHeaderContent = ( +
    + +
    + ); + + const formItemLayout = { + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + return ( + +
    + +
    + + + {getFieldDecorator('category')( + + 类目一 + 类目二 + 类目三 + 类目四 + + 类目五 + 类目六 + + + )} + + + + +
    + + {getFieldDecorator('author', {})( + + )} + + + + + {getFieldDecorator('rate', {})( + + )} + + + + + + + { + loading && (list.length > 0) && + {cardList} + + } + { + loading && (list.length < 1) &&
    + } + { + !loading && cardList + } + + + ); + } +} diff --git a/src/routes/List/CoverCardList.less b/src/routes/List/CoverCardList.less new file mode 100644 index 00000000..d4c7eade --- /dev/null +++ b/src/routes/List/CoverCardList.less @@ -0,0 +1,24 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.coverCardList { + :global { + .ant-card-meta-description { + font-size: 12px; + } + } + .cardItemContent { + display: flex; + margin-top: 8px; + line-height: 20px; + height: 20px; + span { + flex: 1; + font-size: 12px; + color: @disabled-color; + } + .avatarList { + flex: 0 1 auto; + } + } +} diff --git a/src/routes/List/FilterCardList.js b/src/routes/List/FilterCardList.js new file mode 100644 index 00000000..6f26beb7 --- /dev/null +++ b/src/routes/List/FilterCardList.js @@ -0,0 +1,211 @@ +import React, { PureComponent } from 'react'; +import numeral from 'numeral'; +import { connect } from 'dva'; +import { Row, Col, Form, Card, Select, Spin, Icon, Avatar } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import StandardFormRow from '../../components/StandardFormRow'; +import TagSelect from '../../components/TagSelect'; +import SearchInput from '../../components/SearchInput'; + +import styles from './FilterCardList.less'; + +const Option = Select.Option; +const FormItem = Form.Item; +const TagOption = TagSelect.Option; +const TagExpand = TagSelect.Expand; + +const formatWan = (val) => { + const v = val * 1; + if (!v || isNaN(v)) return ''; + + let result = val; + if (val > 10000) { + result = Math.floor(val / 10000); + result = {result}; + } + return result; +}; + +/* eslint react/no-array-index-key: 0 */ +@Form.create() +@connect(state => ({ + list: state.list, +})) +export default class FilterCardList extends PureComponent { + componentDidMount() { + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + + handleFormSubmit = () => { + const { form, dispatch } = this.props; + // setTimeout 用于保证获取表单值是在所有表单字段更新完毕的时候 + setTimeout(() => { + form.validateFields((err) => { + if (!err) { + // eslint-disable-next-line + dispatch({ + type: 'list/fetch', + payload: { + count: 8, + }, + }); + } + }); + }, 0); + } + + render() { + const { list: { list, loading }, form } = this.props; + const { getFieldDecorator } = form; + + const tabList = [ + { + key: 'docs', + tab: '文章', + }, + { + key: 'apps', + tab: '应用', + default: true, + }, + { + key: 'projects', + tab: '项目', + }, + ]; + + const CardInfo = ({ activeUser, newUser }) => ( +
    +
    +

    活跃用户

    +

    {activeUser}

    + +
    +
    +

    新增用户

    +

    {newUser}

    +
    +
    + ); + + const pageHeaderContent = ( +
    + +
    + ); + + const formItemLayout = { + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + return ( + +
    + +
    + + + {getFieldDecorator('category')( + + 类目一 + 类目二 + 类目三 + 类目四 + + 类目五 + 类目六 + + + )} + + + + +
    + + {getFieldDecorator('author', {})( + + )} + + + + + {getFieldDecorator('rate', {})( + + )} + + + + + + + + { + loading && + } + { + !loading && list && list.map(item => ( + + , , , ]} + > + } + title={item.title} + /> +
    + +
    +
    + + )) + } + + + + ); + } +} diff --git a/src/routes/List/FilterCardList.less b/src/routes/List/FilterCardList.less new file mode 100644 index 00000000..679cec45 --- /dev/null +++ b/src/routes/List/FilterCardList.less @@ -0,0 +1,54 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.filterCardList { + :global { + .ant-card-meta-title { + position: relative; + top: 8px; + } + .ant-card-meta-content { + margin-top: 0; + } + } + .cardInfo { + .clearfix(); + border: 1px solid @border-color-base; + border-radius: @border-radius-base; + padding: 8px 0; + margin-top: 16px; + width: 100%; + & > div { + position: relative; + text-align: center; + float: left; + width: 50%; + & > span { + background-color: @border-color-split; + position: absolute; + top: 0; + right: 0; + width: 1px; + height: 44px; + } + p { + color: @text-color-secondary; + line-height: 32px; + font-size: 24px; + } + p:first-child { + font-size: 12px; + line-height: 20px; + } + } + } +} + +.wan { + position: relative; + top: -2px; + font-size: @font-size-base; + font-style: normal; + line-height: 20px; + margin-left: 2px; +} diff --git a/src/routes/List/SearchList.js b/src/routes/List/SearchList.js new file mode 100644 index 00000000..682c1573 --- /dev/null +++ b/src/routes/List/SearchList.js @@ -0,0 +1,274 @@ +import React, { Component } from 'react'; +import moment from 'moment'; +import { connect } from 'dva'; +import { Form, Card, Select, List, Tag, Icon, Avatar, Row, Col } from 'antd'; + +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import StandardFormRow from '../../components/StandardFormRow'; +import TagSelect from '../../components/TagSelect'; +import SearchInput from '../../components/SearchInput'; +import styles from './SearchList.less'; + +const Option = Select.Option; +const FormItem = Form.Item; +const TagOption = TagSelect.Option; +const TagExpand = TagSelect.Expand; + +@Form.create() +@connect(state => ({ + list: state.list, +})) +export default class SearchList extends Component { + state = { + count: 3, + showLoadMore: true, + loadingMore: false, + } + + componentDidMount() { + const { count } = this.state; + this.props.dispatch({ + type: 'list/fetch', + payload: { + count, + }, + }); + } + + setOwner = () => { + const { form } = this.props; + form.setFieldsValue({ + owner: ['wzj'], + }); + } + + handleLoadMore = () => { + const { count } = this.state; + const nextCount = count + 5; + + this.setState({ + count: nextCount, + loadingMore: true, + }); + this.props.dispatch({ + type: 'list/fetch', + payload: { + count: nextCount, + }, + callback: () => { + this.setState({ + loadingMore: false, + }); + + // fack count + if (nextCount < 10) { + this.setState({ + showLoadMore: false, + }); + } + }, + }); + } + + render() { + const { showLoadMore, loadingMore } = this.state; + const { form, list: { list } } = this.props; + const { getFieldDecorator } = form; + + const owners = [ + { + id: 'wzj', + name: '我自己', + }, + { + id: 'wjh', + name: '吴家豪', + }, + { + id: 'zxx', + name: '周星星', + }, + { + id: 'zly', + name: '赵丽颖', + }, + { + id: 'ym', + name: '姚明', + }, + ]; + + const tabList = [ + { + key: 'docs', + tab: '文章', + }, + { + key: 'app', + tab: '应用', + }, + { + key: 'project', + tab: '项目', + }, + ]; + + const IconText = ({ type, text }) => ( + + + {text} + + ); + + const ListContent = ({ data: { content, updatedAt, avatar, owner, href } }) => ( +
    +

    {content}

    +
    + {owner} 发布在 {href} + {moment(updatedAt).format('YYYY-MM-DD hh:mm')} +
    +
    + ); + + const pageHeaderContent = ( +
    + +
    + ); + + const formItemLayout = { + wrapperCol: { + xs: { span: 24 }, + sm: { span: 16 }, + }, + }; + + return ( + +
    + +
    + + + {getFieldDecorator('category')( + + 类目一 + 类目二 + 类目三 + 类目四 + + 类目五 + 类目六 + + + )} + + + + +
    + + {getFieldDecorator('owner', { + initialValue: ['wjh', 'zxx'], + })( + + )} + 只看自己的 + + + + + + + + + {getFieldDecorator('user', {})( + + )} + + + + + {getFieldDecorator('rate', {})( + + {getFieldDecorator('rate', {})( + + )} + + )} + + + + + + + + 0) && showLoadMore} + onLoadMore={this.handleLoadMore} + itemLayout="vertical" + > + { + list && list.map(item => ( + , , ]} + extra={
    } + > + {item.title}} + description={Ant Design设计语言蚂蚁金服} + /> + + + )) + } + + +
    + + ); + } +} diff --git a/src/routes/List/SearchList.less b/src/routes/List/SearchList.less new file mode 100644 index 00000000..87ef3d77 --- /dev/null +++ b/src/routes/List/SearchList.less @@ -0,0 +1,45 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.listContent { + p { + line-height: 24px; + } + & > div { + color: @text-color-secondary; + margin-top: 16px; + height: 22px; + line-height: 22px; + img { + margin-right: 16px; + } + & > span { + vertical-align: top; + margin-right: 16px; + width: 20px; + height: 20px; + } + & > em { + color: @disabled-color; + font-style: normal; + margin-left: 24px; + } + a { + color: @text-color-secondary; + &:hover { + color: @primary-color; + } + } + } +} +.listItemExtra { + width: 272px; + height: 1px; +} + +@media screen and (max-width: @screen-lg) { + .listItemExtra { + width: 0; + height: 1px; + } +} diff --git a/src/routes/List/TableList.js b/src/routes/List/TableList.js new file mode 100644 index 00000000..a0d9f4f8 --- /dev/null +++ b/src/routes/List/TableList.js @@ -0,0 +1,265 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'dva'; +import { Card, Row, Col, Form, Input, Select, Icon, Button, Dropdown, Menu, InputNumber, DatePicker, Modal, message } from 'antd'; +import StandardTable from '../../components/StandardTable'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +import styles from './TableList.less'; + +const FormItem = Form.Item; +const Option = Select.Option; +const getValue = obj => Object.keys(obj).map(key => obj[key]).join(','); + +@connect(state => ({ + rule: state.rule, +})) +@Form.create() +export default class TableList extends PureComponent { + state = { + addInputValue: '', + modalVisible: false, + expandForm: false, + selectedRows: [], + formValues: {}, + }; + + 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, + }); + } + + handleFormReset = () => { + const { form, dispatch } = this.props; + form.resetFields(); + dispatch({ + type: 'rule/fetch', + payload: {}, + }); + } + + toggleForm = () => { + this.setState({ + expandForm: !this.state.expandForm, + }); + } + + handleMenuClick = (e) => { + const { dispatch } = this.props; + const { selectedRows } = this.state; + + if (!selectedRows) return; + + switch (e.key) { + case 'remove': + dispatch({ + type: 'rule/remove', + payload: { + no: selectedRows.map(row => row.no).join(','), + }, + 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, + }); + } + + handleAddInput = (e) => { + this.setState({ + addInputValue: e.target.value, + }); + } + + handleAdd = () => { + this.props.dispatch({ + type: 'rule/add', + payload: { + description: this.state.addInputValue, + }, + }); + + message.success('添加成功'); + this.setState({ + modalVisible: false, + }); + } + + render() { + const { rule: { loading: ruleLoading, data }, form: { getFieldDecorator } } = this.props; + const { selectedRows, modalVisible, addInputValue } = this.state; + + const formItemLayout = { + labelCol: { span: 5 }, + wrapperCol: { span: 19 }, + }; + + const menu = ( + + 删除 + { + selectedRows.length > 1 && 批量审批 + } + + ); + + return ( + + +
    +
    +
    + +
    + + {getFieldDecorator('no')( + + )} + + + + + {getFieldDecorator('status')( + + )} + + + + + + + { + this.state.expandForm && + + + {getFieldDecorator('updatedAt')( + + )} + + + + + {getFieldDecorator('callNo')( + } + placeholder="请输入" + /> + )} + + + + } + + +
    + + + + + +
    + + + + this.handleModalVisible()} + > + + + + + + ); + } +} diff --git a/src/routes/List/TableList.less b/src/routes/List/TableList.less new file mode 100644 index 00000000..0af787a6 --- /dev/null +++ b/src/routes/List/TableList.less @@ -0,0 +1,23 @@ +@import "~antd/lib/style/themes/default.less"; +@import "../../utils/utils.less"; + +.tableList { + .tableListOperator { + margin-bottom: 16px; + button { + margin-right: 8px; + } + } +} + +.formButton { + margin-left: 40px; + position: relative; + top: 2px; +} + +@media screen and (max-width: @screen-md) { + .formButton { + margin-left: 0; + } +} diff --git a/src/routes/Profile.js b/src/routes/Profile.js new file mode 100644 index 00000000..bf0a354b --- /dev/null +++ b/src/routes/Profile.js @@ -0,0 +1,255 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { Button, Menu, Dropdown, Icon, Row, Col, Steps, Card, Popover, Badge, Table, Tooltip } from 'antd'; +import PageHeaderLayout from '../layouts/PageHeaderLayout'; +import DescriptionList from '../components/DescriptionList'; +import styles from './Profile.less'; + +const { Step } = Steps; +const { Description } = DescriptionList; + +const menu = ( + + 选项一 + 选项二 + 选项三 + +); + +const action = ( +
    + + + + + +
    +); + +const extra = ( + +
    +
    状态
    +
    待审批
    + +
    +
    订单金额
    +
    ¥ 568.08
    + + +); + +const description = ( + + 曲丽丽 + 12421 + 2017-07-07 + 2017-07-07 ~ 2017-08-08 + 修改公司地址:浙江省杭州市西湖区工专路 + +); + +const tabList = [{ + key: 'detail', + tab: '详情', +}, { + key: 'rule', + tab: '规则', +}]; + +const desc1 = ( +
    +
    + 曲丽丽 +
    +
    2016-12-12 12:32
    +
    +); + +const desc2 = ( +
    +
    + 周毛毛 +
    + +
    +); + +const popoverContent = ( +
    + 吴加号 + + + +

    耗时:2小时25分钟

    +
    +); + +const customDot = (dot, { status }) => (status === 'process' ? + + {dot} + + : 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' ? : + ), +}, { + title: '操作时间', + dataIndex: 'updatedAt', + key: 'updatedAt', +}, { + title: '备注', + dataIndex: 'memo', + key: 'memo', +}]; + +@connect(state => ({ + profile: state.profile, +})) +export default class Profile extends Component { + state = { + operationkey: 'tab1', + } + + componentDidMount() { + const { dispatch } = this.props; + dispatch({ + type: 'profile/fetch', + }); + } + + onOperationTabChange = (key) => { + this.setState({ operationkey: key }); + } + + render() { + const { profile } = this.props; + const { loading, operation1, operation2, operation3 } = profile; + const contentList = { + tab1:
    , + tab2:
    , + tab3:
    , + }; + + return ( + } + action={action} + content={description} + extraContent={extra} + tabList={tabList} + > + + + + + + + + + + + 付小小 + 32943898021309809423 + 3321944288191034921 + 18322193472 + 曲丽丽 18100000000 浙江省杭州市西湖区黄姑山路工专路交叉路口 + + + 725 + 2017-08-08 + + 某某数据 + + + + + } + > + 725 + + 2017-08-08 + + + + 林东东 + 1234567 + XX公司 - YY部 + 2017-08-08 + 这段描述很长很长很长很长很长很长很长很长很长很长很长很长很长很长... + +
    + + + Citrullus lanatus (Thunb.) Matsum. et Nakai一年生蔓生藤本;茎、枝粗壮,具明显的棱。卷须较粗.. + + +
    + + 付小小 + 1234568 + + + + +
    + 暂无数据 +
    +
    + + {contentList[this.state.operationkey]} + + + ); + } +} diff --git a/src/routes/Profile.less b/src/routes/Profile.less new file mode 100644 index 00000000..f33e342f --- /dev/null +++ b/src/routes/Profile.less @@ -0,0 +1,27 @@ +@import "~antd/lib/style/themes/default.less"; + +.tabsCard { + margin-bottom: 24px; +} + +.noData { + color: @disabled-color; + text-align: center; + line-height: 64px; +} + +.heading { + color: @heading-color; + font-size: 20px; +} + +.textSecondary { + color: @text-color-secondary; +} + +.divider { + border: 0; + border-top: 1px solid @border-color-split; + height: 1px; + margin: 0 0 24px 0; +} diff --git a/src/routes/Result/Error.js b/src/routes/Result/Error.js new file mode 100644 index 00000000..a047205d --- /dev/null +++ b/src/routes/Result/Error.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Button, Icon, Card } from 'antd'; +import Result from '../../components/Result'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +const extra = ( +
    +
    + 您提交的内容有如下错误: +
    +
    + 您的账户已被冻结 + 立即解冻 +
    +
    + 您的账户还不具备申请资格 + 立即升级 +
    +
    +); + +const actions = ; + +export default () => ( + + + + + +); diff --git a/src/routes/Result/Success.js b/src/routes/Result/Success.js new file mode 100644 index 00000000..a1fb6e51 --- /dev/null +++ b/src/routes/Result/Success.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { Button, Row, Col, Icon, Steps, Card } from 'antd'; +import Result from '../../components/Result'; +import PageHeaderLayout from '../../layouts/PageHeaderLayout'; + +const Step = Steps.Step; + +const desc1 = ( +
    +
    + 曲丽丽 +
    +
    2016-12-12 12:32
    +
    +); + +const desc2 = ( +
    +
    + 周毛毛 +
    + +
    +); + +const extra = ( +
    +
    + 项目名称 +
    + +
    项目 ID:23421 + 负责人:曲丽丽 + 生效时间:2016-12-12 ~ 2017-12-12 + + + + + + + + +); + +const actions = ( +
    + + + +
    +); + +export default () => ( + + + + + +); diff --git a/src/routes/User/Login.js b/src/routes/User/Login.js new file mode 100644 index 00000000..7e424f44 --- /dev/null +++ b/src/routes/User/Login.js @@ -0,0 +1,172 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { routerRedux, Link } from 'dva/router'; +import { Form, Input, Tabs, Button, Icon, Checkbox, Row, Col, Alert } from 'antd'; +import styles from './Login.less'; + +const FormItem = Form.Item; +const TabPane = Tabs.TabPane; + +@connect(state => ({ + login: state.login, +})) +@Form.create() +export default class Login extends Component { + state = { + count: 0, + type: 'account', + } + + componentWillReceiveProps(nextProps) { + if (nextProps.login.status === 'ok') { + this.props.dispatch(routerRedux.push('/')); + } + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + onSwitch = (key) => { + this.setState({ + type: key, + }); + } + + onGetCaptcha = () => { + let count = 59; + this.setState({ count }); + this.interval = setInterval(() => { + count -= 1; + this.setState({ count }); + if (count === 0) { + clearInterval(this.interval); + } + }, 1000); + } + + handleSubmit = (e) => { + e.preventDefault(); + const { type } = this.state; + this.props.form.validateFields({ force: true }, + (err, values) => { + if (!err) { + this.props.dispatch({ + type: `login/${type}Submit`, + payload: values, + }); + } + } + ); + } + + msg = (message) => { + return ; + } + + render() { + const { form, login } = this.props; + const { getFieldDecorator } = form; + const { count, type } = this.state; + return ( +
    +
    + + + {login.status === 'error' && login.type === 'account' && this.msg('账户或密码错误')} + + {getFieldDecorator('userName', { + rules: [{ + required: type === 'account', message: '请输入账户名!', + }], + })( + } + placeholder="账户" + /> + )} + + + {getFieldDecorator('password', { + rules: [{ + required: type === 'account', message: '请输入密码!', + }], + })( + } + type="password" + placeholder="密码" + /> + )} + + + + {login.status === 'error' && login.type === 'mobile' && this.msg('验证码错误')} + + {getFieldDecorator('mobile', { + rules: [{ + required: type === 'mobile', message: '请输入手机号!', + }, { + pattern: /^1\d{10}$/, message: '手机号格式错误!', + }], + })( + } + placeholder="手机号" + /> + )} + + + +
    + {getFieldDecorator('captcha', { + rules: [{ + required: type === 'mobile', message: '请输入验证码!', + }], + })( + } + placeholder="验证码" + /> + )} + + + + + + + + + + {getFieldDecorator('remember', { + valuePropName: 'checked', + initialValue: true, + })( + 自动登录 + )} + 忘记密码 + + + +
    + 其他登录方式 + {/* 需要加到 Icon 中 */} + + + + 注册账户 +
    + + ); + } +} diff --git a/src/routes/User/Login.less b/src/routes/User/Login.less new file mode 100644 index 00000000..1ebb1b29 --- /dev/null +++ b/src/routes/User/Login.less @@ -0,0 +1,79 @@ +@import "~antd/lib/style/themes/default.less"; + +.main { + width: 368px; + margin: 0 auto; + + :global { + .ant-tabs .ant-tabs-bar { + border-bottom: 0; + margin-bottom: 24px; + text-align: center; + } + + .ant-form-item { + margin-bottom: 16px; + } + } + + .getCaptcha { + display: block; + width: 100%; + } + + .additional { + text-align: left; + + .forgot { + float: right; + } + + .submit { + width: 100%; + margin-top: 16px; + } + } + + .iconAlipay, .iconTaobao, .iconWeibo { + display: inline-block; + width: 24px; + height: 24px; + background: url('https://gw.alipayobjects.com/zos/rmsportal/itDzjUnkelhQNsycranf.svg'); + margin-left: 16px; + vertical-align: middle; + cursor: pointer; + } + + .iconAlipay { + background-position: -24px 0; + + &:hover { + background-position: 0 0; + } + } + + .iconTaobao { + background-position: -24px -24px; + + &:hover { + background-position: 0 -24px; + } + } + + .iconWeibo { + background-position: -24px -48px; + + &:hover { + background-position: 0 -48px; + } + } + + .other { + text-align: left; + margin-top: 32px; + + .register { + float: right; + } + } +} diff --git a/src/routes/User/Register.js b/src/routes/User/Register.js new file mode 100644 index 00000000..93fa8e6a --- /dev/null +++ b/src/routes/User/Register.js @@ -0,0 +1,261 @@ +import React, { Component } from 'react'; +import { connect } from 'dva'; +import { routerRedux, Link } from 'dva/router'; +import { Form, Input, Button, Select, Row, Col, Popover, Progress } from 'antd'; +import styles from './Register.less'; + +const FormItem = Form.Item; +const Option = Select.Option; +const InputGroup = Input.Group; + +const passwordStatusMap = { + ok:

    强度:强

    , + pass:

    强度:中

    , + pool:

    强度:太短

    , +}; + +const passwordProgressMap = { + ok: 'success', + pass: 'normal', + pool: 'exception', +}; + +@connect(state => ({ + register: state.register, +})) +@Form.create() +export default class Register extends Component { + state = { + count: 0, + confirmDirty: false, + visible: false, + help: '', + } + + componentWillReceiveProps(nextProps) { + if (nextProps.register.status === 'ok') { + this.props.dispatch(routerRedux.push('/')); + } + } + + 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.form; + const value = form.getFieldValue('password'); + if (value && value.length > 9) { + return 'ok'; + } + if (value && value.length > 5) { + return 'pass'; + } + return 'pool'; + } + + handleSubmit = (e) => { + e.preventDefault(); + this.props.form.validateFields({ force: true }, + (err, values) => { + if (!err) { + this.props.dispatch({ + type: 'register/submit', + payload: values, + }); + } + } + ); + } + + handleConfirmBlur = (e) => { + const value = e.target.value; + this.setState({ confirmDirty: this.state.confirmDirty || !!value }); + } + + checkConfirm = (rule, value, callback) => { + const form = this.props.form; + if (value && value !== form.getFieldValue('password')) { + callback('两次输入的密码不匹配!'); + } else { + callback(); + } + } + + checkPassword = (rule, value, callback) => { + if (!value) { + this.setState({ + help: '请输入密码!', + visible: !!value, + }); + callback('error'); + } else { + this.setState({ + help: '', + }); + if (!this.state.visible) { + this.setState({ + visible: !!value, + }); + } + if (value.length < 6) { + callback('error'); + } else { + const form = this.props.form; + if (value && this.state.confirmDirty) { + form.validateFields(['confirm'], { force: true }); + } + callback(); + } + } + } + + renderPasswordProgress = () => { + const form = this.props.form; + const value = form.getFieldValue('password'); + const passwordStatus = this.getPasswordStatus(); + return value && value.length ? +
    + 100 ? 100 : value.length * 10} + showInfo={false} + /> +
    : null; + } + + render() { + const { form, register } = this.props; + const { getFieldDecorator } = form; + const { count } = this.state; + return ( +
    +

    注册

    +
    + + {getFieldDecorator('mail', { + rules: [{ + required: true, message: '请输入邮箱地址!', + }, { + type: 'email', message: '邮箱地址格式错误!', + }], + })( + + )} + + + + {passwordStatusMap[this.getPasswordStatus()]} + {this.renderPasswordProgress()} +

    请至少输入 6 个字符。请不要使用容易被猜到的密码。

    +
    + } + overlayStyle={{ width: 240 }} + placement="right" + visible={this.state.visible} + > + {getFieldDecorator('password', { + rules: [{ + validator: this.checkPassword, + }], + })( + + )} + + + + {getFieldDecorator('confirm', { + rules: [{ + required: true, message: '请确认密码!', + }, { + validator: this.checkConfirm, + }], + })( + + )} + + + + + {getFieldDecorator('prefix', { + initialValue: '86', + })( + + )} + + + {getFieldDecorator('mobile', { + rules: [{ + required: true, message: '请输入手机号!', + }, { + pattern: /^1\d{10}$/, message: '手机号格式错误!', + }], + })( + + )} + + + + + +
    + {getFieldDecorator('captcha', { + rules: [{ + required: true, message: '请输入验证码!', + }], + })( + + )} + + + + + + + + + 使用已有账户登录 + + + + ); + } +} diff --git a/src/routes/User/Register.less b/src/routes/User/Register.less new file mode 100644 index 00000000..d538f738 --- /dev/null +++ b/src/routes/User/Register.less @@ -0,0 +1,84 @@ +@import "~antd/lib/style/themes/default.less"; + +.main { + width: 368px; + margin: 0 auto; + + :global { + .ant-form-item { + margin-bottom: 16px; + } + } + + h3 { + font-size: 16px; + margin-bottom: 16px; + } + + .mobileGroup { + :global { + .ant-form-item { + margin-bottom: 0; + display: table-cell; + vertical-align: top; + + &:first-child { + width: 20%; + + .ant-select-selection { + border-right-width: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + } + + &:last-child { + width: 80%; + + .ant-input { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + } + } + } + + .getCaptcha { + display: block; + width: 100%; + } + + .submit { + width: 50%; + } + + .login { + float: right; + } +} + +.success, .warning, .error { + transition: color .3s; +} + +.success { + color: @success-color; +} + +.warning { + color: @warning-color; +} + +.error { + color: @error-color; +} + +.progress-pass > .progress { + :global { + .ant-progress-bg { + background-color: @warning-color; + } + } +} + diff --git a/src/routes/User/RegisterResult.js b/src/routes/User/RegisterResult.js new file mode 100644 index 00000000..af91ee67 --- /dev/null +++ b/src/routes/User/RegisterResult.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Button } from 'antd'; +import { Link } from 'dva/router'; +import Result from '../../components/Result'; + +const actions = ( +
    + + +
    +); + +export default () => ( +
    + +
    +); diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 00000000..39984bac --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,82 @@ +import { stringify } from 'qs'; +import request from '../utils/request'; + +export async function queryProjectNotice() { + return request('/api/project/notice'); +} + +export async function queryActivities() { + return request('/api/activities'); +} + +export async function queryRule(params) { + return request(`/api/rule?${stringify(params)}`); +} + +export async function removeRule(params) { + return request('/api/rule', { + method: 'POST', + body: { + ...params, + method: 'delete', + }, + }); +} + +export async function addRule(params) { + return request('/api/rule', { + method: 'POST', + body: { + ...params, + method: 'post', + }, + }); +} + +export async function fakeSubmitForm(params) { + return request('/api/forms', { + method: 'POST', + body: params, + }); +} + +export async function fakeChartData() { + return request('/api/fake_chart_data'); +} + +export async function queryTags() { + return request('/api/tags'); +} + +export async function queryProfile() { + return request('/api/profile'); +} + +export async function queryFakeList(params) { + return request(`/api/fake_list?${stringify(params)}`); +} + +export async function fakeAccountLogin(params) { + return request('/api/login/account', { + method: 'POST', + body: params, + }); +} + +export async function fakeMobileLogin(params) { + return request('/api/login/mobile', { + method: 'POST', + body: params, + }); +} + +export async function fakeRegister(params) { + return request('/api/register', { + method: 'POST', + body: params, + }); +} + +export async function queryNotices() { + return request('/api/notices'); +} diff --git a/src/services/user.js b/src/services/user.js new file mode 100644 index 00000000..c4defb4f --- /dev/null +++ b/src/services/user.js @@ -0,0 +1,9 @@ +import request from '../utils/request'; + +export async function query() { + return request('/api/users'); +} + +export async function queryCurrent() { + return request('/api/currentUser'); +} diff --git a/src/utils/request.js b/src/utils/request.js new file mode 100644 index 00000000..4d62c87f --- /dev/null +++ b/src/utils/request.js @@ -0,0 +1,38 @@ +import fetch from 'dva/fetch'; + +function checkStatus(response) { + if (response.status >= 200 && response.status < 300) { + return response; + } + + const error = new Error(response.statusText); + error.response = response; + throw error; +} + +/** + * Requests a URL, returning a promise. + * + * @param {string} url The URL we want to request + * @param {object} [options] The options we want to pass to "fetch" + * @return {object} An object containing either "data" or "err" + */ +export default function request(url, options) { + const defaultOptions = { + credentials: 'include', + }; + const newOptions = { ...defaultOptions, ...options }; + if (newOptions.method === 'POST' || newOptions.method === 'PUT') { + newOptions.headers = { + Accept: 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + ...newOptions.headers, + }; + newOptions.body = JSON.stringify(newOptions.body); + } + + return fetch(url, newOptions) + .then(checkStatus) + .then(response => response.json()) + .catch(err => ({ err })); +} diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 00000000..47366503 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,55 @@ +import moment from 'moment'; + +function fixedZero(val) { + return val * 1 < 10 ? `0${val}` : val; +} + +function getTimeDistance(type) { + const now = new Date(); + const oneDay = 1000 * 60 * 60 * 24; + + if (type === 'today') { + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + return [moment(now), moment(now.getTime() + (oneDay - 1000))]; + } + + if (type === 'week') { + let day = now.getDay(); + now.setHours(0); + now.setMinutes(0); + now.setSeconds(0); + + if (day === 0) { + day = 6; + } else { + day -= 1; + } + + const beginTime = now.getTime() - (day * oneDay); + + return [moment(beginTime), moment(beginTime + ((7 * oneDay) - 1000))]; + } + + if (type === 'month') { + const year = now.getFullYear(); + const month = now.getMonth(); + const nextDate = moment(now).add(1, 'months'); + const nextYear = nextDate.year(); + const nextMonth = nextDate.month(); + + return [moment(`${year}-${fixedZero(month + 1)}-01 00:00:00`), moment(new Date(`${nextYear}-${fixedZero(nextMonth + 1)}-01 00:00:00`).getTime() - 1000)]; + } + + if (type === 'year') { + const year = now.getFullYear(); + + return [moment(`${year}-01-01 00:00:00`), moment(`${year}-12-31 23:59:59`)]; + } +} + +export default { + fixedZero, + getTimeDistance, +}; diff --git a/src/utils/utils.less b/src/utils/utils.less new file mode 100644 index 00000000..b728d9ae --- /dev/null +++ b/src/utils/utils.less @@ -0,0 +1,48 @@ +.textOverflow() { + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + white-space: nowrap; +} + +.textOverflowMulti(@line: 3) { + overflow: hidden; + position: relative; + line-height: 1.5em; + max-height: @line * 1.5em; + text-align: justify; + margin-right: -1em; + padding-right: 1em; + &:before { + content: '...'; + position: absolute; + right: 0; + bottom: 0; + } + &:after { + background: white; + content: ''; + margin-top: 0.2em; + position: absolute; + right: 0; + width: 1em; + height: 1em; + } +} + +// mixins for clearfix +// ------------------------ +.clearfix() { + zoom: 1; + &:before, + &:after { + content: " "; + display: table; + } + &:after { + clear: both; + visibility: hidden; + font-size: 0; + height: 0; + } +}