<template>
  <div class="et-table" v-loading="loading">
    <el-table ref="table" :data="data" v-bind="$attrs" v-on="$listeners">
      <template v-if="columns && columns.length">
        <template v-for="column in columns">
          <et-table-column v-if="column.hidden ? !column.hidden(column) : true" :key="column.prop" v-bind="column">
            <!-- 表头内容 -->
            <template v-slot:header="scope">
              <template v-if="column.headerType === 'tip'">
                {{ scope.column.label }}
                <el-popover placement="top" trigger="hover" :content="column.headerTip">
                  <i slot="reference" class="el-icon-warning-outline tip-icon"></i>
                </el-popover>
              </template>
              <slot
                v-if="!column.headerType && column.scopeHeaderSlot"
                :name="`${column.prop}-header`"
                v-bind="scope"
              ></slot>
              <template v-if="!column.headerType && !column.scopeHeaderSlot">
                {{ scope.column.label }}
              </template>
            </template>
            <!-- 表格内容 -->
            <template v-slot="scope">
              <template v-if="column.contentType === 'avatar'">
                <el-avatar :src="scope.row[column.prop]"></el-avatar>
              </template>
              <template v-if="column.contentType === 'image'">
                <el-image :src="scope.row[column.prop]"></el-image>
              </template>
              <template v-if="column.contentType === 'link'">
                <link-renderer
                  :row="scope.row"
                  :column="column"
                  :formatted-value="
                    customFormatter(scope.row, scope.column, scope.row[column.prop], scope.$index, column)
                  "
                />
              </template>
              <template v-if="column.contentType === 'tag'">
                <tag-renderer
                  :row="scope.row"
                  :column="column"
                  :formatted-value="
                    customFormatter(scope.row, scope.column, scope.row[column.prop], scope.$index, column)
                  "
                />
              </template>
              <template v-if="column.contentType === 'ctrl' && column.ctrlList && column.ctrlList.length">
                <!-- eslint-disable -->
                <el-link
                  class="et-table__ctrl"
                  v-for="(ctrl, index) in column.ctrlList"
                  v-if="(ctrl.show || (() => true))(scope.row, scope.column, scope.$index)"
                  :key="index"
                  :type="ctrl.type || 'primary'"
                  :icon="ctrl.icon"
                  :underline="false"
                  :disabled="(ctrl.disabled || (() => false))(scope.row, scope.column, scope.$index)"
                  @click.stop=";(ctrl.onClick && ctrl.onClick(scope.row, scope.column, scope.$index)) || noop"
                  >{{ ctrl.name }}</el-link
                >
              </template>
              <slot v-if="!column.contentType && column.scopeSlot" :name="`${column.prop}`" v-bind="scope"></slot>
              <template v-if="!column.contentType && !column.scopeSlot">
                {{ customFormatter(scope.row, scope.column, scope.row[column.prop], scope.$index, column) }}
              </template>
            </template>
          </et-table-column>
        </template>
      </template>
      <template v-if="$slots.default">
        <slot />
      </template>
    </el-table>
    <div v-if="needShowPagination" class="et-table__pagination">
      <et-pagination
        v-bind="pagination"
        :total="total"
        :page="pageIndex"
        :limit="pageSize"
        @pagination="onPageChange"
      ></et-pagination>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop, Emit, Watch, Ref } from 'vue-property-decorator'
import EtPagination from '@/EtPagination/src/index.vue'
import EtTableColumn from '@/EtTableColumn/src/index.vue'
import type { AxiosStatic, AxiosRequestConfig, CancelTokenSource } from 'axios'
import axios from 'axios'
import dayjs from 'dayjs'
import qs from 'qs'
import type { ElTable } from 'element-ui/types/table'
import type { ElTableColumn } from 'element-ui/types/table-column'
import type { ColumnConfig } from './types'
import TagRenderer from '@/EtTable/src/TagRenderer.vue'
import LinkRenderer from '@/EtTable/src/LinkRenderer.vue'

