This commit is contained in:
2023-06-08 21:09:28 +08:00
parent d778ceab61
commit f8947d332a
158 changed files with 17210 additions and 259 deletions

View File

@@ -0,0 +1,293 @@
<template>
<template v-for="row in props.columns" :key="row[options.pk]">
<template v-if="!row.hide">
<a-table-column
:title="row.title"
:width="row.width"
:ellipsis="row.ellipsis ?? true"
:filterable="row.filterable"
:cell-class="row.cellClass"
:header-cell-class="row.headerCellClass"
:body-cell-class="row.bodyCellClass"
:summary-cell-class="row.summaryCellClass"
:cell-style="row.cellStyle"
:header-cell-style="row.headerCellStyle"
:body-cell-style="row.bodyCellStyle"
:summary-cell-style="row.summaryCellStyle"
:tooltip="row.tooltip ?? true"
:align="row.align || 'left'"
:fixed="row.fixed"
v-if="row.children && row.children.length > 0"
>
<column
@refresh="() => refresh()"
:isRecovery="props.isRecovery"
:crudFormRef="props.crudFormRef"
:columns="row.children"
>
<template
v-for="(childRow, childIndex) in row.children"
:key="childIndex"
#[childRow.dataIndex]="{ record, column, rowIndex }"
>
<slot :name="`${childRow.dataIndex}`" v-bind="{ record, column, rowIndex }" />
</template>
</column>
</a-table-column>
<a-table-column
:title="row.title"
:data-index="row.dataIndex"
:width="row.width"
:ellipsis="row.ellipsis ?? true"
:filterable="row.filterable"
:cell-class="row.cellClass"
:header-cell-class="row.headerCellClass"
:body-cell-class="row.bodyCellClass"
:summary-cell-class="row.summaryCellClass"
:cell-style="row.cellStyle"
:header-cell-style="row.headerCellStyle"
:body-cell-style="row.bodyCellStyle"
:summary-cell-style="row.summaryCellStyle"
:tooltip="row.dataIndex === '__operation' ? false : row.tooltip ?? true"
:align="row.align || 'left'"
:fixed="row.fixed"
:sortable="row.sortable"
v-else
>
<template #cell="{ record, column, rowIndex }">
<!-- 操作栏 -->
<template v-if="row.dataIndex === '__operation'">
<a-scrollbar type="track" style="overflow: auto">
<a-space size="mini">
<slot name="operationBeforeExtend" v-bind="{ record, column, rowIndex }"></slot>
<slot name="operationCell" v-bind="{ record, column, rowIndex }">
<!-- <a-link
v-if="
options.see.show
&& ($common.auth(options.see.auth || [])
|| (options.see.role || []))
"
type="primary"
><icon-eye /> {{ options.see.text || '查看' }}</a-link> -->
<a-link
v-if="
(isFunction(options.edit.show)
? options.edit.show(record)
: options.edit.show) && !props.isRecovery
"
type="primary"
@click="editAction(record)"
>
<icon-edit /> {{ options.edit.text || "编辑" }}
</a-link>
<a-popconfirm
content="确定要恢复该数据吗?"
position="bottom"
@ok="recoveryAction(record)"
v-if="
(isFunction(options.recovery.show)
? options.recovery.show(record)
: options.recovery.show) && props.isRecovery
"
>
<a-link type="primary"
><icon-undo /> {{ options.recovery.text || "恢复" }}
</a-link>
</a-popconfirm>
<a-popconfirm
content="确定要删除该数据吗?"
position="bottom"
@ok="deleteAction(record)"
v-if="
isFunction(options.delete.show)
? options.delete.show(record)
: options.delete.show
"
>
<a-link
type="primary"
>
<icon-delete />
{{
props.isRecovery
? options.delete.realText || "删除"
: options.delete.text || "删除"
}}
</a-link>
</a-popconfirm>
</slot>
<slot name="operationAfterExtend" v-bind="{ record, column, rowIndex }"></slot>
</a-space>
</a-scrollbar>
</template>
<template v-else-if="row.customRender">
<custom-render
:column="column"
:record="record"
:render="row.customRender"
:rowIndex="rowIndex"
></custom-render>
</template>
<slot :name="row.dataIndex" v-bind="{ record, column, rowIndex }" v-else>
<template v-if="row.dataIndex === '__index'">{{ getIndex(rowIndex) }}</template>
<template v-if="row.dict && row.dict.translation">
<template v-if="isArray(get(record, row.dataIndex))">
<a-tag v-for="item in get(record, row.dataIndex)" class="ml-1">{{
getDataIndex(row, item)
}}</a-tag>
</template>
<a-tag v-else-if="row.dict.tagColors" :color="getTagColor(row, record)">
{{ getDataIndex(row, record) }}
</a-tag>
<a-tag v-else-if="row.dict.tagColor" :color="row.dict.tagColor">{{
getDataIndex(row, record)
}}</a-tag>
<span v-else>{{ getDataIndex(row, record) }}</span>
</template>
<template v-else-if="row.dataIndex && row.dataIndex.indexOf('.') !== -1">
{{ get(record, row.dataIndex) }}
</template>
<template v-else-if="row.formType === 'upload'">
<a-link @click="imageSee(row, record, row.dataIndex)"><icon-image /> 查看图片</a-link>
</template>
<template v-else>{{ record[row.dataIndex] }}</template>
</slot>
</template>
</a-table-column>
</template>
</template>
</template>
<script setup>
import { inject } from "vue"
import config from "@/config/crud"
import uploadConfig from "@/config/upload"
import { Message } from "@arco-design/web-vue"
import { isFunction, get, isArray, isObject } from "lodash"
import CustomRender from "../js/custom-render"
import tool from "@/utils/tool"
import commonApi from "@/api/common"
const emit = defineEmits(["refresh", "showImage"])
const props = defineProps({
columns: Array,
isRecovery: Boolean,
crudFormRef: Object
})
const options = inject("options")
const requestParams = inject("requestParams")
const dictTrans = inject("dictTrans")
const dictColors = inject("dictColors")
const imageSee = async (row, record, dataIndex) => {
if (row.returnType) {
if (row.returnType === "url") {
emit("showImage", record[dataIndex])
return
}
if (!["id", "hash"].includes(row.returnType)) {
Message.info("该图片无法查看")
return
}
Message.info("获取图片中,请稍等...")
const res =
row.returnType === "id"
? await commonApi.getFileInfoById({ id: record.id })
: await commonApi.getFileInfoByHash({ hash: record.hash })
const result = res?.success ?? false
if (!result) {
Message.info("图片信息无法获取")
return
}
const isImage = res.data.mime_type.indexOf("image") > -1
result &&
emit(
"showImage",
isImage
? tool.attachUrl(res.data.url, uploadConfig.storageMode[res.data.storage_mode])
: "not-image.png"
)
} else {
if (!record[row.dataIndex]) {
Message.info("无图片")
return
}
emit("showImage", record[row.dataIndex] ?? "not-image.png")
}
}
const getTagColor = (row, record) => {
return dictColors(
row.dataIndex,
row.dataIndex.indexOf(".") > -1 ? get(record, row.dataIndex) : record[row.dataIndex]
)
}
const getDataIndex = (row, record) => {
if (isObject(record)) {
return dictTrans(
row.dataIndex,
row.dataIndex.indexOf(".") > -1 ? get(record, row.dataIndex) : record[row.dataIndex]
)
} else {
return dictTrans(row.dataIndex, record)
}
}
const getIndex = (rowIndex) => {
const index = rowIndex + 1
if (requestParams[config.request.page] === 1) {
return index
} else {
return (requestParams[config.request.page] - 1) * requestParams[config.request.pageSize] + index
}
}
const editAction = (record) => {
isFunction(options.beforeOpenEdit) && options.beforeOpenEdit(record)
if (options.edit.action && isFunction(options.edit.action)) {
options.edit.action(record)
} else {
props.crudFormRef.edit(record)
}
}
const recoveryAction = async (record) => {
const response = await options.recovery.api({ ids: [record[options.pk]] })
response.success && Message.success(response.message || `恢复成功!`)
emit("refresh")
}
const deleteAction = async (record) => {
let data = {}
if (isFunction(options.beforeDelete) && !(data = options.beforeDelete([record[options.pk]]))) {
return false
}
const api = props.isRecovery ? options.delete.realApi : options.delete.api
const response = await api(Object.assign({ ids: [record[options.pk]] }, data))
if (options.afterDelete && isFunction(options.afterDelete)) {
options.afterDelete(response, record)
}
response.success && Message.success(response.message || `删除成功!`)
emit("refresh")
}
const refresh = () => {
emit("refresh")
}
defineExpose({ deleteAction, recoveryAction })
</script>
<style scoped>
:deep(.arco-image-img) {
object-fit: contain;
background-color: var(--color-fill-4);
}
</style>

