557 lines
12 KiB
Vue
557 lines
12 KiB
Vue
<!--
|
||
* @Author: your name
|
||
* @Date: 2020-09-08 16:22:26
|
||
* @LastEditTime: 2021-02-04 16:52:15
|
||
* @LastEditors: Please set LastEditors
|
||
* @Description: In User Settings Edit
|
||
* @FilePath: \dbadmin\src\components\TableForm\Table.vue
|
||
-->
|
||
<template>
|
||
<div class="table__component" ref="root" :style="rootStyle">
|
||
<el-button-group>
|
||
<!-- 普通功能按钮插槽 -->
|
||
<slot
|
||
name="btns"
|
||
:selectable="multiselect && selectedRows.length > 0"
|
||
:disabled="!multiselect || selectedRows.length === 0"
|
||
:selectrows="selectedRows"
|
||
:multiStepAction="multiStepReq"
|
||
></slot>
|
||
|
||
<!-- 数据导出按钮插槽 -->
|
||
<slot v-if="allowExport" name="export-btn" :disabled="selectedRows.length === 0" :action="exportData" :loading="exporting"></slot>
|
||
</el-button-group>
|
||
|
||
<el-table
|
||
:data="list"
|
||
@selection-change="onSelectionChange"
|
||
@current-change="$emit('current-change', $event)"
|
||
ref="table"
|
||
:height="tableHeight"
|
||
v-loading="loading"
|
||
:span-method="SpanMethod"
|
||
:default-expand-all="expandAll"
|
||
:class="tableClass"
|
||
v-bind="tableOptions"
|
||
>
|
||
<el-table-column v-if="enableDrag" v-bind="dragColumnOptions">
|
||
<i slot-scope="{ row }" class="el-icon-s-fold handle" style="font-size: 16px; cursor: move"></i>
|
||
</el-table-column>
|
||
<el-table-column type="selection" v-if="multiselect || allowExport" />
|
||
<slot></slot>
|
||
</el-table>
|
||
|
||
<!-- 分页 -->
|
||
<div v-if="visiblePage" class="page">
|
||
<el-pagination
|
||
:total="total"
|
||
:page-size="size"
|
||
background
|
||
layout="sizes, prev, pager, next"
|
||
:current-page.sync="currentPage"
|
||
:page-sizes="[...(isDev ? [1, 2, 5] : []), 10, 20, 50, 100, 200]"
|
||
@size-change="onSizeChange"
|
||
></el-pagination>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { get, del as RequestDelete } from '@/utils/request'
|
||
import Vue from 'vue'
|
||
import { Component, Prop, Watch, Ref, ProvideReactive, Provide } from 'vue-property-decorator'
|
||
import { toTime, toDateTime, toDate } from '@/utils/util'
|
||
import { downloadXlsx } from '@/utils/excel'
|
||
import { debounce } from 'underscore'
|
||
import { getModule } from 'vuex-module-decorators'
|
||
import QueryStore, { DEFAULT_TAG as DEFAULT_QUERY_TAG } from '@/store/modules/query'
|
||
import Sortable from 'sortablejs'
|
||
|
||
// Table组件只读取不写vuex里面的查询参数,但会修改分页参数(入currentPage,pageSize)
|
||
import store from '@/store'
|
||
const queryStore = getModule(QueryStore, store)
|
||
|
||
@Component({
|
||
name: 'Table'
|
||
})
|
||
export default class Table extends Vue {
|
||
@Prop({ type: Boolean, default: false })
|
||
enableDrag
|
||
|
||
@Prop({ type: Object, default: () => ({}) })
|
||
dragColumnOptions
|
||
|
||
@Prop({ type: Object, default: () => ({}) })
|
||
tableOptions
|
||
|
||
@Prop({ type: Boolean, default: false })
|
||
expandAll
|
||
|
||
@Prop({ type: String })
|
||
tableClass
|
||
|
||
@Prop({ type: Boolean, default: false })
|
||
multiselect
|
||
|
||
@Prop({ type: Boolean, default: true })
|
||
visiblePage
|
||
|
||
@Prop({ type: Function })
|
||
responseHandler
|
||
|
||
/**
|
||
* 是否允许导出数据
|
||
*/
|
||
@Prop({ type: Boolean, default: false })
|
||
allowExport
|
||
|
||
/**
|
||
* 导出数据时需要特殊处理的字段
|
||
* {
|
||
* [字段名]:处理方式
|
||
* }
|
||
*/
|
||
@Prop({ type: Object, default: () => ({}) })
|
||
exportSpecialField
|
||
|
||
@Prop({ type: String, required: true })
|
||
api
|
||
|
||
@Prop({ type: Function })
|
||
SpanMethod
|
||
|
||
@Prop({ type: [String, Number] })
|
||
height
|
||
|
||
@Prop({ type: String, default: DEFAULT_QUERY_TAG })
|
||
tag
|
||
|
||
@Ref('table')
|
||
tableRef
|
||
|
||
@Ref('root')
|
||
rootRef
|
||
|
||
/**
|
||
* 当前页码
|
||
*/
|
||
// currentPage = 1
|
||
|
||
get currentPage() {
|
||
if (!this.pageData) return 1
|
||
if (!this.pageData.page) this.pageData.page = 1
|
||
return this.pageData.page
|
||
}
|
||
|
||
set currentPage(page) {
|
||
if (!this.pageData) return
|
||
this.pageData.page = page
|
||
return page
|
||
}
|
||
|
||
// size = 20
|
||
|
||
get size() {
|
||
if (!this.pageData) return 20
|
||
if (!this.pageData.size) this.pageData.size = 20
|
||
return this.pageData.size
|
||
}
|
||
|
||
set size(size) {
|
||
if (!this.pageData) return
|
||
this.pageData.size = size
|
||
return size
|
||
}
|
||
|
||
/**
|
||
* 列表数据
|
||
*/
|
||
@ProvideReactive('table_data')
|
||
list = []
|
||
|
||
/**
|
||
* 列表数据是否加载中
|
||
*/
|
||
loading = false
|
||
|
||
/**
|
||
* 列表数据总长度
|
||
*/
|
||
total = 1
|
||
|
||
/**
|
||
* 选中的行
|
||
*/
|
||
selectedRows = []
|
||
|
||
/**
|
||
* 数据导出处理中
|
||
*/
|
||
exporting = false
|
||
|
||
rootHeight = null
|
||
|
||
tableHeightData = 50
|
||
|
||
get tableHeight() {
|
||
if (this.propHeightOk) {
|
||
return this.height
|
||
}
|
||
|
||
return this.tableHeightData
|
||
}
|
||
|
||
get propHeightOk() {
|
||
return ['string', 'number'].includes(typeof this.height)
|
||
}
|
||
|
||
get rootStyle() {
|
||
const style = {}
|
||
if (this.rootHeight) {
|
||
style.height = `${this.rootHeight}px`
|
||
}
|
||
|
||
return style
|
||
}
|
||
|
||
pageData = {}
|
||
|
||
get path() {
|
||
return this.$route.path
|
||
}
|
||
|
||
async created() {
|
||
queryStore.init({ path: this.path, tag: this.tag })
|
||
queryStore.registerFetch({ path: this.path, func: this.fetch, tag: this.tag })
|
||
|
||
this.$set(this, 'pageData', queryStore.pageData[this.$route.path][this.tag])
|
||
await this.fetch()
|
||
this.resizeHeight()
|
||
}
|
||
|
||
async mounted() {
|
||
this.initSortable()
|
||
window.addEventListener('resize', this.resizeHeight)
|
||
await this.$nextTick()
|
||
}
|
||
|
||
initSortable() {
|
||
if (!this.enableDrag) return
|
||
const tbody = this.tableRef.$el.querySelector('.el-table__body-wrapper table tbody')
|
||
if (!tbody) return
|
||
|
||
const onEnd = async event => {
|
||
const list = this.list.splice(0)
|
||
await this.$nextTick()
|
||
const { oldIndex, newIndex } = event
|
||
const oldObj = list[oldIndex]
|
||
const newObj = list[newIndex]
|
||
oldObj.sort = newIndex
|
||
newObj.sort = oldIndex
|
||
list[oldIndex] = newObj
|
||
list[newIndex] = oldObj
|
||
this.list.push(...list)
|
||
this.$emit('sort', this.list)
|
||
this.$emit('sort-change', { newObj, oldObj })
|
||
}
|
||
|
||
new Sortable(tbody, {
|
||
group: `TableComponent:${this._uid}`,
|
||
handle: '.el-icon-s-fold.handle',
|
||
draggable: 'tr.el-table__row',
|
||
onEnd
|
||
})
|
||
}
|
||
|
||
@Watch('api')
|
||
onWatchApiChange(api) {
|
||
this.currentPage = 1
|
||
this.fetch()
|
||
}
|
||
|
||
beforeDestroy() {
|
||
window.removeEventListener('resize', this.resizeHeight)
|
||
queryStore.unregisterFetch(this.path, this.tag)
|
||
}
|
||
|
||
async setHeight() {
|
||
if (this.propHeightOk) return
|
||
if (!this.tableRef) await this.$nextTick()
|
||
if (!this.tableRef || !this.tableRef.$el) return
|
||
this.rootHeight = document.body.clientHeight - this.tableRef.$el.offsetTop - 60
|
||
this.tableHeightData = this.rootHeight - 85
|
||
}
|
||
|
||
resizeHeight = debounce(this.setHeight.bind(this), 300)
|
||
|
||
@Watch('currentPage')
|
||
/**
|
||
* 页码有变化时获取数据
|
||
*/
|
||
onWatchCurrentPageChange(page) {
|
||
this.fetch()
|
||
}
|
||
|
||
onSizeChange(size) {
|
||
this.size = size
|
||
this.currentPage = 1
|
||
this.fetch()
|
||
}
|
||
|
||
onSelectionChange(rows) {
|
||
this.$emit('selection-change', rows)
|
||
this.$set(this, 'selectedRows', rows)
|
||
}
|
||
|
||
/**
|
||
* 导出数据
|
||
* @param {string} name 工作薄名
|
||
* @param {string} filename 文件名
|
||
* @param {Array} 英文字段-中文转换表
|
||
*/
|
||
exportData(name, filename, fields) {
|
||
if (this.exporting) return
|
||
this.exporting = true
|
||
const data = this.selectedRows.map(item => {
|
||
const result = {}
|
||
|
||
fields.forEach(([en, cn]) => {
|
||
result[cn] = item[en]
|
||
|
||
// 特殊处理的字段
|
||
if (en in this.exportSpecialField) {
|
||
const type = this.exportSpecialField[en]
|
||
switch (type) {
|
||
case 'time':
|
||
result[cn] = toTime(result[cn])
|
||
return
|
||
case 'date-time':
|
||
result[cn] = toDateTime(result[cn])
|
||
return
|
||
case 'date':
|
||
result[cn] = toDate(result[cn])
|
||
return
|
||
default:
|
||
break
|
||
}
|
||
} else if (typeof result[cn] === 'boolean') {
|
||
result[cn] = result[cn] ? '是' : '否'
|
||
}
|
||
})
|
||
|
||
return result
|
||
})
|
||
downloadXlsx(
|
||
{
|
||
[name]: data
|
||
},
|
||
filename
|
||
)
|
||
this.exporting = false
|
||
}
|
||
|
||
/**
|
||
* 获取列表数据
|
||
*/
|
||
@Provide('fetch')
|
||
async fetch(query) {
|
||
if (this.loading) return
|
||
this.loading = true
|
||
let data, params
|
||
|
||
if (query) {
|
||
this.currentPage = 1
|
||
params = query
|
||
} else {
|
||
params = queryStore.data[this.path][this.tag]
|
||
}
|
||
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if ([undefined, NaN, null, ''].includes(value) || key === '__ob__') {
|
||
delete params[key]
|
||
}
|
||
})
|
||
|
||
const page_params = {}
|
||
|
||
if (this.visiblePage) {
|
||
page_params.limit = this.size
|
||
page_params.page = this.currentPage
|
||
}
|
||
|
||
try {
|
||
data = await get(this.api, {
|
||
params: {
|
||
...params,
|
||
...page_params
|
||
}
|
||
})
|
||
this.$emit('response', data)
|
||
} catch (err) {
|
||
this.loading = false
|
||
return
|
||
}
|
||
this.total = data.count
|
||
this.currentPage = data.page || data.currentPage
|
||
const list = data.results.map(detail => {
|
||
if (detail.created_time) {
|
||
detail.created_time = toDateTime(detail.created_time)
|
||
}
|
||
if (detail.date_joined) {
|
||
detail.date_joined = toDateTime(detail.date_joined)
|
||
}
|
||
if (detail.release_time) {
|
||
detail.release_time = toDateTime(detail.release_time)
|
||
}
|
||
if (this.responseHandler) return this.responseHandler(detail)
|
||
return detail
|
||
})
|
||
this.$emit('fetch-success', list)
|
||
this.list.splice(0)
|
||
this.$set(this, 'list', list)
|
||
await this.$nextTick()
|
||
this.$emit('get-table-ref', this.$refs.table)
|
||
this.loading = false
|
||
}
|
||
|
||
/**
|
||
* 下一页
|
||
*/
|
||
nextPage() {
|
||
this.currentPage++
|
||
}
|
||
|
||
/**
|
||
* 上一页
|
||
*/
|
||
prevPage() {
|
||
this.currentPage--
|
||
}
|
||
|
||
/**
|
||
* 重置当前页码
|
||
*/
|
||
resetPage() {
|
||
this.currentPage = 1
|
||
}
|
||
|
||
/**
|
||
* 切换某行某列的布尔值
|
||
* @param {number} rowIndex 行
|
||
* @param {string} name 字段
|
||
* @param {boolean} status 修改的布尔值
|
||
*/
|
||
toggleColumnBoolean(rowIndex, name, status) {
|
||
const detail = this.list[rowIndex]
|
||
if (!detail) throw new Error(`列${rowIndex}不存在`)
|
||
if (typeof detail[name] !== 'boolean') throw new Error(`列${rowIndex}-键${name}不是布尔值`)
|
||
const oldValue = detail[name]
|
||
detail[name] = status
|
||
this.$emit('column-bool-change', {
|
||
row: rowIndex,
|
||
name,
|
||
value: status,
|
||
oldValue
|
||
})
|
||
return status
|
||
}
|
||
|
||
/**
|
||
* 设置某行某列的值
|
||
* @param {number} rowIndex 行
|
||
* @param {string} name 字段
|
||
* @param {any} value 修改的值
|
||
*/
|
||
setColumnValue(rowIndex, name, value) {
|
||
const detail = this.list[rowIndex]
|
||
if (!detail) throw new Error(`列${rowIndex}不存在`)
|
||
const oldValue = detail[name]
|
||
detail[name] = value
|
||
this.$emit('column-value-change', {
|
||
row: rowIndex,
|
||
name,
|
||
value,
|
||
oldValue
|
||
})
|
||
return value
|
||
}
|
||
|
||
/**
|
||
* 格式化日期对象
|
||
* @param {string|Date} date 日期对象/字符串
|
||
* @returns {string}
|
||
*/
|
||
formatDate(date, time = true) {
|
||
return time ? toDateTime(date) : toDate(date)
|
||
}
|
||
|
||
/**
|
||
* 格式化日期对象
|
||
* @param {string|Date} date 日期对象/字符串
|
||
* @returns {string}
|
||
*/
|
||
formatTime(date) {
|
||
return toTime(date)
|
||
}
|
||
|
||
/**
|
||
* 调用删除接口
|
||
* @param {string} api 接口地址
|
||
* @param {string} message 提示消息
|
||
* @returns {Promise<boolean>} 是否删除成功
|
||
*/
|
||
async del(api, message = '是否确认删除') {
|
||
await this.$confirm(message)
|
||
try {
|
||
await RequestDelete(api)
|
||
} catch (err) {
|
||
console.error(`
|
||
Table组件调用删除接口失败
|
||
接口地址 ${api}
|
||
原因:
|
||
${err.message || err}
|
||
`)
|
||
return false
|
||
}
|
||
this.fetch()
|
||
return true
|
||
}
|
||
|
||
async multiStepReq(ids, req, options = {}) {
|
||
const baseOptions = {
|
||
confirmMessage: '是否确认此操作?',
|
||
successMessage: '批量操作成功'
|
||
}
|
||
options = Object.assign({}, baseOptions, options)
|
||
await this.$confirm(options.confirmMessage)
|
||
for (const id of ids) {
|
||
await req(id, this.selectedRows)
|
||
}
|
||
this.$message.success(options.successMessage)
|
||
this.fetch()
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-top: 1em;
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss">
|
||
.mo3 {
|
||
overflow: hidden;
|
||
-webkit-line-clamp: 3;
|
||
-webkit-box-orient: vertical;
|
||
display: -webkit-box;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.el-tooltip__popper {
|
||
max-width: 500px;
|
||
}
|
||
</style>
|