/**
 * 封装 element-ui 组件库 Table 表格
 * @see https://element.eleme.cn/#/zh-CN/component/table
 * <br/>
 * <br/>
 * 表头slot动态名称：prop+header 拼接
 * <br/>
 * {object} { column, $index } 当前列属性，列索引值
 * <br/>
 * <br/>
 * 表格slot动态名称：prop
 * <br/>
 * {object} { row, column, $index } 表格数据，当前列属性，列索引值
 */
@Component({
  name: 'EtTable',
  components: {
    LinkRenderer,
    TagRenderer,
    EtPagination,
    EtTableColumn,
  },
})
class EtTable extends Vue {
  @Ref('table') private readonly table!: ElTable

  /**
   * 发送请求的 axios
   */
  @Prop({
    default: () => {
      return (Vue.prototype.$ElementMarket && Vue.prototype.$ElementMarket.axios) || axios.create()
    },
  })
  private axios!: AxiosStatic
  /**
   * 发送请求的 axios 配置，优先级低于 headers
   * @see https://github.com/axios/axios
   */
  @Prop({
    default: () => {
      return {
        paramsSerializer: (params: any) => {
          return qs.stringify(params, { indices: false })
        },
      }
    },
  })
  private axiosConfig!: AxiosRequestConfig

  /**
   * 表格请求接口地址
   */
  @Prop({ default: '', type: String }) url!: string

  /**
   * 表格请求接口方式
   */
  @Prop({
    default: 'get',
    type: String,
    validator: (value: string) => {
      return ['get', 'post'].indexOf(value) !== -1
    },
  })
  method!: string

  /**
   * 表格创建后是否立即发送请求
   */
  @Prop({
    default: true,
    type: Boolean,
  })
  immediate!: boolean

  /**
   * 表格请求头部
   */
  @Prop({
    type: Object,
    default: () => {},
  })
  headers!: Record<string, any>

  /**
   * 表格请求参数
   */
  @Prop({
    type: Object,
    default: () => {},
  })
  params!: Record<string, any>
  /**
   * 表格请求参数变化时，自动发送请求
   */
  @Prop({ default: true, type: Boolean })
  watchParams!: boolean

  @Watch('params', { deep: true })
  onParamsChange() {
    if (this.watchParams) this.fetch()
  }

  /**
   * 返回结果中数据字段名称
   */
  @Prop({
    default: 'list',
    type: String,
  })
  listField!: string
  /**
   * 返回结果中总数字段名称
   */
  @Prop({
    default: 'total',
    type: String,
  })
  totalField!: string
  /**
   * 处理返回原始数据函数
   */
  @Prop({
    default: null,
    type: Function,
  })
  parseResponse!: Function
  /**
   * 处理返回列表数据函数
   */
  @Prop({
    default: null,
    type: Function,
  })
  parseList!: Function
  /**
   * 本地数据，不需要请求接口
   */
  @Prop({
    default: () => [],
    type: Array,
  })
  localData!: Array<any>

  @Watch('localData')
  onLocalDataChange() {
    this.fetch()
  }

  /**
   * 分页组件配置参数，参考 EtPagination 组件
   * page、limit、total 自动计算，暂不支持传入
   */
  @Prop({
    default: () => {},
    type: Object,
  })
  pagination!: typeof EtPagination

  /**
   * 展示分页组件，支持传入 true(展示)、false(不展示)、auto(有数据时展示)
   */
  @Prop({
    default: true,
    type: [Boolean, String],
  })
  showPagination!: boolean | string

  private get needShowPagination() {
    if (typeof this.showPagination === 'boolean') {
      return this.showPagination
    } else if (this.showPagination === 'auto') {
      return this.total > 0
    } else {
      return this.showPagination !== 'false'
    }
  }

