some clean

This commit is contained in:
zuiidea
2016-09-17 14:45:49 +08:00
parent c576487d22
commit 0740f644a4
30 changed files with 194 additions and 2550 deletions
+1
View File
@@ -37,6 +37,7 @@
"react-dom": "15.0.2",
"react-hot-loader": "1.3.0",
"react-router": "2.0.1",
"reqwest": "^2.0.5",
"strip-loader": "0.1.2",
"style-loader": "0.13.1",
"stylelint": "6.6.0",
-112
View File
@@ -1,112 +0,0 @@
import React from 'react';
import {Spin, message} from 'antd';
import Header from '../Header';
import Footer from '../Footer';
import Sidebar from '../Sidebar';
import Login from '../Login';
import Breadcrumb from '../Breadcrumb';
import './index.less';
import globalConfig from 'config.js';
import ajax from '../../utils/ajax';
import Logger from '../../utils/Logger';
const logger = Logger.getLogger('App');
/**
* App组件
* 定义整个页面的布局
*/
class App extends React.Component {
// 全局的状态
state = {
loading: true, // 是否在loading状态
login: false, // 是否登录
userName: '未登录', // 登录后的用户名
};
componentWillMount() {
logger.debug('mount and add styles');
// 为了兼容登录组件
document.body.style.background = 'white'; // FIXME: gross hack
}
componentDidMount() {
if (!this.state.login) {
const hide = message.loading('正在获取用户信息...', 0);
// 先去服务端验证下, 说不定已经登录了
ajax.get(`${globalConfig.getAPIPath()}${globalConfig.login.getCurrentUser}`).end((err, res) => {
// 注意这里, debug模式下每次刷新都必须重新登录
if (ajax.isSuccess(res) && !globalConfig.debug) {
hide();
this.loginSuccess(res.body.data);
} else {
// 如果服务端说没有登录, 就要跳转到sso或者登录界面
if (globalConfig.isSSO()) {
// debug模式不支持调试单点登录
// 因为没有单点登录的地址啊...跳不回来
hide();
logger.debug('not login, redirect to SSO login page');
const redirect = encodeURIComponent(window.location.href);
window.location.href = `${globalConfig.login.sso}${redirect}`;
} else {
hide();
message.error('获取用户信息失败, 请重新登录');
logger.debug('not login, redirect to Login component');
this.setState({loading: false, login: false});
}
}
});
}
}
// 登录成功后的回调
loginSuccess = (name, showMsg) => {
logger.debug('callback logingSuccess, name = %s', name);
if (showMsg)
message.success('登录成功');
document.body.style.background = 'white'; // FIXME: gross hack
this.setState({loading: false, login: true, userName: name});
}
render() {
// 显示一个加载中
if (this.state.loading) {
return <div className="center-div"><Spin spinning={this.state.loading} size="large"/></div>;
}
// 跳转到登录界面
if (!this.state.login) {
return <Login loginSuccess={this.loginSuccess}/>;
}
return (
<div className="ant-layout-aside">
{/*整个页面被一个ant-layout-aside的div包围, 分为sidebar/header/footer/content等几部分*/}
<Sidebar />
<div id="main-content-div" className="ant-layout-main">
<Spin spinning={this.state.loading} size="large">
<Header userName={this.state.userName}/>
<Breadcrumb {...this.props} />
{/*TODO: 这里要组件化*/}
<div className="ant-layout-container">
<div className="ant-layout-content">
{this.state.loading ? '' : this.props.children}
</div>
</div>
<Footer />
</Spin>
</div>
</div>
);
}
}
export default App;
-154
View File
@@ -1,154 +0,0 @@
/*定义整体layout的样式, 直接从antd官网copy的, 稍微修改了一些*/
/*我本来想把样式拆到各个模块去, 但是太麻烦了...*/
.ant-layout-aside {
position: relative;
height: 100%;
}
.ant-layout-aside .ant-layout-logo {
width: 200px;
height: 32px;
background: #333;
border-radius: 6px;
margin: 16px 8px 16px 12px;
transition: all 0.3s ease;
}
.ant-layout-aside-collapse .ant-layout-logo {
width: 32px;
margin: 16px;
transition: all 0.3s ease;
}
.ant-layout-aside .ant-layout-sider {
width: 224px;
background: #404040;
position: absolute;
overflow: auto;
padding-bottom: 24px;
height: 100%;
transition: all 0.3s ease;
}
.ant-layout-aside-collapse .ant-layout-sider {
width: 64px;
transition: all 0.3s ease;
}
.ant-layout-aside .ant-layout-sider > .ant-menu {
margin-bottom: 20px;
}
.ant-layout-aside .ant-layout-sider > .ant-menu > .ant-menu-item {
margin: 16px 0;
}
.ant-layout-aside .ant-layout-sider > .ant-menu > .ant-menu-item .nav-text {
vertical-align: baseline;
display: inline-block;
}
.ant-layout-aside .ant-layout-sider > .ant-menu > .ant-menu-item > .anticon {
transition: font-size .3s;
}
.ant-layout-aside-collapse .ant-layout-sider > .ant-menu > .ant-menu-item {
transition: all 0s ease;
}
.ant-layout-aside-collapse .ant-layout-sider > .ant-menu > .ant-menu-item > .anticon {
font-size: 16px;
display: inline-block;
}
.ant-layout-aside-collapse .ant-layout-sider > .ant-menu > .ant-menu-item .nav-text {
display: none;
}
.ant-layout-aside-collapse .ant-layout-sider > .ant-menu > .ant-menu-item:hover {
background: #2db7f5;
color: #fff;
transition: all 0s ease;
}
.ant-layout-aside-collapse .ant-layout-sider > .ant-menu > .ant-menu-item:hover .nav-text {
display: inline-block;
vertical-align: top;
background: #2db7f5;
color: #fff;
padding-right: 16px;
border-radius: 0 5px 5px 0;
}
/* 实际使用中需要改成 position: fixed */
.ant-layout-aside .ant-aside-action {
height: 42px;
width: 224px;
position: absolute;
bottom: 0;
background: #656565;
color: #fff;
text-align: center;
line-height: 42px;
cursor: pointer;
transition: all 0.3s ease;
}
.ant-layout-aside-collapse .ant-aside-action {
width: 64px;
transition: all 0.3s ease;
}
.ant-layout-aside .ant-layout-header {
background: #fff;
height: 48px;
border-bottom: 1px solid #e9e9e9;
}
.ant-layout-aside .ant-layout-breadcrumb {
margin: 7px 0 -17px 24px;
}
.ant-layout-aside .ant-layout-main {
margin-left: 224px;
transition: all 0.3s ease;
overflow: auto;
height: 100%;
}
.ant-layout-aside-collapse .ant-layout-main {
margin-left: 64px;
transition: all 0.3s ease;
}
.ant-layout-aside .ant-layout-container {
margin: 24px 16px;
}
.ant-layout-aside .ant-layout-content {
background: #fff;
padding: 24px;
}
.ant-layout-aside .ant-layout-footer {
height: 64px;
line-height: 64px;
text-align: center;
font-size: 12px;
color: #999;
background: #fff;
border-top: 1px solid #e9e9e9;
width: 100%;
}
// 为了在屏幕中间显示一个loading
.center-div {
position: absolute; /*绝对定位*/
top: 50%; /* 距顶部50%*/
left: 50%; /* 距左边50%*/
height: 2px;
margin-top: -1px; /*margin-top为height一半的负值*/
width: 2px;
margin-left: -1px; /*margin-left为width一半的负值*/
}
+1 -1
View File
@@ -1,7 +1,7 @@
import React from 'react';
import {Breadcrumb, Icon} from 'antd';
import items from 'menu.js';
import Logger from '../../utils/Logger';
import Logger from '../../utils/logger';
const Item = Breadcrumb.Item;
const logger = Logger.getLogger('Breadcrumb');
-449
View File
@@ -1,449 +0,0 @@
import React from 'react';
import {
Form,
Input,
Row,
Col,
Button,
DatePicker,
Select,
Icon,
Radio,
InputNumber,
Checkbox,
message,
Upload,
notification
} from 'antd';
import globalConfig from 'config.js';
import Logger from '../../utils/Logger';
const FormItem = Form.Item;
const RadioGroup = Radio.Group;
const CheckboxGroup = Checkbox.Group;
const Option = Select.Option;
const logger = Logger.getLogger('InnerForm');
/**
* 内部表单组件
*/
class InnerForm extends React.Component {
// 本来想在初始化时解析schema, 结果发现不行
// 解析schema的过程还是改到render里了
/**
* 辅助函数, 将一个input元素包装下
*
* @param formItem
* @param field
* @returns {XML}
*/
colWrapper = (formItem, field) => {
//const {getFieldProps, getFieldError, isFieldValidating} = this.props.form;
return (
<Col key={field.key} sm={8}>
<FormItem key={field.key} label={field.title} labelCol={{ span: 10 }} wrapperCol={{ span: 14 }}>
{formItem}
</FormItem>
</Col>
);
}
/**
* 将schema中的一列转换为下拉框
*
* @param field
*/
transformSelect = (field) => {
// TODO: 这里是否要做schema校验
const options = [];
const {getFieldProps} = this.props.form;
logger.debug('transform field %o to Select component', field);
field.options.forEach((option) => {
options.push(<Option key={option.key} value={option.key}>{option.value}</Option>);
});
return this.colWrapper((
<Select placeholder={field.placeholder || '请选择'} size="default" {...getFieldProps(field.key)}>
{options}
</Select>
), field);
}
/**
* 将schema中的一列转换为单选框
*
* @param field
*/
transformRadio = (field) => {
const options = [];
const {getFieldProps} = this.props.form;
logger.debug('transform field %o to Radio component', field);
field.options.forEach((option) => {
options.push(<Radio key={option.key} value={option.key}>{option.value}</Radio>);
});
return this.colWrapper((
<RadioGroup {...getFieldProps(field.key)}>
{options}
</RadioGroup>
), field);
}
/**
* 将schema中的一列转换为checkbox
*
* @param field
*/
transformCheckbox = (field) => {
const options = [];
const {getFieldProps} = this.props.form;
logger.debug('transform field %o to Checkbox component', field);
field.options.forEach((option) => {
options.push({label: option.value, value: option.key});
});
return this.colWrapper((
<CheckboxGroup options={options} {...getFieldProps(field.key)}/>
), field);
}
/**
* 转换为下拉多选框
*
* @param field
* @returns {XML}
*/
transformMultiSelect = (field) => {
const options = [];
const {getFieldProps} = this.props.form;
logger.debug('transform field %o to MultipleSelect component', field);
field.options.forEach((option) => {
options.push(<Option key={option.key} value={option.key}>{option.value}</Option>);
});
return this.colWrapper((
<Select multiple placeholder={field.placeholder || '请选择'} size="default" {...getFieldProps(field.key)}>
{options}
</Select>
), field);
}
/**
* 将schema中的一列转换为普通输入框
*
* @param field
* @returns {XML}
*/
transformNormal = (field) => {
const {getFieldProps} = this.props.form;
switch (field.dataType) {
case 'int':
logger.debug('transform field %o to integer input component', field);
return this.colWrapper((
<InputNumber size="default" {...getFieldProps(field.key)}/>
), field);
case 'float':
logger.debug('transform field %o to float input component', field);
return this.colWrapper((
<InputNumber step={0.01} size="default" {...getFieldProps(field.key)}/>
), field);
case 'datetime':
logger.debug('transform field %o to datetime input component', field);
return this.colWrapper((
<DatePicker showTime format="yyyy-MM-dd HH:mm:ss"
placeholder={field.placeholder || '请选择日期'} {...getFieldProps(field.key)}/>
), field);
default: // 默认就是普通的输入框
logger.debug('transform field %o to varchar input component', field);
return this.colWrapper((
<Input placeholder={field.placeholder} size="default" {...getFieldProps(field.key)}/>
), field);
}
}
/**
* 也是个辅助函数, 由于是范围查询, 输入的formItem是两个, 一个用于begin一个用于end
*
* @param beginFormItem
* @param endFormItem
* @param field
* @returns {XML}
*/
betweenColWrapper = (beginFormItem, endFormItem, field) => {
// 布局真是个麻烦事
// col内部又用了一个row做布局
// const {getFieldProps} = this.props.form;
return (
<Col key={`${field.key}Begin`} sm={8}>
<Row>
<Col span={16}>
<FormItem key={`${field.key}Begin`} label={field.title} labelCol={{ span: 15 }} wrapperCol={{ span: 9 }}>
{beginFormItem}
</FormItem>
</Col>
<Col span={7} offset={1}>
<FormItem key={`${field.key}End`} labelCol={{ span: 10 }} wrapperCol={{ span: 14 }}>
{endFormItem}
</FormItem>
</Col>
</Row>
</Col>
);
}
/**
* between类型比较特殊, 普通元素每个宽度是8, int和float的between元素宽度也是8, 但datetime类型的between元素宽度是16
* 否则排版出来不能对齐, 太丑了, 逼死强迫症
* 而且普通的transform函数返回是一个object, 而这个函数返回是一个array, 就是因为datetime的between要占用两列
*
* @param field
*/
transformBetween = (field) => {
const cols = [];
let beginFormItem;
let endFormItem;
const {getFieldProps} = this.props.form;
switch (field.dataType) {
case 'int':
logger.debug('transform field %o to integer BETWEEN component', field);
beginFormItem = (<InputNumber size="default"
placeholder={field.placeholderBegin || '最小值'} {...getFieldProps(`${field.key}Begin`)}/>);
endFormItem = (<InputNumber size="default"
placeholder={field.placeholderEnd || '最大值'} {...getFieldProps(`${field.key}End`)}/>);
cols.push(this.betweenColWrapper(beginFormItem, endFormItem, field));
break;
case 'float':
logger.debug('transform field %o to float BETWEEN component', field);
beginFormItem = (<InputNumber step={0.01} size="default"
placeholder={field.placeholderBegin || '最小值'} {...getFieldProps(`${field.key}Begin`)}/>);
endFormItem = (<InputNumber step={0.01} size="default"
placeholder={field.placeholderEnd || '最大值'} {...getFieldProps(`${field.key}End`)}/>);
cols.push(this.betweenColWrapper(beginFormItem, endFormItem, field));
break;
// datetime类型的between要占用两个Col
// 不写辅助函数了, 直接这里写jsx吧...
case 'datetime':
logger.debug('transform field %o to datetime BETWEEN component', field);
cols.push(
<Col key={`${field.key}Begin`} sm={8}>
<FormItem key={`${field.key}Begin`} label={field.title} labelCol={{ span: 10 }} wrapperCol={{ span:14 }}>
<DatePicker showTime format="yyyy-MM-dd HH:mm:ss"
placeholder={field.placeholderBegin || '开始日期'} {...getFieldProps(`${field.key}Begin`)}/>
</FormItem>
</Col>
);
cols.push(<Col key={`${field.key}End`} sm={8}>
<FormItem key={`${field.key}End`} labelCol={{ span: 10 }} wrapperCol={{ span:14 }}>
<DatePicker showTime format="yyyy-MM-dd HH:mm:ss"
placeholder={field.placeholderEnd || '结束日期'} {...getFieldProps(`${field.key}End`)}/>
</FormItem>
</Col>);
break;
default:
// 理论上来说不会出现这种情况
logger.error('unknown dataType: %s', field.dataType);
}
return cols;
}
/**
* 表单的查询条件不能直接传给后端, 要处理一下
*
* @param oldObj
* @returns {{}}
*/
filterQueryObj(oldObj) {
// 将提交的值中undefined的去掉
const newObj = {};
for (const key in oldObj) {
if (oldObj[key]) {
// 对于js的日期类型, 要转换成字符串再传给后端
if (oldObj[key] instanceof Date) {
newObj[key] = oldObj[key].format('yyyy-MM-dd HH:mm:ss');
} else {
newObj[key] = oldObj[key];
}
}
}
logger.debug('old queryObj: %o, new queryObj %o', oldObj, newObj);
return newObj;
}
/**
* 处理表单提交
*
* @param e
*/
handleSubmit = (e) => {
e.preventDefault();
const oldObj = this.props.form.getFieldsValue();
const newObj = this.filterQueryObj(oldObj);
// 还是要交给上层组件处理, 因为要触发table组件的状态变化...
this.props.parentHandleSubmit(newObj);
}
handleReset = (e) => {
e.preventDefault();
this.props.form.resetFields();
}
/**
* 处理数据导入
*/
handleImport = (info) => {
logger.debug('upload status: %s', info.file.status);
// 正在导入时显示一个提示信息
if (info.file.status === 'uploading') {
if (!this.hideLoading) {
let hide = message.loading('正在导入...');
this.hideLoading = hide;
}
}
// 导入完成, 无论成功或失败, 必须给出提示, 并且要用户手动关闭
else if (info.file.status === 'error') {
this.hideLoading();
this.hideLoading = undefined;
notification.error({
message: '导入失败',
description: '文件上传失败, 请联系管理员',
duration: 0,
});
}
// done的情况下还要判断返回值
else if (info.file.status === 'done') {
this.hideLoading();
this.hideLoading = undefined;
logger.debug('upload result %o', info.file.response);
if (!info.file.response.success) {
notification.error({
message: '导入失败',
description: `请联系管理员, 错误信息: ${info.file.response.message}`,
duration: 0,
});
} else {
notification.success({
message: '导入成功',
description: info.file.response.data,
duration: 0,
});
}
}
}
/**
* 处理数据导出
* 本质上也是提交表单, 跟handleSubmit有点类似
* 但不用再提交给上层组件处理了, 因为不需要改变表格组件的状态
*/
handleExport = (e) => {
e.preventDefault();
const oldObj = this.props.form.getFieldsValue();
const newObj = this.filterQueryObj(oldObj);
// 导出前必须选定了一些查询条件, 不允许导出全表
// 防止误操作
if (Object.keys(newObj).length === 0) {
message.warning('导出时查询条件不能为空', 4.5);
return;
}
// ajax是不能处理下载请求的, 必须交给浏览器自己去处理
// 坏处是我就不知道用户的下载是否成功了
const url = `${globalConfig.getAPIPath()}/${this.props.tableName}/export`;
window.open(`${url}?q=${encodeURIComponent(JSON.stringify(newObj))}`); // 注意url编码
}
render() {
const rows = [];
let cols = [];
// 参见antd的布局, 每行被分为24个格子
// 普通的字段每个占用8格, between类型的字段每个占用16格
let spaceLeft = 24;
this.props.schema.forEach((field)=> {
// 当前列需要占用几个格子? 普通的都是8, 只有datetime between是16
let spaceNeed = 8;
if (field.showType === 'between' && field.dataType === 'datetime') {
spaceNeed = 16;
}
// 如果当前行空间不足, 就换行
if (spaceLeft < spaceNeed) {
rows.push(<Row key={rows.length} gutter={16}>{cols}</Row>);
cols = []; // 不知array有没有clear之类的方法
spaceLeft = 24; // 剩余空间重置
}
// 开始push各种FormItem
switch (field.showType) {
case 'select':
cols.push(this.transformSelect(field));
break;
case 'radio':
cols.push(this.transformRadio(field));
break;
case 'checkbox':
cols.push(this.transformCheckbox(field));
break;
case 'multiSelect':
cols.push(this.transformMultiSelect(field));
break;
case 'between':
for (const col of this.transformBetween(field)) { // between类型比较特殊, 返回的是一个数组
cols.push(col)
}
break;
default:
cols.push(this.transformNormal(field));
}
spaceLeft -= spaceNeed;
});
// 别忘了最后一行
if (cols.length > 0) {
rows.push(<Row key={rows.length} gutter={16}>{cols}</Row>);
}
// 上传相关配置
const uploadProps = {
name: 'file',
action: `${globalConfig.getAPIPath()}/${this.props.tableName}/import`,
showUploadList: false,
onChange: this.handleImport,
};
// 表单的前面是一堆输入框, 最后一行是按钮
return (
<Form horizontal className="ant-advanced-search-form">
{rows}
<Row>
<Col span={12} offset={12} style={{ textAlign: 'right' }}>
<Button type="primary" onClick={this.handleSubmit}><Icon type="search"/>查询</Button>
<Button onClick={this.handleReset}><Icon type="cross"/>清除条件</Button>
{this.props.tableConfig.showExport ?
<Button onClick={this.handleExport}><Icon type="export"/>导出</Button> : ''}
{this.props.tableConfig.showImport ?
<Upload {...uploadProps}><Button><Icon type="upload"/>导入</Button></Upload> : ''}
</Col>
</Row>
</Form>
);
}
}
InnerForm = Form.create()(InnerForm); // antd中的表单组件还要这么包装一层
export default InnerForm;
-28
View File
@@ -1,28 +0,0 @@
import React from 'react';
import {Pagination, Select} from 'antd';
/**
* 内部分页器组件
*/
class InnerPagination extends React.Component {
render() {
// 有些状态要传到父组件中去处理
return (
<div className="db-pagination">
<Pagination
showQuickJumper
selectComponentClass={Select}
total={this.props.total}
showTotal={(total) => `每页${this.props.pageSize}条, 共 ${total} 条`}
pageSize={this.props.pageSize} defaultCurrent={1}
current={this.props.currentPage}
onChange={this.props.parentHandlePageChange}
/>
</div>
);
}
}
export default InnerPagination;
-425
View File
@@ -1,425 +0,0 @@
import React from 'react';
import {
Form,
Input,
Button,
DatePicker,
Select,
Table,
Icon,
Radio,
InputNumber,
Checkbox,
Modal,
message,
notification,
Affix
} from 'antd';
import globalConfig from 'config.js';
import Logger from '../../utils/Logger';
import ajax from '../../utils/ajax';
const FormItem = Form.Item;
const ButtonGroup = Button.Group;
const logger = Logger.getLogger('InnerTable');
/**
* 内部表格组件
*/
class InnerTable extends React.Component {
// 很多时候都要在antd的组件上再包一层
state = {
modalVisible: false, // modal是否可见
modalTitle: '新增', // modal标题
modalInsert: true, // 当前modal是用来insert还是update
selectedRowKeys: [], // 当前有哪些行被选中, 这里只保存key
selectedRows: [], // 当前有哪些行被选中, 保存完整数据
};
/**
* InnerTable组件的重render有两种可能:
* 1. 上层组件调用的render方法, 这个时候会触发componentWillReceiveProps方法
* 2. 自身状态变化引起的重新render
* 注意区分
*
* 对于第一种情况, 要将组件的状态还原到初始状态
*
* @param nextProps
*/
componentWillReceiveProps = (nextProps) => {
logger.debug('receive new props and try to render, nextProps=%o', nextProps);
// 其实传入的props和当前的props可能是一样的, 这个方法不会判断修改才触发
// 要自己判断props是否有变化
// 蛋疼的是, 要区分是上层组件引起的这个方法调用还是InnerTable组件本身引起的
// 本来自身的变化是不应该触发的, 但不知antd的form组件做了什么, 每次给表单设置值的时候就会触发, 我讨厌黑盒...
// 利用了一个特性: 如果是上层触发的, tableLoading必定是true
if (nextProps.tableLoading === true) {
// 所有状态都要手动还原到初始值
// 这个方法里setState不会触发render
this.setState({
modalVisible: false,
modalTitle: '新增',
modalInsert: true,
selectedRowKeys: [],
selectedRows: [],
});
}
}
/**
* 点击新增按钮, 弹出一个内嵌表单的modal
*
* @param e
*/
onClickInsert = (e) => {
e.preventDefault();
this.props.form.resetFields();
this.setState({modalVisible: true, modalTitle: '新增', modalInsert: true});
}
/**
* 点击更新按钮, 弹出一个内嵌表单的modal
* 注意区分单条更新和批量更新
*
* @param e
*/
onClickUpdate = (e) => {
e.preventDefault();
this.props.form.resetFields();
const multiSelected = this.state.selectedRowKeys.length > 1; // 是否选择了多项
// 如果只选择了一项, 就把原来的值填到表单里
// 否则就只把要更新的主键填到表单里
if (!multiSelected) {
logger.debug('update single record, and fill original values');
this.props.form.setFieldsValue(this.state.selectedRows.pop()); // FIXME: 理论上来说不应该这样去修改state中的值
} else {
const tmpObj = {};
tmpObj[this.primaryKey] = this.state.selectedRowKeys.join(', ');
logger.debug('update multiple records, keys = %s', tmpObj[this.primaryKey]);
this.props.form.setFieldsValue(tmpObj);
}
if (multiSelected) {
this.setState({modalVisible: true, modalTitle: '批量更新', modalInsert: false});
} else {
this.setState({modalVisible: true, modalTitle: '更新', modalInsert: false});
}
}
/**
* 点击删除按钮, 弹出一个确认对话框
* 注意区分单条删除和批量删除
*
* @param e
*/
onClickDelete = (e) => {
e.preventDefault();
Modal.confirm({
title: this.state.selectedRowKeys.length > 1 ? '确认批量删除' : '确认删除',
content: `当前被选中的行: ${this.state.selectedRowKeys.join(', ')}`,
// 这里注意要用箭头函数, 否则this不生效
onOk: () => {
this.handleDelete();
},
});
}
/**
* 隐藏modal
*/
hideModal = () => {
this.setState({modalVisible: false});
}
/**
* 点击modal中确认按钮的回调
*/
handleModalOk = () => {
// 将表单中的undefined去掉
// 表单传过来的主键是逗号分隔的字符串, 这里转换成数组
const newObj = {};
const primaryKeyArray = [];
const oldObj = this.props.form.getFieldsValue();
for (const key in oldObj) {
if (!oldObj[key])
continue;
if (key === this.primaryKey && typeof oldObj[key] === 'string') {
for (const str of oldObj[key].split(', ')) {
// 按schema中的约定, 主键只能是int/varchar
if (this.primaryKeyType === 'int') {
primaryKeyArray.push(parseInt(str));
} else {
primaryKeyArray.push(str);
}
}
} else {
newObj[key] = oldObj[key];
}
}
if (this.primaryKey && primaryKeyArray.length > 0) {
newObj[this.primaryKey] = primaryKeyArray;
}
this.hideModal();
logger.debug('click modal OK and the form obj = %o', newObj);
if (this.state.modalInsert) {
this.handleInsert(newObj);
} else {
this.handleUpdate(newObj);
}
}
/*下面开始才是真正的数据库操作*/
/**
* 真正去处理新增数据
*/
handleInsert = (obj) => {
const url = `${globalConfig.getAPIPath()}/${this.props.tableName}/insert`;
const hide = message.loading('正在新增...', 0);
logger.debug('handleInsert: url = %s, obj = %o', url, obj);
ajax.post(url).send(obj).end((err, res) => {
hide();
logger.debug('handleInsert: return error = %o, res = %o', err, res);
// err就是一个字符串
// res是一个Response对象, 其中的body字段才是服务端真正返回的数据
if (err || !res.body.success) {
notification.error({
message: '新增失败',
description: err ? '请求insert接口出错, 请联系管理员' : `请联系管理员, 错误信息: ${res.body.message}`,
duration: 0,
});
} else {
notification.success({
message: '新增成功',
description: this.primaryKey ? `新增数据行 主键=${res.body.data[this.primaryKey]}` : '',
duration: 0,
});
}
});
}
/**
* 真正去更新数据
*/
handleUpdate = (obj) => {
const keys = obj[this.primaryKey];
const url = `${globalConfig.getAPIPath()}/${this.props.tableName}/update?keys=${keys instanceof Array ? keys.join(',') : keys}`;
const hide = message.loading('正在更新...', 0);
obj[this.primaryKey] = undefined;
logger.debug('handleUpdate: url = %s, obj = %o', url, obj);
ajax.post(url).send(obj).end((err, res) => {
hide();
logger.debug('handleUpdate: return error = %o, res = %o', err, res);
if (err || !res.body.success) {
notification.error({
message: '更新失败',
description: err ? '请求update接口出错, 请联系管理员' : `请联系管理员, 错误信息: ${res.body.message}`,
duration: 0,
});
} else {
notification.success({
message: '更新成功',
description: `更新${res.body.data}条数据`,
duration: 0,
});
// 更新数据后, 注意刷新下整个页面
this.props.refresh();
}
});
}
/**
* 真正去删除数据
*/
handleDelete = () => {
const url = `${globalConfig.getAPIPath()}/${this.props.tableName}/delete?keys=${this.state.selectedRowKeys.join(',')}`;
const hide = message.loading('正在删除...', 0);
logger.debug('handleDelete: url = %s', url);
ajax.post(url).end((err, res) => {
hide();
logger.debug('handleDelete: return error = %o, res = %o', err, res);
if (err || !res.body.success) {
notification.error({
message: '删除失败',
description: err ? '请求delete接口出错, 请联系管理员' : `请联系管理员, 错误信息: ${res.body.message}`,
duration: 0,
});
} else {
notification.success({
message: '删除成功',
description: `删除${res.body.data}条数据`,
duration: 0,
});
// 更新数据后, 注意刷新下整个页面
this.props.refresh();
}
});
}
/**
* 辅助函数
*
* @param formItem
* @param field
* @returns {XML}
*/
colWrapper = (formItem, field) => {
//const {getFieldProps, getFieldError, isFieldValidating} = this.props.form;
return (
<FormItem key={field.key} label={field.title} labelCol={{ span: 4 }} wrapperCol={{ span: 20 }}>
{formItem}
</FormItem>
);
}
/**
* 将schema中的一个字段转换为表单的一项
*
* @param field
*/
transFormField = (field) => {
const {getFieldProps} = this.props.form;
// 对于主键, 直接返回一个不可编辑的textarea
if (this.primaryKey === field.key) {
logger.debug('key %o is primary, transform to text area', field);
return this.colWrapper((
<Input type="textarea" autosize={{ minRows: 1, maxRows: 10 }} disabled
size="default" {...getFieldProps(field.key)}/>
), field);
}
switch (field.dataType) {
case 'int':
logger.debug('transform field %o to integer input', field);
return this.colWrapper((
<InputNumber size="default" {...getFieldProps(field.key)}/>
), field);
case 'float':
logger.debug('transform field %o to float input', field);
return this.colWrapper((
<InputNumber step={0.01} size="default" {...getFieldProps(field.key)}/>
), field);
case 'datetime':
logger.debug('transform field %o to datetime input', field);
return this.colWrapper((
<DatePicker showTime format="yyyy-MM-dd HH:mm:ss"
placeholder={field.placeholder || '请选择日期'} {...getFieldProps(field.key)}/>
), field);
default: // 默认就是普通的输入框
logger.debug('transform field %o to varchar input', field);
return this.colWrapper((
<Input placeholder={field.placeholder} size="default" {...getFieldProps(field.key)}/>
), field);
}
}
/**
* 处理表格的选择事件
*
* @param selectedRowKeys
* @param selectedRows
*/
handleSelectChange = (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys, selectedRows});
}
render() {
// 解析schema
const newCols = [];
this.props.schema.forEach((field) => {
const col = {};
col.key = field.key;
col.dataIndex = field.key;
col.title = field.title;
if (field.render) {
col.render = field.render;
}
newCols.push(col);
// 当前列是否是主键?
if (field.primary) {
this.primaryKey = field.key;
this.primaryKeyType = field.dataType;
}
});
// 对数据也要处理一下
// 每行数据都必须有个key属性, 如果指定了主键, 就以主键为key
// 否则直接用个自增数字做key
const newData = [];
let i = 0;
this.props.data.forEach((obj) => {
const newObj = Object.assign({}, obj);
if (this.primaryKey) {
newObj.key = obj[this.primaryKey];
} else {
newObj.key = i;
i++;
}
newData.push(newObj);
});
// 生成表单项
const formItems = [];
this.props.schema.forEach((field) => {
formItems.push(this.transFormField(field));
});
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: this.handleSelectChange,
};
const hasSelected = this.state.selectedRowKeys.length > 0; // 是否选择
const multiSelected = this.state.selectedRowKeys.length > 1; // 是否选择了多项
return (
<div>
<div className="db-table-button">
<Affix offsetTop={8} target={() => document.getElementById('main-content-div')}>
<ButtonGroup>
<Button type="primary" onClick={this.onClickInsert}>
<Icon type="plus-circle-o"/> 新增
</Button>
{/* 注意这里, 如果schema中没有定义主键, 不允许update或delete */}
<Button type="primary" disabled={!hasSelected || !this.primaryKey} onClick={this.onClickUpdate}>
<Icon type="edit"/> {multiSelected ? '批量修改' : '修改'}
</Button>
<Button type="primary" disabled={!hasSelected || !this.primaryKey} onClick={this.onClickDelete}>
<Icon type="delete"/> {multiSelected ? '批量删除' : '删除'}
</Button>
</ButtonGroup>
</Affix>
<Modal title={this.state.modalTitle} visible={this.state.modalVisible} onOk={this.handleModalOk}
onCancel={this.hideModal}>
<Form horizontal>
{formItems}
</Form>
</Modal>
</div>
<Table rowSelection={rowSelection} columns={newCols} dataSource={newData} pagination={false}
loading={this.props.tableLoading}/>
</div>
);
}
}
InnerTable = Form.create()(InnerTable);
export default InnerTable;
-276
View File
@@ -1,276 +0,0 @@
import React from 'react';
import {message, notification} from 'antd';
import Error from '../Error';
import InnerForm from './InnerForm.js';
import InnerTable from './InnerTable.js';
import InnerPagination from './InnerPagination.js';
import './index.less';
import globalConfig from 'config.js';
import ajax from '../../utils/ajax';
import Logger from '../../utils/Logger';
const logger = Logger.getLogger('DBTable');
/**
* 操作数据库中的一张表的组件, 又可以分为3个组件: 表单+表格+分页器
*/
class DBTable extends React.Component {
// 代替componentWillMount
constructor(props) {
super(props);
// 组件初始化时尝试获取schema
this.tryFetchSchema(this.props);
}
// 父组件要保存子组件的状态...非常蛋疼...
// 破坏了子组件的"封闭"原则
// 注意: 向父组件传状态, 通过回调函数的形式
// 从父组件接收状态, 通过props的形式
state = {
// 表单组件的状态
queryObj: {}, // 表单中的查询条件
// 表格组件的状态
data: [], // 表格中显示的数据
tableLoading: false, // 表格是否是loading状态
// 分页器的状态
currentPage: 1, // 当前第几页
pageSize: 50, // pageSize暂时不可修改, 固定50
total: 0, // 总共有多少条数据
};
/**
* 尝试获取某个表的querySchema和dataSchema
* 无论是从远端获取还是从本地配置读取, 这个方法必须是同步的
*
* @param dbName
* @param tableName
*/
tryFetchSchema(props) {
const routes = props.routes;
// 这个tableName是路由表配置中传过来的
// 可以用这个方法向组件传值
const tableName = routes.pop().tableName;
if (tableName) {
logger.info('init component DBTable with tableName = %s', tableName);
} else {
logger.error('can not find tableName, check your router config');
this.inited = false; // 是否成功获取schema
this.errorMsg = '找不到表名, 请检查路由配置'; // 如果没能成功获取schema, 错误信息是什么?
return;
}
// 其实很多这种this.xxx变量也可以做成状态, 看情况了
// 关键是这些变量变化时, 是否要触发重新render?
this.tableName = tableName;
// 尝试加载querySchema
try {
this.querySchema = require(`../../schema/${tableName}.querySchema.js`);
} catch (e) {
logger.error('load query schema error: %o', e);
this.inited = false;
this.errorMsg = `加载${tableName}表的querySchema出错, 请检查配置`;
return;
}
// 尝试加载dataSchema
try {
this.dataSchema = require(`../../schema/${tableName}.dataSchema.js`);
} catch (e) {
logger.error('load data schema error: %o', e);
this.inited = false;
this.errorMsg = `加载${tableName}表的dataSchema出错, 请检查配置`;
return;
}
// 尝试加载个性化配置, 加载失败也没影响
try {
this.tableConfig = require(`../../schema/${tableName}.config.js`);
} catch (e) {
logger.warn('can not find config for table %s, use default instead', tableName);
// TODO: 默认配置写在这里
this.tableConfig = {
showExport: true, // 显示导出按钮, 默认true
showImport: true, // 显示导入按钮, 默认true
}
}
this.inited = true;
}
/**
* 刚进入页面时触发一次查询
*/
componentDidMount() {
this.refresh();
}
// 在react router中切换时, 组件不会重新mount, 只有props会变化
componentWillReceiveProps = (nextProps) => {
logger.debug('receive new props and try to render, nextProps = %o', nextProps);
this.tryFetchSchema(nextProps); // 根据新的props重新获取配置
// 所有状态都要手动还原到初始值
// this.setState({
// queryObj: {},
// data: [],
// tableLoading: false,
// currentPage: 1,
// pageSize: 50,
// total: 0,
// });
// 这种写法和setState有何区别?
// 似乎setState不是"立即"发生的, 会导致下面refresh时有bug
this.state.queryObj = {};
this.state.data = [];
this.state.tableLoading = false;
this.state.currentPage = 1;
this.state.pageSize = 50;
this.state.total = 0;
// 切换时也要重新查询一次数据
this.refresh();
}
/**
* 切换分页时触发查询
*
* @param page
*/
handlePageChange = (page) => {
logger.debug('handlePageChange, page = %d', page);
this.setState({tableLoading: true});
this.select(this.state.queryObj, page, this.state.pageSize).then((result) => {
//message.success('查询成功');
this.setState({
currentPage: page,
data: result.data,
total: result.total,
tableLoading: false,
});
}, this.handleError);
}
/**
* 点击提交按钮时触发查询
*
* @param queryObj
*/
handleFormSubmit = (queryObj) => {
logger.debug('handleFormSubmit, queryObj = %o', queryObj);
this.setState({tableLoading: true});
// 这时查询条件已经变了, 要从第一页开始查
this.select(queryObj, 1, this.state.pageSize).then((result) => {
//message.success('查询成功');
this.setState({
currentPage: 1,
data: result.data,
total: result.total,
tableLoading: false,
queryObj: queryObj,
});
}, this.handleError);
}
/**
* 统一处理ajax失败时的回调
*
* @param errorMsg
*/
handleError = (errorMsg) => {
// 对于错误信息, 要很明显的提示用户, 这个通知框要用户手动关闭
notification.error({
message: '出错啦!',
description: `请联系管理员, 错误信息: ${errorMsg}`,
duration: 0,
});
this.setState({tableLoading: false});
};
/**
* 按当前的查询条件重新查询一次
*/
refresh = () => {
this.setState({tableLoading: true});
this.select(this.state.queryObj, this.state.currentPage, this.state.pageSize).then((result) => {
//message.success('查询成功');
this.setState({
data: result.data,
total: result.total,
tableLoading: false,
});
}, this.handleError);
}
/**
* 向服务端发送select请求, 会返回一个promise对象
*
* @param queryObj 包含了form中所有的查询条件, 再加上page和pageSize, 后端就能拼成完整的sql
* @param page
* @param pageSize
* @returns {Promise}
*/
select(queryObj, page, pageSize) {
const hide = message.loading('正在查询...', 0);
// superagent请求会直接返回一个promise对象, 但不能直接用, 还是要包装一层
const tmpObj = Object.assign({}, queryObj); // 创建一个新的临时对象, 其实直接修改queryObj也可以
tmpObj.page = page;
tmpObj.pageSize = pageSize;
const url = `${globalConfig.getAPIPath()}/${this.tableName}/select`; // 拼接要请求的url地址
logger.debug('select: url = %s, queryObj = %o', url, tmpObj);
// 只是为了试下ES6的Promise特性...其实直接用superagent的API也可以
const promise = new Promise((resolve, reject) => {
ajax.post(url).send(tmpObj).end((err, res) => {
hide();
// err就是一个字符串
// res是一个Response对象, 其中的body字段才是服务端真正返回的数据
if (err || !res.body.success) {
reject(err ? '请求select接口出错, 请联系管理员' : res.body.message);
} else {
resolve(res.body);
}
});
});
return promise;
}
render() {
// 如果没能成功加载schema, 显示错误信息
if (!this.inited) {
return (
<Error errorMsg={this.errorMsg}/>
);
}
// 之前是直接{...this.state}, 感觉会影响效率
// 父组件传进去的方法名都是parentHandleXXX
// InnerForm和InnerPagination都是无状态的
// 但InnerTable还是要维护自己的一些状态
return (
<div>
<InnerForm parentHandleSubmit={this.handleFormSubmit} schema={this.querySchema} tableConfig={this.tableConfig}
tableName={this.tableName}/>
<InnerTable data={this.state.data} tableLoading={this.state.tableLoading}
schema={this.dataSchema} refresh={this.refresh}
tableConfig={this.tableConfig} tableName={this.tableName}/>
<InnerPagination currentPage={this.state.currentPage} total={this.state.total} pageSize={this.state.pageSize}
parentHandlePageChange={this.handlePageChange} tableConfig={this.tableConfig}
tableName={this.tableName}/>
</div>
);
}
}
export default DBTable;
-32
View File
@@ -1,32 +0,0 @@
.ant-advanced-search-form {
padding: 16px 8px;
background: #f8f8f8;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
/* 由于输入标签长度不确定,所以需要微调使之看上去居中 */
.ant-advanced-search-form > .ant-row {
position: relative;
left: -6px;
}
.ant-advanced-search-form .ant-btn + .ant-btn {
margin-left: 8px;
}
/* 我讨厌写css... */
.db-pagination {
margin-top: 8px;
margin-bottom: 8px;
float: left;
}
.db-table-button {
margin-top: 8px;
margin-bottom: 8px;
}
.ant-upload {
margin-left: 8px;
}
-80
View File
@@ -1,80 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import globalConfig from 'config';
import ajax from '../../utils/ajax';
import Logger from '../../utils/Logger';
import {message} from 'antd';
import './index.less';
const logger = Logger.getLogger('Login');
/**
* 定义Login组件
*/
class Login extends React.Component {
// 这个login组件是直接从网上找的: https://colorlib.com/wp/html5-and-css3-login-forms/
// 不是react风格...
// TODO: 以后要把这个login组件改掉
handleSubmit = (e) => {
e.preventDefault(); // 这个很重要, 防止跳转
const hide = message.loading('正在验证...', 0);
const username = ReactDOM.findDOMNode(this.refs.user).value;
const password = ReactDOM.findDOMNode(this.refs.pass).value;
logger.debug('username = %s, password = %s', username, password);
const button = ReactDOM.findDOMNode(this.refs.button);
button.setAttribute('disabled', 'true'); // 暂时屏蔽提交按钮, 防止用户重复点击
// 服务端验证
ajax.post(`${globalConfig.getAPIPath()}${globalConfig.login.validate}`).type('form').send({
username,
password,
}).end((err, res) => {
hide();
logger.debug('login validate return: error %o result %o', err, res);
if (ajax.isSuccess(res)) {
// 如果登录成功, 调用回调函数, 把状态往上层组件传
if (this.props.loginSuccess)
this.props.loginSuccess(res.body.data, true);
else
message.info(`登录成功, 用户名: ${res.body.data}`);
} else {
message.error(`登录失败: ${res.body.message}, 请联系管理员`);
button.removeAttribute('disabled'); // 登录失败的话, 重新让按钮可用, 让用户重新登录
}
});
}
componentWillMount() {
logger.debug('mount and add styles');
document.body.style.background = null;
}
componentWillUnmount() {
logger.debug('unmount and clear styles');
// 组件unmount时设置下样式, 不然其他组件的显示会有问题
// TODO: 不知道有没有更好的办法
document.body.style.background = 'white';
}
render() {
return (
<div>
<div className="login">
<h1>{globalConfig.name}</h1>
<form onSubmit={this.handleSubmit}>
<input className="login-input" type="text" ref="user" name="u" placeholder="用户名" required="required"/>
<input className="login-input" type="password" ref="pass" name="p" placeholder="密码" required="required"/>
<button ref="button" type="submit" className="btn btn-primary btn-block btn-large">登录</button>
</form>
</div>
</div>
);
}
}
export default Login;
-164
View File
@@ -1,164 +0,0 @@
// 都是直接抄来的样式
// 对我来说就像天书一样好嘛...
// 这玩意真的是人工手写出来的???
.btn {
display: inline-block;
*display: inline;
*zoom: 1;
padding: 4px 10px 4px;
margin-bottom: 0;
font-size: 13px;
line-height: 18px;
color: #333333;
text-align: center;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
vertical-align: middle;
background-color: #f5f5f5;
background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
background-image: linear-gradient(top, #ffffff, #e6e6e6);
background-repeat: repeat-x;
filter: progid:dximagetransform.microsoft.gradient(startColorstr=#ffffff, endColorstr=#e6e6e6, GradientType=0);
border-color: #e6e6e6 #e6e6e6 #e6e6e6;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
border: 1px solid #e6e6e6;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
cursor: pointer;
*margin-left: .3em;
}
.btn:hover, .btn:active, .btn.active, .btn.disabled, .btn[disabled] {
background-color: #e6e6e6;
}
.btn-large {
padding: 9px 14px;
font-size: 15px;
line-height: normal;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.btn:hover {
color: #333333;
text-decoration: none;
background-color: #e6e6e6;
background-position: 0 -15px;
-webkit-transition: background-position 0.1s linear;
-moz-transition: background-position 0.1s linear;
-ms-transition: background-position 0.1s linear;
-o-transition: background-position 0.1s linear;
transition: background-position 0.1s linear;
}
.btn-primary, .btn-primary:hover {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
color: #ffffff;
}
.btn-primary.active {
color: rgba(255, 255, 255, 0.75);
}
.btn-primary {
background-color: #4a77d4;
background-image: -moz-linear-gradient(top, #6eb6de, #4a77d4);
background-image: -ms-linear-gradient(top, #6eb6de, #4a77d4);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#6eb6de), to(#4a77d4));
background-image: -webkit-linear-gradient(top, #6eb6de, #4a77d4);
background-image: -o-linear-gradient(top, #6eb6de, #4a77d4);
background-image: linear-gradient(top, #6eb6de, #4a77d4);
background-repeat: repeat-x;
filter: progid:dximagetransform.microsoft.gradient(startColorstr=#6eb6de, endColorstr=#4a77d4, GradientType=0);
border: 1px solid #3762bc;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.5);
}
.btn-primary:hover, .btn-primary:active, .btn-primary.active, .btn-primary.disabled, .btn-primary[disabled] {
filter: none;
background-color: #4a77d4;
}
.btn-block {
width: 100%;
display: block;
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
}
html {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
width: 100%;
height: 100%;
font-family: 'Open Sans', sans-serif;
background: #092756;
background: -moz-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -moz-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -moz-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -webkit-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -webkit-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -o-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -o-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -o-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -ms-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), -ms-linear-gradient(top, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), -ms-linear-gradient(-45deg, #670d10 0%, #092756 100%);
background: -webkit-radial-gradient(0% 100%, ellipse cover, rgba(104, 128, 138, .4) 10%, rgba(138, 114, 76, 0) 40%), linear-gradient(to bottom, rgba(57, 173, 219, .25) 0%, rgba(42, 60, 87, .4) 100%), linear-gradient(135deg, #670d10 0%, #092756 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#3E1D6D', endColorstr='#092756', GradientType=1);
}
.login {
position: absolute;
top: 50%;
left: 50%;
margin: -150px 0 0 -150px;
width: 300px;
height: 300px;
}
.login h1 {
color: #fff;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
letter-spacing: 1px;
text-align: center;
padding-bottom: 15px;
}
.login-input {
width: 100%;
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.3);
border: none;
outline: none;
padding: 10px;
font-size: 13px;
color: #fff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 4px;
box-shadow: inset 0 -5px 45px rgba(100, 100, 100, 0.2), 0 1px 1px rgba(255, 255, 255, 0.2);
-webkit-transition: box-shadow .5s ease;
-moz-transition: box-shadow .5s ease;
-o-transition: box-shadow .5s ease;
-ms-transition: box-shadow .5s ease;
transition: box-shadow .5s ease;
}
.login-input:focus {
box-shadow: inset 0 -5px 45px rgba(100, 100, 100, 0.4), 0 1px 1px rgba(255, 255, 255, 0.2);
}
-23
View File
@@ -1,23 +0,0 @@
import React from 'react';
import globalConfig from 'config.js';
import './index.less';
/**
* 定义Logo组件
*/
class Logo extends React.Component {
render() {
// TODO: 这个logo的样式还是要优化下
return (
<div className="ant-layout-logo">
<div className="logo-text">
{globalConfig.name}
</div>
</div>
);
}
}
export default Logo;
-6
View File
@@ -1,6 +0,0 @@
.logo-text {
text-align: center;
font-family: "Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,SimSun,sans-serif";
font-size: 22px;
color: #2db7f5;
}
+1 -3
View File
@@ -1,8 +1,7 @@
import React from 'react'
import { Link } from 'react-router'
import { Menu, Icon } from 'antd'
import Logo from '../Logo'
import Logger from '../../utils/Logger'
import Logger from '../../utils/logger'
import items from 'menu.js'
const SubMenu = Menu.SubMenu
@@ -47,7 +46,6 @@ const Sider = React.createClass({
},
transFormMenuItem(obj, paths) {
const parentPath = paths.join('/')
logger.debug('transform %o to path %s', obj, parentPath)
return (
<MenuItem key={obj.key}>
{obj.child ? obj.name : <Link to={`/${parentPath}`}>{obj.name}</Link> }
-19
View File
@@ -1,19 +0,0 @@
import React from 'react';
import './index.less';
/**
* 展示欢迎界面
*/
class Welcome extends React.Component {
render() {
return (
<div>
Hello,React
</div>
);
}
}
export default Welcome;
-3
View File
@@ -1,3 +0,0 @@
.welcome {
color: red;
}
+10 -19
View File
@@ -1,31 +1,23 @@
/**
* 定义整个项目的全局配置
*/
// 约定优于配置
// 我可以提供尽量多的配置, 但尽量不要太个性化, 接口的路径/名称/格式之类的
// 遵循统一的规范, 好维护, 交给其他人也比较简单
module.exports = {
name: 'Ant Design 管理后台',
footer: 'Ant Design Admin 版权所有 © 2016 由zuiidea支持',
logoImg:'https://t.alipayobjects.com/images/rmsweb/T1B9hfXcdvXXXXXXXX.svg',
logoText:'Ant Design',
debug: true, // 是否开启debug模式, 不会请求后端接口, 使用mock的数据
logLevel: 'debug', // 日志级别, 目前支持debug/info/warn/error 4种级别
debug: true,
logLevel: 'debug',
needLogin:false,
api: {
host: 'http://localhost', // 调用ajax接口的地址, 默认值空, 如果是跨域的, 服务端要支持CORS
path: '/api', // ajax请求的路径
timeout: 15000, // 请求的超时时间, 单位毫秒
host: 'http://localhost',
path: '/api',
timeout: 15000,
},
login: { // 登录相关配置
getCurrentUser: '/getCurrentUser', // 后端必须要提供接口校验当前用户的身份, 如果拿不到用户信息, 才会尝试登录
sso: '', // 是否使用单点登录? 是的话我会把地址encode后加到后面, 然后跳转, 如果这个是空字符串, 说明不使用单点登录
validate: '/login', // 校验用户信息, 表单的submit地址. 如果登录成功, 必须返回用户名
logout: '/logout', // 退出的url, 用户点击退出时, 浏览器会直接跳转到这个链接
login: {
getCurrentUser: '/getCurrentUser',
sso: '',
validate: '/login',
logout: '/logout',
},
/**
@@ -106,5 +98,4 @@ module.exports = {
this.tmpApiPath = tmpApiPath;
return tmpApiPath;
},
};
+1 -1
View File
@@ -8,7 +8,7 @@
</head>
<body>
<div id="root" style="height:100%;"></div>
<div id="view"></div>
<script src="http://localhost:8080/webpack-dev-server.js"></script>
</body>
</html>
+3 -35
View File
@@ -1,56 +1,24 @@
/**
* 程序的入口, 类似java中的main
*/
import React from 'react'
import ReactDOM from 'react-dom'
import {Router, Route, IndexRoute, hashHistory} from 'react-router'
import './utils/index.js' // 引入各种prototype辅助方法
import './utils/index.js'
// 开始引入各种自定义的组件
import App from './view/index'
import SignIn from './view/user/signIn'
import List from './view/data/list'
import Welcome from './components/Welcome'
import Error from './components/Error'
import Hello from './components/Hello'
import DBTable from './components/DBTable'
const routes = (
<Router history={hashHistory}>
<Route path="/" component={App}>
<IndexRoute component={Welcome}/>
<IndexRoute component={List}/>
<Route path="index">
<Route path="list" tableName="list" component={List}/>
<Route path="option1" tableName="test" component={DBTable}/>
<Route path="option2" tableName="testSms" component={DBTable}/>
<Route path="option3" component={Hello}/>
</Route>
<Route path="daohang">
<Route path="555" component={Hello}/>
<Route path="sanji">
<Route path="666" component={Hello}/>
<Route path="777" component={Hello}/>
<Route path="888" component={Hello}/>
<Route path="999" component={Hello}/>
</Route>
</Route>
<Route path="test">
<Route path="aaa" component={Hello}/>
<Route path="bbb" component={Hello}/>
<Route path="ccc" component={Hello}/>
<Route path="sanjiaaa">
<Route path="666aa" component={Hello}/>
</Route>
<Route path="sanjibbb">
<Route path="666bb" component={Hello}/>
</Route>
</Route>
<Route path="*" component={Error}/>
</Route>
</Router>
)
ReactDOM.render(routes, document.getElementById('root'))
ReactDOM.render(routes, document.getElementById('view'))
+86 -116
View File
@@ -1,143 +1,113 @@
/**
* 定义sidebar中的菜单项
*
* 一些约定:
* 1.菜单最多3层;
* 2.只有顶层的菜单可以带图标;
* 3.只有"叶子"节点才能跳转;
* 4.所有的key都不能重复;
*/
module.exports = [
{
key: 'index',
name: '菜单一',
name: '数据相关',
icon: 'user',
child: [
{
key: 'list',
name: '数据列表',
},
{
key: 'option1',
name: '模拟CRUD',
},
{
key: 'option2',
name: '短信表管理',
},
{
key: 'option3',
name: '选项3',
},
],
},
{
key: 'daohang',
name: '菜单二',
key: 'navigation',
name: '测试导航',
icon: 'setting',
child: [
{
key: '555',
name: '选项5',
key: 'navigation1',
name: '二级导航1',
},
{
key: 'sanji', // 最多只能到三级导航
name: '三级导航',
key: 'navigation2',
name: '二级导航2',
child: [
{
key: '666',
name: '选项6',
key: 'navigation21',
name: '三级导航1',
},
{
key: '777',
name: '选项7',
},
{
key: '888',
name: '选项8',
},
{
key: '999',
name: '选项9',
},
],
},
],
},
{
key: 'test',
name: '菜单三',
icon: 'laptop',
child: [
{
key: 'aaa',
name: '选项a',
},
{
key: 'bbb',
name: '选项b',
},
{
key: 'ccc',
name: '选项c',
},
{
key: 'sanjiaaa', // 最多只能到三级导航
name: '三级导航aaa',
child: [
{
key: '666aa',
name: '选项6',
},
],
},
{
key: 'sanjibbb', // 最多只能到三级导航
name: '三级导航bbb',
child: [
{
key: '666bb',
name: '选项6',
},
],
},
],
},
{
key: 'menu4',
name: '菜单四',
icon: 'notification',
child: [
{
key: 'menu4',
name: '菜单四一',
},
],
},
{
key: 'menu5',
name: '菜单五',
icon: 'folder',
child: [
{
key: 'menu51',
name: '菜单五一',
},
{
key: 'menu52',
name: '菜单五二',
},
{
key: 'menu53',
name: '菜单五三',
child: [
{
key: 'menu531',
name: '菜单五三一',
key: 'navigation22',
name: '三级导航2',
},
],
},
],
},
// {
// key: 'test',
// name: '菜单三',
// icon: 'laptop',
// child: [
// {
// key: 'aaa',
// name: '选项a',
// },
// {
// key: 'bbb',
// name: '选项b',
// },
// {
// key: 'ccc',
// name: '选项c',
// },
// {
// key: 'sanjiaaa', // 最多只能到三级导航
// name: '三级导航aaa',
// child: [
// {
// key: '666aa',
// name: '选项6',
// },
// ],
// },
// {
// key: 'sanjibbb', // 最多只能到三级导航
// name: '三级导航bbb',
// child: [
// {
// key: '666bb',
// name: '选项6',
// },
// ],
// },
// ],
// },
// {
// key: 'menu4',
// name: '菜单四',
// icon: 'notification',
// child: [
// {
// key: 'menu4',
// name: '菜单四一',
// },
// ],
// },
// {
// key: 'menu5',
// name: '菜单五',
// icon: 'folder',
// child: [
// {
// key: 'menu51',
// name: '菜单五一',
// },
// {
// key: 'menu52',
// name: '菜单五二',
// },
// {
// key: 'menu53',
// name: '菜单五三',
// child: [
// {
// key: 'menu531',
// name: '菜单五三一',
// },
// ],
// },
// ],
// },
];
-8
View File
@@ -1,8 +0,0 @@
// 定义某个表渲染时的配置
// 如果不需要个性化, 可以不要这个文件, 所有配置项都会使用默认值
// 注意! config/dataSchema/querySchema文件的命名应该遵循CamelCase原则, 不要包含下划线/中划线
module.exports = {
showExport: false, // 显示导出按钮, 默认true
showImport: false, // 显示导入按钮, 默认true
}
-48
View File
@@ -1,48 +0,0 @@
// 定义某个表的dataSchema, 结构跟querySchema很相似, 见下面的例子
// 注意: 所有的key不能重复
module.exports = [
{
key: 'id', // 传递给后端的key
title: 'ID', // 前端显示的名字
dataType: 'int', // 数据类型, 目前可用的: int/float/varchar/datetime
// 这一列是否是主键?
// 如果不指定主键, 不能update/delete, 但可以insert
// 如果指定了主键, insert/update时不能填写主键的值;
// 如果是批量update/delete, 会把主键的一个数组传给后端
// 只有int/varchar可以作为主键
primary: true,
// showType目前还不支持, 其实dataSchema和querySchema一样, 都是有showType属性的
// 以后有空加上
// showType: checkbox/radio
// 扩展接口, 决定了这一列渲染成什么样子
render: (text, record) => text,
},
{
key: 'name',
title: '用户名',
dataType: 'varchar',
},
{
key: 'score',
title: '分数',
dataType: 'int',
},
{
key: 'gpa',
title: 'GPA',
dataType: 'float',
},
{
key: 'birthday',
title: '生日',
// 对于日期类型要注意下, 在js端日期被表示为yyyy-MM-dd HH:mm:ss的字符串, 在java端日期被表示为java.util.Date对象
// fastjson反序列化时可以自动识别
// 序列化倒是不用特别配置, 看自己需求, fastjson会序列化为一个字符串, 前端原样展示
dataType: 'datetime',
},
];
-101
View File
@@ -1,101 +0,0 @@
// 定义某个表的querySchema
// schema的结构和含义参考下面的例子
// 注意: 所有的key不能重复
module.exports = [
{
key: 'name', // 传递给后端的字段名
title: '用户名', // 前端显示的名称
placeholder: '请输入用户名', // 提示语, 可选
// 数据类型, 前端会根据数据类型展示不同的输入框
// 目前可用的dataType: int/float/varchar/datetime
dataType: 'varchar',
// 显示类型, 一些可枚举的字段, 比如type, 可以被显示为单选框或下拉框
// 默认显示类型是normal, 就是一个普通的输入框, 这时可以省略showType字段
// 目前可用的showType: normal/select/radio/between
// select和radio只能用于int和varchar
// between只能用于int/float/datetime, 会显示2个输入框, 用于范围查询
showType: 'normal',
// 有一点要注意, 就算showType是normal, 也不意味是等值查询, 只是说传递给后台的是单独的一个值
// 至于后台用这个值做等值/大于/小于/like, 前端不关心
},
{
key: 'age',
title: '年龄',
placeholder: '请输入年龄',
dataType: 'int',
},
{
key: 'weight',
title: '体重(kg)',
dataType: 'float', // 小数会统一保留2位
},
{
key: 'type',
title: '类型',
dataType: 'int',
showType: 'select', // 下拉框选择, dataType可以是int或varchar, 但必须和options中的key类型一致
options: [{key: 1, value: '类型1'}, {key: 2, value: '类型2'}],
},
{
key: 'userType',
title: '用户类型',
dataType: 'varchar', // 这个数据类型要和options中key的类型一致
showType: 'radio', // 单选框, 和下拉框schema是一样的, 只是显示时有差别
options: [{key: 'typeA', value: '类型A'}, {key: 'typeB', value: '类型B'}],
},
{
key: 'score',
title: '分数',
dataType: 'int',
showType: 'between', // 整数范围查询, 对于范围查询, 会自动生成xxBegin/xxEnd两个key传递给后端
},
{
key: 'gpa',
title: 'GPA',
dataType: 'float',
showType: 'between', // 小数也可以范围查询, 固定两位小数
placeholderBegin: '哈哈', // 对于范围查询, 可以定义placeholderBegin和placeholderBegin, 用于两个框的提示语
placeholderEnd: '切克闹', // 如果不定义, 对于int/float的范围查询, 提示语是"最小值"/"最大值", 对于日期的范围查询, 提示语是"开始日期"/"结束日期"
},
{
key: 'height',
title: '身高(cm)',
dataType: 'float', // 小数等值查询
},
{
key: 'duoxuan1',
title: '多选1',
dataType: 'int',
showType: 'checkbox', // checkbox
options: [{key: 1, value: '类型1'}, {key: 2, value: '类型2'}],
},
{
key: 'duoxuan2',
title: '多选2',
dataType: 'varchar',
showType: 'multiSelect', // 另一种多选
options: [{key: 'type1', value: '类型1'}, {key: 'type2', value: '类型2'}],
},
{
key: 'primarySchool',
title: '入学日期',
dataType: 'datetime', // 日期范围查询, 日期范围查询占用的显示空间会很大, 注意排版
showType: 'between',
},
{
key: 'birthday',
title: '出生日期',
dataType: 'datetime',
showType: 'between',
},
{
key: 'xxbirthday',
title: 'XX日期',
dataType: 'datetime', // 日期等值查询
},
];
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
showExport: false, // 显示导出按钮, 默认true
showImport: false, // 显示导入按钮, 默认true
}
-34
View File
@@ -1,34 +0,0 @@
module.exports = [
{
key: 'id',
title: 'ID',
dataType: 'int',
primary: true,
},
{
key: 'content',
title: '内容',
dataType: 'varchar',
},
{
key: 'phoneModel',
title: '手机型号',
dataType: 'varchar',
},
{
key: 'experience',
title: '使用经验',
dataType: 'varchar',
},
{
key: 'frequency',
title: '使用频率',
dataType: 'varchar',
},
{
key: 'isNative',
title: '是否母语',
dataType: 'varchar',
placeholder: '字符串yes/no',
},
];
-38
View File
@@ -1,38 +0,0 @@
// 定义某个表的querySchema
// schema的结构和含义参考下面的例子
// 注意: 所有的key不能重复
module.exports = [
{
key: 'id', // 传递给后端的字段名
title: 'ID', // 前端显示的名称
dataType: 'int',
},
{
key: 'content',
title: '内容',
dataType: 'varchar',
},
{
key: 'phoneModel',
title: '手机型号',
dataType: 'varchar',
},
{
key: 'experience',
title: '使用经验',
dataType: 'varchar',
},
{
key: 'frequency',
title: '使用频率',
dataType: 'varchar',
},
{
key: 'isNative',
title: '是否母语',
dataType: 'varchar',
showType: 'radio',
options: [{key: 'yes', value: '是'}, {key: 'no', value: '否'}],
},
];
+63 -124
View File
@@ -1,168 +1,107 @@
import globalConfig from '../config';
import config from '../config'
/**
* 日志工具类 <br/>
*
* <p>
* 工程变大之后, 日志就很重要了, 总不能一直console.log.
* 本来想看看有没有现成的工具, 找到一个log4js的库, 但只能用在node环境下, 浏览器环境下似乎没什么好用的.
* 作为一个用惯了slf4j的人, 干脆自己写个吧, 练练手.
* 目前有以下功能:
* <ul>
* <li>支持常用的日志级别: debug/info/warn/error, 说实话其他的级别极少用到</li>
* <li>支持变量替换: 类似slf4j中的logger.info("a={}",a)这种, 其实console本身已经支持的</li>
* <li>根据日志级别设置样式: debug是黑色, info是默认, warn是黄色, error是红色, 看起来清晰很多</li>
* <li>定义每个logger的名字: 也算是常规功能了吧</li>
* </ul>
* 我是尽量按着slf4j的习惯来设计的, 目前还比较简单.
* 不支持pattern/appender之类的, 但对于二手前端来说, 也算够用了.
* </p>
*/
class Logger {
// 定义一些预设的日志级别
// 目前只有4种级别
static LOG_LEVEL_DEBUG = 1
static LOG_LEVEL_INFO = 2
static LOG_LEVEL_WARN = 3
static LOG_LEVEL_ERROR = 4
static LOG_LEVEL_DEBUG = 1;
static LOG_LEVEL_INFO = 2;
static LOG_LEVEL_WARN = 3;
static LOG_LEVEL_ERROR = 4;
static loggerMap = new Map()
/*暂存所有logger*/
static loggerMap = new Map();
static defaultLogger = new Logger()
/*默认的logger*/
static defaultLogger = new Logger();
/**
* 获取一个Logger实例
*
* @param name
* @returns {*}
*/
static getLogger(name) {
if (name && name !== '') {
// 缓存
if (Logger.loggerMap.has(name)) {
return Logger.loggerMap.get(name);
return Logger.loggerMap.get(name)
}
const logger = new Logger(name);
Logger.loggerMap.set(name, logger);
return logger;
const logger = new Logger(name)
Logger.loggerMap.set(name, logger)
return logger
} else {
return Logger.defaultLogger;
return Logger.defaultLogger
}
}
constructor(name) {
this.name = name; // logger的名字
const configLogLevel = globalConfig.logLevel;
this.name = name
const configLogLevel = config.logLevel
if (configLogLevel === 'debug') {
this.logLevel = Logger.LOG_LEVEL_DEBUG;
this.logLevel = Logger.LOG_LEVEL_DEBUG
} else if (configLogLevel === 'info') {
this.logLevel = Logger.LOG_LEVEL_INFO;
this.logLevel = Logger.LOG_LEVEL_INFO
} else if (configLogLevel === 'warn') {
this.logLevel = Logger.LOG_LEVEL_WARN;
this.logLevel = Logger.LOG_LEVEL_WARN
} else if (configLogLevel === 'error') {
this.logLevel = Logger.LOG_LEVEL_ERROR;
this.logLevel = Logger.LOG_LEVEL_ERROR
} else {
// 默认都是info级别
this.error('unsupported logLevel: %s, use INFO instead', configLogLevel);
this.logLevel = Logger.LOG_LEVEL_INFO;
this.error('unsupported logLevel: %s, use INFO instead', configLogLevel)
this.logLevel = Logger.LOG_LEVEL_INFO
}
}
/**
* 设置日志级别, 只有4种级别可选
*
* @param newLogLevel 1~4之间的一个数字
*/
setLogLevel(newLogLevel) {
if (isNaN(newLogLevel)) {
this.error('setLogLevel error, not a number: %s', newLogLevel);
this.error('setLogLevel error, not a number: %s', newLogLevel)
}
if (newLogLevel < 1 || newLogLevel > 4) {
this.error('setLogLevel error, input = %s, must between 1 and 4', newLogLevel);
this.error('setLogLevel error, input = %s, must between 1 and 4', newLogLevel)
}
this.logLevel = newLogLevel;
this.logLevel = newLogLevel
}
/**
* 打印info日志
*
* @param pattern 日志格式, 支持%d/%s等占位符
* @param args 可变参数, 用于替换pattern中的占位符
*/
info(pattern, ...args) {
// 先判断日志级别
if (this.logLevel > Logger.LOG_LEVEL_INFO)
return;
if (this.name)
args.unshift(`${this.name}: ${pattern}`);
else
args.unshift(pattern);
console.log.apply(console, args);
if (this.logLevel > Logger.LOG_LEVEL_INFO) {
return
}
if (this.name) {
args.unshift(`${this.name}: ${pattern}`)
} else {
args.unshift(pattern)
console.log.apply(console, args)
}
}
/**
* 打印error日志
*
* @param pattern
* @param args
*/
error(pattern, ...args) {
if (this.logLevel > Logger.LOG_LEVEL_ERROR)
return;
args.unshift('background: red; color: #bada55;');
if (this.name)
args.unshift(`%c${this.name}: ${pattern}`);
else
args.unshift(`%c${pattern}`);
console.error.apply(console, args);
if (this.logLevel > Logger.LOG_LEVEL_ERROR) {
return
}
args.unshift('background: red color: #bada55')
if (this.name) {
args.unshift(`%c${this.name}: ${pattern}`)
} else {
args.unshift(`%c${pattern}`)
console.error.apply(console, args)
}
}
/**
* 打印debug日志
*
* @param pattern
* @param args
*/
debug(pattern, ...args) {
if (this.logLevel > Logger.LOG_LEVEL_DEBUG)
return;
args.unshift('background: black; color: #bada55;');
if (this.name)
args.unshift(`%c${this.name}: ${pattern}`);
else
args.unshift(`%c${pattern}`);
console.debug.apply(console, args);
if (this.logLevel > Logger.LOG_LEVEL_DEBUG) {
return
}
args.unshift('background: black color: #bada55')
if (this.name) {
args.unshift(`%c${this.name}: ${pattern}`)
} else {
args.unshift(`%c${pattern}`)
console.debug.apply(console, args)
}
}
/**
* 打印warn日志
*
* @param pattern
* @param args
*/
warn(pattern, ...args) {
if (this.logLevel > Logger.LOG_LEVEL_WARN)
return;
args.unshift('background: yellow; color: black;');
if (this.name)
args.unshift(`%c${this.name}: ${pattern}`);
else
args.unshift(`%c${pattern}`);
console.warn.apply(console, args);
if (this.logLevel > Logger.LOG_LEVEL_WARN) {
return
}
args.unshift('background: yellow color: black')
if (this.name) {
args.unshift(`%c${this.name}: ${pattern}`)
} else {
args.unshift(`%c${pattern}`)
console.warn.apply(console, args)
}
}
}
export default Logger;
export default Logger
-227
View File
@@ -1,227 +0,0 @@
/**
* 对superagent api的一个mock, 用于debug
*/
import Logger from './Logger';
import superagent from 'superagent';
import globalConfig from '../config';
// superagent的文档: http://visionmedia.github.io/superagent/#error-handling
// 常用的api形式: 只要mock这两种就可以了
// ajax.get(url).end(func(err,res))
// ajax.post(url).send(obj).end(func(err,res))
const logger = Logger.getLogger('mockAjax');
let currentUrl; // 一个临时变量, 当前正在请求的url
const error = {}; // 理论上服务端不会返回error, 都是HTTP200
const result = { // 暂存mock的ajax返回, 总共有5个字段
success: true,
code: 0,
message: 'just a mock ;) ',
total: 10000,
data: {},
};
const successResult = (data) => {
result.success = true;
result.data = data;
result.code = 0;
result.message = '';
result.total = 10000;
};
const errorResult = (code, message) => {
result.success = false;
result.data = {};
result.code = code;
result.message = message;
result.total = 0;
};
// 从url中取出表名
const getTableNameFromUrl = (url) => {
// url的格式是apiPath/{tableName}/[insert|update|delete]
const apiPath = globalConfig.getAPIPath();
let begin = apiPath.length + 1;
while (url.charAt(begin) === '/') {
begin++;
}
let end = begin;
while (url.charAt(end) !== '/') {
end++;
}
const tableName = url.substring(begin, end);
logger.debug('getTableNameFromUrl: begin=%d, end=%d, tableName=%s', begin, end, tableName);
return tableName;
};
// 根据某个表的dataSchema创造mock数据
const mockResult = (tableName, queryObj) => {
logger.debug('begin to mock data for table %s', tableName);
// 尝试加载schema文件
let schema;
try {
schema = require(`../schema/${tableName}.dataSchema.js`);
} catch (e) {
logger.error('can not find dataSchema file for table %s', tableName);
logger.error('%o', e); // 这个方式有点不爽...但是如果不设置pattern, 就不能用样式了, 纠结...
// 设置返回结果为失败
errorResult(100, `can not find dataSchema file for table ${tableName}`);
return;
}
// 一般来说, 传入的查询条件都是肯定会有page/pageSize的, 以防万一
if (!queryObj.page) {
queryObj.page = 1;
}
if (!queryObj.pageSize) {
queryObj.pageSize = 50;
}
const tmp = [];
for (let i = 0; i < queryObj.pageSize; i++) {
const record = {};
// 为了让mock的数据有些区别, 把page算进去
schema.forEach((column)=> {
if (column.dataType === 'int') {
record[column.key] = 10000 * queryObj.page + i;
} else if (column.dataType === 'float') {
record[column.key] = 1.0 * queryObj.page + i * 0.01;
} else if (column.dataType === 'varchar') {
record[column.key] = `page=${queryObj.page} num=${i}`;
} else if (column.dataType === 'datetime') {
record[column.key] = new Date().plusDays(i).format('yyyy-MM-dd HH:mm:ss');
} else {
logger.error('unsupported dataType %s', column.dataType);
}
});
tmp.push(record);
}
successResult(tmp);
};
// 开始mock supergent的各个方法
const end = (func) => {
// 模拟一个延时
// 总是感觉setTimeout有点low啊, 不知道有没有更好的办法
setTimeout(func, 1500, null, {body: result}); // 延时执行func函数, 参数跟在最后
};
const get = (url) => {
logger.debug('get url %s', url);
currentUrl = url;
// TODO: 要根据url返回不同的数据, 这里设置各种get的url
if (url === `${globalConfig.getAPIPath()}${globalConfig.login.getCurrentUser}`)
successResult('guest'); // debug模式下都是guest用户
else
errorResult(200, `unsupported url ${url}`);
return {end};
};
const type = (str) => {
logger.debug('set type = %s', str);
return {send};
};
const send = (obj) => {
logger.debug('send obj: %o', obj);
// TODO: 要根据url返回不同的数据, 这里设置各种post的url
if (currentUrl === `${globalConfig.getAPIPath()}${globalConfig.login.validate}`) {
// 验证登录的用户名和密码
if (obj.username === 'guest' && obj.password === 'guest') {
successResult('guest');
} else {
errorResult(300, 'error username or password');
}
} else {
// 如果不是登录请求, 目前只能是CRUD请求, 当然以后可能会扩展
const tableName = getTableNameFromUrl(currentUrl);
if (tableName) {
// 要根据增删改查的情况分别模拟
if (currentUrl.indexOf('select') > 0) {
mockResult(tableName, obj);
} else if (currentUrl.indexOf('insert') > 0) {
mockResult(tableName, obj);
const tmp = result.data[0];
successResult(tmp);
} else if (currentUrl.indexOf('update') > 0) {
const tmp = currentUrl.indexOf('keys=');
successResult(currentUrl.substring(tmp).split(',').length);
} else {
errorResult(400, `unsupported url ${currentUrl}`);
}
} else {
errorResult(400, `unsupported url ${currentUrl}`);
}
}
return {end};
};
const post = (url) => {
logger.debug('post url %s', url);
currentUrl = url;
// TODO: 要根据url返回不同的数据, 这里设置各种post的url
if (currentUrl.indexOf('delete') > 0) {
const tmp = currentUrl.indexOf('keys=');
successResult(currentUrl.substring(tmp).split(',').length);
}
return {send, type, end};
};
// 一个辅助的方法, superagent判断返回是否成功太麻烦了啊...要判断3个条件
const isSuccess = (res) => {
if (res && res.body && res.body.success) {
return true;
} else {
return false;
}
};
// mock的版本只提供简单的API, 以后可能要扩展
const mockAjax = {get, post, isSuccess};
// 对superagent的API简单包装下
const realAjax = {
get: function (url) {
return this.innerConfig(superagent.get(url));
},
post: function (url) {
return this.innerConfig(superagent.post(url));
},
// 一些全局的配置
innerConfig: function (req) {
let tmp = req;
// 是否是跨域请求
if (globalConfig.isCrossDomain()) {
tmp = tmp.withCredentials();
}
// 设置全局的超时时间
if (globalConfig.api.timeout && !isNaN(globalConfig.api.timeout)) {
tmp = tmp.timeout(globalConfig.api.timeout);
}
return tmp;
},
isSuccess,
};
if (globalConfig.debug === true) {
module.exports = mockAjax;
} else {
module.exports = realAjax;
}
+15 -4
View File
@@ -1,7 +1,18 @@
import config from 'config'
import ajax from './ajax'
import Logger from './Logger'
import 'whatwg-fetch'
import Logger from './logger'
import reqwest from 'reqwest'
import './lib.less'
module.exports = {ajax,config,Logger}
function ajax(data){
if(config.debug){
console.log(data);
}else {
reqwest(data)
}
}
module.exports = {
config,
ajax,
Logger
}
+13 -16
View File
@@ -18,6 +18,7 @@ let List = React.createClass({
showTotal: total => `共 ${total} 条`
},
data:{},
params:{},
loading: false,
};
},
@@ -27,23 +28,19 @@ let List = React.createClass({
fetch() {
const _this = this
this.setState({loading: true})
fetch('https://randomuser.me/api/')
.then(function(response) {
console.log(response);
ajax({
url: 'https://randomuser.me/api/',
data: {
..._this.state.params
},
success: function (result) {
console.log(result);
const pager = _this.state.pager
const params = _this.state.params
pager.total = result.total
_this.setState({loading: false, data: result.talentList, pager, params})
}
})
// Ajax({
// url: contextPath + '/talent/index',
// data: {
// ..._this.state.params
// },
// success: function (result) {
// const pager = _this.state.pager
// const params = _this.state.params
// pager.total = result.total
// _this.setState({loading: false, data: result.talentList, pager, params})
// }
// })
},
render() {
return (