初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
+758
View File
@@ -0,0 +1,758 @@
<template>
<div v-if="crudSchema?.useVxeTable" class="bg-card h-full flex flex-col">
<vxe-grid
ref="gridRef"
:id="crudSchemaRef?.rowKey"
:columns="vxeTableColumns"
:toolbar-config="{
refresh: true, // 刷新
print: false, // 打印
export: false, // 导出
custom: true, // 自定义列
zoom: true, // 最大化最小化,
slots: {
buttons: 'toolbar-left',
tools: 'toolbar-right'
}
}"
:keepSource="true"
:tree-config="
crudSchemaRef.tree
? {
childrenField: crudSchemaRef.tree?.children || 'children',
rowField: crudSchemaRef.tree?.id || 'id'
}
: undefined
"
height="auto"
:pager-config="{
enabled: crudSchemaRef.tree ? false : true,
layouts: [
'Total',
'Sizes',
'Home',
'PrevJump',
'PrevPage',
'Number',
'NextPage',
'NextJump',
'End'
],
pageSize: 20,
background: true,
pageSizes: [10, 20, 30, 50, 100, 200],
className: 'w-full',
size: 'mini' as const,
align: 'center'
}"
:proxy-config="{
ajax: {
query: ({ page }, formValues) => {
return funApi({
[tableConfig.paginationKey.current || 'pageNum']: page.currentPage,
[tableConfig.paginationKey.size || 'pageSize']: page.pageSize,
...formValues
})
}
},
response: {
result: ({ data }) => {
const fields = tableConfig.recordFields || ['rows']
for (const field of fields) {
if (data[field]) {
return data[field]
}
}
return []
},
total: ({ data }) => {
const fields = tableConfig.totalFields || ['recordCount']
for (const field of fields) {
if (data[field]) {
return data[field]
}
}
return 0
},
list: ({ data }) => {
if(crudSchemaRef.tree) return data;
const fields = tableConfig.totalFields || ['rows']
for (const field of fields) {
if (data[field]) {
return data[field]
}
}
return data
},
}
}"
@checkboxChange="handleCheckboxChange"
@checkboxAll="handleCheckboxAll"
v-bind="{ ...$attrs }"
>
<template #toolbar-left>
<slot name="toolbar-actions">
<TableAction :actions="toolbarActions" :drop-down-actions="dropDownToolbarActions">
<template #more>
<ElButton>
<template #icon
><Icon class="icon-more" icon="ant-design:down-outlined"
/></template>
更多操作</ElButton
>
</template>
</TableAction>
</slot>
</template>
<template #toolbar-right>
<div class="pr-[7px]">
<button
:key="item.auth"
:title="item.label"
v-for="item in crudSchemaRef.toolbarConfig?.tools || []"
class="vxe-button type--button is--circle"
@click="handleToolbarToolClick(item.auth)"
>
<i class="iconfont-sys">
<Icon :icon="item.icon" />
</i>
</button>
</div>
<slot name="toolbar-tools"></slot>
</template>
<template #form>
<slot name="form">
<div class="relative" v-if="crudSchemaRef.useSearchForm !== false">
<SearchBar @reset="handleReset" @submit="handleSearch" />
</div>
</slot>
</template>
<template v-for="(_, slotName) in $slots" #[slotName]>
<slot :name="slotName"></slot>
</template>
</vxe-grid>
</div>
<template v-else>
<SearchBar
v-if="crudSchemaRef.useSearchForm !== false"
@reset="handleReset"
@submit="handleSearch"
/>
<ElCard class="art-table-card" shadow="never">
<!-- 表格头部 -->
<ArtTableHeader
v-model:columns="columnChecks"
@refresh="refreshData"
@export="handleExport"
:layout="headerLayout"
:show-export-button="showExportButton"
>
<template #left>
<slot name="toolbar-actions">
<TableAction :actions="toolbarActions" :drop-down-actions="dropDownToolbarActions">
<template #more>
<ElButton>
<template #icon
><Icon class="icon-more" icon="ant-design:down-outlined"
/></template>
更多操作</ElButton
>
</template>
</TableAction>
</slot>
</template>
<template #tools>
<div
:key="item.auth"
v-for="item in crudSchemaRef.toolbarConfig?.tools || []"
class="button active"
@click="handleToolbarToolClick(item.auth)"
>
<i class="iconfont-sys">
<Icon :icon="item.icon" />
</i>
</div>
</template>
<template #right>
<slot name="toolbar-tools"></slot>
</template>
</ArtTableHeader>
<!-- 表格 -->
<ArtTable
:rowKey="crudSchemaRef.rowKey ?? 'id'"
:loading="loading"
:data="crudSchemaRef.data ?? tableData"
:columns="columns"
:pagination="crudSchemaRef.tree ? undefined : pagination"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
:stripe="true"
v-bind="{ ...$attrs }"
>
<template v-for="(_, slotName) in $slots" #[slotName]>
<slot :name="slotName"></slot>
</template>
</ArtTable>
</ElCard>
</template>
<DialogForm
v-bind="{ ...crudSchemaRef.dialogProps }"
ref="dialogFormRef"
:crud-schema="crudSchemaRef"
/>
<ElDialog v-model="exportDialogVisible" title="导出数据" width="620px" destroy-on-close>
<div class="text-sm text-gray-500 -mt-4 mb-5">选择导出字段、文件格式和数据范围</div>
<ElForm label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="文件名称">
<ElInput v-model="exportConfig.filename" maxlength="80" show-word-limit />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="工作表名称">
<ElInput v-model="exportConfig.sheetName" maxlength="31" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="导出格式">
<ElSelect v-model="exportConfig.format" class="w-full">
<ElOption label="Excel 工作簿(XLSX)" value="xlsx" />
<ElOption label="CSV 文件" value="csv" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="选择数据">
<ElSelect v-model="exportConfig.scope" class="w-full">
<ElOption label="当前页数据" value="current" />
<ElOption label="选择的数据" value="selected" />
<ElOption label="全部筛选结果" value="all" />
</ElSelect>
</ElFormItem>
</ElCol>
</ElRow>
<div class="border border-gray-200 rounded-md px-4 py-3">
<div class="flex items-center justify-between mb-3">
<ElCheckbox :model-value="isAllExportFieldsSelected" :indeterminate="isExportFieldsIndeterminate" @change="toggleExportFields">全选字段</ElCheckbox>
<span class="text-xs text-gray-400">已选择 {{ selectedExportFields.length }} / {{ exportFields.length }} 个字段</span>
</div>
<ElCheckboxGroup v-model="selectedExportFields" class="grid grid-cols-2 gap-x-4 gap-y-3">
<ElCheckbox v-for="field in exportFields" :key="field.field" :label="field.field">{{ field.title }}</ElCheckbox>
</ElCheckboxGroup>
</div>
</ElForm>
<template #footer>
<ElButton @click="exportDialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="exporting" @click="confirmExport">确认导出</ElButton>
</template>
</ElDialog>
</template>
<script setup lang="tsx">
import { useSearchBar } from '@/components/search-bar'
import { CrudInstance, CrudReadonlyState, CrudSchema } from './types'
import request from '@/utils/http'
import { TableAction } from '../table-action'
import DialogForm from './dialog-form.vue'
import { useAction } from './use-action'
import { Icon } from '@/components/icon'
import { downloadFile, getTreeNodeAndChildrenIds } from '@/utils/tool'
import { useAuth, useTable } from '@/hooks'
import { useColumns } from './use-columns'
import { tableConfig } from '@/utils/table/tableConfig'
defineOptions({
inheritAttrs: false
})
const props = withDefaults(
defineProps<{
crudSchema: CrudSchema
crudMethods?: any
}>(),
{
crudSchema: () => {
return {
columns: [],
api: '',
useSearchForm: true,
useVxeTable: false,
searchFormSchema: {
schema: []
}
}
}
}
)
const emit = defineEmits<{
(e: 'submit', values: any): void
(e: 'reset'): void
(e: 'register', methods: CrudInstance): void
(e: 'toolbarToolClick', code: string): void
}>()
const { hasAuth } = useAuth()
const crudSchemaRef = ref(props.crudSchema)
const dialogFormRef = ref<any>(null)
// 计算各种操作按钮配置
const { toolbarActions, dropDownToolbarActions, tableActions, dropDownActions } = useAction(
crudSchemaRef.value,
dialogFormRef,
props.crudMethods
)
const headerLayout = computed(() => {
const defaultLayout = ['refresh', 'size', 'fullscreen', 'columns', 'settings'] as const
const toolbarConfig = crudSchemaRef.value.toolbarConfig || {}
// 过滤掉被禁用的选项
const layout = defaultLayout.filter((item) => toolbarConfig[item] !== false)
return layout.join(',')
})
const resolveExportBaseUrl = () => {
const crudApiExport = crudSchemaRef.value.crudApi?.export as any
if (crudApiExport?.__baseUrl) {
return crudApiExport.__baseUrl as string
}
const api = crudSchemaRef.value.api as any
if (typeof api === 'string' && api) {
return api
}
if (typeof api === 'function' && api.__baseUrl) {
return api.__baseUrl as string
}
return ''
}
const showExportButton = computed(() => {
if (crudSchemaRef.value.hasExport === false || crudSchemaRef.value.toolbarConfig?.export === false) {
return false
}
const exportAuth = crudSchemaRef.value.exportAuth
if (exportAuth && !hasAuth(exportAuth)) {
return false
}
return !!(crudSchemaRef.value.crudApi?.export || resolveExportBaseUrl())
})
const funApi = computed(() => {
const { api, requestMethod, beforeFetch, afterFetch } = props.crudSchema
if (!api) return api as any
const fun = (params: any) => {
// 处理请求前的参数
let processedParams = beforeFetch ? beforeFetch(params) : params
// 发起请求
const apiRequest =
typeof api === 'string'
? request[requestMethod ?? 'post']({ url: api, params: processedParams })
: api(processedParams)
// 处理响应后的结果
return afterFetch ? apiRequest.then(afterFetch) : apiRequest
}
return fun
})
const actionItemRecord = ref()
const handleActionItemClick = (record: any) => {
actionItemRecord.value = record
}
const [SearchBar, searchBarApi] = useSearchBar(crudSchemaRef.value.searchFormSchema || {})
const tableObject = useTable<any>({
core: {
apiFn: funApi.value,
immediate:
(crudSchemaRef.value.immediate ?? true) && crudSchemaRef.value?.useVxeTable !== true,
apiParams: {
// 其他搜索参数
},
columnsFactory: () => {
const { columns } = useColumns(
crudSchemaRef,
tableActions,
dropDownActions,
handleActionItemClick
)
return columns as any
}
}
})
const {
data: tableData,
columns,
loading,
pagination,
columnChecks,
getData,
refreshData,
handleSizeChange,
handleCurrentChange
} = tableObject
const getExportFields = () => {
if (crudSchemaRef.value.exportFields?.length) {
return crudSchemaRef.value.exportFields
}
const sourceColumns = (columnChecks.value?.length
? columnChecks.value
: (typeof crudSchemaRef.value.columns === 'function'
? crudSchemaRef.value.columns()
: crudSchemaRef.value.columns) || []) as any[]
return sourceColumns
.filter((column) => {
if (!column?.prop) {
return false
}
if (column.prop === '__ACTION__') {
return false
}
if (['selection', 'expand', 'index', 'globalIndex'].includes(column.type)) {
return false
}
if (column.visible === false || column.checked === false) {
return false
}
return true
})
.map((column) => ({
field: column.prop,
title: column.label || column.prop
}))
}
type ExportScope = 'all' | 'current' | 'selected'
type ExportFormat = 'xlsx' | 'csv'
interface ExportField {
field: string
title: string
}
const exportDialogVisible = ref(false)
const exporting = ref(false)
const exportFields = ref<ExportField[]>([])
const selectedExportFields = ref<string[]>([])
const exportConfig = ref<{ filename: string; sheetName: string; format: ExportFormat; scope: ExportScope }>({
filename: '',
sheetName: '',
format: 'xlsx',
scope: 'all'
})
const isAllExportFieldsSelected = computed(() => exportFields.value.length > 0 && selectedExportFields.value.length === exportFields.value.length)
const isExportFieldsIndeterminate = computed(() => selectedExportFields.value.length > 0 && selectedExportFields.value.length < exportFields.value.length)
const toggleExportFields = (checked: boolean | string | number) => {
selectedExportFields.value = checked === true ? exportFields.value.map((field) => field.field) : []
}
const buildExportQuery = async () => {
const values = crudSchemaRef.value.useSearchForm !== false ? await searchBarApi.getValues() : {}
let query = { ...values }
if (crudSchemaRef.value.beforeFetch) {
query = crudSchemaRef.value.beforeFetch({ ...query }) || query
}
delete query.page
delete query.limit
delete query.format
delete query.field
delete query.order
delete query.filters
delete query.sorter
delete query.sorters
delete query.orders
delete query.columns
delete query.columnChecks
return query
}
const handleExport = () => {
const fields = getExportFields() as ExportField[]
if (!fields.length) {
ElMessage.warning('没有可导出的字段')
return
}
const exportName = crudSchemaRef.value.exportFileName || crudSchemaRef.value.dialogTitle || '数据导出'
exportFields.value = fields
selectedExportFields.value = fields.map((field) => field.field)
exportConfig.value = {
filename: exportName,
sheetName: exportName.slice(0, 31),
format: 'xlsx',
scope: 'all'
}
exportDialogVisible.value = true
}
const confirmExport = async () => {
const exportRequest = crudSchemaRef.value.crudApi?.export
? crudSchemaRef.value.crudApi.export
: (() => {
const baseUrl = resolveExportBaseUrl()
if (!baseUrl) {
return null
}
return (params: any) => request.post({ url: `${baseUrl}/export`, params })
})()
if (!exportRequest) {
ElMessage.warning('当前页面暂未配置导出能力')
return
}
const fields = exportFields.value.filter((field) => selectedExportFields.value.includes(field.field))
if (!fields.length) {
ElMessage.warning('请至少选择一个导出字段')
return
}
const query = await buildExportQuery()
if (exportConfig.value.scope === 'current') {
query.__export_page = pagination.current
query.__export_limit = pagination.size
}
if (exportConfig.value.scope === 'selected') {
const selectedIds = multipleSelection.value
.map((row: Record<string, any>) => row[crudSchemaRef.value.rowKey || 'id'])
.filter((id: unknown) => id !== undefined && id !== null && id !== '')
if (!selectedIds.length) {
ElMessage.warning('请先勾选需要导出的数据')
return
}
query.__export_ids = selectedIds
}
exporting.value = true
try {
const exportResult: any = await exportRequest({
filename: exportConfig.value.filename.trim() || '数据导出',
fileName: exportConfig.value.filename.trim() || '数据导出',
sheetName: exportConfig.value.sheetName.trim() || '数据导出',
isHeader: true,
fields,
query,
format: exportConfig.value.format,
scope: exportConfig.value.scope
})
const downloadParams = exportResult?.params
? { ...exportResult.params }
: {}
if ((!downloadParams.file_path || !downloadParams.file) && exportResult?.url) {
const url = new URL(exportResult.url, window.location.origin)
downloadParams.file_path = downloadParams.file_path || url.searchParams.get('file_path') || ''
downloadParams.file = downloadParams.file || url.searchParams.get('file') || `${exportConfig.value.filename || '数据导出'}.${exportConfig.value.format}`
}
if (!downloadParams.file_path) {
ElMessage.error('导出文件地址生成失败')
return
}
const blob = await request.get({
url: '/export/download-excel',
params: downloadParams,
responseType: 'blob',
showErrorMessage: true
})
downloadFile(blob, downloadParams.file || `${exportConfig.value.filename || '数据导出'}.${exportConfig.value.format}`)
exportDialogVisible.value = false
ElMessage.success('导出成功')
} finally {
exporting.value = false
}
}
const handleSearch = (values: any) => {
//Object.assign(searchParams, values)
emit('submit', values)
if (crudSchemaRef.value.useVxeTable === true) {
gridRef.value?.commitProxy('query', values)
return
}
getData(values)
}
const handleReset = () => {
//Object.assign(searchParams, {})
emit('reset')
if (crudSchemaRef.value.useVxeTable === true) {
gridRef.value?.commitProxy('query', {})
return
}
getData({})
}
// 删除
const handleDelete = (row: any) => {
const rowKey = props.crudSchema.rowKey || 'id'
let ids = row?.[rowKey]
? [row[rowKey]]
: multipleSelection.value.map((item: any) => item[rowKey])
// 如果是树形结构,需要获取节点及其所有子节点
if (crudSchemaRef.value.tree) {
const idKey = crudSchemaRef.value.tree.id || 'id'
const childrenKey = crudSchemaRef.value.tree.children || 'children'
// 收集所有选中节点及其子节点的ID
const allIds: any[] = []
const selectedRows = row?.[rowKey] ? [row] : multipleSelection.value
selectedRows.forEach((selectedRow: any) => {
const nodeIds = getTreeNodeAndChildrenIds(
tableData.value,
selectedRow[idKey],
idKey,
childrenKey
)
allIds.push(...nodeIds)
})
ids = allIds
}
props.crudSchema.crudApi
?.remove?.({
[`${rowKey}s`]: ids
})
.then(() => {
ElMessage.success(`删除${props.crudSchema.dialogTitle}成功`)
if (crudSchemaRef.value.useVxeTable === true) {
props.crudMethods?.refreshRemove(ids)
} else {
tableObject.refreshRemove()
}
})
}
const handleToolbarToolClick = (key: any) => {
emit('toolbarToolClick', key)
}
// 定义一些只读状态
const readonlyState = reactive<CrudReadonlyState>({
selection: []
})
const multipleSelection = ref<any>([])
const handleSelectionChange = (selection: any) => {
multipleSelection.value = selection
readonlyState.selection = selection
}
const crudInstanceMethods: CrudInstance = {
async reload(params) {
const values = await searchBarApi.getValues()
if (crudSchemaRef.value.useVxeTable === true) {
return await gridRef.value?.commitProxy('query', { ...values, ...params })
}
return getData({
...values,
...params
})
},
// 设置组件状态(props)
setState(state: CrudSchema) {
return new Promise((resolve) => {
crudSchemaRef.value = {
...crudSchemaRef.value,
...state
}
nextTick(() => {
resolve()
})
})
},
getState(): Promise<CrudSchema> {
return new Promise((resolve) => {
resolve(crudSchemaRef.value)
})
},
getReadonlyState() {
return readonlyState
},
getArtTableInstance() {
return tableObject
},
async exportData() {
await handleExport()
},
getVxeTableInstance() {
return gridRef.value
},
getRowSelection() {
return multipleSelection.value
},
removeByApi(row) {
handleDelete(row)
},
openAddDialog() {
dialogFormRef.value?.show({
data: {},
type: 'add',
getCrudApi() {
return props.crudMethods
}
})
},
openEditDialog() {
dialogFormRef.value?.show({
data: actionItemRecord.value,
type: 'edit',
getCrudApi() {
return props.crudMethods
}
})
},
openViewDialog() {
dialogFormRef.value?.show({
data: {},
type: 'view',
getCrudApi() {
return props.crudMethods
}
})
},
executeRemove() {
handleDelete(actionItemRecord.value)
},
executeBatchRemove() {
handleDelete({})
}
}
onMounted(() => {
emit('register', crudInstanceMethods)
})
// vxe-table部分逻辑
const gridRef = ref()
const vxeTableColumns = computed<any>(() => {
if (crudSchemaRef.value?.useVxeTable === true) {
const { columns } = useColumns(
crudSchemaRef,
tableActions,
dropDownActions,
handleActionItemClick
)
return columns
}
return []
})
const handleCheckboxChange = () => {
const records = gridRef.value.getCheckboxRecords()
multipleSelection.value = records
readonlyState.selection = records
}
const handleCheckboxAll = () => {
const records = gridRef.value.getCheckboxRecords()
multipleSelection.value = records
readonlyState.selection = records
}
</script>
<style scoped>
@reference '@styles/core/tailwind.css';
.button {
@apply ml-2
size-8
flex
items-center
justify-center
cursor-pointer
rounded-md
bg-g-300/55
dark:bg-g-300/40
text-g-700
hover:bg-g-300
md:ml-0
md:mr-2.5;
}
</style>
@@ -0,0 +1,151 @@
<template>
<Dialog>
<DetailForm v-if="viewType == 'view'" :model="record"> </DetailForm>
<BasicForm v-else :model="formModel" />
</Dialog>
</template>
<script setup lang="ts">
import { ref, computed, reactive, nextTick } from 'vue'
import { useDetailForm, useForm } from '@/components/form'
import { useDialog } from '@/components/dialog'
import { ElMessage } from 'element-plus'
import { CrudMethods, CrudSchema } from '@/components/crud'
const props = defineProps<{
crudSchema: CrudSchema
}>()
const record = ref<any>({})
const updateFlag = ref(false)
const formModel = reactive<any>({})
const crudApi = ref<CrudMethods>()
const viewType = ref('view')
const [BasicForm, formApi] = useForm(props.crudSchema.formSchema || {})
const [DetailForm] = useDetailForm(props.crudSchema.formSchema || {})
const dialogTitle = computed(() => {
return props.crudSchema.dialogTitle || '标题'
})
const [Dialog, dialogApi] = useDialog({
title: dialogTitle.value,
...props.crudSchema.dialogProps,
destroyOnClose: true,
draggable: true,
modal: true,
onConfirm: () => {
handleSubmit()
}
})
const show = ({
data,
getCrudApi,
type = 'view'
}: {
data: any
getCrudApi: () => any
type?: 'add' | 'edit' | 'view'
}) => {
viewType.value = type
dialogApi.setState(props.crudSchema.dialogProps || {})
if (type === 'view') {
dialogApi.setState({
footer: false,
closeOnClickModal: true
})
} else {
dialogApi.setState({
footer: true,
closeOnClickModal: false
})
}
updateFlag.value = type === 'edit'
record.value = data || {}
crudApi.value = getCrudApi()
dialogApi.open()
if (type === 'view' || type === 'edit') {
const detailApi = props.crudSchema.crudApi?.view
const rowKey = props.crudSchema.rowKey || 'id'
detailApi?.(record.value[rowKey]).then((res) => {
record.value = {
...record.value,
...res
}
nextTick(() => {
formApi.setValues(record.value)
})
})
} else {
nextTick(() => {
formApi.setValues(record.value)
})
}
const viewButtonText = props.crudSchema.viewButtonText || '详情'
const editButtonText = props.crudSchema.editButtonText || '编辑'
const addButtonText = props.crudSchema.addButtonText || '新增'
let title = `${dialogTitle.value}${viewButtonText}`
if (type === 'add') {
title = `${addButtonText}${dialogTitle.value}`
// 这里处理一下树型结构
if (props.crudSchema.tree) {
const idKey = props.crudSchema.tree.id || 'id'
const pidKey = props.crudSchema.tree.pid || 'parentId'
record.value = {
[pidKey]: record.value[idKey]
}
}
} else if (type === 'edit') {
title = `${editButtonText}${dialogTitle.value}`
}
dialogApi.setState({
title: title
})
}
const handleSubmit = () => {
dialogApi.setState({ loading: true, confirmLoading: true })
formApi
.validate()
.then(async () => {
const values = await formApi.getValues()
// 转换表单值
let submitValues = values
if (props.crudSchema.transformFormValues) {
const type = updateFlag.value ? 'edit' : 'add'
submitValues = props.crudSchema.transformFormValues(values, type)
}
if (updateFlag.value) {
const rowKey = props.crudSchema.rowKey || 'id'
const recordId = record.value?.[rowKey]
if (recordId !== undefined && recordId !== null && recordId !== '') {
submitValues = { ...submitValues, [rowKey]: recordId }
}
}
const submitApi = updateFlag.value
? props.crudSchema.crudApi?.edit
: props.crudSchema.crudApi?.add
submitApi?.(submitValues).then(() => {
const editButtonText = props.crudSchema.editButtonText || '编辑'
const addButtonText = props.crudSchema.addButtonText || '新增'
ElMessage.success(
updateFlag.value
? `${editButtonText}${dialogTitle.value}成功`
: `${addButtonText}${dialogTitle.value}成功`
)
dialogApi.close()
if (updateFlag.value) {
crudApi.value?.refreshUpdate()
} else {
crudApi.value?.refreshCreate()
}
})
})
.finally(() => {
dialogApi.setState({ loading: false, confirmLoading: false })
})
}
defineExpose({
show
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,3 @@
export { default as BasicTable } from './crud.vue'
export { useCrud } from './use-crud'
export type * from './types'
+219
View File
@@ -0,0 +1,219 @@
import { MyDialogProps } from '../dialog'
import { FormSchema } from '../form'
import { ActionItem } from '../table-action'
import type { VxeColumnProps } from 'vxe-table'
export interface CrudSchema {
// 行id
rowKey?: string
// 请求接口
api?: ((params: any) => Promise<any>) | string
// 请求方法
requestMethod?: 'get' | 'post'
// 请求之前对参数进行处理
beforeFetch?: (params: any) => any
// 请求之后对返回值进行处理
afterFetch?: (res: any) => any
// 是否立即加载数据
immediate?: boolean
data?: any
// 树型配置
tree?: {
// 树形id字段名
id?: string
// 树形pid字段名
pid?: string
// 树形children字段名
children?: string
}
// 右侧工具栏
toolbarConfig?: {
// 是否显示刷新按钮
refresh?: boolean
// 是否显示大小按钮
size?: boolean
// 是否显示全屏按钮
fullscreen?: boolean
// 是否显示列设置按钮
columns?: boolean
// 是否显示列设置按钮
settings?: boolean
// 是否显示导出按钮
export?: boolean
// 表格右上工具栏,在左侧追加
tools?: ActionItem[]
}
// 列定义
columns?: CrudColumn[] | (() => CrudColumn[])
// 是否显示搜索表单
useSearchForm?: boolean
// 搜索表单配置
searchFormSchema?: FormSchema | (() => FormSchema)
// crud表单配置
formSchema?: FormSchema | (() => FormSchema)
// 操作列-按钮定义
tableActions?: ActionItem[]
// 操作列-下拉菜单定义
dropDownActions?: ActionItem[]
// 操作列配置
tableActionColumn?: CrudColumn
// 左侧栏工具配置
toolbarActions?: ActionItem[]
// 左侧栏-下拉菜单定义
dropDownToolbarActions?: ActionItem[]
// 内置弹窗配置
dialogProps?: MyDialogProps
// 增删改查接口
crudApi?: CrudApi
// 是否使用crud
useCrud?: boolean
// 模块名
dialogTitle?: string
// 是否有新增
hasAdd?: boolean
// 新增按钮文本
addButtonText?: string
// 新增按钮权限编码
addAuth?: string
// 是否有编辑
hasEdit?: boolean
// 编辑按钮文本
editButtonText?: string
// 编辑按钮权限编码
editAuth?: string
// 是否有详情
hasView?: boolean
// 详情按钮文本
viewButtonText?: string
// 详情按钮权限编码
viewAuth?: string
// 是否有删除
hasRemove?: boolean
// 删除按钮文本
removeButtonText?: string
// 删除按钮权限编码
removeAuth?: string
// 是否启用默认导出
hasExport?: boolean
// 导出按钮权限编码
exportAuth?: string
// 导出文件名
exportFileName?: string
// 自定义导出字段
exportFields?: Array<{
field: string
title: string
format?: Record<string, any>
}>
// 转换表单值的函数,可用于添加隐藏字段、格式化数据等
transformFormValues?: (values: Record<string, any>, type: 'add' | 'edit') => Record<string, any>
// 使用vxe-table
useVxeTable?: boolean
}
// 表格列配置接口
export interface CrudColumn<T = any> {
// 列类型
type?: 'selection' | 'expand' | 'index' | 'globalIndex'
// 列属性名
prop?: string
// 列标题
label?: string
// 列宽度
width?: string | number
// 最小列宽度
minWidth?: string | number
// 固定列
fixed?: boolean | 'left' | 'right'
// 是否可排序
sortable?: boolean
// 过滤器选项
filters?: any[]
// 过滤方法
filterMethod?: (value: any, row: any) => boolean
// 过滤器位置
filterPlacement?: string
// 是否禁用
disabled?: boolean
// 是否选中显示(可用于隐藏列)
checked?: boolean
// 自定义渲染函数
formatter?: (row: T) => any
// 插槽相关配置
// 是否使用插槽渲染内容
useSlot?: boolean
// 插槽名称(默认为 prop 值)
slotName?: string
// 是否使用表头插槽
useHeaderSlot?: boolean
// 表头插槽名称(默认为 `${prop}-header`)
headerSlotName?: string
// 其他属性
[key: string]: any
// 列回显组件
component?: string
// 列回显组件属性
componentProps?: Record<string, any>
// vxe-table列定义
vxeColumn?: VxeColumnProps
}
export interface CrudApi {
add?: (params: any) => Promise<any>
edit?: (params: any) => Promise<any>
remove?: (params: any) => Promise<any>
view?: (params: any) => Promise<any>
export?: (params: any) => Promise<any>
}
// 只读配置
export interface CrudReadonlyState {
selection: []
}
// 表格基础方法,和tableRef和tableApi相同的方法
export interface CrudBaseMethods {
// 重新加载数据
reload: (params?: Record<string, any>) => Promise<any>
// 设置组件状态(props)
setState: (state: CrudSchema) => Promise<void>
// 获取组件状态(props)
getState: () => Promise<CrudSchema>
// 获取勾选行信息
getRowSelection: () => []
// 获取只读状态信息
getReadonlyState: () => CrudReadonlyState
// 获取art table实例
getArtTableInstance: () => any
// 执行默认导出
exportData: () => Promise<void>
// 获取vxe table实例
getVxeTableInstance: () => any
// 通过接口删除行
removeByApi: (row: any) => void
// 打开内置新增表单
openAddDialog: () => void
// 打开内置修改表单
openEditDialog: () => void
// 打开内置详情表单
openViewDialog: () => void
// 执行内置的删除方法-行删除
executeRemove: () => void
// 执行内置的批量删除方法
executeBatchRemove: () => void
}
export type CrudInstance = CrudBaseMethods
// 定义表格方法接口,提供更好的类型提示
export interface CrudMethods extends CrudBaseMethods {
// 获取crud实例
getCrudRef(): any
// 获取art表格实例,可通过该方法来操作原生的art-table组件
getArtTableInstance(): any
// 刷新策略
/** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */
refreshData(): void
/** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */
refreshSoft(): void
/** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */
refreshCreate(): void
/** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */
refreshUpdate(): void
/** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */
refreshRemove(): void
}
@@ -0,0 +1,133 @@
import { ActionItem } from '../table-action'
import { CrudMethods, CrudSchema } from './types'
export function useAction(
crudSchema: CrudSchema,
dialogFormRef: Ref<any>,
crudMethods: CrudMethods
) {
// 左侧栏工具,默认有新增按钮
const toolbarActions = computed(() => {
const actions: ActionItem[] = []
if (crudSchema.useCrud === true && crudSchema.hasAdd !== false) {
actions.push({
label: crudSchema.addButtonText || '新增',
type: 'primary',
icon: 'ant-design:plus-outlined',
auth: crudSchema.addAuth,
dialogRef: dialogFormRef,
dialogParams: {
isUpdate: false,
getCrudApi: () => crudMethods,
type: 'add'
}
})
}
actions.push(...(crudSchema.toolbarActions || []))
return actions
})
// 左侧栏工具-下拉菜单,默认有删除按钮
const dropDownToolbarActions = computed(() => {
const actions: ActionItem[] = []
if (crudSchema.useCrud === true && crudSchema.hasRemove !== false) {
actions.push({
label: '批量删除',
type: 'danger',
icon: 'ant-design:delete-outlined',
auth: crudSchema.removeAuth,
ifShow: () => crudMethods.getReadonlyState().selection.length > 0,
confirm: {
title: '提示',
content: '确定批量删除吗?',
ok() {
crudMethods.removeByApi({})
}
}
})
}
actions.push(...(crudSchema.dropDownToolbarActions || []))
return actions
})
// 行操作-默认有详情、编辑
const tableActions = computed(() => {
const actions: ActionItem[] = []
if (crudSchema.useCrud === true && crudSchema.hasView !== false) {
actions.push({
label: crudSchema.viewButtonText || '详情',
type: 'primary',
link: true,
icon: 'ant-design:eye-outlined',
auth: crudSchema.viewAuth,
dialogRef: dialogFormRef,
dialogParams() {
return {
getCrudApi: () => crudMethods
}
}
})
}
if (crudSchema.useCrud === true && crudSchema.hasEdit !== false) {
actions.push({
label: crudSchema.editButtonText || '编辑',
type: 'primary',
link: true,
icon: 'ant-design:edit-outlined',
auth: crudSchema.editAuth,
dialogRef: dialogFormRef,
dialogParams: {
isUpdate: true,
type: 'edit',
getCrudApi: () => crudMethods
}
})
}
actions.push(...(crudSchema.tableActions || []))
return actions
})
// 行操作-下拉菜单默认有删除按钮,树形表格默认添加子节点按钮
const dropDownActions = computed(() => {
const actions: ActionItem[] = []
if (crudSchema.useCrud === true && crudSchema.hasAdd !== false) {
// 判断一下是否则为树结构
if (crudSchema.tree) {
actions.push({
label: (crudSchema.addButtonText || '新增') + '子节点',
icon: 'ant-design:plus-outlined',
size: 'small',
auth: crudSchema.addAuth,
dialogRef: dialogFormRef,
dialogParams: {
isUpdate: true,
type: 'add',
tree: crudSchema.tree,
getCrudApi: () => crudMethods
}
})
}
}
if (crudSchema.useCrud === true && crudSchema.hasRemove !== false) {
actions.push({
label: crudSchema.removeButtonText || '删除',
type: 'danger',
icon: 'ant-design:delete-outlined',
size: 'small',
auth: crudSchema.removeAuth,
confirm: {
title: '提示',
content: '确定删除吗?',
ok(record: any) {
crudMethods.removeByApi(record)
}
}
})
}
actions.push(...(crudSchema.dropDownActions || []))
return actions
})
return {
toolbarActions,
dropDownToolbarActions,
tableActions,
dropDownActions
}
}
@@ -0,0 +1,101 @@
import { ActionItem, TableAction } from '../table-action'
import { viewColumnFormatter } from '../view/component-map'
import { CrudSchema } from './types'
export function useColumns(
crudSchemaRef: Ref<CrudSchema>,
tableActions: Ref<ActionItem[]>,
dropDownActions: Ref<ActionItem[]>,
handleActionItemClick: (record: any) => void
) {
if (crudSchemaRef.value?.useVxeTable) {
const columns =
(typeof crudSchemaRef.value.columns === 'function'
? crudSchemaRef.value.columns()
: crudSchemaRef.value.columns) || []
const vxeTableColumns = columns.map((column) => {
return {
type: column.type === 'selection' ? 'checkbox' : column.type,
field: column.prop,
title: column.label,
align: column.align,
width: column.type === 'selection' ? 60 : column.width,
showOverflow: 'tooltip',
cellRender: {
name: column.component,
props: column.componentProps
},
...column.vxeColumn
}
})
// 有操作列配置才添加操作列
if (
(tableActions.value && tableActions.value.length > 0) ||
(dropDownActions.value && dropDownActions.value.length > 0)
) {
// @ts-nocheck 如果想忽略整个文件检查可使用此注释
vxeTableColumns.push({
type: 'default',
field: '__ACTION__',
title: '操作',
width: 220,
align: 'center',
showOverflow: 'tooltip', // 显式定义以避免类型错误
slots: {
default: ({ row }: { row: any }) => {
return (
<TableAction
onActionItemClick={handleActionItemClick}
record={row}
actions={tableActions.value}
dropDownActions={dropDownActions.value}
/>
)
}
},
...crudSchemaRef.value.tableActionColumn?.vxeColumn
} as any)
}
return { columns: vxeTableColumns }
}
const columns = (
(typeof crudSchemaRef.value.columns === 'function'
? crudSchemaRef.value.columns()
: crudSchemaRef.value.columns) || []
).map((column) => {
const c = { ...column }
viewColumnFormatter(column)
c.formatter = column.formatter
delete c.component
delete c.componentProps
return c
})
// 有操作列配置才添加操作列
if (
(tableActions.value && tableActions.value.length > 0) ||
(dropDownActions.value && dropDownActions.value.length > 0)
) {
columns.push({
prop: '__ACTION__',
label: '操作',
width: 220,
align: 'center',
fixed: 'right', // 固定列
formatter: (row: unknown) => {
return (
<TableAction
onActionItemClick={handleActionItemClick}
record={row}
actions={tableActions.value}
dropDownActions={dropDownActions.value}
/>
)
},
...crudSchemaRef.value.tableActionColumn
})
}
return {
columns
}
}
@@ -0,0 +1,150 @@
import { CrudMethods, CrudSchema } from '.'
import Crud from './crud.vue'
/**
*
* 防止未来重构影响
* 通过统一的 useCrud 钩子封装,即使内部实现发生重构,外部调用方也不会受到影响
* 提供稳定的API接口,隔离底层实现细节的变化
* @param crudSchema
* @returns
*/
export function useCrud(crudSchema: CrudSchema) {
const crudRef = ref<any>(null)
const register = (crudInstance: any) => {
crudRef.value = crudInstance
}
const crudMethods: CrudMethods = {
// 获取表格实例
getCrudRef() {
return crudRef.value
},
// 设置组件状态(props)
setState(state: any) {
return crudRef.value?.setState(state)
},
// 获取组件状态
getState() {
return crudRef.value?.getState()
},
getRowSelection() {
return crudRef.value?.getRowSelection()
},
// 刷新表格数据,保留当前表单搜索条件
reload(params?: Record<string, any>) {
return crudRef.value?.reload(params)
},
// 获取只读状态信息
getReadonlyState() {
return crudRef.value?.getReadonlyState() || { selection: [] }
},
// 通过接口删除行
removeByApi(row) {
return crudRef.value?.removeByApi(row)
},
// 获取art表格实例,可通过该方法来操作原生的art-table组件
// 个人建议,可以在该文件上包装一层再暴露给外部使用,如上述的reload方法一样
getArtTableInstance() {
return crudRef.value?.getArtTableInstance()
},
exportData() {
return crudRef.value?.exportData()
},
getVxeTableInstance() {
return crudRef.value?.getVxeTableInstance()
},
// 刷新策略
/** 全量刷新:清空所有缓存,重新获取数据(适用于手动刷新按钮) */
refreshData() {
if (crudSchema.useVxeTable) {
return this.getVxeTableInstance()?.commitProxy('reload', {})
}
return this.getArtTableInstance()?.refreshData()
},
/** 轻量刷新:仅清空当前搜索条件的缓存,保持分页状态(适用于定时刷新) */
refreshSoft() {
if (crudSchema.useVxeTable) {
return this.getVxeTableInstance()?.commitProxy('query', {})
}
return this.getArtTableInstance()?.refreshSoft()
},
/** 新增后刷新:回到第一页并清空分页缓存(适用于新增数据后) */
refreshCreate() {
if (crudSchema.useVxeTable) {
return this.getVxeTableInstance()?.commitProxy('reload', {})
}
return this.getArtTableInstance()?.refreshCreate()
},
/** 更新后刷新:保持当前页,仅清空当前搜索缓存(适用于更新数据后) */
refreshUpdate() {
if (crudSchema.useVxeTable) {
return this.getVxeTableInstance()?.commitProxy('query', {})
}
return this.getArtTableInstance()?.refreshUpdate()
},
/** 删除后刷新:智能处理页码,避免空页面(适用于删除数据后) */
refreshRemove() {
if (crudSchema.useVxeTable) {
const gridApi = this.getVxeTableInstance()
const records = gridApi.getCheckboxRecords()
if (
gridApi.getTableData()?.fullData?.length === 1 ||
gridApi.getTableData()?.fullData?.length === records.length
) {
const { pager }: any = gridApi.getProxyInfo()
if (pager.currentPage > 1) {
pager.currentPage = pager.currentPage - 1
return this.getVxeTableInstance()?.commitProxy('query', {})
} else {
if (pager.total >= gridApi.getTableData()?.fullData?.length) {
return this.getVxeTableInstance()?.commitProxy('query', {})
} else {
return this.getVxeTableInstance()?.commitProxy('reload', {})
}
}
} else {
return this.getVxeTableInstance()?.commitProxy('query', {})
}
}
return this.getArtTableInstance()?.refreshRemove()
},
// 打开内置新增表单
openAddDialog() {
return this.getCrudRef()?.openAddDialog()
},
// 打开内置修改表单
openEditDialog() {
return this.getCrudRef()?.openEditDialog()
},
// 打开内置详情表单
openViewDialog() {
return this.getCrudRef()?.openViewDialog()
},
// 执行内置的删除方法
executeRemove() {
return this.getCrudRef()?.executeRemove()
},
// 执行内置的批量删除方法
executeBatchRemove() {
return this.getCrudRef()?.executeBatchRemove()
}
}
// 创建表单组件的配置
const CrudComponent = defineComponent({
setup(_, { attrs, slots }) {
return () =>
h(
Crud,
{
...attrs,
crudSchema: crudSchema,
onRegister: register,
crudMethods: crudMethods
},
slots
)
}
})
return [CrudComponent, crudMethods] as const
}