View File

@@ -0,0 +1,157 @@
<template>
<ul
class="ma-crud-contextmenu shadow-lg"
v-show="crudContextMenuVisible"
:style="{ left: left + 'px', top: top + 'px', height: 'auto' }"
>
<template v-for="item in options?.contextMenu?.items ?? []">
<li v-if="item.operation === 'divider'"><a-divider margin="8px" /></li>
<li v-if="item.operation === 'print'">
<div class="context-menu-item" @click="execCommand(item)">
<icon-printer /> <span class="ml-2">{{ item.text ?? "打印表格" }}</span>
</div>
</li>
<li v-if="item.operation === 'refresh'">
<div class="context-menu-item" @click="execCommand(item)">
<icon-refresh /> <span class="ml-2">{{ item.text ?? "刷新表格" }}</span>
</div>
</li>
<li v-if="item.operation === 'add' && showVerify('add')">
<div class="context-menu-item" @click="execCommand(item)">
<icon-plus /> <span class="ml-2">{{ item.text ?? "新增数据" }}</span>
</div>
</li>
<li v-if="item.operation === 'edit' && showVerify('edit')">
<div class="context-menu-item" @click="execCommand(item)">
<icon-edit /> <span class="ml-2">{{ item.text ?? "编辑数据" }}</span>
</div>
</li>
<li v-if="item.operation === 'delete' && showVerify('delete')">
<a-popconfirm content="确实要删除此数据吗?" @ok="execCommand(item)">
<div class="context-menu-item">
<icon-delete /> <span class="ml-2">{{ item.text ?? "删除数据" }}</span>
</div>
</a-popconfirm>
</li>
<li v-if="!defaultOperation.includes(item.operation)">
<div class="context-menu-item" @click="execCommand(item)">
<component v-if="item.icon" :is="getIcon(item)" />
<span class="ml-2">{{ getText(item) }}</span>
</div>
</li>
</template>
</ul>
</template>
<script setup>
import { ref, inject, watch, nextTick } from "vue"
import checkAuth from "@/directives/auth/auth"
import { isArray } from "lodash"
const left = ref(0)
const top = ref(0)
const crudContextMenuVisible = ref(false)
const currentRow = ref()
const emit = defineEmits(["execCommand"])
const options = inject("options")
const isRecovery = inject("isRecovery")
const defaultOperation = ["refresh", "print", "edit", "divider", "delete", "add", "nextPage", "prevPage"]
const showVerify = (type) => {
if (!options[type].show) {
return false
}
if (isRecovery.value === true) {
return false
}
const authList = options[type].auth
if (isArray(authList)) {
for (let index in authList) {
if (!checkAuth(authList[index])) {
return false
}
}
}
return true
}
watch(
() => crudContextMenuVisible.value,
(value) => {
const crudContextMenuhandler = (e) => {
const dom = document.querySelector(".ma-crud-contextmenu")
if (dom && !dom.contains(e.target)) {
closeCrudcontextMenu()
}
}
value
? document.body.addEventListener("click", (e) => crudContextMenuhandler(e))
: document.body.removeEventListener("click", (e) => crudContextMenuhandler(e))
}
)
const openContextMenu = async (ev, record) => {
crudContextMenuVisible.value = true
currentRow.value = record
await nextTick(() => {
const domHeight = document.querySelector(".ma-crud-contextmenu").offsetHeight
if (document.body.offsetHeight - ev.pageY < domHeight) {
top.value = ev.clientY - domHeight
} else {
top.value = ev.clientY
}
left.value = ev.clientX
})
}
const closeCrudcontextMenu = () => {
currentRow.value = null
crudContextMenuVisible.value = false
}
const getIcon = (item) => {
if (!defaultOperation.includes(item.operation)) {
return item.icon
}
}
const getText = (item) => {
if (!defaultOperation.includes(item.operation)) {
return item.text
}
}
const execCommand = (item) => {
emit("execCommand", { contextItem: item, record: currentRow.value })
crudContextMenuVisible.value = false
}
defineExpose({
openContextMenu,
closeCrudcontextMenu
})
</script>
<style scoped lang="less">
.ma-crud-contextmenu {
position: fixed;
min-width: 200px;
z-index: 999;
background: var(--color-bg-2);
border: 1px solid var(--color-border-2);
padding: 7px 0;
border-radius: 4px;
li .context-menu-item {
cursor: pointer;
padding: 5px 10px;
}
li:hover {
background-color: var(--color-primary-light-1);
}
}
</style>

View File