  /**
   * 表格列配置，支持 el-table-column 所有配置项，并增加了额外的配置项 <br/>
   * {<br/>
   * <br/>
   * &nbsp;&nbsp;  formatter: (row, column, cellValue, index) => {}, // 格式化表格数据，同 el-table-column 的 formatter
   * &nbsp;&nbsp;  formatType: 'timestamp', // 格式化表格数据，支持 timestamp，优先级高于 el-table-column 的 formatter <br/>
   * <br/>
   * &nbsp;&nbsp;  headerType: 'tip', // 表头内容类型，支持 tip <br/>
   * &nbsp;&nbsp;  headerTip: '这是一个tip提示', // tip内容 <br/>
   * <br/>
   * &nbsp;&nbsp;  contentType: 'avatar', // 表格内容类型，支持 avatar | image | link | tag | ctrl  <br/>
   * <br/>
   * &nbsp;&nbsp;  contentType: 'avatar' | 'image' | 'link', // 需要传入string字符串  <br/>
   * &nbsp;&nbsp;  tagConfig: { // link展示的配置，非必填  <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; type: 'primary', // 对应 el-link 的 type 属性 <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; external: true, // 是否在新窗口打开链接 <br/>
   * &nbsp;&nbsp;  },<br/>
   * &nbsp;&nbsp;  contentType: 'tag', // 需要传入string字符串或数组  <br/>
   * &nbsp;&nbsp;  tagConfig: { // tag展示的配置，非必填  <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; type: '', // 对应 el-tag 的 type 属性。也可以传入一个函数，接收标签内容和单元格原始内容，返回属性值 <br/>
   * &nbsp;&nbsp;  },<br/>
   * <br/>
   * &nbsp;&nbsp;  contentType: 'ctrl', // 需要传入配置对象列表  <br/>
   * &nbsp;&nbsp;  ctrlList: { // 需要传入配置对象  <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; type: '', // 类型，对应 el-link 的类型 <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; icon: '', // 图标，对应 el-link 的图标类名 <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; name: '', // 名称 <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; show: (row, column, index) => boolean, // 是否展示 <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; disabled: (row, column, index) => boolean, // 是否禁用 <br/>
   * &nbsp;&nbsp;&nbsp;&nbsp; onClick: (row, column, index) => void, // 点击回调 <br/>
   * &nbsp;&nbsp;  },<br/>
   * <br/>
   * &nbsp;&nbsp;  scopeSlot: true, // 自定义表头内容，slot 动态名称：prop+header 拼接
   * <br/>
   * &nbsp;&nbsp;  scopeHeaderSlot: true, // 自定义表格内容，slot 动态名称：prop
   * <br/>
   * <br/>
   * &nbsp;&nbsp;  hidden: (column) => {}, // 判断是否显示该列 <br/>
   * }<br/>
   */
  @Prop({
    default: () => [],
    type: Array,
  })
  columns!: Array<ColumnConfig>

  // 显示的数据
  private data: Array<any> = []
  // 总条目数
  private total = 0
  // 加载数据中
  private loading = false

  // 当前页数
  private pageIndex = 1
  // 每页显示条目个数
  private pageSize = 10

  // 空函数
  private noop = () => {}

  created() {
    if (this.immediate) {
      this.fetch()
    }
  }

