├── .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 |
this.handleSubmit(e)} 343 | {...formProps}> 344 | {formBody} 345 |
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 |
this.handleSubmit(e)} 383 | {...formProps} > 384 | {formBody} 385 |
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 |
this.handleSubmit(e)} 434 | className="advanced-search-form" 435 | {...formProps}> 436 | {formBody} 437 |
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 |
54 | 55 | 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 | 21 | { 22 | topMenu.map(function(menu) { 23 | if (menu.divider) { 24 | return (); 25 | } else { 26 | return ( 27 | 28 | 29 | {menu.label} 30 | 31 | 32 | ); 33 | } 34 | }) 35 | } 36 | 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 |
51 | 64 |
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 | 39 | { 40 | sideMenu.subMenus.map(function(subMenu) { 41 | const subMenuTitle = ( 42 | 43 | 44 | {subMenu.title.label} 45 | 46 | ); 47 | return ( 48 | 49 | { 50 | subMenu.menus.map(function(menu){ 51 | return {menu.label}; 52 | }) 53 | } 54 | 55 | ); 56 | }) 57 | } 58 | ); 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 |
30 | 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 |
11 | 12 | 15 |
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 |
37 | xxx平台 用户注册 38 | 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 |
48 | xxx平台 用户注册 49 | 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
53 | 54 | With image field (file) 55 | ------------------------- 56 | * TODO 57 | 58 | Multiple fields in one 59 | --------------------------------- 60 | * !!! Currently not support, you should put them in multiple 61 | 62 | organized by Row/Col (Advanced search form) 63 | ------------------------------------------------------ 64 | * By array config 65 | */ 66 | 67 | //////////////////////////////////////////////////////////////////////////////// 68 | //// Demo List 69 | //////////////////////////////////////////////////////////////////////////////// 70 | 71 | ////////// Inline 表单 ////////// 72 | let DemoInlineForm = React.createClass({ 73 | handleSubmit(e) { 74 | e.preventDefault(); 75 | console.log('收到表单值:', this.props.form.getFieldsValue()); 76 | }, 77 | 78 | render() { 79 | const { getFieldProps } = this.props.form; 80 | return ( 81 | 82 | 84 | 86 | 87 | 89 | 91 | 92 | 93 | 97 | 98 | 99 | 100 | ); 101 | } 102 | }); 103 | 104 | DemoInlineForm = Form.create()(DemoInlineForm); 105 | 106 | 107 | ////////// 校验其他组件 ////////// 108 | 109 | let DemoValidateOtherForm = React.createClass({ 110 | componentDidMount() { 111 | this.props.form.setFieldsValue({ 112 | eat: true, 113 | sleep: true, 114 | beat: true, 115 | }); 116 | }, 117 | 118 | handleReset(e) { 119 | e.preventDefault(); 120 | this.props.form.resetFields(); 121 | }, 122 | 123 | handleSubmit(e) { 124 | e.preventDefault(); 125 | this.props.form.validateFieldsAndScroll((errors, values) => {if (!!errors) { 126 | console.log('Errors in form!!!', errors); 127 | return; 128 | } 129 | console.log('Submit!!!'); 130 | console.log(values); 131 | }); 132 | }, 133 | 134 | checkBirthday(rule, value, callback) { 135 | if (value && value.getTime() >= Date.now()) { 136 | callback(new Error('你不可能在未来出生吧!')); 137 | } else { 138 | callback(); 139 | } 140 | }, 141 | 142 | checkPrime(rule, value, callback) { 143 | if (value !== 11) { 144 | callback(new Error('8~12之间的质数明明是11啊!')); 145 | } else { 146 | callback(); 147 | } 148 | }, 149 | 150 | render() { 151 | const address = [{ 152 | value: 'zhejiang', 153 | label: '浙江', 154 | children: [{ 155 | value: 'hangzhou', 156 | label: '杭州', 157 | }], 158 | }]; 159 | const { getFieldProps } = this.props.form; 160 | const selectProps = getFieldProps('select', { 161 | rules: [ 162 | { required: true, message: '请选择您的国籍' } 163 | ], 164 | }); 165 | const multiSelectProps = getFieldProps('multiSelect', { 166 | rules: [ 167 | { required: true, message: '请选择您喜欢的颜色', type: 'array' }, 168 | ] 169 | }); 170 | const radioProps = getFieldProps('radio', { 171 | rules: [ 172 | { required: true, message: '请选择您的性别' } 173 | ] 174 | }); 175 | const birthdayProps = getFieldProps('birthday', { 176 | rules: [ 177 | { 178 | required: true, 179 | type: 'date', 180 | message: '你的生日是什么呢?', 181 | }, { 182 | validator: this.checkBirthday, 183 | } 184 | ] 185 | }); 186 | const primeNumberProps = getFieldProps('primeNumber', { 187 | rules: [{ validator: this.checkPrime }], 188 | }); 189 | const addressProps = getFieldProps('address', { 190 | rules: [{ required: true, type: 'array' }], 191 | }); 192 | const formItemLayout = { 193 | labelCol: { span: 7 }, 194 | wrapperCol: { span: 12 }, 195 | }; 196 | return ( 197 |
198 | 201 | 208 | 209 | 210 | 213 | 220 | 221 | 222 | 225 | 226 | 227 | 228 | 229 | 暂不支持其它性别 230 | 231 | 232 | 235 | 吃饭饭   238 | 睡觉觉   241 | 打豆豆   244 | 245 | 246 | 249 | 250 | 251 | 252 | 255 | 256 | 257 | 258 | 261 | 262 | 263 | 264 | 266 | 267 |     268 | 269 | 270 | 271 | ); 272 | }, 273 | }); 274 | 275 | DemoValidateOtherForm = createForm()(DemoValidateOtherForm); 276 | 277 | 278 | 279 | ////////// Modal ////////// 280 | class DemoValidateOtherFormModal extends Component { 281 | static propTypes = { 282 | visible: PropTypes.bool, 283 | alwaysPropagate: PropTypes.bool, 284 | onSubmit: PropTypes.func, /* function onSubmit(form) */ 285 | onCancel: PropTypes.func, /* function onCancel() */ 286 | } 287 | 288 | static defaultProps = { 289 | alwaysPropagate: false, 290 | } 291 | 292 | constructor(props) { 293 | super(props); 294 | this.state = {visible: this.props.visible}; 295 | } 296 | 297 | componentWillReceiveProps(newProps) { 298 | this.setState({visible: newProps.visible}); 299 | } 300 | 301 | componentDidMount() { 302 | this.props.form.setFieldsValue({ 303 | eat: true, 304 | sleep: false, 305 | beat: true, 306 | }); 307 | } 308 | 309 | handleReset(e) { 310 | e.preventDefault(); 311 | this.props.form.resetFields(); 312 | } 313 | 314 | handleSubmit(e) { 315 | e.preventDefault(); 316 | this.props.form.validateFieldsAndScroll((errors, values) => {if (!!errors) { 317 | console.log('Errors in form!!!', errors); 318 | return; 319 | } 320 | console.log('Submit!!!'); 321 | console.log(values); 322 | }); 323 | } 324 | 325 | checkBirthday(rule, value, callback) { 326 | if (value && value.getTime() >= Date.now()) { 327 | callback(new Error('你不可能在未来出生吧!')); 328 | } else { 329 | callback(); 330 | } 331 | } 332 | 333 | checkPrime(rule, value, callback) { 334 | if (value !== 11) { 335 | callback(new Error('8~12之间的质数明明是11啊!')); 336 | } else { 337 | callback(); 338 | } 339 | } 340 | 341 | render() { 342 | const address = [{ 343 | value: 'zhejiang', 344 | label: '浙江', 345 | children: [{ 346 | value: 'hangzhou', 347 | label: '杭州', 348 | }], 349 | }]; 350 | const { getFieldProps } = this.props.form; 351 | const selectProps = getFieldProps('select', { 352 | rules: [ 353 | { required: true, message: '请选择您的国籍' } 354 | ], 355 | }); 356 | const multiSelectProps = getFieldProps('multiSelect', { 357 | rules: [ 358 | { required: true, message: '请选择您喜欢的颜色', type: 'array' }, 359 | ] 360 | }); 361 | const radioProps = getFieldProps('radio', { 362 | rules: [ 363 | { required: true, message: '请选择您的性别'} 364 | ], 365 | }); 366 | const birthdayProps = getFieldProps('birthday', { 367 | rules: [ 368 | { 369 | required: true, 370 | type: 'date', 371 | message: '你的生日是什么呢?', 372 | }, { 373 | validator: this.checkBirthday, 374 | } 375 | ] 376 | }); 377 | const primeNumberProps = getFieldProps('primeNumber', { 378 | rules: [{ validator: this.checkPrime }], 379 | }); 380 | /* 381 | const addressProps = getFieldProps('address', { 382 | rules: [{ required: true, type: 'array' }], 383 | }); 384 | */ 385 | const formItemLayout = { 386 | labelCol: { span: 7 }, 387 | wrapperCol: { span: 12 }, 388 | }; 389 | 390 | const ModalFooter = ( 391 |
392 | 394 | 396 |
397 | ); 398 | 399 | console.log('sleep field >>>>', getFieldProps('sleep', {valuePropName: 'checked'})); 400 | return ( 401 | {this.props.onCancel(e)}}> 405 |
406 | 409 | 416 | 417 | 418 | 421 | 428 | 429 | 430 | 433 | 434 | 435 | 436 | 437 | 暂不支持其它性别 438 | 439 | 440 | 443 | 吃饭饭 446 | 睡觉觉 449 | 打豆豆 452 | 453 | 454 | 457 | 458 | 459 | 460 | 463 | 464 | 465 | { 466 | /* 467 | 470 | 471 | 472 | */ 473 | } 474 | 475 |
476 | ); 477 | } 478 | } 479 | 480 | DemoValidateOtherFormModal = createForm()(DemoValidateOtherFormModal); 481 | 482 | class CreateUserFormModal extends FormModal { 483 | static defaultProps = { 484 | ...FormModal.defaultProps, 485 | type: "create", 486 | fields: ['role_id', 'login', 'name', 487 | 'email', 'password', 'blocked'], 488 | items: { 489 | role_id({ form, field, itemProps }) { 490 | itemProps.wrapperCol.span = 8; 491 | const options = this.state.roles.map(function(role) { 492 | return ; 493 | }); 494 | const inputProps = form.getFieldProps(field, { 495 | rules: [{required: true, message: "请选择用户角色"}] 496 | }); 497 | return ( 498 | 499 | 503 | 504 | ); 505 | }, 506 | login({ form, field, itemProps }) { 507 | const inputProps = form.getFieldProps(field, { 508 | rules: [{required: true, min: 4, message: "用户名至少为 4 个字符"}] 509 | }); 510 | return 511 | 512 | ; 513 | }, 514 | name({ form, field, itemProps }) { 515 | const inputProps = form.getFieldProps(field, { 516 | rules: [{required: true, min: 2, message: "长度至少为2"}] 517 | }); 518 | return 519 | 520 | ; 521 | }, 522 | email({ form, field, itemProps }) { 523 | const inputProps = form.getFieldProps(field, { 524 | rules: [{required: false, type: "string", message: "请输入正确的邮箱"}] 525 | }); 526 | return 527 | 528 | ; 529 | }, 530 | password({ form, field, itemProps }) { 531 | const inputProps = form.getFieldProps(field, { 532 | rules: [{required: true, Whitespace: true, message: "请填写密码"}] 533 | }); 534 | return 535 | 537 | ; 538 | }, 539 | blocked({ form, field, itemProps }) { 540 | itemProps.wrapperCol.span = 6; 541 | const inputProps = form.getFieldProps(field, { 542 | rules: [{required: true, message: "请选择是否禁用用户"}] 543 | }); 544 | const options = [["0", "否"], ["1", "是"]].map(function(item) { 545 | return ; 546 | }); 547 | return 548 | 552 | ; 553 | } 554 | } 555 | } 556 | 557 | constructor(props) { 558 | super(props); 559 | Object.assign(this.state, {roles: []}); 560 | } 561 | 562 | componentDidMount() { 563 | this.setState({roles: [ 564 | { id: 1, descr: '管理员' }, 565 | { id: 2, descr: '版主' }]}, () => { 566 | this.resetForm(); 567 | }); 568 | } 569 | 570 | onSubmit(values, callback) { 571 | callback(); 572 | message.success(`添加成功! ${JSON.stringify(values)}`, 3); 573 | } 574 | } 575 | 576 | class UpdateUserFormModal extends CreateUserFormModal { 577 | static defaultProps = { 578 | ...CreateUserFormModal.defaultProps, 579 | type: 'update', 580 | items: { 581 | ...CreateUserFormModal.defaultProps.items, 582 | password: { 583 | render({ form, field, itemProps }) { 584 | const passwordProps = form.getFieldProps(field); 585 | return 586 | 588 | ; 589 | } 590 | } 591 | } 592 | } 593 | 594 | onSubmit(values, callback) { 595 | message.success(`修改成功: ${JSON.stringify(values)}`, 3); 596 | callback(values); 597 | } 598 | } 599 | 600 | CreateUserFormModal = Form.create()(CreateUserFormModal); 601 | UpdateUserFormModal = Form.create()(UpdateUserFormModal); 602 | 603 | ////////// 含图片(文件) ////////// 604 | 605 | class DemoImageForm extends Component { 606 | constructor(props) { 607 | super(props); 608 | } 609 | 610 | handleReset(e) { 611 | e.preventDefault(); 612 | this.props.form.resetFields(); 613 | } 614 | 615 | handleSubmit(e) { 616 | e.preventDefault(); 617 | this.props.form.validateFieldsAndScroll((errors, values) => {if (!!errors) { 618 | console.log('Errors in form!!!', errors); 619 | return; 620 | } 621 | console.log('Submit!!! values:', values); 622 | }); 623 | } 624 | 625 | normalFile(e) { 626 | if (Array.isArray(e)) { 627 | return e; 628 | } 629 | return e && e.fileList; 630 | } 631 | 632 | render() { 633 | const { getFieldProps } = this.props.form; 634 | 635 | const formItemLayout = { 636 | labelCol: { span: 7 }, 637 | wrapperCol: { span: 12 }, 638 | }; 639 | 640 | let imageProps = getFieldProps('image1', { 641 | valuePropName: 'fileList', 642 | normalize: (e) => { 643 | console.log('normalize.e:', e); 644 | if (Array.isArray(e)) { return e; } 645 | return e && e.fileList; 646 | } 647 | }); 648 | Object.assign(imageProps, { 649 | action: 'http://demo-static.huhulab.com:9956/upload', 650 | accept: "image/*", 651 | listType: "picture", 652 | multiple: true, 653 | beforeUpload(file) { 654 | console.log('handleBeforeUpload:', file); 655 | }, 656 | onChange(info) { 657 | if (info.file.status === 'done') { 658 | console.log('handleChange:', info.file); 659 | console.log('Upload response:', info.file.response); 660 | info.file.url = info.file.response.url; 661 | } 662 | }, 663 | defaultFileList: [{ 664 | uid: -1, 665 | name: 'xxx.png', 666 | status: 'done', 667 | url: 'https://os.alipayobjects.com/rmsportal/NDbkJhpzmLxtPhB.png', 668 | thumbUrl: 'https://os.alipayobjects.com/rmsportal/NDbkJhpzmLxtPhB.png', 669 | }] 670 | }); 671 | 672 | let imageProps2 = getFieldProps('image2', { 673 | valuePropName: 'fileList', 674 | normalize: (e) => { 675 | console.log('normalize.e:', e); 676 | if (Array.isArray(e)) { return e; } 677 | return e && e.fileList; 678 | } 679 | }); 680 | Object.assign(imageProps2, { 681 | action: 'http://demo-static.huhulab.com:9956/upload', 682 | accept: "image/*", 683 | listType: "picture", 684 | multiple: true, 685 | beforeUpload(file) { 686 | console.log('handleBeforeUpload:', file); 687 | }, 688 | defaultFileList: [{ 689 | uid: -1, 690 | name: 'xxx.png', 691 | status: 'done', 692 | url: 'https://os.alipayobjects.com/rmsportal/NDbkJhpzmLxtPhB.png', 693 | thumbUrl: 'https://os.alipayobjects.com/rmsportal/NDbkJhpzmLxtPhB.png', 694 | }] 695 | }); 696 | 697 | return ( 698 |
699 | 700 | 704 | 705 | 708 | 709 | 710 | 711 | 715 | 716 | 719 | 720 | 721 | 722 | 724 | 725 |     726 | 727 | 728 | 729 | ); 730 | } 731 | }; 732 | 733 | DemoImageForm = createForm()(DemoImageForm); 734 | 735 | ////////// 单行多字段 ////////// 736 | class DemoMultipleFieldForm extends Component { 737 | render() { 738 | return ( 739 |
740 | 746 | 747 | 748 | 749 | 754 | 755 | 756 | 757 | 764 | 765 | 766 | 767 | 773 | 774 | 775 | 776 | 782 | 783 | 784 | 785 | 792 | 793 | 794 | 795 | 799 |
800 | 801 | 802 | 803 | 804 | 805 |