@@ -0,0 +1,350 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<component
:is="componentName"
v-model:visible="dataVisible"
:on-before-ok="submit"
@cancel="close"
ok-text="保存"
cancel-text="关闭"
draggable
:width="options.formOption.width"
:fullscreen="options.formOption.isFull || false"
unmount-on-close
>
<template #title>{{ actionTitle }}</template>
<a-spin :loading="dataLoading" tip="加载中..." class="w-full">
<ma-form v-model="form" :columns="formColumns" :options="{ showButtons: false }" ref="maFormRef">
<template v-for="slot in Object.keys($slots)" #[slot]="component">
<slot :name="slot" v-bind="component" />
</template>
</ma-form>
</a-spin>
</component>
</template>
<script setup>
import { ref, toRaw, getCurrentInstance, inject, provide } from "vue"
import { Message } from "@arco-design/web-vue"
import { containerItems } from "@cps/ma-form/js/utils"
import { isArray, isFunction, get, cloneDeep, isUndefined } from "lodash"
import { useRouter } from "vue-router"
import tool from "@/utils/tool"
import { useFormStore } from "@/store/index"
const formStore = useFormStore()
const router = useRouter()
const app = getCurrentInstance().appContext.app
const maFormRef = ref()
const componentName = ref("a-modal")
const columns = inject("columns")
const layoutColumns = ref(new Map())
const options = inject("options")
const formColumns = ref([])
const currentAction = ref("")
const dataVisible = ref(false)
const form = ref({})
const actionTitle = ref("")
const dataLoading = ref(true)
const emit = defineEmits(["success", "error"])
provide("form", toRaw(form))
const submit = async () => {
const formData = maFormRef.value.getFormData()
if (await maFormRef.value.validateForm()) {
return false
}
let response
if (currentAction.value === "add") {
isFunction(options.beforeAdd) && (await options.beforeAdd(formData))
response = await options.add.api(formData)
isFunction(options.afterAdd) && (await options.afterAdd(response, formData))
} else {
isFunction(options.beforeEdit) && (await options.beforeEdit(formData))
response = await options.edit.api(formData[options.pk], formData)
isFunction(options.afterEdit) && (await options.afterEdit(response, formData))
}
if (response.success) {
Message.success(response.message || `${actionTitle.value}成功!`)
emit("success", response)
return true
} else if (response.success === false && (typeof response.code === "undefined" || response.code !== 200)) {
Message.error(response.message || `${actionTitle.value}失败!`)
return false
}
}
const open = () => {
formColumns.value = []
layoutColumns.value = new Map()
init()
if (options.formOption.viewType === "tag") {
if (!options.formOption.tagId) {
Message.info("未配置 tagId")
return
}
if (!options.formOption.tagName) {
Message.info("未配置 tagName")
return
}
const config = {
options,
formColumns: formColumns.value
}
formStore.crudList[options.id] = false
const queryParams = {
tagId: options.formOption.tagId,
op: currentAction.value
}
queryParams.key = form.value[options.formOption?.titleDataIndex ?? ""] ?? form.value[options.pk]
if (formStore.formList[options.formOption.tagId] === undefined) {
formStore.formList[options.formOption.tagId] = {
config,
addData: {},
editData: {}
}
}
if (currentAction.value === "add") {
formStore.formList[options.formOption.tagId].addData = cloneDeep(form.value)
} else {
formStore.formList[options.formOption.tagId].editData[queryParams.key] = cloneDeep(form.value)
}
form.value = {}
router.push(`/openForm/${options.formOption.tagId}` + tool.httpBuild(queryParams, true))
} else {
componentName.value = options.formOption.viewType === "drawer" ? "a-drawer" : "a-modal"
dataVisible.value = true
}
}
const close = () => {
dataVisible.value = false
formColumns.value = []
form.value = {}
}
const add = () => {
actionTitle.value = "新增"
currentAction.value = "add"
form.value = {}
open()
}
const edit = (data) => {
actionTitle.value = "编辑"
currentAction.value = "edit"
form.value = {}
for (let i in data) form.value[i] = data[i]
open(data)
}
const init = () => {
dataLoading.value = true
const layout = JSON.parse(JSON.stringify(options?.formOption?.layout ?? []))
// const layout = options?.formOption?.layout ?? []
columns.map(async (item) => {
await columnItemHandle(item)
})
// 设置表单布局
settingFormLayout(layout)
if (isArray(layout) && layout.length > 0) {
formColumns.value = layout
const excludeColumns = ["__index", "__operation"]
columns.map((item) => {
if (options.formExcludePk) excludeColumns.push(options.pk)
if (excludeColumns.includes(item.dataIndex)) return
!item.__formLayoutSetting && formColumns.value.push(item)
})
}
dataLoading.value = false
}
const columnItemHandle = async (item) => {
const excludeColumns = ["__index", "__operation"]
if (options.formExcludePk) excludeColumns.push(options.pk)
if (excludeColumns.includes(item.dataIndex)) {
layoutColumns.value.set(item.dataIndex, { dataIndex: item.dataIndex, layoutFormRemove: true })
return
}
layoutColumns.value.set(item.dataIndex, item)
formColumns.value.push(item)
// 针对带点的数据处理
if (item.dataIndex && item.dataIndex.indexOf(".") > -1) {
form.value[item.dataIndex] = get(form.value, item.dataIndex)
}
// add 默认值处理
if (currentAction.value === "add") {
if (item.addDefaultValue && isFunction(item.addDefaultValue)) {
form.value[item.dataIndex] = await item.addDefaultValue(form.value)
} else if (typeof item.addDefaultValue != "undefined") {
form.value[item.dataIndex] = item.addDefaultValue
}
}
// edit 默认值处理
if (currentAction.value === "edit") {
if (item.editDefaultValue && isFunction(item.editDefaultValue)) {
form.value[item.dataIndex] = await item.editDefaultValue(form.value)
} else if (typeof item.editDefaultValue != "undefined") {
form.value[item.dataIndex] = item.editDefaultValue
}
}
// 其他处理
item.display = formItemShow(item)
item.disabled = formItemDisabled(item)
item.readonly = formItemReadonly(item)
item.labelWidth = formItemLabelWidth(item)
item.rules = getRules(item)
}
const settingFormLayout = (layout) => {
if (!isArray(layout)) {
return
}
layout.map(async (item, index) => {
if (containerItems.includes(item.formType)) {
switch (item.formType) {
case "tabs":
if (item.tabs) {
item.tabs.map((tab) => {
tab.formList && (tab.formList = settingFormLayout(tab.formList))
})
}
break
case "card":
item.formList && (item.formList = settingFormLayout(item.formList))
break
case "grid-tailwind":
case "grid":
if (item.cols) {
item.cols.map((col) => {
col.formList && (col.formList = settingFormLayout(col.formList))
})
}
break
case "table":
if (item.rows) {
item.rows.map((row) => {
if (row.cols) {
row.cols.map((col) => {
col.formList && (col.formList = settingFormLayout(col.formList))
})
}
})
}
break
}
} else {
let column = layoutColumns.value.get(item.dataIndex)
// 公共column存在以dataIndex作为判断获取配置项
if (column) {
// 判断是否layout配置移除
if (column.layoutFormRemove) {
layout[index] = undefined
return
}
column["__formLayoutSetting"] = true
item = column
layout[index] = item
} else {
// 当公共column不存在则执行column配置项处理
await columnItemHandle(item)
let column = layoutColumns.value.get(item.dataIndex)
if (column.layoutFormRemove) {
layout[index] = undefined
return
}
item["__formLayoutSetting"] = true
layout[index] = item
}
}
})
// 移除
return layout.filter((item) => {
return !isUndefined(item)
})
}
const formItemShow = (item) => {
if (currentAction.value === "add") {
return item.addDisplay !== false
}
if (currentAction.value === "edit") {
return item.editDisplay !== false
}
return item.display !== false
}
const formItemDisabled = (item) => {
if (currentAction.value === "add" && !isUndefined(item.addDisabled)) {
return item.addDisabled
}
if (currentAction.value === "edit" && !isUndefined(item.editDisabled)) {
return item.editDisabled
}
if (!isUndefined(item.disabled)) {
return item.disabled
}
return false
}
const formItemReadonly = (item) => {
if (currentAction.value === "add" && !isUndefined(item.addReadonly)) {
return item.addReadonly
}
if (currentAction.value === "edit" && !isUndefined(item.editReadonly)) {
return item.editReadonly
}
if (!isUndefined(item.readonly)) {
return item.readonly
}
return false
}
const formItemLabelWidth = (item) => {
return item.labelWidth ?? options.labelWidth ?? undefined
}
const toRules = (rules) => {
if (!rules) {
return []
}
if (isArray(rules)) {
return rules.map((v) => ({ ...v }))
}
if (!rules.validator && isFunction(rules.validatorFormData)) {
rules.validator = (value, cb) => {
rules.validatorFormData(value, cb, form.value)
}
}
return { ...rules }
}
const getRules = (item) => {
if (currentAction.value === "add") {
return toRules(item.addRules ?? item.commonRules ?? [])
}
if (currentAction.value === "edit") {
return toRules(item.editRules ?? item.commonRules ?? [])
}
}
const getFormColumns = async (type = "add") => {
await init()
return formColumns.value
}
defineExpose({ add, edit, currentAction, form, getFormColumns })
</script>