  /**
   * 返回数据格式
   * {
   *   code: 0,
   *   message: '',
   *   data: {
   *     total: 100,
   *     list: []
   *   }
   * }
   */
  private cancelToken?: CancelTokenSource
  private async fetch() {
    if (this.url) {
      // 如果之前的请求还在进行中，则取消之
      if (this.loading && this.cancelToken) {
        this.cancelToken.cancel()
      }

      this.loading = true
      this.cancelToken = axios.CancelToken.source()

      const params = { ...this.params, page: this.pageIndex, pageIndex: this.pageIndex, pageSize: this.pageSize }
      /**
       * 开始发送请求触发事件
       * @property {object} { params: object } 请求参数
       */
      this.$emit('fetch-start', { params })

      try {
        const config = {
          ...this.axiosConfig,
          headers: this.headers,
        }

        let response = null
        if (this.method === 'get') {
          response = await this.axios({
            method: 'get',
            url: this.url,
            ...config,
            params,
            cancelToken: this.cancelToken.token,
          })
        } else if (this.method === 'post') {
          response = await this.axios({
            method: 'post',
            url: this.url,
            ...config,
            data: params,
            cancelToken: this.cancelToken.token,
          })
        }

        const { data } = this.parseResponse ? this.parseResponse(response) : response
        this.data = this.parseList ? this.parseList(data[this.listField]) : data[this.listField]
        this.total = data[this.totalField]
        this.loading = false

        /**
         * 结束发送请求触发事件
         * <br/>
         * total 总条目数
         * <br/>
         * data 显示的数据
         * <br/>
         * response 为调用 axios 请求返回原始结果，数据结构取决于 axios 是否使用拦截器处理
         * @property {object} { total: number, page: number, pageSize: number, data: Array, response: Object } 请求返回数据
         */
        this.$emit('fetch-end', {
          total: this.total,
          page: this.pageIndex,
          pageSize: this.pageSize,
          data: this.data,
          response,
        })

        // 列表拉取请求结束后回调，由于表格条目的删除、筛选等，导致【当前页无数据，但总条目数不为空】的情况
        // 当检测到这种情况时，就切换到有数据的最后一页（因为 total > 0 所以至少是第一页）
        if (this.data.length === 0 && this.total > 0) {
          this.refresh(Math.ceil(this.total / this.pageSize))
        }
      } catch (err) {
        if (!axios.isCancel(err)) {
          this.loading = false
        }
      }
    } else {
      this.data = this.parseList ? this.parseList(this.localData) : this.localData
    }
  }

  @Emit('page-change')
  private onPageChange({ page: pageIndex, limit: pageSize }: { page: number; limit: number }) {
    this.pageIndex = pageIndex
    this.pageSize = pageSize

    // 分页变化时，请求数据
    this.fetch()

    return { pageIndex, pageSize }
  }

  /**
   * 格式化时间戳
   */
  private formatTimestamp(value: any) {
    if (String(value).length === 13) {
      // 毫秒时间戳
      return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
    } else if (String(value).length === 10) {
      // 秒时间戳
      return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss')
    } else {
      return '-'
    }
  }

  // 内置数据格式化映射
  private formatMap: Record<string, Function> = {
    timestamp: this.formatTimestamp,
  }

  // 表格数据格式化
  private customFormatter(row: object, column: ElTableColumn, value: any, index: any, config: ColumnConfig) {
    if (config.formatType) {
      return this.formatMap[config.formatType](value)
    }

    if (config && config.formatter) {
      return config.formatter(row, column, value, index)
    }
    // 后端返回的空值默认展示 '-'
    if (value === undefined || value === null) {
      return '-'
    }
    return value
  }

  /**
   * 刷新表格数据
   * @public
   * @property {number} pageIndex 刷新表格数据时，指定刷新页索引
   */
  public refresh(pageIndex?: number) {
    if (typeof pageIndex !== 'undefined') {
      this.pageIndex = pageIndex
    }

    this.fetch()
  }
}

/**
 * 代理 ElTable 暴露的方法
 * @see https://element.eleme.cn/#/zh-CN/component/table#table-methods
 * clearSelection
 * toggleRowSelection
 * toggleAllSelection
 * toggleRowExpansion
 * setCurrentRow
 * clearSort
 * clearFilter
 * doLayout
 * sort
 */
const delegateMethods = [
  'clearSelection',
  'toggleRowSelection',
  'toggleAllSelection',
  'toggleRowExpansion',
  'setCurrentRow',
  'clearSort',
  'clearFilter',
  'doLayout',
  'sort',
]

delegateMethods.forEach((method) => {
  ;(EtTable.prototype as any)[method] = function (...args: unknown[]) {
    ;(this.$refs.table as any)[method].call(this.$refs.table, ...args)
  }
})

export default EtTable
</script>