-

806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 819 | 820 | 821 | 822 | 823 |

-

824 | 825 | 826 | 827 | 828 | 829 |

请选择正确日期

830 | 831 | 832 | 833 | ); 834 | } 835 | } 836 | 837 | DemoMultipleFieldForm = createForm()(DemoMultipleFieldForm); 838 | 839 | 840 | ////////// 高级搜索 ////////// 841 | class DemoAdvancedForm extends Component { 842 | render() { 843 | return ( 844 | 845 | 846 | 847 | 851 | 852 | 853 | 854 | 855 | 859 | 860 | 861 | 862 | 863 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 877 | 878 | 879 | 880 | 881 | 885 | 886 | 887 | 888 | 889 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 903 | 904 | 905 | 906 | 907 | 911 | 912 | 913 | 914 | 915 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 |     927 | 928 | 929 | 930 | 931 | ); 932 | } 933 | } 934 | 935 | DemoAdvancedForm = createForm()(DemoAdvancedForm); 936 | 937 | 938 | export { 939 | DemoInlineForm, 940 | DemoValidateOtherForm, 941 | DemoImageForm, 942 | DemoMultipleFieldForm, 943 | DemoAdvancedForm, 944 | DemoValidateOtherFormModal, 945 | CreateUserFormModal, 946 | UpdateUserFormModal, 947 | } 948 | -------------------------------------------------------------------------------- /src/page/demo/TestForms.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react'; 3 | 4 | import { 5 | /* 通用 */ 6 | Col, Row, message, Button, Tabs, Icon, Card, Modal, 7 | } from 'antd'; 8 | 9 | import PageIntro from 'component/content/PageIntro.jsx'; 10 | import Topbar from 'component/content/Topbar.jsx'; 11 | 12 | import { 13 | DemoInlineForm, 14 | DemoValidateOtherForm, 15 | DemoImageForm, 16 | DemoMultipleFieldForm, 17 | DemoAdvancedForm, 18 | DemoValidateOtherFormModal, 19 | CreateUserFormModal, 20 | UpdateUserFormModal, 21 | } from 'page/demo/RawForms.jsx'; 22 | 23 | import { 24 | } from 'page/demo/FactoryForms.jsx'; 25 | 26 | 27 | const TabPane = Tabs.TabPane; 28 | 29 | //////////////////////////////////////////////////////////////////////////////// 30 | //////////////////////////////////////////////////////////////////////////////// 31 | //////////////////////////////////////////////////////////////////////////////// 32 | 33 | class TestForms extends Component { 34 | constructor(props) { 35 | super(props); 36 | this.state = { 37 | rawModalVisible: false, 38 | rawCreateUserVisible: false, 39 | rawUpdateUserVisible: false, 40 | factoryModalVisible: false, 41 | defineModalVisible: false 42 | }; 43 | } 44 | 45 | render() { 46 | const wrapperStyle = { 47 | backgroundColor: 'transparent', 48 | }; 49 | const cardStyle = { 50 | marginBottom: '40px', 51 | }; 52 | 53 | const onCancelRaw = (e) => { 54 | console.log('Cancel raw modal clicked'); 55 | this.setState({rawModalVisible: false}); 56 | }; 57 | const onSubmitRaw = (e, form) => { 58 | console.log('onSubmitRaw:', form); 59 | console.log('Values:', form.getFieldsValue()); 60 | message.success('提交成功!', 2); 61 | this.setState({rawModalVisible: false}); 62 | }; 63 | 64 | return ( 65 |
66 | 67 | 68 | 这里是表单示例页面 69 | 70 | 71 | 72 | 76 |
77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | { 87 | /* 88 | 89 | 90 | 91 | */ 92 | } 93 | 94 | 95 | 96 | 97 | this.setState({ rawCreateUserVisible: false })} /> 102 | this.setState({ rawUpdateUserVisible: false })}/> 108 | 110 | 112 | 114 | 115 |
116 |
117 | 118 |
119 | Factory 120 |
121 |
122 | 123 |
124 | Define 125 |
126 |
127 |
128 |
129 | ); 130 | } 131 | } 132 | 133 | 134 | export default TestForms; 135 | -------------------------------------------------------------------------------- /src/style/form.css: -------------------------------------------------------------------------------- 1 | 2 | .advanced-search-form { 3 | padding: 16px 8px; 4 | background: #f8f8f8; 5 | border: 1px solid #d9d9d9; 6 | border-radius: 6px; 7 | } 8 | 9 | /* 由于输入标签长度不确定,所以需要微调使之看上去居中 */ 10 | .advanced-search-form > .row { 11 | margin-left: -10px; 12 | } 13 | 14 | .advanced-search-form > .row > .col-8 { 15 | padding: 0 8px; 16 | } 17 | 18 | .advanced-search-form .ant-form-item { 19 | margin-bottom: 16px; 20 | } 21 | 22 | .advanced-search-form .ant-btn + .ant-btn { 23 | margin-left: 8px; 24 | } 25 | -------------------------------------------------------------------------------- /src/style/index.js: -------------------------------------------------------------------------------- 1 | import 'antd/style/index.less'; 2 | import 'font-awesome/css/font-awesome.css'; 3 | 4 | import 'style/main.css'; 5 | import 'style/table.css'; 6 | import 'style/form.css'; 7 | -------------------------------------------------------------------------------- /src/style/main.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | height: 100%; 4 | overflow: auto; 5 | } 6 | 7 | body { 8 | font-family: Arial,"Hiragino Sans GB","Microsoft YaHei","微软雅黑",STHeiti,"WenQuanYi Micro Hei",SimSun,sans-serif; 9 | font-size: 12px; 10 | line-height: 16px; 11 | color: #333; 12 | background: #f1f4f6 !important; 13 | } 14 | 15 | a, a:hover { 16 | text-decoration: none; 17 | } 18 | 19 | ul, ol, ul li, ol li { 20 | margin: 0; 21 | padding: 0; 22 | list-style-type: none; 23 | } 24 | 25 | 26 | .app-container { 27 | width: 100%; 28 | height: 100%; 29 | margin: auto; 30 | } 31 | 32 | .app-viewport { 33 | height: 100%; 34 | position: relative; 35 | } 36 | 37 | .list-btn { 38 | margin-right: 3px; 39 | } 40 | 41 | /******************************************/ 42 | /**** =Navbar */ 43 | .nav-global { 44 | height: 48px; 45 | background: #fff; 46 | position: fixed; 47 | width: 100%; 48 | z-index: 99; 49 | } 50 | 51 | .global-logo { 52 | width: 200px; 53 | margin: 0 auto; 54 | display: block; 55 | height: 48px; 56 | /* background: #404040; */ 57 | background: #555; 58 | } 59 | .global-logo, .global-notice { 60 | float: left; 61 | } 62 | 63 | .nav-global .items-inner { 64 | height: 48px; 65 | -webkit-box-shadow: 0 -1px 8px 1px #bbb; 66 | -moz-box-shadow: 0 -1px 8px 1px #bbb; 67 | box-shadow: 0 -1px 8px 1px #bbb; 68 | margin-left: 200px; 69 | } 70 | .global-items { 71 | padding-right: 15px; 72 | } 73 | .global-items { 74 | float: right; 75 | } 76 | .global-items .nav-item { 77 | display: inline-block; 78 | border-left: 1px solid #eee; 79 | height: 48px; 80 | vertical-align: middle; 81 | position: relative; 82 | } 83 | .global-items .nav-item .nav-item-inner { 84 | display: block; 85 | padding: 0 20px; 86 | height: 48px; 87 | position: relative; 88 | } 89 | .global-items .nav-item .nav-item-inner:hover { 90 | background: #f8f8f8; 91 | } 92 | 93 | .global-items .nav-item .nav-text { 94 | color: #333; 95 | padding-left: 10px; 96 | line-height: 48px; 97 | vertical-align: middle; 98 | } 99 | 100 | 101 | 102 | /******************************************/ 103 | /**** Menu */ 104 | .nav-menu { 105 | width: 200px; 106 | /* background: #22222c; */ 107 | /* background: #FAFAFA; */ 108 | background: #404040; 109 | /* border-right: 1px solid #d9d9d9; */ 110 | height: 100%; 111 | float: left; 112 | /* padding-top: 48px; */ 113 | padding-top: 54px; 114 | position: fixed; 115 | z-index: 90; 116 | overflow-y: auto; 117 | } 118 | 119 | .nav-menu-inner { 120 | padding-bottom: 96px; 121 | } 122 | 123 | /******************************************/ 124 | /**** [Content] */ 125 | .content { 126 | color: #2a2d2e; 127 | background: #f1f4f6; 128 | position: relative; 129 | margin-left: 200px; 130 | border-top: 1px solid #ddd; 131 | padding-top: 48px; 132 | } 133 | 134 | .content .wrapper { 135 | height: 100%; 136 | margin: 0 20px; 137 | padding-bottom: 40px; 138 | } 139 | 140 | /* breadcrumbs */ 141 | .topbar { 142 | overflow: hidden; 143 | margin: 10px 0; 144 | } 145 | .topbar .breadcrumbs { 146 | float: left; 147 | } 148 | .topbar .level { 149 | font-size: 12px; 150 | color: #777; 151 | display: inline-block; 152 | padding-right: 5px; 153 | text-transform: uppercase; 154 | } 155 | 156 | /* Page introduction */ 157 | .page-intro { 158 | background: #e4eaec; 159 | overflow: hidden; 160 | margin: 10px 0 0; 161 | -webkit-border-radius: 3px 3px 0 0; 162 | -moz-border-radius: 3px 3px 0 0; 163 | border-radius: 3px 3px 0 0; 164 | color: #58666e; 165 | } 166 | .page-intro .lead { 167 | margin: 16px 20px; 168 | font-size: 12px; 169 | line-height: 18px; 170 | border-radius: 5px 5px 0 0; 171 | } 172 | 173 | 174 | /* Page tabs */ 175 | .page-tabs { 176 | margin-top: 15px; 177 | padding: 0; 178 | } 179 | 180 | /* Panel introduction */ 181 | .pane-wrapper { 182 | background: #fff; 183 | margin-top: 0px; 184 | padding: 15px 15px 20px 15px; 185 | border-radius: 0 0 5px 5px; 186 | } 187 | .pane-block { 188 | width: 600px; 189 | } 190 | .pane-intro { 191 | color: #777; 192 | line-height: 1.5; 193 | padding: 0; 194 | margin: 0 0; 195 | } 196 | 197 | /* Panel content */ 198 | .pane { 199 | padding: 15px 0 0; 200 | margin: 0 0 20px; 201 | } 202 | 203 | /* The toolbar, usually in panel */ 204 | .toolbar { 205 | position: relative; 206 | z-index: 4; 207 | margin: 10px 0; 208 | } 209 | 210 | 211 | 212 | #test { 213 | height: 1000px; 214 | } 215 | -------------------------------------------------------------------------------- /src/style/table.css: -------------------------------------------------------------------------------- 1 | th.ant-table-selection-column, 2 | td.ant-table-selection-column { 3 | width: 28px; 4 | padding: 0; 5 | } 6 | -------------------------------------------------------------------------------- /static/config.js.jinja2: -------------------------------------------------------------------------------- 1 | 2 | window.globalConfig = { 3 | apiUrl: '{{ api_url }}', 4 | }; 5 | -------------------------------------------------------------------------------- /static/logo48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/huhulab/react-frontend-boilerplate/53c51e9b196a23f3867ec29914c6b2e7d8a2bda5/static/logo48x48.png -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | resolve: { 7 | root: path.resolve('./src') 8 | }, 9 | entry: [ 10 | 'webpack-hot-middleware/client', 11 | './src/entry/index.jsx' 12 | ], 13 | output: { 14 | path: path.join(__dirname, 'dist'), 15 | filename: 'bundle.js', 16 | publicPath: '/static/' 17 | }, 18 | plugins: [ 19 | new webpack.HotModuleReplacementPlugin(), 20 | new webpack.NoErrorsPlugin() 21 | ], 22 | module: { 23 | loaders: [ 24 | { 25 | test: /\.(js|jsx)$/, 26 | loaders: ['babel'], 27 | include: path.join(__dirname, 'src') 28 | }, { 29 | test: /\.(css|less)$/, 30 | loaders: ["style", "css", "less"] 31 | }, { 32 | test: /\.(png|woff|woff2|eot|ttf|svg)(\?v=\d+\.\d+\.\d+)?$/, 33 | loader: 'url-loader?limit=100000' 34 | } 35 | ] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | resolve: { 7 | root: path.resolve('./src') 8 | }, 9 | entry: [ 10 | './src/entry/index.jsx' 11 | ], 12 | output: { 13 | path: path.join(__dirname, 'dist'), 14 | filename: 'bundle.js', 15 | publicPath: '/static/' 16 | }, 17 | plugins: [ 18 | new webpack.optimize.OccurenceOrderPlugin(), 19 | new webpack.DefinePlugin({ 20 | 'process.env': { 21 | 'NODE_ENV': JSON.stringify('production') 22 | } 23 | }), 24 | new webpack.optimize.UglifyJsPlugin({ 25 | compressor: { 26 | warnings: false 27 | } 28 | }) 29 | ], 30 | module: { 31 | loaders: [ 32 | { 33 | test: /\.(js|jsx)$/, 34 | loaders: ['babel'], 35 | include: path.join(__dirname, 'src') 36 | }, { 37 | test: /\.(css|less)$/, 38 | loaders: ["style", "css", "less"] 39 | }, { 40 | test: /\.(png|woff|woff2|eot|ttf|svg)(\?v=\d+\.\d+\.\d+)?$/, 41 | loader: 'url-loader?limit=100000' 42 | } 43 | ] 44 | } 45 | }; 46 | --------------------------------------------------------------------------------