View File

@@ -0,0 +1,69 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-modal v-model:visible="visible" :footer="false" @cancel="close" draggable>
<template #title>导入</template>
<a-upload draggable :custom-request="upload" :show-file-list="false" accept=".xlsx,.xls">
<template #upload-button>
<div
style="background-color: var(--color-fill-2); border: 1px dashed var(--color-fill-4)"
class="rounded text-center p-7"
>
<div>
<icon-upload class="text-5xl text-gray-400" />
<div class="text-red-600 font-bold">导入Excel</div>
将文件拖到此处<span style="color: #3370ff">点击上传</span>只能上传 xls/xlsx 文件
</div>
</div>
</template>
</a-upload>
<div class="mt-5 italic text-right"><a-link @click="sendDownload">下载Excel模板</a-link></div>
</a-modal>
</template>
<script setup>
import { ref, inject } from "vue"
import commonApi from "@/api/common"
import tool from "@/utils/tool"
import { Message } from "@arco-design/web-vue"
const visible = ref(false)
const options = inject("options")
const open = () => (visible.value = true)
const close = () => (visible.value = false)
const upload = (fileOption) => {
Message.info("文件上传导入中...")
const dataForm = new FormData()
dataForm.append("file", fileOption.fileItem.file)
commonApi.importExcel(options.import.url, dataForm).then((res) => {
res.success && Message.success(res.message || "导入成功")
close()
})
}
const sendDownload = () => {
Message.info("请求服务器下载文件中...")
const url = options.import.templateUrl
if (/^(http|https)/g.test(url)) {
window.open(url)
} else {
commonApi.download(url).then((res) => {
tool.download(res)
Message.success("请求成功,文件开始下载")
})
}
}
defineExpose({ open, close })
</script>

View File

