├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── devServer.js
├── index.html
├── package.json
├── src
├── common
│ └── utils.js
├── component
│ ├── App.jsx
│ ├── content
│ │ ├── Form.jsx
│ │ ├── PageIntro.jsx
│ │ ├── PageTabs.jsx
│ │ ├── Pane.jsx
│ │ ├── Table.jsx
│ │ └── Topbar.jsx
│ ├── factory.js
│ └── nav
│ │ ├── NavGlobal.jsx
│ │ └── NavMenu.jsx
├── data
│ ├── api.js
│ └── config.js
├── entry
│ └── index.jsx
├── page
│ ├── common
│ │ ├── Blank.jsx
│ │ ├── ChangePassword.jsx
│ │ ├── EmailVerify.jsx
│ │ ├── Err404.jsx
│ │ ├── Signin.jsx
│ │ └── Signup.jsx
│ └── demo
│ │ ├── FactoryForms.jsx
│ │ ├── RawForms.jsx
│ │ └── TestForms.jsx
└── style
│ ├── form.css
│ ├── index.js
│ ├── main.css
│ └── table.css
├── static
├── config.js.jinja2
└── logo48x48.png
├── webpack.config.dev.js
└── webpack.config.prod.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-1"],
3 | "env": {
4 | "development": {
5 | "presets": ["react-hmre"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | \#*\#
3 | .\#*
4 | *.swp
5 |
6 | node_modules
7 | dist
8 |
9 | npm-debug.log
10 | static/*.svg
11 | static/*.ttf
12 | static/bundle.js
13 | static/bundle.js.map
14 | static/config.js
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 HuhuLab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## What's this
2 | React([ant-design](https://github.com/ant-design/ant-design)) based frontend boilerplate project.
3 |
4 | ## How to run it
5 | ``` bash
6 | npm install
7 | # More commands see: package.json
8 | npm start
9 | # Open http://localhost:3000/#/demo/test_forms
10 | ```
11 |
--------------------------------------------------------------------------------
/devServer.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var express = require('express');
3 | var webpack = require('webpack');
4 | var config = require('./webpack.config.dev');
5 |
6 | var app = express();
7 | var compiler = webpack(config);
8 |
9 | app.use(require('webpack-dev-middleware')(compiler, {
10 | noInfo: true,
11 | publicPath: config.output.publicPath
12 | }));
13 |
14 | app.use(require('webpack-hot-middleware')(compiler));
15 |
16 | app.get('/static/config.js', function(req, res) {
17 | res.sendFile(path.join(__dirname, 'static/config.js'));
18 | });
19 |
20 | app.post('/upload', function(req, res) {
21 | res.setHeader('Content-Type', 'application/json');
22 | res.json({url: 'https://www.google.com'});
23 | });
24 |
25 | app.get('*', function(req, res) {
26 | res.sendFile(path.join(__dirname, 'index.html'));
27 | });
28 |
29 | app.listen(3000, '0.0.0.0', function(err) {
30 | if (err) {
31 | console.log(err);
32 | return;
33 | }
34 |
35 | console.log('Listening at http://0.0.0.0:3000');
36 | });
37 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-frontend-boilerplate",
3 | "version": "1.0.0",
4 | "dependencies": {
5 | "react": "^0.14.0",
6 | "react-dom": "^0.14.0",
7 | "axios": "~0.7.0",
8 | "lodash": "~4.0.0",
9 | "moment": "~2.10.6",
10 | "es6-promise": "~3.0.2",
11 | "font-awesome": "4.4.0",
12 | "react-router": "~1.0.0",
13 | "react-cookie": "^0.4.2",
14 | "react-highcharts": "^5.0.4",
15 | "babel-polyfill": "^6.3.14",
16 | "antd": "1.0.0-beta.2"
17 | },
18 | "devDependencies": {
19 | "url-loader": "~0.5.6",
20 | "style-loader": "~0.13.0",
21 | "css-loader": "~0.22.0",
22 | "less-loader": "~2.2.1",
23 | "expect.js": "~0.3.1",
24 | "babel-core": "^6.6.5",
25 | "babel-eslint": "^5.0.0-beta4",
26 | "babel-loader": "^6.2.4",
27 | "babel-preset-es2015": "^6.3.13",
28 | "babel-preset-stage-1": "^6.1.18",
29 | "babel-preset-react": "^6.3.13",
30 | "babel-preset-react-hmre": "^1.1.1",
31 | "cross-env": "^1.0.7",
32 | "eslint": "^1.10.3",
33 | "eslint-plugin-babel": "^3.0.0",
34 | "eslint-plugin-react": "^3.11.3",
35 | "eventsource-polyfill": "^0.9.6",
36 | "express": "^4.13.3",
37 | "rimraf": "^2.4.3",
38 | "webpack": "^1.12.9",
39 | "webpack-dev-middleware": "^1.4.0",
40 | "webpack-hot-middleware": "^2.9.1"
41 | },
42 | "pre-commit": [
43 | "lint"
44 | ],
45 | "scripts": {
46 | "clean": "rimraf dist",
47 | "build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js",
48 | "build": "npm run clean && npm run build:webpack",
49 | "start": "node devServer.js",
50 | "lint": "eslint src"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/common/utils.js:
--------------------------------------------------------------------------------
1 | function appendFiles(fd, name, files) {
2 | files.forEach(function(item) {
3 | if (item instanceof FileList) {
4 | var cnt = item.length;
5 | for (var i = 0; i < cnt; i++) {
6 | var file = item[i];
7 | fd.append(name, file);
8 | }
9 | } else if (item instanceof File) {
10 | fd.append(name, item);
11 | }
12 | });
13 | }
14 |
15 | export function makeFormData(obj) {
16 | var fd = new FormData();
17 | Object.keys(obj).forEach(function(key) {
18 | var value = obj[key];
19 | /* console.log('fd.k-v: ', key, value); */
20 | if (value === undefined || value === null) {
21 | fd.append(key, '');
22 | } else if (value instanceof FileList) {
23 | // Append files
24 | appendFiles(fd, key, [value]);
25 | } else if (value instanceof Array && value.length > 0 && (value[0] instanceof FileList || value[0] instanceof File)) {
26 | // Append files
27 | appendFiles(fd, key, value);
28 | } else if (value === true || value === false) {
29 | // Boolean value
30 | fd.append(key, value ? 1 : 0);
31 | } else if (value instanceof Array) {
32 | value.forEach(function(item) {
33 | fd.append(key, item);
34 | });
35 | } else if (value instanceof Object) {
36 | fd.append(key, JSON.stringify(value));
37 | } else {
38 | fd.append(key, value);
39 | }
40 | });
41 | return fd
42 | }
43 |
44 | export function getStatusClassArray(status, originValue) {
45 | const value = (originValue === undefined || originValue === null) ? originValue : String(originValue);
46 | const obj = {
47 | 'error': status.errors,
48 | 'validating': status.isValidating,
49 | 'success': value && !status.errors && !status.isValidating
50 | }
51 | return Object.keys(obj).filter(function(key) {
52 | return obj[key];
53 | });
54 | }
55 |
56 | export function getStatusClasses(status, originValue) {
57 | return getStatusClassArray(status, originValue).join(' ');
58 | }
59 |
60 | export function getStatusHelp(status) {
61 | return status.isValidating ? "正在校验中.." : status.errors ? status.errors.join(',') : null
62 | }
63 |
64 | export function updateFilters(oldFilters, name, operation, value) {
65 | let newFilters = [];
66 | let matched = false;
67 | oldFilters.forEach(function(item) {
68 | if (item[0] === name && item[1] === operation) {
69 | if (value !== undefined) {
70 | newFilters.push([name, operation, value]);
71 | }
72 | matched = true;
73 | } else {
74 | newFilters.push(item);
75 | }
76 | });
77 |
78 | if (!matched) {
79 | newFilters.push([name, operation, value]);
80 | }
81 | return newFilters;
82 | }
83 |
--------------------------------------------------------------------------------
/src/component/App.jsx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | import React from 'react';
5 | import { History } from 'react-router'
6 | import cookie from 'react-cookie';
7 | import { message, Menu, Link } from 'antd';
8 |
9 | import { tokenKey, sideMenus } from 'data/config';
10 | import { httpGet } from 'data/api';
11 | import NavGlobal from './nav/NavGlobal.jsx';
12 | import NavMenu from './nav/NavMenu.jsx';
13 |
14 |
15 | function signOut(e) {
16 | cookie.remove(tokenKey);
17 | }
18 |
19 | const topMenu = [
20 | {key: "2", href:"/user/change_password", label: "修改密码"},
21 | {key: "-1", divider: true},
22 | {key: "3", href:"/signin", onClick: signOut, iconClass: "fa fa-fw fa-sign-out", label: "注销"},
23 | ];
24 |
25 |
26 | const user = {
27 | name: '某某用户',
28 | role: {
29 | name: 'customer'
30 | }
31 | };
32 |
33 | const App = React.createClass({
34 | displayName: 'App',
35 | mixins: [ History ],
36 |
37 | getInitialState() {
38 | /* return {user: undefined}; */
39 | return {
40 | user: {
41 | name: 'Test user',
42 | role: {
43 | name: 'user',
44 | descr: 'Test role descr',
45 | }
46 | }
47 | };
48 | },
49 |
50 | componentDidMount(){
51 | console.log('NavGlobal:componentDidMount');
52 | },
53 |
54 | render() {
55 | console.log('App.props:', this.props);
56 |
57 | if (cookie.load(tokenKey) === undefined) {
58 | console.log('X-Token is missing!');
59 | /* message.error('请先登录系统!');
60 | this.history.pushState(null, '/signin'); */
61 | }
62 |
63 | console.log('current user:', this.state.user);
64 | const navMenu = this.state.user ? : '';
65 | return (
66 |
67 |
68 |
69 | {navMenu}
70 |
71 | {this.props.children}
72 |
73 |
74 |
75 | );
76 | },
77 | });
78 |
79 |
80 | /* Global pages */
81 | import Signin from 'page/common/Signin.jsx';
82 | import Signup from 'page/common/Signup.jsx';
83 | import Blank from 'page/common/Blank.jsx';
84 | import Err404 from 'page/common/Err404.jsx';
85 |
86 | /* Sub pages */
87 | import ChangePassword from 'page/common/ChangePassword.jsx';
88 | import TestForms from 'page/demo/TestForms.jsx';
89 |
90 | export const routes = [
91 | { path: '/signin', component: Signin },
92 | { path: '/signup', component: Signup },
93 | {
94 | path: '/',
95 | component: App,
96 | indexRoute: { component: Blank },
97 | childRoutes: [
98 | /* Common */
99 | {path: '/demo/test_forms', component: TestForms},
100 | {path: '/user/change_password', component: ChangePassword},
101 | /* Others */
102 | {path: '*', component: Err404},
103 | ]
104 | },
105 | ]
106 |
--------------------------------------------------------------------------------
/src/component/content/Form.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component, PropTypes } from 'react';
3 | import {
4 | Table, Button, Modal, Row, Col,
5 | /* 表单 */
6 | Form, // 表单
7 | Input, // 普通输入框:
8 | InputNumber, // 数字输入框
9 | Checkbox, // 多选框
10 | Radio, // 单选框
11 | Cascader, // 级联选择
12 | Transfer, // 穿梭框
13 | Select, // 选择器
14 | TreeSelect, // 树选择
15 | Slider, // 滑动输入条
16 | Switch, // 开关
17 | DatePicker, // 日期选择
18 | TimePicker, // 时间选择
19 | Upload, // 上传
20 | } from 'antd';
21 |
22 | import * as _ from 'lodash';
23 | import moment from 'moment';
24 |
25 | export const FormItem = Form.Item;
26 |
27 | const MAX_SPAN = 24;
28 |
29 |
30 | //// Form Helpers
31 | export const formHelpers = {
32 | // See: http://react-component.github.io/form/examples/file-input.html
33 | getFileValueProps(value) {
34 | console.log('getFileValueProps:', value);
35 | if (value && value.target) {
36 | return { value: value.target.value };
37 | }
38 | return { value };
39 | },
40 | getValueFromFileEvent({ target }) {
41 | return { target };
42 | },
43 | makeOptionElements(options) {
44 | return options.map(function(item) {
45 | return ;
46 | });
47 | }
48 | };
49 |
50 | //// Form Rules
51 | export const formRules = {
52 | fileRequired(message) {
53 | return function(rule, value, callback) {
54 | console.log('checkFile:', value);
55 | if (!value || !value.target || !value.target.files
56 | || value.target.files.length == 0) {
57 | return callback(message);
58 | }
59 | callback();
60 | };
61 | },
62 | imageDimensions(dimensions) {
63 | if (!dimensions) return undefined;
64 | if (_.isObject(dimensions)) {
65 | dimensions = [dimensions];
66 | }
67 |
68 | return function(rule, value, callback) {
69 | if (!!value && !!value.target && !!value.target.files && value.target.files.length > 0) {
70 | let promises = [];
71 | for (var i = 0; i < value.target.files.length; i++) {
72 | promises.push(new Promise(function(resolve, reject) {
73 | const file = value.target.files[i];
74 | let fr = new FileReader;
75 | fr.onload = function() { // file is loaded
76 | let img = new Image;
77 | img.onload = function() {
78 | let matched = false;
79 | dimensions.forEach(function(dimension) {
80 | if (dimension.width == img.width && dimension.height == img.height) {
81 | matched = true;
82 | }
83 | });
84 | if (!matched) {
85 | reject(`尺寸不合法: 名字=${file.name}, 宽=${img.width}, 高=${img.height}`);
86 | } else {
87 | resolve();
88 | }
89 | };
90 | img.src = fr.result; // is the data URL because called with readAsDataURL
91 | };
92 | fr.readAsDataURL(file);
93 | }));
94 | }
95 | Promise.all(promises).then(function() {
96 | callback();
97 | }).catch(function(error) {
98 | callback(error);
99 | });
100 | } else {
101 | callback();
102 | }
103 | };
104 | }
105 | };
106 |
107 |
108 | ////////////////////////////////////////////////////////////////////////////////
109 | //// Classes
110 | ////////////////////////////////////////////////////////////////////////////////
111 |
112 | class FormRow {
113 | constructor(content) {
114 | if (!_.isArray(content)) {
115 | content = [content];
116 | }
117 |
118 | if (content.length == 0) {
119 | console.error('Got empty .');
120 | }
121 |
122 | if (_.isString(content[0])) {
123 | const span = Math.floor(MAX_SPAN / content.length);
124 | this.cols = content.map(function(name) {
125 | return { name: name, span: span };
126 | });
127 | } else {
128 | this.cols = content;
129 | }
130 |
131 | /* console.log('columns:', this.cols); */
132 | /* Check column */
133 | this.cols.forEach(function(col) {
134 | if (!_.isString(col.name) || !_.isNumber(col.span)) {
135 | console.error('Invalid column:', col);
136 | }
137 | });
138 | }
139 |
140 | renderFormItem(context, name) {
141 | const { form, items, labelCol, wrapperCol } = context.props;
142 | const util = {
143 | form: form,
144 | field: name,
145 | itemProps: {
146 | key: `field-${name}`,
147 | labelCol: { span: labelCol },
148 | wrapperCol: { span: wrapperCol },
149 | }
150 | };
151 | // console.log('>>> util', util, '>>> context:', context);
152 | const item = items[name];
153 | const render = _.isFunction(item) ? item : item.render;
154 | return render.bind(context)(util);
155 | }
156 |
157 | render(context, key) {
158 | if (this.cols.length == 1 && this.cols[0].span == MAX_SPAN) {
159 | return this.renderFormItem(context, this.cols[0].name);
160 | } else {
161 | const columns = this.cols.map((col) => {
162 | const formItem = this.renderFormItem(context, col.name);
163 | let colProps = { key: col.name, span: col.span };
164 | if (_.isNumber(col.offset)) {
165 | colProps.offset = col.offset;
166 | }
167 | return { formItem };
168 | });
169 | return { columns }
;
170 | }
171 | }
172 | }
173 |
174 |
175 | export class BaseForm extends Component {
176 |
177 | static propTypes = {
178 | type : PropTypes.oneOf(["create", "update"]).isRequired,
179 | labelCol : PropTypes.number,
180 | wrapperCol : PropTypes.number,
181 | formProps : PropTypes.object,
182 | fields : PropTypes.oneOfType(
183 | [PropTypes.array, PropTypes.func]).isRequired,
184 | layout : PropTypes.oneOfType(
185 | [PropTypes.array, PropTypes.func]),
186 | items : PropTypes.object.isRequired,
187 | object : PropTypes.object,
188 | onSubmit : PropTypes.func, /* function(values, callback) or function(context, values, callback) */
189 | onSuccess : PropTypes.func, /* function(object) */
190 | }
191 |
192 | static defaultProps = {
193 | labelCol : 6,
194 | wrapperCol : 14,
195 | formProps : {horizontal: true},
196 | layout : null,
197 | object : {},
198 | onSuccess : function(object) {}
199 | }
200 |
201 |
202 | constructor(props) {
203 | super(props);
204 | console.log('BaseForm.constructor, props.object=',
205 | JSON.stringify(this.props.object));
206 | this.state = {object: this.props.object};
207 | }
208 |
209 | componentDidMount() {
210 | this.resetForm();
211 | }
212 |
213 | componentWillReceiveProps(newProps) {
214 | console.log('componentWillReceiveProps', this.props.type, newProps);
215 | if (!_.isEqual(newProps.object, this.props.object)) {
216 | this.setState({object: newProps.object}, () => {
217 | this.resetForm();
218 | });
219 | }
220 | }
221 |
222 | getFields() {
223 | const { fields } = this.props;
224 | return _.isFunction(fields) ? fields.call(this) : fields;
225 | }
226 |
227 | getLayout() {
228 | const { layout } = this.props;
229 | return _.isFunction(layout) ? layout.call(this) : (
230 | !!layout ? layout : this.getFields());
231 | }
232 |
233 | formatObject(object) {
234 | console.log('formatObject:', object);
235 | let result = {};
236 | const { items } = this.props;
237 | const fields = this.getFields();
238 | fields.map(function(field) {
239 | const item = items[field];
240 | let value = object[field];
241 | if (_.isBoolean(value)) {
242 | value = value ? '1' : '0';
243 | } else if (_.isNumber(value)) {
244 | value = String(value);
245 | } else if (_.isObject(item) && item.type === "file") {
246 | value = undefined;
247 | }
248 | result[field] = value;
249 | });
250 | console.log('>>> fields:', fields, 'result', result);
251 | return result;
252 | }
253 |
254 | handleReset() {
255 | const { type, object, form } = this.props
256 | form.resetFields();
257 | console.log('setFieldDefaults', type, object, this.state.object);
258 | const targetObject = type === "create" ? object : this.state.object;
259 | form.setFieldsValue(this.formatObject(targetObject));
260 | }
261 | resetForm() { this.handleReset() }
262 |
263 | handleSubmit(e) {
264 | console.log('BaseForm.handleSubmit', e);
265 | e.preventDefault();
266 | const { type, items, form, onSuccess, object } = this.props;
267 | const fields = this.getFields();
268 | form.validateFieldsAndScroll((errors, values) => {
269 | if (!!errors) {
270 | console.log('Errors in form!!!', errors);
271 | return;
272 | }
273 |
274 | // preprocess values
275 | if (object.id !== undefined) {
276 | values.id = object.id;
277 | }
278 | fields.forEach(function(field) {
279 | const item = items[field];
280 | const value = values[field];
281 | switch (item.type) {
282 | case "date":
283 | if (!!value ) {
284 | values[field] = moment(value).format('YYYY-MM-DD');
285 | }
286 | break;
287 | case "file":
288 | if (_.isObject(item) && value) {
289 | values[field] = value.target.files;
290 | }
291 | break;
292 | default:
293 | break;
294 | }
295 | });
296 |
297 | const callback = (newObject) => {
298 | if (type === "create") {
299 | this.handleReset();
300 | onSuccess();
301 | } else {
302 | this.setState({object: newObject}, () => {
303 | this.handleReset();
304 | onSuccess(newObject);
305 | });
306 | }
307 | };
308 |
309 | if (!!this.onSubmit) {
310 | this.onSubmit(values, callback);
311 | } else {
312 | this.props.onSubmit(this, values, callback);
313 | }
314 | console.log('Submit!!!', values);
315 | });
316 | }
317 |
318 | renderFormBody() {
319 | const { form, items, labelCol, wrapperCol } = this.props;
320 | /* console.log('renderFormBody:', JSON.stringify([
321 | this.props.type, this.state.object, this.props.object])); */
322 | const rows = this.getLayout(); // rows == layout
323 | /* console.log('renderFormBody.fields:', this.props.type, fields); */
324 | return rows.map((row, index) => {
325 | if (!(row instanceof FormRow)) {
326 | row = new FormRow(row);
327 | }
328 | return row.render(this, `row-${index}`);
329 | });
330 | }
331 |
332 | render() {
333 | const { form, formProps, labelCol, wrapperCol } = this.props;
334 | const footerItem =
335 |
336 |
337 | ;
338 | let formBody = this.renderFormBody();
339 | formBody.push(footerItem);
340 |
341 | return (
342 |
346 | );
347 | }
348 | }
349 |
350 |
351 | export class FormModal extends BaseForm {
352 |
353 | static propTypes = {
354 | ...BaseForm.propTypes,
355 | visible : PropTypes.bool.isRequired,
356 | title : PropTypes.string,
357 | modalProps : PropTypes.object,
358 | onCancel : PropTypes.func, /* function() */
359 | }
360 |
361 | static defaultProps = {
362 | ...BaseForm.defaultProps,
363 | title: "表单",
364 | modalProps: {}
365 | }
366 |
367 |
368 | render() {
369 | const { form, formProps, modalProps } = this.props;
370 | const formBody = this.renderFormBody();
371 | const footer =
372 |
373 |
374 |
;
375 |
376 | return (
377 | this.props.onCancel()}
381 | {...modalProps}>
382 |
386 |
387 | );
388 | }
389 | }
390 |
391 |
392 | export class SearchForm extends BaseForm {
393 | static propTypes = {
394 | ...BaseForm.propTypes,
395 | visible : PropTypes.bool.isRequired,
396 | }
397 |
398 | static defaultProps = {
399 | ...BaseForm.defaultProps,
400 | type: "update",
401 | labelCol: 10,
402 | wrapperCol: 14,
403 | }
404 |
405 | handleReset(e) {
406 | const { form, object } = this.props;
407 | let targetObject = object;
408 | if (!e) {
409 | targetObject = this.state.object;
410 | }
411 | form.setFieldsValue(this.formatObject(targetObject));
412 | }
413 |
414 | render() {
415 | if (!this.props.visible) {
416 | return null;
417 | }
418 |
419 | const { form, formProps, animProps } = this.props;
420 | const footerItem = (
421 |
422 |
423 |
425 |
426 |
427 |
428 | );
429 | let formBody = this.renderFormBody();
430 | formBody.push(footerItem);
431 |
432 | return (
433 |
438 | );
439 | }
440 | }
441 |
--------------------------------------------------------------------------------
/src/component/content/PageIntro.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | var PageIntro = React.createClass({
4 | render() {
5 | return (
6 |
7 |
{ this.props.children }
8 |
9 | );
10 | }
11 | });
12 |
13 | export default PageIntro;
14 |
--------------------------------------------------------------------------------
/src/component/content/PageTabs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Tabs, Validation, Select, Radio} from 'antd';
3 |
4 | import Pane from './Pane.jsx';
5 | import TestForm from './TestForm.jsx';
6 |
7 | const TabPane = Tabs.TabPane;
8 | function callback(key) {
9 | console.log(key);
10 | }
11 |
12 | const PageTabs = React.createClass({
13 | render() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | });
33 |
34 | export default PageTabs;
35 |
--------------------------------------------------------------------------------
/src/component/content/Pane.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Table} from 'antd';
3 |
4 | var columns = [{
5 | title: '姓名',
6 | dataIndex: 'name',
7 | render: function(text) {
8 | return {text};
9 | }
10 | }, {
11 | title: '年龄',
12 | dataIndex: 'age'
13 | }, {
14 | title: '住址',
15 | dataIndex: 'address'
16 | }];
17 |
18 | var data = [{
19 | key: '1',
20 | name: '胡彦斌',
21 | age: 32,
22 | address: '西湖区湖底公园1号'
23 | }, {
24 | key: '2',
25 | name: '胡彦祖',
26 | age: 42,
27 | address: '西湖区湖底公园1号'
28 | }, {
29 | key: '3',
30 | name: '李大嘴',
31 | age: 32,
32 | address: '西湖区湖底公园1号'
33 | }];
34 |
35 | // 通过 rowSelection 对象表明需要行选择
36 | var rowSelection = {
37 | onSelect: function(record, selected, selectedRows) {
38 | console.log(record, selected, selectedRows);
39 | },
40 | onSelectAll: function(selected, selectedRows) {
41 | console.log(selected, selectedRows);
42 | }
43 | };
44 |
45 |
46 | var Pane = React.createClass({
47 | render() {
48 | return (
49 |
50 |
51 | 选项卡说明 1
52 |
53 |
56 |
57 | );
58 | }
59 | });
60 |
61 |
62 | export default Pane;
63 |
--------------------------------------------------------------------------------
/src/component/content/Table.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Table, Tabs, Button, message, Select} from 'antd';
3 |
4 | import {renderInputItem, SelectClass, OptionClass} from 'component/content/Form.jsx';
5 | import { ApiQuery } from 'data/api';
6 | import {clone, getDefault} from 'common/utils';
7 |
8 | /* Re design use reuseable component:
9 | > https://facebook.github.io/react/docs/reusable-components.html
10 | */
11 |
12 | const Option = Select.Option;
13 |
14 | const Integer = Number;
15 | const FilterConfig = {
16 | names: [String, String, String],
17 | items: {
18 | name: {
19 | attrs: undefined || {
20 | width: '120px',
21 | marginRight: '3px',
22 | display: 'inline-block',
23 | },
24 | typeClass: undefined || Object, // Default: InputClass || SelectClass
25 | optionClass: undefined || Object, // Default: OptionClass
26 | options: undefined || Array, // For: Select, Radio
27 | render: undefined || Function,
28 | },
29 | }
30 | };
31 |
32 | const ColumnNames = [String, String, String] || {
33 | name: [String, String, String],
34 | name2: [String, String, String],
35 | };
36 |
37 | const Columns = {
38 | name: {
39 | title: String,
40 | dataIndex: String,
41 | OTHER: undefined,
42 | },
43 | name2: {},
44 | }
45 |
46 |
47 | export const TableMixin = {
48 |
49 | getBaseState(Model, columns, columnNames, defaultQuery) {
50 | const query = defaultQuery === undefined ? new ApiQuery(1, 20, [], []) : defaultQuery;
51 |
52 | Object.keys(columns).forEach(function(name) {
53 | if (columns[name].dataIndex === undefined) {
54 | columns[name].dataIndex = name;
55 | }
56 | });
57 |
58 | return {
59 | loading: false,
60 | Model: Model,
61 | columns: columns,
62 | columnNames: columnNames,
63 | /* filterConfig: filterConfig, */
64 | selectedRows: [],
65 | query: query,
66 | total: 0,
67 | objects: [],
68 | };
69 | },
70 |
71 | //// For table attributes
72 | //////////////////////////////////////////////////
73 | rowKey(record) {
74 | return String(record.id);
75 | },
76 |
77 | expandedRowRender(record) {
78 | const handleUpdateClick = (e) => {
79 | this.handleUpdateClick(record);
80 | };
81 | const btnStyle = {float: 'left'};
82 | return (
83 |
84 |
85 |
);
86 | },
87 |
88 | //// Use in `render()`
89 | //////////////////////////////////////////////////
90 | handleModalDismiss(e, name) {
91 | const newState = {};
92 | newState[name] = false;
93 | this.setState(newState);
94 | },
95 |
96 | getColumns(key) {
97 | const columnNames = this.state.columnNames;
98 | console.log('getColumns.columnNames:', key, columnNames);
99 | const fields = columnNames.constructor === Array ? columnNames : columnNames[key];
100 | return fields.map((field) => {
101 | return this.state.columns[field];
102 | });
103 | },
104 |
105 | getPagination() {
106 | const query = this.state.query;
107 | return {
108 | current: query.page,
109 | pageSize: query.perpage,
110 | total: this.state.total,
111 | };
112 | },
113 |
114 | getRowSelection() {
115 | return {
116 | onSelect: (record, selected, selectedRows) => {
117 | console.log('onSelect:', record, selected, selectedRows);
118 | this.setState({selectedRows: selectedRows});
119 | },
120 | onSelectAll: (selected, selectedRows) => {
121 | console.log('onSelectAll:', selected, selectedRows);
122 | this.setState({selectedRows: selectedRows});
123 | }
124 | }
125 | },
126 |
127 | //// Table logic
128 | _loadData(okCallback, errorCallback) {
129 | const query = this.state.query;
130 | this.state.Model.objects(query).then((resp) => {
131 | okCallback(resp.data);
132 | }).catch((resp) => {
133 | errorCallback(resp);
134 | });
135 | },
136 |
137 | loadPage(e) {
138 | this.setState({loading: true}, () => {
139 | const loadDataFunc = this.loadData === undefined ? this._loadData : this.loadData;
140 | loadDataFunc((data) => {
141 | /// Success callback
142 | this.setState({
143 | loading: false,
144 | total: data.total,
145 | objects: data.objects
146 | });
147 | if (e !== undefined) {
148 | message.success('刷新成功', 1);
149 | }
150 | }, (resp) => {
151 | console.log('Error response:', resp);
152 | /// Error callback
153 | if (resp.status == 400) {
154 | this.setState({loading: false}, () => {
155 | let query = this.state.query;
156 | query.page = 1;
157 | this.setState({query: query}, () => {
158 | this.loadPage();
159 | });
160 | });
161 | } else {
162 | message.error(`加载失败: ${resp.data.message}`);
163 | }
164 | });
165 | });
166 | },
167 |
168 | onTableChanged(pagination, filters, sorter){
169 | console.log('onTableChanged', pagination, filters, sorter);
170 | let query = this.state.query;
171 | let sort = this.state.sort;
172 | if (Object.keys(sorter).length > 0) {
173 | const theOrder = {
174 | 'ascend': 'asc',
175 | 'descend': 'desc',
176 | }[sorter.order];
177 | sort = [[sorter.field, theOrder]];
178 | }
179 | query.page = pagination.current;
180 | query.perpage = pagination.pageSize;
181 | query.sort = sort;
182 | this.setState({query: query}, () => {
183 | this.loadPage();
184 | });
185 | },
186 |
187 | //// Unused
188 | renderFilters() {
189 | const filterConfig = this.state.filterConfig;
190 | const items = filterConfig.items;
191 | return filterConfig.names.map((name) => {
192 | return renderInputItem(items[name], name);
193 | });
194 | },
195 | };
196 |
--------------------------------------------------------------------------------
/src/component/content/Topbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Breadcrumb} from 'antd';
3 |
4 | const Topbar = React.createClass({
5 | render() {
6 | const breadcrumb = this.props.breadcrumb.map(function(item) {
7 | return (
8 | { item }
9 | )
10 | })
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | { breadcrumb }
18 |
19 |
20 | );
21 | }
22 | });
23 |
24 | export default Topbar;
25 |
--------------------------------------------------------------------------------
/src/component/factory.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import {
4 | /* 通用 */
5 | Col, Row, message, Button, Icon, Tabs,
6 | /* 表单 */
7 | Form, // 表单
8 | Input, // 普通输入框:
9 | InputNumber, // 数字输入框
10 | Checkbox, // 多选框
11 | Radio, // 单选框
12 | Cascader, // 级联选择
13 | Transfer, // 穿梭框
14 | Select, // 选择器
15 | TreeSelect, // 树选择
16 | Slider, // 滑动输入条
17 | Switch, // 开关
18 | DatePicker, // 日期选择
19 | TimePicker, // 时间选择
20 | Upload, // 上传
21 | /* 动画 */
22 | QueueAnim,
23 | } from 'antd';
24 |
25 | // import { getStatusClasses, getStatusHelp } from 'common/utils';
26 | // import * as _ from 'lodash';
27 |
28 |
29 | /* antd.Common */
30 | export const ButtonClass = React.createFactory(Button);
31 | export const IconClass = React.createFactory(Icon);
32 | export const TabsClass = React.createFactory(Tabs);
33 | export const TabPaneClass = React.createFactory(Tabs.TabPane);
34 |
35 |
36 | /* antd.Form */
37 | export const FormClass = React.createFactory(Form);
38 | export const FormItemClass = React.createFactory(Form.Item);
39 |
40 | export const InputClass = React.createFactory(Input);
41 |
42 | export const InputNumberClass = React.createFactory(InputNumber);
43 |
44 | export const CheckboxClass = React.createFactory(Checkbox);
45 |
46 | export const RadioClass = React.createFactory(Radio);
47 | export const RadioButtonClass = React.createFactory(Radio.Button);
48 | export const RadioGroupClass = React.createFactory(Radio.Group);
49 |
50 | export const CascaderClass = React.createFactory(Cascader);
51 |
52 | export const TransferClass = React.createFactory(Transfer);
53 |
54 | export const SelectClass = React.createFactory(Select);
55 | export const OptionClass = React.createFactory(Select.Option);
56 |
57 | export const TreeSelectClass = React.createFactory(TreeSelect)
58 | export const SliderClass = React.createFactory(Slider);
59 | export const SwitchClass = React.createFactory(Switch);
60 | export const DatePickerClass = React.createFactory(DatePicker);
61 | export const RangePickerClass = React.createFactory(DatePicker.RangePicker);
62 | export const TimePickerClass = React.createFactory(TimePicker);
63 | export const UploadClass = React.createFactory(Upload);
64 |
65 |
66 | export const div = React.createFactory('div');
67 | export const span = React.createFactory('span');
68 | export const img = React.createFactory('img');
69 | export const a = React.createFactory('a');
70 |
--------------------------------------------------------------------------------
/src/component/nav/NavGlobal.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { Link } from 'react-router';
4 | import cookie from 'react-cookie';
5 | import { Menu, Dropdown } from 'antd';
6 |
7 | import { siteTitle } from 'data/config';
8 |
9 |
10 | export default React.createClass({
11 | displayName: 'NavGlobal',
12 |
13 | componentWillReceiveProps(newProps) {
14 | this.setState({});
15 | },
16 |
17 | render() {
18 | const topMenu = this.props.topMenu;
19 | let menu = (
20 |
37 | );
38 |
39 | const user = this.props.user;
40 | const userName = user ? user.name : '';
41 | const roleDescr = user ? user.role.descr : '';
42 | return (
43 |
44 |
45 |
46 |
47 |
{siteTitle}
48 |
49 |
50 |
65 |
66 |
67 | );
68 | }
69 | });
70 |
--------------------------------------------------------------------------------
/src/component/nav/NavMenu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Menu } from 'antd';
3 | import { History } from 'react-router';
4 |
5 | const SubMenu = Menu.SubMenu;
6 |
7 | const NavMenu = React.createClass({
8 | displayName: 'NavMenu',
9 | mixins: [History],
10 | getInitialState() {
11 | return {
12 | current: '1'
13 | }
14 | },
15 | handleMenuItemClick(to) {
16 | console.log('handleMenuItemClick', to);
17 | /* this.history.pushState(null, to); */
18 | },
19 | handleClick(e) {
20 | console.log('click ', e);
21 | this.setState({
22 | current: e.key
23 | });
24 | if (e.key && e.key.length > 0 && e.key[0] == '/') {
25 | this.history.pushState(null, e.key);
26 | }
27 | },
28 |
29 |
30 | render() {
31 | const sideMenu = this.props.sideMenu;
32 | let menu = (
33 | );
59 |
60 | return (
61 |
62 |
63 |
64 | {menu}
65 |
66 |
67 |
68 | );
69 | }
70 | });
71 |
72 | export default NavMenu;
73 |
--------------------------------------------------------------------------------
/src/data/api.js:
--------------------------------------------------------------------------------
1 |
2 | import axios from 'axios';
3 | import cookie from 'react-cookie';
4 | import { message } from 'antd';
5 |
6 | import { tokenKey, resourceMapping } from 'data/config';
7 | import { makeFormData } from 'common/utils';
8 |
9 | /*
10 | // Add a response interceptor
11 | axios.interceptors.response.use(function (response) {
12 | // Do something with response data
13 | console.log('Response.data', data);
14 | return response;
15 | }, function (error) {
16 | // Do something with response error
17 | console.log('Response.error', error);
18 | if (error.status === 401) {
19 | cookie.remove(tokenKey);
20 | }
21 | return Promise.reject(error);
22 | });
23 | */
24 |
25 |
26 | /*
27 | axios#request(config)
28 | axios#get(url[, config])
29 | axios#delete(url[, config])
30 | axios#head(url[, config])
31 |
32 | axios#post(url[, data[, config]])
33 | axios#put(url[, data[, config]])
34 | axios#patch(url[, data[, config]])
35 | */
36 |
37 | export function httpErrorCallback(resp) {
38 | console.error('axios Response:', resp);
39 | if (resp.data.message !== undefined) {
40 | message.error(`请求失败!: ${resp.data.message}`);
41 | } else {
42 | message.error('请求失败!');
43 | }
44 | }
45 |
46 | export function httpRequest(path, config) {
47 | if (config.headers === undefined) {
48 | config.headers = {};
49 | }
50 | config.headers['Content-Type'] = undefined;
51 | const token = cookie.load(tokenKey);
52 | if (token) {
53 | config.headers[tokenKey] = token;
54 | }
55 | config.url = globalConfig.apiUrl + path;
56 | return axios(config);
57 | }
58 | /* Without data */
59 | export function httpGet(path, config) {
60 | if (config === undefined) {
61 | config = {};
62 | }
63 | config.method = 'get';
64 | return httpRequest(path, config);
65 | }
66 | export function httpDelete(path, config) {
67 | if (config === undefined) {
68 | config = {};
69 | }
70 | config.method = 'delete';
71 | return httpRequest(path, config);
72 | }
73 | export function httpHead(path, config) {
74 | if (config === undefined) {
75 | config = {};
76 | }
77 | config.method = 'head';
78 | return httpRequest(path, config);
79 | }
80 | /* With data */
81 | export function httpPost(path, data, config, isRawData) {
82 | if (config === undefined) {
83 | config = {};
84 | }
85 | config.method = 'post';
86 | config.data = isRawData ? JSON.stringify(data) : makeFormData(data);
87 | return httpRequest(path, config);
88 | }
89 | export function httpPut(path, data, config, isRawData) {
90 | if (config === undefined) {
91 | config = {};
92 | }
93 | config.method = 'put';
94 | config.data = isRawData ? JSON.stringify(data) : makeFormData(data);
95 | return httpRequest(path, config);
96 | }
97 | export function httpPatch(path, data, config, isRawData) {
98 | if (config === undefined) {
99 | config = {};
100 | }
101 | config.method = 'patch';
102 | config.data = isRawData ? JSON.stringify(data) : makeFormData(data);
103 | return httpRequest(path, config);
104 | }
105 |
106 |
107 | class Resource {
108 | constructor(path) {
109 | this.path = path;
110 | }
111 |
112 | create(obj, isRawData) {
113 | return httpPost(this.path, obj, {}, isRawData);
114 | }
115 |
116 | update(obj, isRawData) {
117 | return httpPut(this.path + obj.id, obj, {}, isRawData)
118 | }
119 |
120 | updateAll(ids, obj, isRawData) {
121 | return httpPut(this.path + ids.join(), obj, {}, isRawData)
122 | }
123 |
124 | delete(id) {
125 | return httpDelete(this.path + id);
126 | }
127 |
128 | deleteAll(ids) {
129 | return httpDelete(this.path + ids.join());
130 | }
131 |
132 | get(id) {
133 | return httpGet(this.path + id);
134 | }
135 |
136 | // var query = {
137 | // type : STRING,
138 | // page : NUMBER,
139 | // perpage : NUMBER,
140 | // filters : [[FIELD, OP, VALUE], [FIELD, OP, VALUE], ...],
141 | // sort : [[FIELD, ORDER], [FIELD, ORDER], ...]
142 | // };
143 | objects(query) {
144 | const q = query instanceof ApiQuery ? query.dict() : query;
145 | return httpGet(this.path, {
146 | params: { q: JSON.stringify(q) },
147 | });
148 | }
149 |
150 | all(query) {
151 | if (query === undefined) {
152 | query = {};
153 | }
154 | query.page = 1;
155 | query.perpage = -1;
156 | return this.objects(query);
157 | }
158 | }
159 |
160 |
161 | function initResources(mapping) {
162 | /* defined in /static/config.js */
163 | let models = {}
164 | mapping.forEach(function(args) {
165 | const name = args[0];
166 | const path = args[1];
167 | console.log('Resource:', name, path);
168 | models[name] = new Resource(path);
169 | });
170 | return models;
171 | }
172 |
173 | export const Api = initResources(resourceMapping);
174 |
175 | export class ApiQuery {
176 | constructor(page, perpage, filters, sort) {
177 | this.page = page === undefined ? 1 : page;
178 | this.perpage = perpage === undefined ? 20 : perpage;
179 | this.filters = filters === undefined ? [] : filters;
180 | this.sort = sort === undefined ? [] : sort;
181 | }
182 |
183 | updateFilter(name, operation, value) {
184 | // console.log('updateFilter', name, operation, value);
185 | let newFilters = [];
186 | let matched = false;
187 | if ((operation == "ilike" || operation == "like")
188 | && (value !== undefined && value !== "")) {
189 | value = `%${value}%`;
190 | }
191 |
192 | this.filters.forEach(function(item) {
193 | if (item[0] === name && item[1] === operation) {
194 | matched = true;
195 | if (value !== undefined && value !== "") {
196 | newFilters.push([name, operation, value]);
197 | }
198 | } else {
199 | newFilters.push(item);
200 | }
201 | });
202 |
203 | if (!matched && value !== undefined && value !== "") {
204 | newFilters.push([name, operation, value]);
205 | }
206 | // console.log('newFilters:', newFilters);
207 | this.filters = newFilters;
208 | }
209 |
210 | updateSort(name, order) {
211 | let newSort = []
212 | let matched = false;
213 | this.sort.forEach(function(item) {
214 | if (item[0] === name) {
215 | matched = true;
216 | if (order !== undefined) {
217 | newSort.push([name, order]);
218 | }
219 | } else {
220 | newSort.push(item);
221 | }
222 | });
223 | if (!matched && order !== undefined) {
224 | newSort.push([name, order]);
225 | }
226 | this.sort = newSort;
227 | }
228 |
229 | dict() {
230 | return {
231 | type: this.type,
232 | page: this.page,
233 | perpage: this.perpage,
234 | filters: this.filters,
235 | sort: this.sort,
236 | };
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/src/data/config.js:
--------------------------------------------------------------------------------
1 |
2 | export const tokenKey = 'X-Token';
3 | export const siteTitle = '某某管理后台';
4 | export const docTitle = '某某管理后台 - [呼呼科技]';
5 |
6 |
7 | export const provinces = [
8 | ['BJ', '北京市'],
9 | ['SH', '上海市'],
10 | ['TJ', '天津市'],
11 | ['CQ', '重庆市'],
12 | ['HL', '黑龙江省'],
13 | ['HE', '河北省'],
14 | ['SX', '山西省'],
15 | ['LN', '辽宁省'],
16 | ['JL', '吉林省'],
17 | ['JS', '江苏省'],
18 | ['ZJ', '浙江省'],
19 | ['AH', '安徽省'],
20 | ['FJ', '福建省'],
21 | ['JX', '江西省'],
22 | ['SD', '山东省'],
23 | ['HA', '河南省'],
24 | ['HB', '湖北省'],
25 | ['HN', '湖南省'],
26 | ['GD', '广东省'],
27 | ['HI', '海南省'],
28 | ['SC', '四川省'],
29 | ['GZ', '贵州省'],
30 | ['YN', '云南省'],
31 | ['SN', '陕西省'],
32 | ['GS', '甘肃省'],
33 | ['QH', '青海省'],
34 | ['NM', '内蒙古'],
35 | ['XZ', '西藏'],
36 | ['GX', '广西'],
37 | ['NX', '宁夏'],
38 | ['XJ', '新疆'],
39 | ['HK', '香港'],
40 | ['MO', '澳门'],
41 | ['TW', '台湾省'],
42 | ];
43 |
44 | export let provinceMap = {};
45 |
46 | provinces.forEach(function(item) {
47 | provinceMap[item[0]] = item[1];
48 | });
49 |
50 |
51 | export const resourceMapping = [
52 | /* common */
53 | ['Role', '/roles/'], // 角色
54 | ['User', '/users/'], // 用户
55 | /* admin: 系统管理员 */
56 | ];
57 |
58 |
59 | export const sideMenus = {
60 | /* 广告主菜单 */
61 | 'user': {
62 | defaultOpenKeys: ['sub1'],
63 | subMenus: [
64 | {
65 | key: "sub1",
66 | title: {iconClass: "fa fa-fw fa-folder", label: "一级目录"},
67 | menus: [
68 | {key: "/demo/test_forms", label: "表单示例"},
69 | {key: "/user/change_password", label: "修改密码"},
70 | ]
71 | },
72 | ]
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/src/entry/index.jsx:
--------------------------------------------------------------------------------
1 | import 'style';
2 | import es6Promise from 'es6-promise';
3 | import 'babel-polyfill';
4 |
5 | import React from 'react';
6 | import {render} from 'react-dom';
7 | import {Router} from 'react-router';
8 | import cookie from 'react-cookie';
9 | import {tokenKey, docTitle} from 'data/config';
10 |
11 | import { routes } from 'component/App.jsx';
12 |
13 |
14 | /* The real main part */
15 | ////////////////////////////////////////////////////////////
16 |
17 | es6Promise.polyfill();
18 | /* Setup page title */
19 | window.document.title = docTitle;
20 |
21 | console.log('routes:', routes);
22 |
23 | render(, document.getElementById('app'));
24 |
--------------------------------------------------------------------------------
/src/page/common/Blank.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Topbar from 'component/content/Topbar.jsx';
3 | import PageIntro from 'component/content/PageIntro.jsx';
4 |
5 | export default React.createClass({
6 | displayName: 'Blank',
7 | render() {
8 | return (
9 |
10 |
11 |
提示: 请点击左侧菜单栏来操作.
12 |
13 | )
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/src/page/common/ChangePassword.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Button, Form, Input, message } from 'antd';
4 | import PageIntro from 'component/content/PageIntro.jsx';
5 | import Topbar from 'component/content/Topbar.jsx';
6 |
7 | import { httpPut } from 'data/api';
8 |
9 | const FormItem = Form.Item;
10 |
11 |
12 | export default React.createClass({
13 | displayName: 'ChangePassword',
14 |
15 | handleSubmit(e) {
16 | e.preventDefault();
17 | console.log('Submit:');
18 | },
19 |
20 | render() {
21 | const labelCol = 6;
22 | const wrapperCol = 12;
23 |
24 | return (
25 |
26 |
27 |
修改密码
28 |
29 |
31 |
32 |
33 | );
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/src/page/common/EmailVerify.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 | import { message } from 'antd';
4 | import { Link, History } from 'react-router';
5 |
6 | import { Api, httpGet, httpErrorCallback } from 'data/api';
7 |
8 | const emailExists = function(rule, value, callback) {
9 | if (!value) {
10 | callback();
11 | } else {
12 | Api.User.objects({
13 | filters: [['email', '==', value.trim()]]
14 | }).then(function(resp) {
15 | console.log(resp);
16 | if (resp.data.total > 0) {
17 | callback();
18 | } else {
19 | callback([new Error('抱歉,不存在该邮箱。')]);
20 | }
21 | }).catch(httpErrorCallback);
22 | }
23 | };
24 |
25 | const formConfig = {
26 | submitLabel: '重发激活邮件',
27 | labelCol: 4,
28 | wrapperCol: 20,
29 | names: ['email'],
30 | items: {
31 | email: {
32 | attrs: {
33 | label: '邮箱:',
34 | required: true,
35 | hasFeedback: true,
36 | },
37 | validateStatus: true,
38 | help: true,
39 | update: {
40 | validator: {
41 | rules: [{required: true, type:'email', message: '请输入正确的邮箱地址'},
42 | {validator: emailExists}]
43 | },
44 | input: {
45 | attrs: {
46 | type: 'email',
47 | placeholder: '请输入注册过的邮箱!'
48 | }
49 | }
50 | }
51 | }
52 | }
53 | };
54 |
55 | export default React.createClass({
56 | displayName: 'EmailVerify',
57 |
58 | getInitialState() {
59 | return {verified: false};
60 | },
61 |
62 | componentDidMount() {
63 | console.log('Query params:', this.props.location.query, this.isMounted());
64 | const query = this.props.location.query;
65 | if (query) {
66 | const token = query.token;
67 | if (token) {
68 | httpGet('/verify', {
69 | params: {token: token}
70 | }).then((resp) => {
71 | message.success(resp.data.message);
72 | this.setState({verified: true})
73 | }).catch((resp) => {
74 | message.error(resp.data.message);
75 | });
76 | }
77 | }
78 | },
79 |
80 | sendVerify() {
81 | },
82 |
83 | render() {
84 | const wrapperStyle = {
85 | width: '320px',
86 | padding: '40px 20px',
87 | margin: '60px auto 20px',
88 | border: '1px solid #CCC',
89 | borderRadius: '5px',
90 | background: '#fff',
91 | };
92 | const main = this.state.verified ? 邮箱验证成功! 请等待管理员开启帐号. : (
93 |
94 | 请登录你的邮箱验证帐号!
95 |
96 | );
97 | return (
98 |
99 |
{main}
100 |
101 |
102 | 注册
103 | 登录
104 |
105 |
106 | );
107 | }
108 | });
109 |
--------------------------------------------------------------------------------
/src/page/common/Err404.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert } from 'antd';
3 | import Topbar from 'component/content/Topbar.jsx';
4 | import PageIntro from 'component/content/PageIntro.jsx';
5 |
6 | export default React.createClass({
7 | displayName: 'Err404',
8 | render() {
9 | return (
10 |
16 | )
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/src/page/common/Signin.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Checkbox, Form, Button, Input, message } from 'antd';
3 | import { Link, History } from 'react-router';
4 |
5 | import { httpPost } from 'data/api';
6 | import { tokenKey } from 'data/config';
7 | import cookie from 'react-cookie';
8 |
9 |
10 | const FormItem = Form.Item;
11 |
12 | export default React.createClass({
13 | displayName: 'Signin',
14 |
15 | handleSubmit(e) {
16 | e.preventDefault();
17 | console.log('Submit:');
18 | cookie.save(tokenKey, 'Test token');
19 | this.history.pushState(null, '/');
20 | },
21 |
22 | render() {
23 | const labelCol = 8;
24 | const wrapperCol = 12;
25 | const wrapperStyle = {
26 | width: '320px',
27 | padding: '40px 20px',
28 | margin: '60px auto 20px',
29 | border: '1px solid #CCC',
30 | borderRadius: '5px',
31 | background: '#fff',
32 | };
33 |
34 | return (
35 |
36 |
39 |
40 | );
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/src/page/common/Signup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, History } from 'react-router';
3 | import { Button, Form, Input, Select, message } from 'antd';
4 |
5 | import { signupRoles } from 'data/config';
6 | import { Api, httpErrorCallback } from 'data/api';
7 |
8 | const FormItem = Form.Item;
9 | const Option = Select.Option;
10 |
11 |
12 | function noop() {
13 | return false;
14 | }
15 |
16 | function handleSelectChange(value) {
17 | console.log('selected ' + value);
18 | }
19 |
20 |
21 | const Signup = React.createClass({
22 | displayName: 'Signup',
23 |
24 | handleSubmit(e) {
25 | e.preventDefault();
26 | console.log('Submit:');
27 | },
28 |
29 | handleReset(e) {
30 | e.preventDefault();
31 | },
32 |
33 | render() {
34 | const labelCol = 7;
35 | const wrapperCol = 15;
36 | const wrapperStyle = {
37 | width: '360px',
38 | padding: '40px 20px',
39 | margin: '60px auto 20px',
40 | border: '1px solid #CCC',
41 | borderRadius: '5px',
42 | background: '#fff',
43 | };
44 |
45 | return (
46 |
47 |
50 |
51 | );
52 | }
53 | });
54 |
55 | export default Signup;
56 |
--------------------------------------------------------------------------------
/src/page/demo/FactoryForms.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component, PropTypes } from 'react';
3 |
4 | import {
5 | FormClass, FormItemClass,
6 | InputClass,
7 | InputNumberClass,
8 | CheckboxClass, // 多选框
9 | RadioClass, // 单选框
10 | RadioButtonClass,
11 | RadioGroupClass,
12 | CascaderClass, // 级联选择
13 | TransferClass, // 穿梭框
14 | SelectClass, // 选择器
15 | OptionClass,
16 | TreeSelectClass, // 树选择
17 | SliderClass, // 滑动输入条
18 | SwitchClass, // 开关
19 | DatePickerClass, // 日期选择
20 | TimePickerClass, // 时间选择
21 | UploadClass, // 上传
22 | } from 'component/factory.js';
23 |
--------------------------------------------------------------------------------
/src/page/demo/RawForms.jsx:
--------------------------------------------------------------------------------
1 |
2 | import React, { Component, PropTypes } from 'react';
3 |
4 | import {
5 | /* 通用 */
6 | Col, Row, message, Button, Tabs, Icon, Card, Modal,
7 | /* 表单 */
8 | Form, // 表单
9 | Input, // 普通输入框:
10 | InputNumber, // 数字输入框
11 | Checkbox, // 多选框
12 | Radio, // 单选框
13 | Cascader, // 级联选择
14 | Transfer, // 穿梭框
15 | Select, // 选择器
16 | TreeSelect, // 树选择
17 | Slider, // 滑动输入条
18 | Switch, // 开关
19 | DatePicker, // 日期选择
20 | TimePicker, // 时间选择
21 | Upload, // 上传
22 | /* 动画 */
23 | QueueAnim,
24 | } from 'antd';
25 |
26 | import { FormModal } from 'component/content/Form.jsx';
27 |
28 | const createForm = Form.create;
29 |
30 | const FormItem = Form.Item;
31 | const Option = Select.Option;
32 | const RadioGroup = Radio.Group;
33 |
34 |
35 | /*
36 | [ Form types ]
37 | ==============
38 | * Read only
39 | * Inline
40 | * With image field (file)
41 | * Multiple fields in one
42 | * organized by Row/Col (Advanced search form)
43 |
44 |
45 | Read only
46 | ---------
47 | * The Content
48 | * The Content
49 |
50 | Inline
51 | ------
52 | * Just add `inline` property to