This commit is contained in:
周炽键 2021-02-18 14:52:39 +08:00
parent eb781109e1
commit 38b8332c91
4 changed files with 779 additions and 0 deletions

View File

@ -0,0 +1,108 @@
<!--
* @Author: your name
* @Date: 2020-10-21 14:22:25
* @LastEditTime: 2021-02-04 15:46:01
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \dbadmin\src\components\HeaderQueryForm.vue
-->
<template>
<div class="header-query__component">
<el-form
:model="form"
:rules="rules"
label-position="left"
:label-width="labelWidth"
:size="size"
@keydown.enter.native.prevent="request"
inline
v-bind="$attrs"
>
<slot></slot>
<el-button v-if="btn" type="primary" :size="size" @click="request">
{{ queryLabel }}
</el-button>
</el-form>
</div>
</template>
<script>
import { Component, Prop, Emit } from 'vue-property-decorator'
import Vue from 'vue'
import { getModule } from 'vuex-module-decorators'
import QueryStore, { DEFAULT_TAG as DEFAULT_QUERY_TAG } from '@/store/modules/query'
import store from '@/store'
const queryStore = getModule(QueryStore, store)
@Component({
name: 'HeaderQuery'
})
export default class HeaderQuery extends Vue {
@Prop({ type: String, default: DEFAULT_QUERY_TAG })
tag
@Prop({ type: Object, required: true })
form
@Prop({ type: Object, default: () => ({}) })
rules
@Prop({ type: String, default: 'mini' })
size
@Prop({ type: String, default: '查询' })
queryLabel
@Prop({ type: Boolean, default: true })
btn
@Prop({ type: Boolean, default: true })
cache
@Prop({ type: String, default: undefined })
labelWidth
pageData = null
get storeData() {
return queryStore.data[this.path][this.tag] || {}
}
set storeData(data) {
queryStore.setData({ path: this.path, data, tag: this.tag })
}
get path() {
return this.$route.path
}
get fetch() {
return queryStore.fetchs[this.path][this.tag]
}
created() {
queryStore.init({ tag: this.tag, path: this.path, initData: Object.assign({}, this.form) })
this.pageData = queryStore.pageData[this.path][this.tag]
this.emitUpdateQuery()
}
/**
* 触发外部更新值
*/
@Emit('update:form')
emitUpdateQuery() {
return Object.assign({}, this.storeData)
}
@Emit('submit')
async request() {
this.pageData.page = 1
this.storeData = this.form
this.fetch()
return this.storeData
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,556 @@
<!--
* @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'
// TablevuexcurrentPagepageSize
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>

27
vue-table/store/index.js Normal file
View File

@ -0,0 +1,27 @@
/*
* @Author: your name
* @Date: 2020-09-07 10:17:01
* @LastEditTime: 2021-02-18 14:51:58
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit,
* @FilePath: \dbadmin\src\store\index.js
*/
import Vue from 'vue'
import Vuex from 'vuex'
import query from './modules/query'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
api,
user,
tagsView,
permission,
query
},
getters
})
export default store

View File

@ -0,0 +1,88 @@
/*
* @Author: your name
* @Date: 2020-10-28 14:40:52
* @LastEditTime: 2020-12-29 14:33:05
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \dbadmin\src\store\modules\query.js
*/
import { Module, VuexModule, Mutation } from 'vuex-module-decorators'
export const DEFAULT_TAG = 'default'
function nullObj() {
return Object.create(null)
}
@Module({ name: 'query', namespaced: true })
class Query extends VuexModule {
data = nullObj()
pageData = nullObj()
fetchs = nullObj()
@Mutation
init(data) {
let path = ''
let initData = nullObj()
let tag = DEFAULT_TAG
if (typeof data === 'string') {
path = data
} else if (typeof data === 'object') {
if (data.path) path = data.path
if (data.initData) initData = data.initData
if (data.tag) tag = data.tag
}
if (!path) throw new Error('path不能为空')
initPath.call(this, path)
if (!this.data[path][tag]) {
this.data[path][tag] = initData
}
if (!this.pageData[path][tag]) {
this.pageData[path][tag] = {
page: 1,
size: 20
}
}
}
@Mutation
setData({ path, data, tag = DEFAULT_TAG }) {
initPath.call(this, path)
this.data[path][tag] = data
}
@Mutation
registerFetch({ path, func, tag = DEFAULT_TAG }) {
initPath.call(this, path)
this.fetchs[path][tag] = func
}
@Mutation
unregisterFetch(path, tag = DEFAULT_TAG) {
initPath.call(this, path)
delete this.fetchs[path][tag]
}
}
function initPath(path) {
if (this instanceof Window) {
throw new Error('initPath this对象没有绑定')
}
if (!this.data[path]) this.data[path] = nullObj()
if (!this.pageData[path]) this.pageData[path] = nullObj()
if (!this.fetchs[path]) this.fetchs[path] = nullObj()
}
// function initTag(path, tag) {
// if (this instanceof Window) {
// throw new Error('initPath this对象没有绑定')
// }
// initTag.call(path)
// if (!this.data[path][tag]) this.data[path][tag] = nullObj()
// if (!this.pageData[path][tag]) this.pageData[path][tag] = nullObj()
// if (!this.fetchs[path][tag]) this.fetchs[path][tag] = nullObj()
// }
export default Query