Files
cdTestPlant3/cdTMP/src/components/ma-crud/index.vue
2023-08-24 19:24:00 +08:00

845 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<a-layout-content class="flex flex-col lg:h-full relative w-full">
<div class="_crud-header flex flex-col mb-2" ref="crudHeaderRef">
<a-tabs
v-if="isArray(options.tabs.data) && options.tabs.data.length > 0"
v-model:active-key="options.tabs.defaultKey"
:trigger="options.tabs.trigger"
:type="options.tabs.type"
:hide-content="true"
@change="tabChange"
@tab-click="maEvent.customeEvent(options.tabs, $event, 'onClick')"
class="ma-tabs mb-5"
>
<template #extra><slot name="tabExtra"></slot></template>
<a-tab-pane :key="item.value" :title="item.label" v-for="item in options.tabs.data"></a-tab-pane>
</a-tabs>
<ma-search @search="searchSubmitHandler" class="__search-panel" ref="crudSearchRef">
<template v-for="(slot, slotIndex) in searchSlots" :key="slotIndex" #[slot]="{ searchForm, component }">
<slot :name="`search-${slot}`" v-bind="{ searchForm, component }" />
</template>
<template #searchBeforeButtons>
<slot name="searchBeforeButtons"></slot>
</template>
<template #searchButtons>
<slot name="searchButtons"></slot>
</template>
<template #searchAfterButtons>
<slot name="searchAfterButtons"></slot>
</template>
</ma-search>
</div>
<div class="_crud-content">
<div class="operation-tools lg:flex justify-between mb-3" ref="crudOperationRef">
<a-space class="lg:flex block">
<slot name="tableBeforeButtons"></slot>
<slot name="tableButtons">
<a-button
v-if="options.add.show"
@click="addAction"
type="primary"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon><icon-plus /></template>{{ options.add.text || "新增" }}
</a-button>
<a-popconfirm content="确定要删除数据吗?" position="bottom" @ok="deletesMultipleAction">
<a-button
v-if="options.delete.show"
type="primary"
status="danger"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon><icon-delete /></template>
{{ isRecovery ? options.delete.realText || "删除" : options.delete.text || "删除" }}
</a-button>
</a-popconfirm>
<a-popconfirm content="确定要恢复数据吗?" position="bottom" @ok="recoverysMultipleAction">
<a-button
v-if="options.recovery.show && isRecovery"
type="primary"
status="success"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon><icon-undo /></template>{{ options.recovery.text || "恢复" }}</a-button
>
</a-popconfirm>
<a-button v-if="options.import.show" @click="importAction" class="w-full lg:w-auto mt-2 lg:mt-0"
><template #icon><icon-upload /></template>{{ options.import.text || "导入" }}</a-button
>
<a-button v-if="options.export.show" @click="exportAction" class="w-full lg:w-auto mt-2 lg:mt-0"
><template #icon><icon-download /></template>{{ options.export.text || "导出" }}</a-button
>
<a-button
type="secondary"
@click="handlerExpand"
v-if="options.isExpand"
class="w-full lg:w-auto mt-2 lg:mt-0"
>
<template #icon>
<icon-expand v-if="!expandState" />
<icon-shrink v-else />
</template>
{{ expandState ? " 折叠" : " 展开" }}
</a-button>
</slot>
<slot name="tableAfterButtons"></slot>
</a-space>
<a-space class="lg:mt-0 mt-2" v-if="options.showTools">
<slot name="tools"></slot>
<a-tooltip
:content="isRecovery ? '显示正常数据' : '显示回收站数据'"
v-if="options.recycleApi && isFunction(options.recycleApi)"
>
<a-button shape="circle" @click="switchDataType"><icon-swap /></a-button>
</a-tooltip>
<a-tooltip content="刷新表格"
><a-button shape="circle" @click="refresh"><icon-refresh /></a-button
></a-tooltip>
<a-tooltip content="显隐搜索"
><a-button shape="circle" @click="toggleSearch"><icon-search /></a-button
></a-tooltip>
<a-tooltip content="打印表格"
><a-button shape="circle" @click="printTable"><icon-printer /></a-button
></a-tooltip>
<a-tooltip content="设置"
><a-button shape="circle" @click="tableSetting"><icon-settings /></a-button
></a-tooltip>
</a-space>
</div>
<div ref="crudContentRef">
<slot name="content" v-bind="tableData">
<a-table
v-bind="$attrs"
ref="tableRef"
:key="options.pk"
:data="tableData"
:loading="loading"
:sticky-header="options.stickyHeader"
:pagination="options.tablePagination"
:stripe="options.stripe"
:bordered="options.bordered"
:rowSelection="options.rowSelection || undefined"
:row-key="options?.rowSelection?.key ?? options.pk"
:scroll="options.scroll"
:column-resizable="options.resizable"
:size="options.size"
:row-class="options.rowClass"
:hide-expand-button-on-empty="options.hideExpandButtonOnEmpty"
:default-expand-all-rows="options.expandAllRows"
:summary="options.customerSummary || __summary || options.showSummary"
@selection-change="setSelecteds"
@sorter-change="handlerSort"
>
<template #tr="{ record }">
<tr
class="ma-crud-table-tr"
:class="
isFunction(options.rowCustomClass)
? options.rowCustomClass(record, rowIndex) ?? []
: options.rowCustomClass
"
@contextmenu.prevent="openContextMenu($event, record)"
@dblclick="dbClickOpenEdit(record)"
/>
</template>
<template #expand-row="record" v-if="options.showExpandRow">
<slot name="expand-row" v-bind="record"></slot>
</template>
<template #columns>
<ma-column
ref="crudColumnRef"
v-if="reloadColumn"
:columns="props.columns"
:isRecovery="isRecovery"
:crudFormRef="crudFormRef"
@refresh="() => refresh()"
@showImage="showImage"
>
<template #operationBeforeExtend="{ record, column, rowIndex }">
<slot name="operationBeforeExtend" v-bind="{ record, column, rowIndex }"></slot>
</template>
<template #operationCell="{ record, column, rowIndex }">
<slot name="operationCell" v-bind="{ record, column, rowIndex }"></slot>
</template>
<template #operationAfterExtend="{ record, column, rowIndex }">
<slot name="operationAfterExtend" v-bind="{ record, column, rowIndex }"></slot>
</template>
<template
v-for="(slot, slotIndex) in slots"
:key="slotIndex"
#[slot]="{ record, column, rowIndex }"
>
<slot :name="`${slot}`" v-bind="{ record, column, rowIndex }" />
</template>
</ma-column>
</template>
<template
#summary-cell="{ column, record, rowIndex }"
v-if="options.customerSummary || options.showSummary"
>
<slot name="summaryCell" v-bind="{ record, column, rowIndex }">{{
record[column.dataIndex]
}}</slot>
</template>
</a-table>
</slot>
</div>
</div>
<div
class="_crud-footer mt-3 text-right"
ref="crudFooterRef"
v-if="total > 0 && openPagination && !options.tablePagination"
>
<a-pagination
:total="total"
show-total
show-jumper
show-page-size
:page-size-options="options.pageSizeOption"
@page-size-change="pageSizeChangeHandler"
@change="pageChangeHandler"
v-model:current="requestParams[config.request.page]"
:page-size="requestParams[config.request.pageSize]"
style="display: inline-flex"
/>
</div>
<ma-setting ref="crudSettingRef" />
<ma-form ref="crudFormRef" @success="requestSuccess">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-form>
<ma-import ref="crudImportRef" />
<ma-context-menu ref="crudContextMenuRef" @execCommand="execContextMenuCommand" />
<a-image-preview :src="imgUrl" v-model:visible="imgVisible" />
</a-layout-content>
</template>
<script setup>
import config from "@/config/crud"
import { ref, watch, provide, nextTick, onMounted, onUnmounted } from "vue"
import defaultOptions from "./js/defaultOptions"
import { loadDict } from "@cps/ma-form/js/networkRequest.js"
import ColumnService from "./js/columnService"
import MaSearch from "./components/search.vue"
import MaForm from "./components/form.vue"
import MaSetting from "./components/setting.vue"
import MaImport from "./components/import.vue"
import MaColumn from "./components/column.vue"
import MaContextMenu from "./components/contextMenu.vue"
import { Message } from "@arco-design/web-vue"
import { request } from "@/utils/request"
import tool from "@/utils/tool"
import Print from "@/utils/print"
import { isArray, isFunction, isObject, isUndefined } from "lodash"
import { maEvent } from "@cps/ma-form/js/formItemMixin.js"
import globalColumn from "@/config/column.js"
import { useFormStore } from "@/store/index"
const formStore = useFormStore()
const props = defineProps({
// 表格数据
data: { type: [Function, Array], default: () => null },
// 增删改查设置
options: { type: Object, default: {} },
crud: { type: Object, default: {} },
// 字段列设置
columns: { type: Array, default: [] }
})
const loading = ref(true)
const dicts = ref({})
const cascaders = ref([])
const reloadColumn = ref(true)
const openPagination = ref(false)
const imgVisible = ref(false)
const imgUrl = ref(import.meta.env.VITE_APP_BASE + "not-image.png")
const total = ref(0)
const requestParams = ref({})
const slots = ref([])
const searchSlots = ref([])
const isRecovery = ref(false)
const expandState = ref(false)
const crudHeaderRef = ref()
const crudOperationRef = ref()
const crudContentRef = ref()
const crudFooterRef = ref()
const crudSearchRef = ref()
const crudSettingRef = ref()
const crudFormRef = ref()
const crudImportRef = ref()
const crudColumnRef = ref()
const crudContextMenuRef = ref()
const options = ref(Object.assign(JSON.parse(JSON.stringify(defaultOptions)), props.options, props.crud))
const columns = ref(props.columns)
const headerHeight = ref(0)
const selecteds = ref([])
const tableData = ref([])
const tableRef = ref()
const currentApi = ref()
// 初始化
const init = async () => {
// 设置 组件id
if (isUndefined(options.value.id)) {
options.value.id = "MaCrud_" + Math.floor(Math.random() * 100000 + Math.random() * 20000 + Math.random() * 5000)
}
// 收集数据
props.columns.map((item) => {
if (item.cascaderItem && item.cascaderItem.length > 0) {
cascaders.value.push(...item.cascaderItem)
}
})
await props.columns.map(async (item) => {
// 字典
if (!cascaders.value.includes(item.dataIndex) && item.dict) {
await loadDict(dicts.value, item)
}
})
await tabsHandler()
}
const dictTrans = (dataIndex, value) => {
if (dicts.value[dataIndex] && dicts.value[dataIndex].tran) {
return dicts.value[dataIndex].tran[value]
} else {
return value
}
}
const dictColors = (dataIndex, value) => {
if (dicts.value[dataIndex] && dicts.value[dataIndex].colors) {
return dicts.value[dataIndex].colors[value]
} else {
return undefined
}
}
// 公用模板
columns.value.map((item, index) => {
if (item.common && globalColumn[item.dataIndex]) {
columns.value[index] = globalColumn[item.dataIndex]
item = columns.value[index]
}
!item.width && (item.width = options.value.columnWidth)
})
provide("options", options.value)
provide("columns", props.columns)
provide("layout", props.layout)
provide("dicts", dicts.value)
provide("dictColors", dictColors.value)
provide("requestParams", requestParams.value)
provide("dictTrans", dictTrans)
provide("dictColors", dictColors)
provide("isRecovery", isRecovery)
watch(
() => props.options.api,
(vl) => (options.value.api = vl)
)
watch(
() => props.crud.api,
(vl) => (options.value.api = vl)
)
watch(
() => openPagination.value,
() => options.value.pageLayout === "fixed" && settingFixedPage()
)
watch(
() => formStore.crudList[options.value.id],
async (vl) => {
vl === true && (await requestData())
formStore.crudList[options.value.id] = false
}
)
const getSlot = (cls = []) => {
let sls = []
cls.map((item) => {
if (item.children && item.children.length > 0) {
let tmp = getSlot(item.children)
sls.push(...tmp)
} else if (item.dataIndex) {
sls.push(item.dataIndex)
}
})
return sls
}
const showImage = (url) => {
imgUrl.value = url
imgVisible.value = true
}
const getSearchSlot = () => {
let sls = []
props.columns.map((item) => {
if (item.search && item.search === true) {
sls.push(item.dataIndex)
}
})
return sls
}
slots.value = getSlot(props.columns)
searchSlots.value = getSearchSlot(props.columns)
const requestData = async () => {
await init()
if (options.value.showIndex && columns.value.length > 0 && columns.value[0].dataIndex !== "__index") {
columns.value.unshift({
title: options.value.indexLabel,
dataIndex: "__index",
width: options.value.indexColumnWidth,
fixed: options.value.indexColumnFixed
})
}
if (
options.value.operationColumn &&
columns.value.length > 0 &&
columns.value[columns.value.length - 1].dataIndex !== "__operation"
) {
columns.value.push({
title: options.value.operationColumnText,
dataIndex: "__operation",
width: options.value.operationColumnWidth ?? options.value.operationWidth,
align: options.value.operationColumnAlign,
fixed: options.value.operationColumnFixed
})
}
initRequestParams()
if (!options.value.tabs?.dataIndex && !options.value.tabs.data) {
await refresh()
} else {
options.value.tabs.defaultKey = options.value.tabs?.defaultKey ?? options.value.tabs.data[0].value
await tabChange(options.value.tabs?.defaultKey)
}
}
const initRequestParams = () => {
requestParams.value[config.request.page] = 1
requestParams.value[config.request.pageSize] = options.value.pageSize ?? 10
if (options.value.requestParamsLabel) {
requestParams.value[options.value.requestParamsLabel] = options.value.requestParams
} else {
requestParams.value = Object.assign(requestParams.value, options.value.requestParams)
}
}
const requestHandle = async () => {
loading.value = true
isFunction(options.value.beforeRequest) && options.value.beforeRequest(requestParams.value)
if (isFunction(currentApi.value)) {
if (options.value.parameters) {
requestParams.value = { ...requestParams.value, ...options.value.parameters }
}
const response = config.parseResponseData(await currentApi.value(requestParams.value))
if (response.rows) {
tableData.value = response.rows
if (response.pageInfo) {
// 这里去找total字段
total.value = response.pageInfo.total
openPagination.value = true
} else {
openPagination.value = false
}
} else {
tableData.value = response
}
} else {
console.error(`ma-crud errorcrud.api 不是一个 Function.`)
}
isFunction(options.value.afterRequest) && options.value.afterRequest(tableData.value)
loading.value = false
}
const refresh = async () => {
if (props.data) {
loading.value = true
const data = isArray(props.data) ? props.data : config.parseResponseData(await props.data(requestParams.value))
if (data.rows) {
tableData.value = data.rows
openPagination.value = true
} else {
tableData.value = data
}
loading.value = false
} else {
currentApi.value =
isRecovery.value && options.value.recycleApi && isFunction(options.value.recycleApi)
? options.value.recycleApi
: options.value.api
await requestHandle()
}
}
const searchSubmitHandler = async (formData) => {
if (options.value.requestParamsLabel && requestParams.value[options.value.requestParamsLabel]) {
requestParams.value[options.value.requestParamsLabel] = Object.assign(
requestParams.value[options.value.requestParamsLabel],
formData
)
} else if (options.value.requestParamsLabel) {
requestParams.value[options.value.requestParamsLabel] = Object.assign({}, formData)
} else {
requestParams.value = Object.assign(requestParams.value, formData)
}
if (options.value.beforeSearch && isFunction(options.value.beforeSearch)) {
options.value.beforeSearch(requestParams.value)
}
await pageChangeHandler(1)
if (options.value.afterSearch && isFunction(options.value.afterSearch)) {
options.value.afterSearch(requestParams.value)
}
}
const pageSizeChangeHandler = async (pageSize) => {
requestParams.value[config.request.page] = 1
requestParams.value[config.request.pageSize] = pageSize
await refresh()
}
const pageChangeHandler = async (currentPage) => {
requestParams.value[config.request.page] = currentPage
await refresh()
}
const toggleSearch = async () => {
const dom = crudHeaderRef.value?.style
if (dom) {
crudSearchRef.value.showSearch ? crudSearchRef.value.setSearchHidden() : crudSearchRef.value.setSearchDisplay()
await nextTick(() => {
headerHeight.value = crudHeaderRef.value.offsetHeight
options.value.pageLayout === "fixed" && settingFixedPage()
})
}
}
const settingFixedPage = () => {
const workAreaHeight = document.querySelector(".work-area").offsetHeight
const tableHeight = workAreaHeight - headerHeight.value - (openPagination.value ? 152 : 108)
crudContentRef.value.style.height = tableHeight + "px"
}
const tableSetting = () => {
crudSettingRef.value.open()
}
const requestSuccess = async (response) => {
if (response && response.code && response.code === 200) {
options.value.dataCompleteRefresh && (await refresh())
if (reloadColumn.value) {
reloadColumn.value = false
await nextTick(() => {
reloadColumn.value = true
})
}
}
}
const addAction = () => {
if (isFunction(options.value.beforeOpenAdd) && !options.value.beforeOpenAdd()) {
return false
}
if (options.value.add.action && isFunction(options.value.add.action)) {
options.value.add.action()
} else {
crudFormRef.value.add()
}
}
const editAction = (record) => {
if (isFunction(options.value.beforeOpenEdit) && !options.value.beforeOpenEdit(record)) {
return false
}
if (options.value.edit.action && isFunction(options.value.edit.action)) {
options.value.edit.action(record)
} else {
crudFormRef.value.edit(record)
}
}
const dbClickOpenEdit = (record) => {
if (options.value.isDbClickEdit) {
if (isRecovery.value) {
Message.error("回收站数据不可编辑")
return
}
if (options.value.edit.api && isFunction(options.value.edit.api)) {
editAction(record)
}
}
}
const importAction = () => crudImportRef.value.open()
const exportAction = () => {
Message.info("请求服务器下载文件中...")
const data = options.value.requestParamsLabel
? requestParams.value[options.value.requestParamsLabel]
: requestParams.value
const download = (url) => request({ url, data, method: "post", timeout: 60 * 1000, responseType: "blob" })
download(options.value.export.url)
.then((res) => {
tool.download(res)
Message.success("请求成功,文件开始下载")
})
.catch(() => {
Message.error("请求服务器错误,下载失败")
})
}
const deletesMultipleAction = async () => {
if (selecteds.value && selecteds.value.length > 0) {
const api = isRecovery.value ? options.value.delete.realApi : options.value.delete.api
let data = {}
if (isFunction(options.value.beforeDelete) && !(data = options.value.beforeDelete(selecteds.value))) {
return false
}
const response = await api(Object.assign({ ids: selecteds.value }, data))
if (options.value.afterDelete && isFunction(options.value.afterDelete)) {
options.value.afterDelete(response)
}
response.success && Message.success(response.message || `删除成功!`)
await refresh()
} else {
Message.error("至少选择一条数据")
}
}
const recoverysMultipleAction = async () => {
if (selecteds.value && selecteds.value.length > 0) {
const response = await options.value.recovery.api({ ids: selecteds.value })
response.success && Message.success(response.message || `恢复成功!`)
await refresh()
} else {
Message.error("至少选择一条数据")
}
}
const setSelecteds = (key) => {
selecteds.value = key
}
const switchDataType = async () => {
isRecovery.value = !isRecovery.value
currentApi.value =
isRecovery.value && options.value.recycleApi && isFunction(options.value.recycleApi)
? options.value.recycleApi
: options.value.api
await requestData()
}
const handlerExpand = () => {
expandState.value = !expandState.value
expandState.value ? tableRef.value.expandAll(true) : tableRef.value.expandAll(false)
}
const handlerSort = async (name, type) => {
const col = columns.value.find((item) => name === item.dataIndex)
if (col.sortable && col.sortable.sorter) {
if (type) {
requestParams.value.orderBy = name
requestParams.value.orderType = type === "ascend" ? "asc" : "desc"
} else {
requestParams.value.orderBy = undefined
requestParams.value.orderType = undefined
}
await refresh()
}
}
const getTableData = () => {
return tableData.value
}
const __summary = ({ data }) => {
if (options.value.showSummary && isArray(options.value.summary)) {
const summary = options.value.summary
let summaryData = {}
let summaryPrefixText = {}
let summarySuffixText = {}
let length = data.length || 0
summary.map((item) => {
summaryData[item.dataIndex] = 0
summaryPrefixText[item.dataIndex] = item?.prefixText ?? ""
summarySuffixText[item.dataIndex] = item?.suffixText ?? ""
data.map((record) => {
if (record[item.dataIndex]) {
if (item.action && item.action === "sum") {
summaryData[item.dataIndex] += parseFloat(record[item.dataIndex])
}
if (item.action && item.action === "avg") {
summaryData[item.dataIndex] += parseFloat(record[item.dataIndex]) / length
}
}
})
})
for (let i in summaryData) {
summaryData[i] =
summaryPrefixText[i] + tool.groupSeparator(summaryData[i].toFixed(2)) + summarySuffixText[i]
}
return [summaryData]
}
}
const resizeHandler = () => {
headerHeight.value = crudHeaderRef.value.offsetHeight
settingFixedPage()
}
const tabChange = async (value) => {
const searchKey = options.value.tabs?.searchKey ?? options.value.tabs?.dataIndex ?? "tabValue"
const params = {}
params[searchKey] = value
requestParams.value = Object.assign(requestParams.value, params)
await maEvent.customeEvent(options.value.tabs, value, "onChange")
await refresh()
}
const printTable = () => {
new Print(crudContentRef.value)
}
const openContextMenu = (ev, record) => {
options.value?.contextMenu?.enabled === true && crudContextMenuRef.value.openContextMenu(ev, record)
}
const execContextMenuCommand = async (args) => {
const item = args.contextItem
const record = args.record
switch (item.operation) {
case "print":
await printTable()
break
case "refresh":
await refresh()
break
case "add":
addAction()
break
case "edit":
editAction(record)
break
case "delete":
crudColumnRef.value.deleteAction(record)
break
default:
await maEvent.customeEvent(item, args, "onCommand")
break
}
}
const tabsHandler = async () => {
// 处理tabs
const tabs = options.value.tabs
if (isFunction(tabs.data) || isArray(tabs.data)) {
tabs.data = isFunction(tabs.data) ? await tabs.data() : tabs.data
} else if (!isUndefined(tabs.dataIndex)) {
const col = props.columns.find((item) => item.dataIndex === tabs.dataIndex)
if (col.search === true && isObject(col.dict)) {
tabs.data = dicts.value[tabs.dataIndex]
}
}
}
onMounted(async () => {
if (typeof options.value.autoRequest == "undefined" || options.value.autoRequest) {
await requestData()
}
if (!options.value.expandSearch && crudSearchRef.value) {
crudSearchRef.value.setSearchHidden()
}
if (options.value.pageLayout === "fixed") {
window.addEventListener("resize", resizeHandler, false)
headerHeight.value = crudHeaderRef.value.offsetHeight
settingFixedPage()
}
})
onUnmounted(() => {
if (options.value.pageLayout === "fixed") {
window.removeEventListener("resize", resizeHandler, false)
}
})
const getCurrentAction = () => crudFormRef.value.currentAction
const getFormData = () => crudFormRef.value.form
const getFormColumns = async (type = "add") => {
return await crudFormRef.value.getFormColumns(type)
}
/**
* 获取column属性服务类
* @returns ColumnService
*/
const getColumnService = (strictMode = true) => {
return new ColumnService({ columns: columns.value, cascaders: cascaders.value, dicts: dicts.value }, strictMode)
}
defineExpose({
refresh,
requestData,
addAction,
editAction,
getTableData,
setSelecteds,
getCurrentAction,
getFormData,
getFormColumns,
getColumnService,
requestParams,
isRecovery,
tableRef,
crudFormRef,
crudSearchRef,
crudImportRef,
crudSettingRef
})
</script>
<style scoped lang="less">
.__search-panel {
transition: display 1s;
overflow: hidden;
width: 100%;
}
._crud-footer {
z-index: 10;
}
</style>