text: import React from 'react';
  import {message, notification, Spin} from 'antd';
  import Error from '../Error';
  import InnerForm from './InnerForm.js';
  import InnerTable from './InnerTable.js';
  import InnerPagination from './InnerPagination.js';
  import TableUtils from './TableUtils.js';
  import './index.less';
  import ajax from '../../utils/ajax';
  import Utils from '../../utils';
  import globalConfig from '../../config.js';
  import Logger from '../../utils/Logger';
  
  const logger = Logger.getLogger('DBTable');
  
  /**
   * 操作数据库中的一张表的组件, 又可以分为3个组件: 表单+表格+分页器
   */
  class DBTable extends React.PureComponent {
  
    // 父组件要保存子组件的状态...非常蛋疼...
    // 破坏了子组件的"封闭"原则
    // 但这是官方推荐的做法: https://facebook.github.io/react/docs/lifting-state-up.html
  
    // 注意: 向父组件传状态, 通过回调函数的形式
    // 从父组件接收状态, 通过props的形式
  
    state = {
      // 本身的状态
      loadingSchema: false,  // 是否正在从远程加载schema
  
      // 表单组件的状态
      queryObj: {},  // 表单中的查询条件
  
      // 表格组件的状态
      data: [],  // 表格中显示的数据
      tableLoading: false,  // 表格是否是loading状态
  
      // 分页器的状态
      currentPage: 1,  // 当前第几页, 注意页码是从1开始的, 以前总是纠结页码从0还是1开始, 这里统一下, 跟显示给用户的一致
      pageSize: globalConfig.DBTable.pageSize || 50,  // pageSize默认值50, 这个值一旦初始化就是不可变的
      total: 0,  // 总共有多少条数据
    };
  
  
    // 这里有个很有意思的问题, 就是异步操作的局限性, 你没办法控制callback何时被调用
    // 我本来的写法是这样的:
    // async componentWillMount() {
    //   // tryFetchSchema方法可能是同步也可能是异步, 跟tableConfig.asyncSchema有关
    //   // 如果是同步调用, 会直接返回一个resolved状态的promise
    //   // 如果是异步调用, 会返回一个pending状态的promise
    //   // 注意, 所有async方法, 直接调用的话, 必然会返回promise, 这是语言特性决定的
    //   const res = await this.tryFetchSchema(this.props);
    //   this.updateTableState(res);
    //   if (this.state.loadingSchema) {
    //     this.setState({loadingSchema: false}, this.refresh);
    //   }
    // }
    // 注意其中的tryFetchSchema可能同步也可能异步
    // 我本来期望着如果是同步调用的话, 下面的updateTableState语句会立刻执行, 如果是异步调用, 就等异步操作结束后再执行updateTableState
    // 但实际情况是, 即使是同步调用(直接返回一个resolved状态的promise), 下面的代码也不会立刻执行
    // 这可能和async函数的特性有关, 即使直接return一个常量, 也会被当作异步操作对待
    // async/await语义只保证语句的"执行顺序", 而不保证执行的"间隔"
    // 同理, 各种回调都是不能保证事件发生后"立即"被执行的, 这是js event loop的局限, 也许应该说是"特性"?
  
    // 于是我只能改成下面这种普通的callback方式, 手动控制何时执行callback, 不能用async/await了
    // 如果是同步操作就立刻执行callback, 否则等异步操作结束再执行callback
  
    // 另一个有意思的问题就是, 如果将react的生命周期方法做成async的会怎样?
    // 关键要了解async函数的执行逻辑, 尤其是多个async函数嵌套时, 了解代码执行权的交换过程
    // 如果知道async/await的本质就是生成器, 而生成器的本质就是协程, 那就很好理解了
  
  
    componentWillMount() {
      // 处理url参数
      this.processQueryParams();
      // 组件初始化时尝试获取schema
      this.tryFetchSchema(this.props, (res) => {
        this.updateTableState(res);
        // 这个参数用于判断获取schema是同步还是异步
        if (this.state.loadingSchema) {
          this.setState({loadingSchema: false}, this.refresh);
        }
      });
    }
  
    /**
     * 刚进入页面时触发一次查询
     */
    componentDidMount() {
      // 如果是异步获取schema的话, 后面有callback会调用refresh的, 这里就不用调了
      if (!this.state.loadingSchema) {
        this.refresh();
      }
    }
  
    // 在react router中切换时, 组件不会重新mount, 只有props会变化
    componentWillReceiveProps(nextProps) {
      // 普通模式下, 所有的CRUD操作都是通过同一个DBTable组件进行的, 只是传入的tableName不同而已
      // 但是在tab模式下, 为了防止不同tab之间的干扰, 每个tab下都必须是一个"独立"的组件, 换句话说有很多不同DBTable组件的实例
      // 类似单例和多实例的区别
      if (globalConfig.tabMode.enable === true) {
        logger.debug('ignore props update under tabMode');
        return;
      }
  
      // FIXME: hack, 和App组件中componentWillReceiveProps方法类似
      const action = this.props.location.action;
      if (action === 'PUSH') {
        return;
      }
  
      logger.debug('receive new props and try to render, nextProps = %o', nextProps);
      // 应该只有react router会触发这个方法
      if (nextProps.routes) {
        // 如果表名不变的话, 没必要重新加载schema/refresh, 直接return
        const routes = nextProps.routes;
        const nextTableName = routes[routes.length - 1].tableName;
        if (nextTableName === this.tableName) {
          return;
        }
      }
  
      // 在表名切换后要做什么?
      // 1. 根据新的表名重新获取schema
      // 2. 还原初始状态
      // 3. 调用一次refresh(), 重新查询数据
  
      // 和组件挂载时类似, 同样注意区分同步/异步
      this.tryFetchSchema(nextProps, (res) => {
        this.updateTableState(res);
        // 处理url参数
        this.state.queryObj = {};
        this.processQueryParams();
        this.setState({
          data: [],
          tableLoading: false,
          currentPage: 1,
          total: 0,
          loadingSchema: false,
        }, this.refresh);
      });
    }
  
    /**
     * 尝试获取schema, 可能是同步也可能是异步
     * 获取schema成功后, 调用回调
     *
     * @param props
     * @param callback
     * @returns {undefined}
     */
    async tryFetchSchema(props, callback) {
      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;
      }
  
      const tableConfig = TableUtils.getTableConfig(tableName);
  
      // 这里注意, 区分同步/异步
      let tmp = TableUtils.getCacheSchema(tableName);
      if (!tmp) {
        if (tableConfig.asyncSchema === true) {
          // 如果是异步的, 必须给用户一个loading提示
          this.state.loadingSchema = true;
          tmp = await TableUtils.getRemoteSchema(tableName);
        } else {
          tmp = TableUtils.getLocalSchema(tableName);
        }
      }
  
      const res = {...tmp, tableName, tableConfig};
      callback(res);
    }
  
    /**
     * fetch schema后, 更新当前组件的状态, 主要是更新一些this.XXX变量
     * 必须和tryFetchSchema方法配合使用
     *
     * @param input
     */
    updateTableState(input) {
      // 其实很多这种this.xxx变量也可以做成状态, 看情况了
      // 关键是这些变量变化时, 是否要触发重新render?
  
      // 这两项是肯定会有的
      this.tableName = input.tableName;
      this.tableConfig = input.tableConfig;
  
      if (input.querySchema) {
        this.querySchema = input.querySchema;
      } else {
        this.inited = false;
        this.errorMsg = `加载${input.tableName}表的querySchema出错, 请检查配置`;
        return;
      }
  
      if (input.dataSchema) {
        this.dataSchema = input.dataSchema;
      } else {
        this.inited = false;
        this.errorMsg = `加载${input.tableName}表的dataSchema出错, 请检查配置`;
        return;
      }
  
      // 如果一切正常, 设置init=true
      this.inited = true;
    }
  
    /**
     * 可以在url上加参数, 改变查询条件
     */
    processQueryParams() {
      // 这个方法可以算作一个后门, 甚至可以传入一些querySchema中没配置的参数, 只要后端能处理就可以
      const params = Utils.getAllQueryParams();
      // 如果url上有参数
      if (Object.keys(params).length > 0) {
        this.state.queryObj = Object.assign({}, this.state.queryObj, params);
      }
    }
  
    /**
     * 按当前的查询条件重新查询一次
     */
    refresh = async() => {
      // 如果加载schema失败, 就不要查询了
      if (!this.inited) {
        return;
      }
  
      const res = await this.select(this.state.queryObj, this.state.currentPage, this.state.pageSize);
      //message.success('查询成功');
      if (res.success) {
        this.setState({
          data: res.data,
          total: res.total,
          tableLoading: false,
        });
      } else {
        this.error(res.message);
      }
    };
  
    /**
     * 弹出错误信息
     *
     * @param errorMsg
     */
    error = (errorMsg) => {
      // 对于错误信息, 要很明显的提示用户, 这个通知框要用户手动关闭
      notification.error({
        message: '出错啦!',
        description: `请联系管理员, 错误信息: ${errorMsg}`,
        duration: 0,
      });
      this.setState({tableLoading: false});
    };
  
    /**
     * 向服务端发送select请求
     *
     * @param queryObj 包含了form中所有的查询条件, 再加上page和pageSize, 后端就能拼成完整的sql
     * @param page
     * @param pageSize
     * @returns {Promise}
     */
    async select(queryObj, page, pageSize) {
      // 为啥这个方法不用箭头函数, 但也不会有this的问题呢? 我猜测是因为这个方法都是被其他箭头函数调用的, 所以也会自动bind this
      // 同理上面的error函数似乎也不需要是箭头函数
      const tmpObj = Object.assign({}, queryObj);  // 创建一个新的临时对象, 其实直接修改queryObj也可以
      tmpObj.page = page;
      tmpObj.pageSize = pageSize;
  
      // 每次查询时, 要显示一个提示, 同时table组件也要变为loading状态
      const hide = message.loading('正在查询...', 0);
      try {
        const CRUD = ajax.CRUD(this.tableName);
        this.setState({tableLoading: true});
        const res = await CRUD.select(tmpObj);
        // 请求结束后, 提示消失, 但不要急着还原tableLoading的状态, 让上层调用的方法去还原
        hide();
        return res;
      } catch (ex) {  // 统一的异常处理, 上层方法不用关心
        logger.error('select exception, %o', ex);
        hide();
        const res = {};  // 手动构造一个res返回
        res.success = false;
        res.message = `网络请求出错: ${ex.message}`;
        return Promise.resolve(res);  // 还是要返回一个promise对象
      }
    }
  
    /**
     * 切换分页时触发查询
     *
     * @param page
     */
    handlePageChange = async(page) => {
      logger.debug('handlePageChange, page = %d', page);
      const res = await this.select(this.state.queryObj, page, this.state.pageSize);
      if (res.success) {
        this.setState({
          currentPage: page,
          data: res.data,
          total: res.total,
          tableLoading: false,
        });
      } else {
        this.error(res.message);
      }
    };
  
    /**
     * 点击提交按钮时触发查询
     *
     * @param queryObj
     */
    handleFormSubmit = async(queryObj) => {
      logger.debug('handleFormSubmit, queryObj = %o', queryObj);
      // 这时查询条件已经变了, 要从第一页开始查
      const res = await this.select(queryObj, 1, this.state.pageSize);
      if (res.success) {
        this.setState({
          currentPage: 1,
          data: res.data,
          total: res.total,
          tableLoading: false,
          queryObj: queryObj,
        });
      } else {
        this.error(res.message);
      }
    };
  
  
    render() {
      // 一段有些tricky的代码, 某些情况下显示一个特殊的loading
      // 主要是为了用户第一次进入的时候, 交互更友好
      // FIXME: 这段代码非常丑, (!this.inited && !this.errorMsg)这个条件是为了hack一个react-router的问题
      // 如果从首页点击侧边栏进入DBTable组件, 会依次触发componentWillMount和componentWillReceiveProps, 而直接从url进入的话则只会触发componentWillMount
      // 感觉react-router坑好多啊
      if (this.state.loadingSchema && (!this.notFirstRender || (!this.inited && !this.errorMsg))) {
        this.notFirstRender = true;
        return (
          <Spin tip="loading schema..." spinning={this.state.loadingSchema} delay={500}>
            <div style={{ height: '150px', width: '100%' }}></div>
          </Spin>
        );
      }
      this.notFirstRender = true;
  
      // 如果没能成功加载schema, 显示错误信息
      // 注意从错误信息切换到另一个表时, 也可能出现loading状态
      if (!this.inited) {
        return (
          <Spin tip="loading schema..." spinning={this.state.loadingSchema} delay={500}>
            <Error errorMsg={this.errorMsg}/>
          </Spin>
        );
      }
  
      // 1. 之前传props是直接{...this.state}, 感觉会影响效率, 传很多无用的属性
      // 2. 父组件传进去的方法名都是parentHandleXXX
      // 3. InnerForm和InnerPagination都是无状态的, 但InnerTable还是要维护自己的一些状态
  
      return (
        <Spin spinning={this.state.loadingSchema} delay={500}>
          <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}/>
        </Spin>
      );
    }
  
  }
  
  export default DBTable;
  