@@ -0,0 +1,180 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-spin :loading="searchLoading" :tip="options.searchLoadingText" v-if="showSearch">
<a-form
:model="searchForm"
layout="inline"
:label-align="options?.searchLabelAlign"
ref="searchRef"
@submit="handlerSearch"
>
<div :class="[gridClass, options?.searchCustomClass]">
<template v-for="(component, index) in searchColumns" :key="index">
<a-form-item
:field="component.dataIndex"
:label="component.title"
label-col-flex="auto"
:label-col-style="{ width: component.searchLabelWidth ?? options.searchLabelWidth }"
>
<slot :name="`${component.dataIndex}`" v-bind="{ searchForm, component }">
<component :is="getComponentName(component.formType)" :component="component" />
</slot>
</a-form-item>
</template>
</div>
<div class="text-center mt-5 w-full" v-if="searchColumns.length > 0">
<a-space size="medium">
<slot name="searchBeforeButtons" />
<slot name="searchButtons">
<a-button type="primary" html-type="submit">
<template #icon><icon-search /></template>
{{ options.searchSubmitButtonText }}
</a-button>
<a-button @click="resetSearch">
<template #icon><icon-delete /></template>
{{ options.searchResetButtonText }}
</a-button>
</slot>
<slot name="searchAfterButtons" />
</a-space>
</div>
</a-form>
</a-spin>
</template>
<script setup>
import { ref, inject, provide, markRaw } from "vue"
import MaFormInput from "./searchFormItem/form-input.vue"
import MaFormPicker from "./searchFormItem/form-picker.vue"
import MaFormSelect from "./searchFormItem/form-select.vue"
import MaFormCascader from "./searchFormItem/form-cascader.vue"
import MaFormTreeSelect from "./searchFormItem/form-tree-select.vue"
import { cloneDeep, isFunction } from "lodash"
const options = inject("options")
const columns = inject("columns")
const requestParams = inject("requestParams")
const gridClass = ref(["ma-search-grid", "w-full", "grid", "lg:grid-cols-" + options.searchColNumber ?? 4])
const searchLoading = ref(false)
const showSearch = ref(true)
const searchRef = ref(null)
const searchColumns = ref([])
const searchForm = ref({})
provide("searchForm", searchForm)
provide("columns", columns)
const emit = defineEmits(["search"])
if (columns.length > 0) {
searchColumns.value = cloneDeep(
columns.filter((item) => item.search === true && options.tabs?.dataIndex != item.dataIndex)
)
}
const handlerSearch = () => {
emit("search", searchForm.value)
}
const resetSearch = async () => {
searchRef.value.resetFields()
if (options.resetSearch && isFunction(options.resetSearch)) {
await options.resetSearch(searchForm.value)
}
emit("search", searchForm.value)
}
const componentList = ref({
MaFormSelect: markRaw(MaFormSelect),
MaFormPicker: markRaw(MaFormPicker),
MaFormCascader: markRaw(MaFormCascader),
MaFormTreeSelect: markRaw(MaFormTreeSelect),
MaFormInput: markRaw(MaFormInput)
})
const getComponentName = (formType) => {
if (["select", "radio", "checkbox", "transfer"].includes(formType)) {
return componentList.value["MaFormSelect"]
} else if (["date", "month", "year", "week", "quarter", "range", "time"].includes(formType)) {
return componentList.value["MaFormPicker"]
} else if (formType === "cascader") {
return componentList.value["MaFormCascader"]
} else if (formType === "tree-select") {
return componentList.value["MaFormTreeSelect"]
} else {
return componentList.value["MaFormInput"]
}
}
const setSearchHidden = () => (showSearch.value = false)
const setSearchDisplay = () => (showSearch.value = true)
const setSearchLoading = () => (searchLoading.value = true)
const setSearchUnLoading = () => (searchLoading.value = false)
const getSearchFormRef = () => searchRef.value
const getSearchColumns = () => searchColumns.value
defineExpose({
getSearchFormRef,
getSearchColumns,
showSearch,
setSearchHidden,
setSearchDisplay,
setSearchLoading,
setSearchUnLoading
})
</script>
<style scoped lang="less">
:deep(.arco-form-item-label-required-symbol svg) {
vertical-align: baseline !important;
}
@media (min-width: 1024px) {
.lg\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.lg\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.lg\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.lg\:grid-cols-5 {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.lg\:grid-cols-6 {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.lg\:grid-cols-7 {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.lg\:grid-cols-8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.lg\:grid-cols-9 {
grid-template-columns: repeat(9, minmax(0, 1fr));
}
.lg\:grid-cols-10 {
grid-template-columns: repeat(10, minmax(0, 1fr));
}
.lg\:grid-cols-11 {
grid-template-columns: repeat(11, minmax(0, 1fr));
}
.lg\:grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
}
</style>

View File

@@ -0,0 +1,42 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-cascader
v-model="value"
:placeholder="props.component.searchPlaceholder || `请选择${props.component.title}`"
allow-clear
allow-search
:expand-trigger="props.component.trigger || 'click'"
:options="dicts[props.component.dataIndex]"
:multiple="props.component.multiple"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const dicts = inject("dicts")
const value = ref(get(searchForm.value, props.component.dataIndex, props.component.searchDefaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,37 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-input
v-model="value"
:placeholder="props.component.searchPlaceholder ?? `请输入${props.component.title}`"
allow-clear
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const value = ref(get(searchForm.value, props.component.dataIndex, props.component.searchDefaultValue ?? ""))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,61 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<component
:is="getComponentName()"
v-model="value"
:placeholder="
props.component.formType === 'range'
? ['请选择开始时间', '请选择结束时间']
: `请选择${props.component.title}`
"
:time-picker-props="props.component.formType == 'range' ? { defaultValue: ['00:00:00', '23:59:59'] } : {}"
:show-time="props.component.showTime"
:format="props.component.format || ''"
:mode="props.component.mode"
allow-clear
style="width: 100%"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const getComponentName = () => {
if (["date", "month", "year", "week", "quarter", "range", "time"].includes(props.component.formType)) {
return `a-${props.component.formType}-picker`
}
}
let defaultValue
if (props.component.formType === "range") {
defaultValue = props.component.searchDefaultValue ?? []
} else {
defaultValue = props.component.searchDefaultValue ?? ""
}
const value = ref(get(searchForm.value, props.component.dataIndex, defaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,60 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-select
v-model="value"
:virtual-list-props="props.component.virtualListProps"
:placeholder="props.component.searchPlaceholder ?? `请选择${props.component.title}`"
allow-clear
allow-search
:scrollbar="props.component.scrollbar"
:format-label="props.component.formatLabel"
:max-tag-count="1"
:options="dicts[props.component.dataIndex]"
:multiple="props.component.multiple || ['transfer', 'checkbox'].includes(props.component.formType)"
@change="handlerChangeeEvent"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { handlerCascader } from "@cps/ma-form/js/networkRequest"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const columns = inject("columns")
const dicts = inject("dicts")
let defaultValue
if (props.component.multiple === true) {
defaultValue = props.component.searchDefaultValue ?? []
} else {
defaultValue = props.component.searchDefaultValue ?? ""
}
const value = ref(get(searchForm.value, props.component.dataIndex, defaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
const handlerChangeeEvent = (value) => {
handlerCascader(value, props.component, columns, dicts, searchForm.value)
}
</script>

View File

@@ -0,0 +1,52 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-tree-select
v-model="value"
:treeProps="props.component.treeProps"
:placeholder="props.component.searchPlaceholder || `请选择${props.component.title}`"
allow-clear
allow-search
:field-names="props.component.dict.props || { key: 'value', title: 'label' }"
:tree-checkable="props.component.multiple"
:multiple="props.component.multiple"
:data="dicts[props.component.dataIndex]"
/>
</template>
<script setup>
import { ref, inject, watch } from "vue"
import { get, set } from "lodash"
const props = defineProps({
component: Object
})
const searchForm = inject("searchForm")
const dicts = inject("dicts")
let defaultValue
if (props.component.multiple === true) {
defaultValue = props.component.searchDefaultValue ?? []
} else {
defaultValue = props.component.searchDefaultValue ?? ""
}
const value = ref(get(searchForm.value, props.component.dataIndex, defaultValue))
set(searchForm.value, props.component.dataIndex, value.value)
watch(
() => get(searchForm.value, props.component.dataIndex),
(vl) => (value.value = vl)
)
watch(
() => value.value,
(v) => set(searchForm.value, props.component.dataIndex, v)
)
</script>

View File

@@ -0,0 +1,178 @@
<!--
- MineAdmin is committed to providing solutions for quickly building web applications
- Please view the LICENSE file that was distributed with this source code,
- For the full copyright and license information.
- Thank you very much for using MineAdmin.
-
- @Author X.Mo<root@imoi.cn>
- @Link https://gitee.com/xmo/mineadmin-vue
-->
<template>
<a-drawer :visible="visible" unmountOnClose :footer="false" :width="900" @cancel="onCancel">
<template #title>设置</template>
<a-space class="mt-3">
<span>表格大小</span>
<a-radio-group type="button" v-model="options.size">
<a-radio value="mini">迷你</a-radio>
<a-radio value="small"></a-radio>
<a-radio value="medium"></a-radio>
<a-radio value="large"></a-radio>
</a-radio-group>
<span class="ml-3">表格边框</span>
<a-radio-group type="button" v-model="bordered" @change="changeBordered">
<a-radio value="hide">不显示外边框</a-radio>
<a-radio value="show">全部显示</a-radio>
<a-radio value="row">不显示行</a-radio>
<a-radio value="column">不显示列</a-radio>
</a-radio-group>
<a-checkbox v-model="options.stripe" class="ml-3">斑马纹</a-checkbox>
</a-space>
<a-alert type="warning" class="mt-2"
>排序本页是指当前页排序服务器是指后台排序若自定义服务器排序可用
<a-tag>@sorterChange</a-tag> 事件来实现</a-alert
>
<a-table
:data="allowShowColumns"
:pagination="false"
:bordered="{ wrapper: true, cell: false }"
:draggable="{ type: 'handle', width: 40 }"
@change="onTableChange"
stripe
class="mt-3"
>
<template #columns>
<a-table-column title="列名称" data-index="title" align="center">
<template #cell="{ record }">{{ record.title }}</template>
</a-table-column>
<a-table-column title="宽度" data-index="width" align="center">
<template #cell="{ record }">
<a-input-number
v-if="!['__index', '__operation'].includes(record.dataIndex)"
style="width: 150px"
placeholder="列宽度"
v-model="record.width"
mode="button"
@change="changeColumn($event, 'width', record.dataIndex)"
/>
<span v-else> / </span>
</template>
</a-table-column>
<a-table-column title="隐藏" data-index="hide" align="center">
<template #cell="{ record }"
><a-checkbox v-model="record.hide" @change="changeColumn($event, 'hide', record.dataIndex)"
/></template>
</a-table-column>
<a-table-column title="固定" data-index="fixed" align="center">
<template #cell="{ record }">
<a-space v-if="!['__index', '__operation'].includes(record.dataIndex)">
<a-radio
v-model="record.fixed"
value=""
@change="changeColumn($event, 'fixed', record.dataIndex)"
></a-radio
>
<a-radio
v-model="record.fixed"
value="left"
@change="changeColumn($event, 'fixed', record.dataIndex)"
></a-radio
>
<a-radio
v-model="record.fixed"
value="right"
@change="changeColumn($event, 'fixed', record.dataIndex)"
></a-radio
>
</a-space>
<span v-else> / </span>
</template>
</a-table-column>
<a-table-column title="排序" data-index="order" align="center">
<template #cell="{ record }">
<a-space v-if="!['__index', '__operation'].includes(record.dataIndex)">
<a-radio
v-model="record.__order"
value=""
@change="changeColumn($event, 'order', record.dataIndex)"
></a-radio
>
<a-radio
v-model="record.__order"
value="page"
@change="changeColumn($event, 'order', record.dataIndex)"
>本页</a-radio
>
<a-radio
v-model="record.__order"
value="serve"
@change="changeColumn($event, 'order', record.dataIndex)"
>服务器</a-radio
>
</a-space>
<span v-else> / </span>
</template>
</a-table-column>
</template>
</a-table>
</a-drawer>
</template>
<script setup>
import { ref, inject } from "vue"
const options = inject("options")
let columns = inject("columns")
const allowShowColumns = columns.filter((item) => {
return !(item?.settingHide ?? false)
})
const visible = ref(false)
const bordered = ref("column")
const open = () => {
visible.value = true
}
const onCancel = () => {
visible.value = false
}
const changeColumn = (ev, type, name) => {
const column = columns.find((item) => item.dataIndex === name)
switch (type) {
case "order":
if (ev === "page") {
column.sortable = { sortDirections: ["ascend", "descend"], sorter: false }
} else if (ev === "serve") {
column.sortable = { sortDirections: ["ascend", "descend"], sorter: true }
} else {
column.sortable = undefined
}
break
}
}
const changeBordered = (v) => {
if (v === "hide") {
options.bordered = { wrapper: false, cell: false }
}
if (v === "show") {
options.bordered = { wrapper: true, cell: true }
}
if (v === "row") {
options.bordered = { wrapper: false, cell: true }
}
if (v === "column") {
options.bordered = { wrapper: true, cell: false }
}
}
const onTableChange = (_data) => {
columns = _data
}
defineExpose({ open })
</script>

View File

@@ -0,0 +1,841 @@
<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 lg:inline-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 checkAuth from "@/directives/auth/auth"
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)) {
const response = config.parseResponseData(await currentApi.value(requestParams.value))
if (response.rows) {
tableData.value = response.rows
if (response.pageInfo) {
total.value = response.pageInfo.total
openPagination.value = true
} else {
openPagination.value = false
}
} else {
tableData.value = response
}
} else {
console.error(`ma-crud errorcrud.api not is 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>

View File

@@ -0,0 +1,111 @@
import { loadDict } from "@cps/ma-form/js/networkRequest"
/**
* columnService 列服务处理类
* 首先感谢 @NEKGod 提交的PR此功能原本写在了 Ma-Crud 组件,我特意摘出来,封装成类通过引用来调用
* @author NEKGod, X.Mo <root@imoi.cn>
*/
const objectService = function (item) {
this.setAttr = (key, value) => {
item[key] = value
}
this.getAttr = (key) => {
return item[key]
}
this.get = () => {
return item
}
this.set = (config = {}) => {
for (let [key, value] of Object.entries(config)) {
item[key] = value
}
}
}
class ColumnService {
/**
* @type {Map<string, Object>}
*/
columnMap = new Map()
columns
cascaders
dicts
strictMode
/**
* @param data
* @param strictMode
*/
constructor(data, strictMode) {
this.columns = data.columns
this.cascaders = data.cascaders
this.dicts = data.dicts
this.strictMode = strictMode
this.columns.forEach((item) => {
this.columnMap.set(item.dataIndex, new objectService(item))
})
}
get(dataIndex) {
return this.columnMap.get(dataIndex)
}
isEmpty(dataIndex) {
return !this.columnMap.has(dataIndex)
}
exist(dataIndex) {
return !this.isEmpty(dataIndex)
}
async append(item, appendStartDataIndex = null) {
if (this.strictMode === true && item.dataIndex && this.exist(item.dataIndex)) {
console.warn(
`严格模式columnService.append(item) 参数中未有item.dataIndex属性或者item.dataIndex已存在column.${item.dataIndex}`
)
return false
}
if (this.cascaders.includes(item.dataIndex) && item.dict) {
await loadDict(this.dicts, item)
}
this.columns.push(item)
this.columnMap.set(item.dataIndex, new objectService(item))
// 获取排序
if (appendStartDataIndex !== null) {
let appendIndex =
this.columns
.map((item) => {
return item.dataIndex
})
?.indexOf(appendStartDataIndex) ?? -1
if (appendIndex === -1) {
return this.append(item, null)
}
let sortIndex = 0
let appendPosIndex = 0
this.columns.forEach((sortItem) => {
if (sortItem.dataIndex === appendStartDataIndex) {
appendPosIndex = sortIndex
} else if (sortItem.dataIndex === item.dataIndex) {
sortIndex = appendPosIndex + 1
} else {
}
sortItem.sortIndex = sortIndex
sortIndex += 2
})
this.columns.sort((a, b) => a.sortIndex - b.sortIndex)
}
return true
}
}
export default ColumnService

View File

@@ -0,0 +1,16 @@
import { defineComponent } from "vue"
export default defineComponent({
name: "CustomRender",
props: {
record: Object,
render: Function,
rowIndex: Number,
column: {
type: Object,
default: null
}
},
render() {
return this.render({ record: this.record, column: this.column, rowIndex: this.rowIndex })
}
})

View File

@@ -0,0 +1,277 @@
export default {
// 当前crud组件的 id全局唯一不指定则随机生成一个
id: undefined,
// 主键名称
pk: "id",
// 表单是否排除PK
formExcludePk: true,
// 请求api方法
api: () => {},
// 请求回收站api方法
recycleApi: () => {},
// 是否自动请求
autoRequest: true,
// 请求参数
requestParams: {},
// 设置分页组件每页记录数
pageSizeOption: [10, 20, 30, 50, 100],
// 是否开启表格分页
tablePagination: false,
// 设置选择列
rowSelection: undefined,
// 是否显示边框
bordered: { wrapper: true, cell: false },
// 是否开启拖拽排序
dragSort: false,
// 每页记录数
pageSize: 10,
// 子节点为空隐藏节点按钮
hideExpandButtonOnEmpty: true,
// 默认展开所有行
expandAllRows: false,
// 默认展开搜索
expandSearch: true,
// 斑马线
stripe: true,
// 新增、编辑、删除完成后是否刷新表格
dataCompleteRefresh: true,
// 表格大小
size: "large",
// 是否开启双击编辑数据
isDbClickEdit: true,
// 是否显示展开/折叠按钮
isExpand: false,
// 是否显示自定义
showExpandRow: false,
// 是否显示总结行
showSummary: false,
// 自定义总结行,要传入函数
customerSummary: false,
// 是否显示工具栏
showTools: true,
// 表头是否吸顶
stickyHeader: true,
// 页面布局方式,支持 normal标准和 fixed固定两种
pageLayout: "normal",
// 默认统一设置列宽度
columnWidth: 100,
// 搜索标签对齐方式
searchLabelAlign: "right",
// 全局搜索标签宽度
searchLabelWidth: "85px",
// 搜索每行列数
searchColNumber: 4,
// 搜索提交按钮文案
searchSubmitButtonText: "搜索",
// 搜索重置按钮文案
searchResetButtonText: "重置",
// 搜索栏加载提示文案
searchLoadingText: "加载数据中...",
// 搜索提交前置方法
beforeSearch: (requestParams) => {},
// 搜索提交后置方法
afterSearch: (requestParams) => {},
// 重置搜索钩子
resetSearch: (searchData) => {},
// 请求前置处理
beforeRequest: (requestParams) => {},
// 请求后置处理
afterRequest: (tableData) => {},
// 新增打开前方法
beforeOpenAdd: () => {},
// 新增提交前方法
beforeAdd: (formData) => {},
// 新增提交后方法
afterAdd: (response, formData) => {},
// 编辑打开前方法
beforeOpenEdit: (record) => {},
// 编辑提交前方法
beforeEdit: (formData) => {},
// 编辑提交后方法
afterEdit: (response, formData) => {},
// 删除前方法
beforeDelete: (ids) => {},
// 删除后方法
afterDelete: (response) => {},
// 组件初始化事件
onInit: () => {},
// 列表 选项卡 参数配置项
tabs: {
// 选项卡类型,参考 arco 官方 tabs 的api
type: "line",
// 选项卡触发方式: click | hover
trigger: "click",
// 指定一个字段作为选项卡,该字段的 search 必须为 true 并且使用了字典
dataIndex: undefined,
// 自定义选项卡项 [{ label: 'tab 1', value: 1, disabled: false }],也可函数返回一个数组
data: undefined,
// 默认选中的 tab
defaultKey: undefined,
// 切换选项卡时,请求后台数据的参数名
searchKey: undefined,
// 选项卡切换事件
onChange: (value) => {},
// 选项卡单击事件
onClick: (value) => {}
},
// 表单配置项
formOption: {
// 显示方式支持模态框和抽屉: modal drawer tag
viewType: "modal",
// 只有 viewType 为 tag 时生效,此值在所有 MaCrud 内唯一
tagId: "",
// 只有 viewType 为 tag 时生效tag标题名称
tagName: "",
// tag页设置标签标题的字段名称
titleDataIndex: undefined,
// 显示宽度
width: 600,
// 是否全屏只有modal有效
isFull: false,
// 表单布局
layout: []
},
add: {
// 新增api
api: undefined,
// 显示新增按钮的权限
auth: [],
// 显示新增按钮的角色
role: [],
// 按钮文案
text: "新增",
// 是否显示
show: false
},
edit: {
// 编辑api
api: undefined,
// 显示编辑按钮的权限
auth: [],
// 显示编辑按钮的角色
role: [],
// 按钮文案
text: "编辑",
// 是否显示
show: false
},
delete: {
// 删除api
api: undefined,
// 显示删除按钮的权限
auth: [],
// 显示删除按钮的角色
role: [],
// 按钮文案
text: "删除",
// 真实删除api
realApi: undefined,
// 显示真实删除按钮的权限
realAuth: [],
// 显示真实删除按钮的角色
realRole: [],
// 真实按钮文案
realText: "删除",
// 是否显示
show: false
},
recovery: {
// 恢复api
api: undefined,
// 显示恢复按钮的权限
auth: [],
// 显示恢复按钮的角色
role: [],
// 按钮文案
text: "恢复",
// 是否显示
show: false
},
// see: {
// // 显示查看按钮的权限
// auth: [],
// // 显示查看按钮的角色
// role: [],
// // 按钮文案
// text: '查看',
// // 是否显示
// show: false,
// },
import: {
// 导入url
url: undefined,
// 下载模板地址
templateUrl: undefined,
// 显示导入按钮的权限
auth: [],
// 显示导入按钮的角色
role: [],
// 按钮文案
text: "导入",
// 是否显示
show: false
},
export: {
// 导出url
url: undefined,
// 显示导出按钮的权限
auth: [],
// 显示导出按钮的角色
role: [],
// 按钮文案
text: "导出",
// 是否显示
show: false
},
// 行自定义 class 名称
rowCustomClass: (record, rowIndex) => [],
// 是否显示索引列
showIndex: false,
// 索引列名称
indexLabel: "序号",
// 索引列宽度
indexColumnWidth: 70,
// 索引列固定方向false 为不固定
indexColumnFixed: "left",
// 设置请求数据label
requestParamsLabel: undefined,
// 表格滚动默认宽高
scroll: { x: "100%", y: "100%" },
// 调整列宽
resizable: true,
// 是否显示操作列
operationColumn: false,
// 操作列宽度
operationWidth: 160,
// 操作列宽度 (新api)
operationColumnWidth: 160,
// 操作列名称
operationColumnText: "操作",
// 操作列文字对齐方式
operationColumnAlign: "right",
// 操作列固定方向false 为不固定
operationColumnFixed: "right",
// 右键菜单配置
contextMenu: {
// 是否开启右键菜单
enabled: true,
// 右键菜单配置
items: [
{ operation: "print" },
{ operation: "refresh" },
{ operation: "divider" },
{ operation: "add" },
{ operation: "edit" },
{ operation: "delete" }
]
}
}

View File

@@ -0,0 +1,116 @@
import { VNodeChild } from "vue"
/**
* 表单组件类型
*/
import { FieldRule } from "@arco-design/web-vue"
export type FormDateType =
| "radio"
| "checkbox"
| "select"
| "transfer"
| "tree-select"
| "cascader"
| "date"
| "month"
| "year"
| "week"
| "quarter"
| "range"
| "time"
| "input"
| "password"
| "textarea"
| "upload"
| "select-user"
| "editor"
| "code-editor"
| "icon"
| "user-info"
| "city-linkage"
| "form-group"
| "select-resource"
/**
* 列字典
*/
export interface ColumnDict {
// 字典名称,快捷查询字典接口查询
name?: string
// 自定义url查询
url?: string
// url查询方法,填写url之后生效
method?: "GET" | "POST" | "PUT" | "DELETE"
// url查询params数据,填写url之后生效
params?: object
// url查询body数据,填写url之后生效
body?: object
// 直接设置字典值
data?: object | Function
// 表格列的值是否翻译为字典对应标签
translation?: boolean
// 表格key 和 value的props设置
props?: {
label?: string
value?: string
}
}
export interface BasicColumn {
// 标题
title: string
// 字段名称
dataIndex: string
// 组件类型
formType?: FormDateType
// 表格列对齐方式
align?: "center" | "right" | "left"
// 字段是否加入搜索
search?: boolean
// 列宽
width?: number | "auto"
// 表格列是否隐藏
hide?: boolean
// 编辑|创建 通用是否显示字段
display?: boolean
// 添加弹窗是否显示字段
addDisplay?: boolean
// 编辑弹窗是否显示字段
editDisplay?: boolean
// 编辑|创建 通用是否禁用字段
disabled?: boolean
// 添加弹窗是否禁用字段
addDisabled?: boolean
// 编辑弹窗是否禁用字段
editDisabled?: boolean
// 编辑|创建 通用是否只读字段
readonly?: boolean
// 添加弹窗是否只读字段
addReadonly?: boolean
// 编辑弹窗是否只读字段
editReadonly?: boolean
// 自定义渲染
customRender?: (({ record, column, rowIndex }) => VNodeChild | JSX.Element) | VNodeChild | JSX.Element
// 字段新增时默认值
addDefaultValue?: number | string | boolean | undefined | ((record) => number | string | boolean | undefined)
// 字段编辑时默认值
editDefaultValue?: number | string | boolean | undefined | ((record) => number | string | boolean | undefined)
// select,radio,treeSelect,下拉字典配置
dict?: ColumnDict
// 继承公用配置
common?: boolean
// select 和 tree-select 组件是否开启虚拟列表
virtualList?: boolean
// 搜索默认值
searchDefaultValue?: number | string | undefined
// 搜索描述
searchPlaceholder?: string
//编辑|创建 通用规则
commonRules?: FieldRule | FieldRule[]
// 创建时规则
addRules?: FieldRule | FieldRule[]
// 编辑时规则
editRules?: FieldRule | FieldRule[]
// 子表单数据
children?: BasicColumn[]
}

View File

@@ -0,0 +1,189 @@
export interface BasicCrud {
// 表格接口
api?: undefined | any
// 主键名称
pk?: string
// 设置选择列
rowSelection?:
| undefined
| {
// 选择值的标识默认id
key?: string
// 选择列是否显示全选
showCheckedAll?: boolean
// 行选择器类型
type?: "checkbox" | "radio"
// 选择器列标题
title?: string | "#"
// 列宽度
width?: number | 60
// 是否固定
fixed?: boolean | false
// 是否仅展示当前页的keys
onlyCurrent?: boolean | true
}
// 搜索label宽度
searchLabelWidth?: string | "auto"
// 搜索label对齐方式
searchLabelAlign?: "center" | "right" | "left"
// 一行多少列
searchLabelCols?: number
// 是否显示边框
bordered?: { wrapper?: boolean; cell?: boolean }
// 是否开启拖拽排序
dragSort?: boolean
// 子节点为空隐藏节点按钮
hideExpandButtonOnEmpty?: boolean
// 默认展开所有行
expandAllRows?: boolean
// 斑马线
stripe?: boolean
// 新增、编辑、删除完成后是否刷新表格
dataCompleteRefresh?: boolean
// 表格大小
size?: "mini" | "small" | "medium" | "large"
// 是否开启双击编辑数据
isDbClickEdit?: boolean
// 是否显示展开/折叠按钮
isExpand?: boolean
// 是否显示自定义
showExpandRow?: boolean
// 是否显示总结行
showSummary?: boolean
// 自定义总结行,要传入函数
customerSummary?: boolean
// 是否显示工具栏
showTools?: boolean
// 新增和编辑显示设置
formOption?: {
// 显示方式支持模态框和抽屉?: modal drawer
viewType?: "modal" | "drawer"
// 显示宽度
width?: number
// 是否全屏只有modal有效
isFull?: boolean
}
//新增确定之前修改form值
beforeAdd?: (form) => void
//新增确定之后调用,返回接口response和form值
afterAdd?: (response, form) => void
//编辑确定之前修改form值
beforeEdit?: (form) => void
//编辑确定之后调用,返回接口response和form值
afterEdit?: (response, form) => void
add?: {
// 新增api
api?: undefined | any
// 显示新增按钮的权限
auth?: string[]
// 显示新增按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
edit?: {
// 编辑api
api?: undefined | any
// 显示编辑按钮的权限
auth?: string[]
// 显示编辑按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
delete?: {
// 删除api
api?: undefined | any
// 显示删除按钮的权限
auth?: string[]
// 显示删除按钮的角色
role?: string[]
// 按钮文案
text?: string
// 真实删除api
realApi?: undefined | any
// 显示真实删除按钮的权限
realAuth?: string[]
// 显示真实删除按钮的角色
realRole?: string[]
// 真实按钮文案
realText?: string
// 是否显示
show?: boolean
}
// Todo
recycleApi?: any
recovery?: {
// 显示恢复按钮的权限
auth?: string[]
// 显示恢复按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
// 恢复列表查询api
api?: undefined | any
}
// see?: {
// // 显示查看按钮的权限
// auth?: string[]
// // 显示查看按钮的角色
// role?: string[]
// // 按钮文案
// text?: string
// // 是否显示
// show?: boolean
// }
import?: {
// 导入url
url?: undefined | any
// 下载模板地址
templateUrl?: undefined | any
// 显示导入按钮的权限
auth?: string[]
// 显示导入按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
export?: {
// 导出url
url?: undefined | any
// 显示导出按钮的权限
auth?: string[]
// 显示导出按钮的角色
role?: string[]
// 按钮文案
text?: string
// 是否显示
show?: boolean
}
// 是否显示索引列
showIndex?: boolean
// 索引列名称
indexLabel?: string
// 设置请求数据label
requestParamsLabel?: undefined
// 表格滚动默认宽高
scroll?: {
x?: string
y?: string
}
// 调整列宽
resizable?: boolean
// 是否显示操作列
operationColumn?: boolean
// 操作列宽度
operationWidth?: number
// 操作列名称
operationColumnText?: string
}

View File

@@ -0,0 +1,2 @@
export * from "./columns"
export * from "./crud"