first commiit
This commit is contained in:
976
src/components/CURD/PageContent.vue
Normal file
976
src/components/CURD/PageContent.vue
Normal file
@@ -0,0 +1,976 @@
|
||||
<template>
|
||||
<el-card shadow="never">
|
||||
<!-- 表格工具栏 -->
|
||||
<div class="flex-x-between mb-[10px]">
|
||||
<!-- 左侧工具栏 -->
|
||||
<div>
|
||||
<template v-for="item in toolbar" :key="item">
|
||||
<template v-if="typeof item === 'string'">
|
||||
<!-- 新增 -->
|
||||
<template v-if="item === 'add'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleToolbar(item)"
|
||||
>
|
||||
新增
|
||||
</el-button>
|
||||
</template>
|
||||
<!-- 删除 -->
|
||||
<template v-else-if="item === 'delete'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="removeIds.length === 0"
|
||||
@click="handleToolbar(item)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<!-- 导入 -->
|
||||
<template v-else-if="item === 'import'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
|
||||
type="default"
|
||||
icon="upload"
|
||||
@click="handleToolbar(item)"
|
||||
>
|
||||
导入
|
||||
</el-button>
|
||||
</template>
|
||||
<!-- 导出 -->
|
||||
<template v-else-if="item === 'export'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
|
||||
type="default"
|
||||
icon="download"
|
||||
@click="handleToolbar(item)"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 其他 -->
|
||||
<template v-else-if="typeof item === 'object'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
|
||||
:icon="item.icon"
|
||||
:type="item.type ?? 'default'"
|
||||
@click="handleToolbar(item.name)"
|
||||
>
|
||||
{{ item.text }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 右侧工具栏 -->
|
||||
<div>
|
||||
<template v-for="item in defaultToolbar" :key="item">
|
||||
<template v-if="typeof item === 'string'">
|
||||
<!-- 刷新 -->
|
||||
<template v-if="item === 'refresh'">
|
||||
<el-button icon="refresh" circle title="刷新" @click="handleToolbar(item)" />
|
||||
</template>
|
||||
<!-- 筛选列 -->
|
||||
<template v-else-if="item === 'filter'">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button icon="Operation" circle title="筛选列" />
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="col in cols" :key="col">
|
||||
<el-checkbox v-if="col.prop" v-model="col.show" :label="col.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</template>
|
||||
<!-- 导出 -->
|
||||
<template v-else-if="item === 'exports'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:export`]"
|
||||
icon="download"
|
||||
circle
|
||||
title="导出"
|
||||
@click="handleToolbar(item)"
|
||||
/>
|
||||
</template>
|
||||
<!-- 导入 -->
|
||||
<template v-else-if="item === 'imports'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:import`]"
|
||||
icon="upload"
|
||||
circle
|
||||
title="导入"
|
||||
@click="handleToolbar(item)"
|
||||
/>
|
||||
</template>
|
||||
<!-- 搜索 -->
|
||||
<template v-else-if="item === 'search'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:query`]"
|
||||
icon="search"
|
||||
circle
|
||||
title="搜索"
|
||||
@click="handleToolbar(item)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 其他 -->
|
||||
<template v-else-if="typeof item === 'object'">
|
||||
<template v-if="item.auth">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
|
||||
:icon="item.icon"
|
||||
circle
|
||||
:title="item.title"
|
||||
@click="handleToolbar(item.name)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
:icon="item.icon"
|
||||
circle
|
||||
:title="item.title"
|
||||
@click="handleToolbar(item.name)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
v-bind="contentConfig.table"
|
||||
:data="pageData"
|
||||
:row-key="pk"
|
||||
@selection-change="handleSelectionChange"
|
||||
@filter-change="handleFilterChange"
|
||||
>
|
||||
<template v-for="col in cols" :key="col">
|
||||
<el-table-column v-if="col.show" v-bind="col">
|
||||
<template #default="scope">
|
||||
<!-- 显示图片 -->
|
||||
<template v-if="col.templet === 'image'">
|
||||
<template v-if="col.prop">
|
||||
<template v-if="Array.isArray(scope.row[col.prop])">
|
||||
<template v-for="(item, index) in scope.row[col.prop]" :key="item">
|
||||
<el-image
|
||||
:src="item"
|
||||
:preview-src-list="scope.row[col.prop]"
|
||||
:initial-index="index"
|
||||
:preview-teleported="true"
|
||||
:style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-image
|
||||
:src="scope.row[col.prop]"
|
||||
:preview-src-list="[scope.row[col.prop]]"
|
||||
:preview-teleported="true"
|
||||
:style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 根据行的selectList属性返回对应列表值 -->
|
||||
<template v-else-if="col.templet === 'list'">
|
||||
<template v-if="col.prop">
|
||||
{{ (col.selectList ?? {})[scope.row[col.prop]] }}
|
||||
</template>
|
||||
</template>
|
||||
<!-- 格式化显示链接 -->
|
||||
<template v-else-if="col.templet === 'url'">
|
||||
<template v-if="col.prop">
|
||||
<el-link type="primary" :href="scope.row[col.prop]" target="_blank">
|
||||
{{ scope.row[col.prop] }}
|
||||
</el-link>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 生成开关组件 -->
|
||||
<template v-else-if="col.templet === 'switch'">
|
||||
<template v-if="col.prop">
|
||||
<!-- pageData.length>0: 解决el-switch组件会在表格初始化的时候触发一次change事件 -->
|
||||
<el-switch
|
||||
v-model="scope.row[col.prop]"
|
||||
:active-value="col.activeValue ?? 1"
|
||||
:inactive-value="col.inactiveValue ?? 0"
|
||||
:inline-prompt="true"
|
||||
:active-text="col.activeText ?? ''"
|
||||
:inactive-text="col.inactiveText ?? ''"
|
||||
:validate-event="false"
|
||||
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)"
|
||||
@change="
|
||||
pageData.length > 0 && handleModify(col.prop, scope.row[col.prop], scope.row)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 生成输入框组件 -->
|
||||
<template v-else-if="col.templet === 'input'">
|
||||
<template v-if="col.prop">
|
||||
<el-input
|
||||
v-model="scope.row[col.prop]"
|
||||
:type="col.inputType ?? 'text'"
|
||||
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)"
|
||||
@blur="handleModify(col.prop, scope.row[col.prop], scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 格式化为价格 -->
|
||||
<template v-else-if="col.templet === 'price'">
|
||||
<template v-if="col.prop">
|
||||
{{ `${col.priceFormat ?? "¥"}${scope.row[col.prop]}` }}
|
||||
</template>
|
||||
</template>
|
||||
<!-- 格式化为百分比 -->
|
||||
<template v-else-if="col.templet === 'percent'">
|
||||
<template v-if="col.prop">{{ scope.row[col.prop] }}%</template>
|
||||
</template>
|
||||
<!-- 显示图标 -->
|
||||
<template v-else-if="col.templet === 'icon'">
|
||||
<template v-if="col.prop">
|
||||
<template v-if="scope.row[col.prop].startsWith('el-icon-')">
|
||||
<el-icon>
|
||||
<component :is="scope.row[col.prop].replace('el-icon-', '')" />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
<svg-icon :icon-class="scope.row[col.prop]" />
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 格式化时间 -->
|
||||
<template v-else-if="col.templet === 'date'">
|
||||
<template v-if="col.prop">
|
||||
{{
|
||||
scope.row[col.prop]
|
||||
? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss")
|
||||
.value
|
||||
: ""
|
||||
}}
|
||||
</template>
|
||||
</template>
|
||||
<!-- 列操作栏 -->
|
||||
<template v-else-if="col.templet === 'tool'">
|
||||
<template v-for="item in col.operat ?? ['edit', 'delete']" :key="item">
|
||||
<template v-if="typeof item === 'string'">
|
||||
<!-- 编辑/删除 -->
|
||||
<template v-if="item === 'edit' || item === 'delete'">
|
||||
<el-button
|
||||
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
|
||||
:type="item === 'edit' ? 'primary' : 'danger'"
|
||||
:icon="item"
|
||||
size="small"
|
||||
link
|
||||
@click="
|
||||
handleOperat({
|
||||
name: item,
|
||||
row: scope.row,
|
||||
column: scope.column,
|
||||
$index: scope.$index,
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ item === "edit" ? "编辑" : "删除" }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 其他 -->
|
||||
<template v-else-if="typeof item === 'object'">
|
||||
<el-button
|
||||
v-if="item.render === undefined || item.render(scope.row)"
|
||||
v-bind="
|
||||
item.auth ? { 'v-hasPerm': [`${contentConfig.pageName}:${item.auth}`] } : {}
|
||||
"
|
||||
:icon="item.icon"
|
||||
:type="item.type ?? 'primary'"
|
||||
size="small"
|
||||
link
|
||||
@click="
|
||||
handleOperat({
|
||||
name: item.name,
|
||||
row: scope.row,
|
||||
column: scope.column,
|
||||
$index: scope.$index,
|
||||
})
|
||||
"
|
||||
>
|
||||
{{ item.text }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 自定义 -->
|
||||
<template v-else-if="col.templet === 'custom'">
|
||||
<slot :name="col.slotName ?? col.prop" :prop="col.prop" v-bind="scope" />
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<template v-if="showPagination">
|
||||
<el-scrollbar>
|
||||
<div class="mt-[12px]">
|
||||
<el-pagination
|
||||
v-bind="pagination"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
<!-- 导出弹窗 -->
|
||||
<el-dialog
|
||||
v-model="exportsModalVisible"
|
||||
:align-center="true"
|
||||
title="导出数据"
|
||||
width="600px"
|
||||
style="padding-right: 0"
|
||||
@close="handleCloseExportsModal"
|
||||
>
|
||||
<!-- 滚动 -->
|
||||
<el-scrollbar max-height="60vh">
|
||||
<!-- 表单 -->
|
||||
<el-form
|
||||
ref="exportsFormRef"
|
||||
label-width="auto"
|
||||
style="padding-right: var(--el-dialog-padding-primary)"
|
||||
:model="exportsFormData"
|
||||
:rules="exportsFormRules"
|
||||
>
|
||||
<el-form-item label="文件名" prop="filename">
|
||||
<el-input v-model="exportsFormData.filename" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="工作表名" prop="sheetname">
|
||||
<el-input v-model="exportsFormData.sheetname" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据源" prop="origin">
|
||||
<el-select v-model="exportsFormData.origin">
|
||||
<el-option label="当前数据 (当前页的数据)" :value="ExportsOriginEnum.CURRENT" />
|
||||
<el-option
|
||||
label="选中数据 (所有选中的数据)"
|
||||
:value="ExportsOriginEnum.SELECTED"
|
||||
:disabled="selectionData.length <= 0"
|
||||
/>
|
||||
<el-option
|
||||
label="全量数据 (所有分页的数据)"
|
||||
:value="ExportsOriginEnum.REMOTE"
|
||||
:disabled="contentConfig.exportsAction === undefined"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字段" prop="fields">
|
||||
<el-checkbox-group v-model="exportsFormData.fields">
|
||||
<template v-for="col in cols" :key="col">
|
||||
<el-checkbox v-if="col.prop" :value="col.prop" :label="col.label" />
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
<!-- 弹窗底部操作按钮 -->
|
||||
<template #footer>
|
||||
<div style="padding-right: var(--el-dialog-padding-primary)">
|
||||
<el-button type="primary" @click="handleExportsSubmit">确 定</el-button>
|
||||
<el-button @click="handleCloseExportsModal">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 导入弹窗 -->
|
||||
<el-dialog
|
||||
v-model="importModalVisible"
|
||||
:align-center="true"
|
||||
title="导入数据"
|
||||
width="600px"
|
||||
style="padding-right: 0"
|
||||
@close="handleCloseImportModal"
|
||||
>
|
||||
<!-- 滚动 -->
|
||||
<el-scrollbar max-height="60vh">
|
||||
<!-- 表单 -->
|
||||
<el-form
|
||||
ref="importFormRef"
|
||||
label-width="auto"
|
||||
style="padding-right: var(--el-dialog-padding-primary)"
|
||||
:model="importFormData"
|
||||
:rules="importFormRules"
|
||||
>
|
||||
<el-form-item label="文件名" prop="files">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="importFormData.files"
|
||||
class="w-full"
|
||||
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
:drag="true"
|
||||
:limit="1"
|
||||
:auto-upload="false"
|
||||
:on-exceed="handleFileExceed"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
<span>将文件拖到此处,或</span>
|
||||
<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
*.xlsx / *.xls
|
||||
<el-link
|
||||
v-if="contentConfig.importTemplate"
|
||||
type="primary"
|
||||
icon="download"
|
||||
:underline="false"
|
||||
@click="handleDownloadTemplate"
|
||||
>
|
||||
下载模板
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
<!-- 弹窗底部操作按钮 -->
|
||||
<template #footer>
|
||||
<div style="padding-right: var(--el-dialog-padding-primary)">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="importFormData.files.length === 0"
|
||||
@click="handleImportSubmit"
|
||||
>
|
||||
确 定
|
||||
</el-button>
|
||||
<el-button @click="handleCloseImportModal">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import { hasAuth } from "@/plugins/permission";
|
||||
import { useDateFormat, useThrottleFn } from "@vueuse/core";
|
||||
import {
|
||||
genFileId,
|
||||
type FormInstance,
|
||||
type FormRules,
|
||||
type UploadInstance,
|
||||
type UploadRawFile,
|
||||
type UploadUserFile,
|
||||
type TableInstance,
|
||||
} from "element-plus";
|
||||
import ExcelJS from "exceljs";
|
||||
import { reactive, ref } from "vue";
|
||||
import type { IContentConfig, IObject, IOperatData } from "./types";
|
||||
|
||||
// 定义接收的属性
|
||||
const props = defineProps<{
|
||||
contentConfig: IContentConfig;
|
||||
}>();
|
||||
// 定义自定义事件
|
||||
const emit = defineEmits<{
|
||||
addClick: [];
|
||||
exportClick: [];
|
||||
searchClick: [];
|
||||
toolbarClick: [name: string];
|
||||
editClick: [row: IObject];
|
||||
operatClick: [data: IOperatData];
|
||||
filterChange: [data: IObject];
|
||||
}>();
|
||||
|
||||
// 主键
|
||||
const pk = props.contentConfig.pk ?? "id";
|
||||
// 表格左侧工具栏
|
||||
const toolbar = props.contentConfig.toolbar ?? ["add", "delete"];
|
||||
// 表格右侧工具栏
|
||||
const defaultToolbar = props.contentConfig.defaultToolbar ?? ["refresh", "filter"];
|
||||
// 表格列
|
||||
const cols = ref(
|
||||
props.contentConfig.cols.map((col) => {
|
||||
col.initFn && col.initFn(col);
|
||||
if (col.show === undefined) {
|
||||
col.show = true;
|
||||
}
|
||||
if (col.prop !== undefined && col.columnKey === undefined && col["column-key"] === undefined) {
|
||||
col.columnKey = col.prop;
|
||||
}
|
||||
if (
|
||||
col.type === "selection" &&
|
||||
col.reserveSelection === undefined &&
|
||||
col["reserve-selection"] === undefined
|
||||
) {
|
||||
// 配合表格row-key实现跨页多选
|
||||
col.reserveSelection = true;
|
||||
}
|
||||
return col;
|
||||
})
|
||||
);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
// 列表数据
|
||||
const pageData = ref<IObject[]>([]);
|
||||
// 显示分页
|
||||
const showPagination = props.contentConfig.pagination !== false;
|
||||
// 分页配置
|
||||
const defalutPagination = {
|
||||
background: true,
|
||||
layout: "total, sizes, prev, pager, next, jumper",
|
||||
pageSize: 20,
|
||||
pageSizes: [10, 20, 30, 50],
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
};
|
||||
const pagination = reactive(
|
||||
typeof props.contentConfig.pagination === "object"
|
||||
? { ...defalutPagination, ...props.contentConfig.pagination }
|
||||
: defalutPagination
|
||||
);
|
||||
// 分页相关的请求参数
|
||||
const request = props.contentConfig.request ?? {
|
||||
pageName: "pageNum",
|
||||
limitName: "pageSize",
|
||||
};
|
||||
|
||||
const tableRef = ref<TableInstance>();
|
||||
|
||||
// 行选中
|
||||
const selectionData = ref<IObject[]>([]);
|
||||
// 删除ID集合 用于批量删除
|
||||
const removeIds = ref<(number | string)[]>([]);
|
||||
function handleSelectionChange(selection: any[]) {
|
||||
selectionData.value = selection;
|
||||
removeIds.value = selection.map((item) => item[pk]);
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function handleRefresh(isRestart = false) {
|
||||
fetchPageData(lastFormData, isRestart);
|
||||
}
|
||||
|
||||
// 删除
|
||||
function handleDelete(id?: number | string) {
|
||||
const ids = [id || removeIds.value].join(",");
|
||||
if (!ids) {
|
||||
ElMessage.warning("请勾选删除项");
|
||||
return;
|
||||
}
|
||||
|
||||
ElMessageBox.confirm("确认删除?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(function () {
|
||||
if (props.contentConfig.deleteAction) {
|
||||
props.contentConfig.deleteAction(ids).then(() => {
|
||||
ElMessage.success("删除成功");
|
||||
removeIds.value = [];
|
||||
//清空选中项
|
||||
tableRef.value?.clearSelection();
|
||||
handleRefresh(true);
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("未配置deleteAction");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出表单
|
||||
const fields: string[] = [];
|
||||
cols.value.forEach((item) => {
|
||||
if (item.prop !== undefined) {
|
||||
fields.push(item.prop);
|
||||
}
|
||||
});
|
||||
const enum ExportsOriginEnum {
|
||||
CURRENT = "current",
|
||||
SELECTED = "selected",
|
||||
REMOTE = "remote",
|
||||
}
|
||||
const exportsModalVisible = ref(false);
|
||||
const exportsFormRef = ref<FormInstance>();
|
||||
const exportsFormData = reactive({
|
||||
filename: "",
|
||||
sheetname: "",
|
||||
fields: fields,
|
||||
origin: ExportsOriginEnum.CURRENT,
|
||||
});
|
||||
const exportsFormRules: FormRules = {
|
||||
fields: [{ required: true, message: "请选择字段" }],
|
||||
origin: [{ required: true, message: "请选择数据源" }],
|
||||
};
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsModalVisible.value = true;
|
||||
}
|
||||
// 导出确认
|
||||
const handleExportsSubmit = useThrottleFn(() => {
|
||||
exportsFormRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
handleExports();
|
||||
handleCloseExportsModal();
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
// 关闭导出弹窗
|
||||
function handleCloseExportsModal() {
|
||||
exportsModalVisible.value = false;
|
||||
exportsFormRef.value?.resetFields();
|
||||
nextTick(() => {
|
||||
exportsFormRef.value?.clearValidate();
|
||||
});
|
||||
}
|
||||
// 导出
|
||||
function handleExports() {
|
||||
const filename = exportsFormData.filename
|
||||
? exportsFormData.filename
|
||||
: props.contentConfig.pageName;
|
||||
const sheetname = exportsFormData.sheetname ? exportsFormData.sheetname : "sheet";
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet(sheetname);
|
||||
const columns: Partial<ExcelJS.Column>[] = [];
|
||||
cols.value.forEach((col) => {
|
||||
if (col.label && col.prop && exportsFormData.fields.includes(col.prop)) {
|
||||
columns.push({ header: col.label, key: col.prop });
|
||||
}
|
||||
});
|
||||
worksheet.columns = columns;
|
||||
if (exportsFormData.origin === ExportsOriginEnum.REMOTE) {
|
||||
if (props.contentConfig.exportsAction) {
|
||||
props.contentConfig.exportsAction(lastFormData).then((res) => {
|
||||
worksheet.addRows(res);
|
||||
workbook.xlsx
|
||||
.writeBuffer()
|
||||
.then((buffer) => {
|
||||
saveXlsx(buffer, filename);
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("未配置exportsAction");
|
||||
}
|
||||
} else {
|
||||
worksheet.addRows(
|
||||
exportsFormData.origin === ExportsOriginEnum.SELECTED ? selectionData.value : pageData.value
|
||||
);
|
||||
workbook.xlsx
|
||||
.writeBuffer()
|
||||
.then((buffer) => {
|
||||
saveXlsx(buffer, filename);
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
}
|
||||
}
|
||||
|
||||
// 导入表单
|
||||
let isFileImport = false;
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const importModalVisible = ref(false);
|
||||
const importFormRef = ref<FormInstance>();
|
||||
const importFormData = reactive<{
|
||||
files: UploadUserFile[];
|
||||
}>({
|
||||
files: [],
|
||||
});
|
||||
const importFormRules: FormRules = {
|
||||
files: [{ required: true, message: "请选择文件" }],
|
||||
};
|
||||
// 打开导入弹窗
|
||||
function handleOpenImportModal(isFile: boolean = false) {
|
||||
importModalVisible.value = true;
|
||||
isFileImport = isFile;
|
||||
}
|
||||
// 覆盖前一个文件
|
||||
function handleFileExceed(files: File[]) {
|
||||
uploadRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadRef.value!.handleStart(file);
|
||||
}
|
||||
// 下载导入模板
|
||||
function handleDownloadTemplate() {
|
||||
const importTemplate = props.contentConfig.importTemplate;
|
||||
if (typeof importTemplate === "string") {
|
||||
window.open(importTemplate);
|
||||
} else if (typeof importTemplate === "function") {
|
||||
importTemplate().then((response) => {
|
||||
const fileData = response.data;
|
||||
const fileName = decodeURI(
|
||||
response.headers["content-disposition"].split(";")[1].split("=")[1]
|
||||
);
|
||||
saveXlsx(fileData, fileName);
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("未配置importTemplate");
|
||||
}
|
||||
}
|
||||
// 导入确认
|
||||
const handleImportSubmit = useThrottleFn(() => {
|
||||
importFormRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
if (isFileImport) {
|
||||
handleImport();
|
||||
} else {
|
||||
handleImports();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
// 关闭导入弹窗
|
||||
function handleCloseImportModal() {
|
||||
importModalVisible.value = false;
|
||||
importFormRef.value?.resetFields();
|
||||
nextTick(() => {
|
||||
importFormRef.value?.clearValidate();
|
||||
});
|
||||
}
|
||||
// 文件导入
|
||||
function handleImport() {
|
||||
const importAction = props.contentConfig.importAction;
|
||||
if (importAction === undefined) {
|
||||
ElMessage.error("未配置importAction");
|
||||
return;
|
||||
}
|
||||
importAction(importFormData.files[0].raw as File).then(() => {
|
||||
ElMessage.success("导入数据成功");
|
||||
handleCloseImportModal();
|
||||
handleRefresh(true);
|
||||
});
|
||||
}
|
||||
// 导入
|
||||
function handleImports() {
|
||||
const importsAction = props.contentConfig.importsAction;
|
||||
if (importsAction === undefined) {
|
||||
ElMessage.error("未配置importsAction");
|
||||
return;
|
||||
}
|
||||
// 获取选择的文件
|
||||
const file = importFormData.files[0].raw as File;
|
||||
// 创建Workbook实例
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
// 使用FileReader对象来读取文件内容
|
||||
const fileReader = new FileReader();
|
||||
// 二进制字符串的形式加载文件
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
fileReader.onload = (ev) => {
|
||||
if (ev.target !== null && ev.target.result !== null) {
|
||||
const result = ev.target.result as ArrayBuffer;
|
||||
// 从 buffer中加载数据解析
|
||||
workbook.xlsx
|
||||
.load(result)
|
||||
.then((workbook) => {
|
||||
// 解析后的数据
|
||||
const data = [];
|
||||
// 获取第一个worksheet内容
|
||||
const worksheet = workbook.getWorksheet(1);
|
||||
if (worksheet) {
|
||||
// 获取第一行的标题
|
||||
const fields: any[] = [];
|
||||
worksheet.getRow(1).eachCell((cell) => {
|
||||
fields.push(cell.value);
|
||||
});
|
||||
// 遍历工作表的每一行(从第二行开始,因为第一行通常是标题行)
|
||||
for (let rowNumber = 2; rowNumber <= worksheet.rowCount; rowNumber++) {
|
||||
const rowData: IObject = {};
|
||||
const row = worksheet.getRow(rowNumber);
|
||||
// 遍历当前行的每个单元格
|
||||
row.eachCell((cell, colNumber) => {
|
||||
// 获取标题对应的键,并将当前单元格的值存储到相应的属性名中
|
||||
rowData[fields[colNumber - 1]] = cell.value;
|
||||
});
|
||||
// 将当前行的数据对象添加到数组中
|
||||
data.push(rowData);
|
||||
}
|
||||
}
|
||||
if (data.length === 0) {
|
||||
ElMessage.error("未解析到数据");
|
||||
return;
|
||||
}
|
||||
importsAction(data).then(() => {
|
||||
ElMessage.success("导入数据成功");
|
||||
handleCloseImportModal();
|
||||
handleRefresh(true);
|
||||
});
|
||||
})
|
||||
.catch((error) => console.log(error));
|
||||
} else {
|
||||
ElMessage.error("读取文件失败");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 操作栏
|
||||
function handleToolbar(name: string) {
|
||||
switch (name) {
|
||||
case "refresh":
|
||||
handleRefresh();
|
||||
break;
|
||||
case "exports":
|
||||
handleOpenExportsModal();
|
||||
break;
|
||||
case "imports":
|
||||
handleOpenImportModal();
|
||||
break;
|
||||
case "search":
|
||||
emit("searchClick");
|
||||
break;
|
||||
case "add":
|
||||
emit("addClick");
|
||||
break;
|
||||
case "delete":
|
||||
handleDelete();
|
||||
break;
|
||||
case "import":
|
||||
handleOpenImportModal(true);
|
||||
break;
|
||||
case "export":
|
||||
emit("exportClick");
|
||||
break;
|
||||
default:
|
||||
emit("toolbarClick", name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 操作列
|
||||
function handleOperat(data: IOperatData) {
|
||||
switch (data.name) {
|
||||
case "edit":
|
||||
emit("editClick", data.row);
|
||||
break;
|
||||
case "delete":
|
||||
handleDelete(data.row[pk]);
|
||||
break;
|
||||
default:
|
||||
emit("operatClick", data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 属性修改
|
||||
function handleModify(field: string, value: boolean | string | number, row: Record<string, any>) {
|
||||
if (props.contentConfig.modifyAction) {
|
||||
props.contentConfig.modifyAction({
|
||||
[pk]: row[pk],
|
||||
field: field,
|
||||
value: value,
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("未配置modifyAction");
|
||||
}
|
||||
}
|
||||
|
||||
// 分页切换
|
||||
function handleSizeChange(value: number) {
|
||||
pagination.pageSize = value;
|
||||
handleRefresh();
|
||||
}
|
||||
function handleCurrentChange(value: number) {
|
||||
pagination.currentPage = value;
|
||||
handleRefresh();
|
||||
}
|
||||
|
||||
// 远程数据筛选
|
||||
let filterParams: IObject = {};
|
||||
function handleFilterChange(newFilters: any) {
|
||||
const filters: IObject = {};
|
||||
for (const key in newFilters) {
|
||||
const col = cols.value.find((col) => {
|
||||
return col.columnKey === key || col["column-key"] === key;
|
||||
});
|
||||
if (col && col.filterJoin !== undefined) {
|
||||
filters[key] = newFilters[key].join(col.filterJoin);
|
||||
} else {
|
||||
filters[key] = newFilters[key];
|
||||
}
|
||||
}
|
||||
filterParams = { ...filterParams, ...filters };
|
||||
emit("filterChange", filterParams);
|
||||
}
|
||||
|
||||
// 获取筛选条件
|
||||
function getFilterParams() {
|
||||
return filterParams;
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
let lastFormData = {};
|
||||
function fetchPageData(formData: IObject = {}, isRestart = false) {
|
||||
loading.value = true;
|
||||
// 上一次搜索条件
|
||||
lastFormData = formData;
|
||||
// 重置页码
|
||||
if (isRestart) {
|
||||
pagination.currentPage = 1;
|
||||
}
|
||||
props.contentConfig
|
||||
.indexAction(
|
||||
showPagination
|
||||
? {
|
||||
[request.pageName]: pagination.currentPage,
|
||||
[request.limitName]: pagination.pageSize,
|
||||
...formData,
|
||||
}
|
||||
: formData
|
||||
)
|
||||
.then((data) => {
|
||||
if (showPagination) {
|
||||
if (props.contentConfig.parseData) {
|
||||
data = props.contentConfig.parseData(data);
|
||||
}
|
||||
pagination.total = data.total;
|
||||
pageData.value = data.list;
|
||||
} else {
|
||||
pageData.value = data;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
fetchPageData();
|
||||
|
||||
// 导出Excel
|
||||
function exportPageData(formData: IObject = {}) {
|
||||
if (props.contentConfig.exportAction) {
|
||||
props.contentConfig.exportAction(formData).then((response) => {
|
||||
const fileData = response.data;
|
||||
const fileName = decodeURI(
|
||||
response.headers["content-disposition"].split(";")[1].split("=")[1]
|
||||
);
|
||||
saveXlsx(fileData, fileName);
|
||||
});
|
||||
} else {
|
||||
ElMessage.error("未配置exportAction");
|
||||
}
|
||||
}
|
||||
|
||||
// 浏览器保存文件
|
||||
function saveXlsx(fileData: BlobPart, fileName: string) {
|
||||
const fileType =
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
|
||||
|
||||
const blob = new Blob([fileData], { type: fileType });
|
||||
const downloadUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.href = downloadUrl;
|
||||
downloadLink.download = fileName;
|
||||
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
|
||||
document.body.removeChild(downloadLink);
|
||||
window.URL.revokeObjectURL(downloadUrl);
|
||||
}
|
||||
|
||||
// 暴露的属性和方法
|
||||
defineExpose({ fetchPageData, exportPageData, getFilterParams });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
156
src/components/CURD/PageForm.vue
Normal file
156
src/components/CURD/PageForm.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<el-form ref="formRef" label-width="auto" v-bind="form" :model="formData" :rules="formRules">
|
||||
<el-row :gutter="20">
|
||||
<template v-for="item in formItems" :key="item.prop">
|
||||
<el-col v-show="!item.hidden" v-bind="item.col">
|
||||
<el-form-item :label="item.label" :prop="item.prop">
|
||||
<!-- Label -->
|
||||
<template v-if="item.tips" #label>
|
||||
<span>
|
||||
{{ item.label }}
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
:content="item.tips"
|
||||
:raw-content="true"
|
||||
>
|
||||
<el-icon style="vertical-align: -0.15em" size="16">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Input 输入框 -->
|
||||
<template v-if="item.type === 'input' || item.type === undefined">
|
||||
<el-input v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Select 选择器 -->
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<el-select v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-option v-bind="option" />
|
||||
</template>
|
||||
</el-select>
|
||||
</template>
|
||||
<!-- Radio 单选框 -->
|
||||
<template v-else-if="item.type === 'radio'">
|
||||
<el-radio-group v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-radio v-bind="option" />
|
||||
</template>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<!-- Checkbox 多选框 -->
|
||||
<template v-else-if="item.type === 'checkbox'">
|
||||
<el-checkbox-group v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-checkbox v-bind="option" />
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<!-- Input Number 数字输入框 -->
|
||||
<template v-else-if="item.type === 'input-number'">
|
||||
<el-input-number v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- TreeSelect 树形选择 -->
|
||||
<template v-else-if="item.type === 'tree-select'">
|
||||
<el-tree-select v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- DatePicker 日期选择器 -->
|
||||
<template v-else-if="item.type === 'date-picker'">
|
||||
<el-date-picker v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Text 文本 -->
|
||||
<template v-else-if="item.type === 'text'">
|
||||
<el-text v-bind="item.attrs">{{ formData[item.prop] }}</el-text>
|
||||
</template>
|
||||
<!-- 自定义 -->
|
||||
<template v-else-if="item.type === 'custom'">
|
||||
<slot
|
||||
:name="item.slotName ?? item.prop"
|
||||
:prop="item.prop"
|
||||
:formData="formData"
|
||||
:attrs="item.attrs"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { reactive, ref, watch, watchEffect } from "vue";
|
||||
import { IObject, IPageForm } from "./types";
|
||||
|
||||
// 定义接收的属性
|
||||
const props = withDefaults(defineProps<IPageForm>(), {
|
||||
pk: "id",
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const formItems = reactive(props.formItems);
|
||||
const formData = reactive<IObject>({});
|
||||
const formRules: FormRules = {};
|
||||
const prepareFuncs = [];
|
||||
for (const item of formItems) {
|
||||
item.initFn && item.initFn(item);
|
||||
formData[item.prop] = item.initialValue ?? "";
|
||||
formRules[item.prop] = item.rules ?? [];
|
||||
|
||||
if (item.watch !== undefined) {
|
||||
prepareFuncs.push(() => {
|
||||
watch(
|
||||
() => formData[item.prop],
|
||||
(newValue, oldValue) => {
|
||||
item.watch && item.watch(newValue, oldValue, formData, formItems);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (item.computed !== undefined) {
|
||||
prepareFuncs.push(() => {
|
||||
watchEffect(() => {
|
||||
item.computed && (formData[item.prop] = item.computed(formData));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (item.watchEffect !== undefined) {
|
||||
prepareFuncs.push(() => {
|
||||
watchEffect(() => {
|
||||
item.watchEffect && item.watchEffect(formData);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
prepareFuncs.forEach((func) => func());
|
||||
|
||||
// 获取表单数据
|
||||
function getFormData(key?: string) {
|
||||
return key === undefined ? formData : (formData[key] ?? undefined);
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
function setFormData(data: IObject) {
|
||||
for (const key in formData) {
|
||||
if (formData.hasOwnProperty(key) && key in data) {
|
||||
formData[key] = data[key];
|
||||
}
|
||||
}
|
||||
if (data?.hasOwnProperty(props.pk)) {
|
||||
formData[props.pk] = data[props.pk];
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单项值
|
||||
function setFormItemData(key: string, value: any) {
|
||||
formData[key] = value;
|
||||
}
|
||||
|
||||
// 暴露的属性和方法
|
||||
defineExpose({ formRef, getFormData, setFormData, setFormItemData });
|
||||
</script>
|
||||
377
src/components/CURD/PageModal.vue
Normal file
377
src/components/CURD/PageModal.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<!-- drawer -->
|
||||
<template v-if="modalConfig.component === 'drawer'">
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:append-to-body="true"
|
||||
v-bind="modalConfig.drawer"
|
||||
@close="handleCloseModal"
|
||||
>
|
||||
<!-- 表单 -->
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-width="auto"
|
||||
v-bind="modalConfig.form"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<template v-for="item in formItems" :key="item.prop">
|
||||
<el-col v-show="!item.hidden" v-bind="item.col">
|
||||
<el-form-item :label="item.label" :prop="item.prop">
|
||||
<!-- Label -->
|
||||
<template v-if="item.tips" #label>
|
||||
<span>
|
||||
{{ item.label }}
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
:content="item.tips"
|
||||
:raw-content="true"
|
||||
>
|
||||
<el-icon style="vertical-align: -0.15em" size="16">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Input 输入框 -->
|
||||
<template v-if="item.type === 'input' || item.type === undefined">
|
||||
<el-input v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Select 选择器 -->
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<el-select v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-option v-bind="option" />
|
||||
</template>
|
||||
</el-select>
|
||||
</template>
|
||||
<!-- Radio 单选框 -->
|
||||
<template v-else-if="item.type === 'radio'">
|
||||
<el-radio-group v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-radio v-bind="option" />
|
||||
</template>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<!-- switch 开关 -->
|
||||
<template v-else-if="item.type === 'switch'">
|
||||
<el-switch v-model="formData[item.prop]" inline-prompt v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Checkbox 多选框 -->
|
||||
<template v-else-if="item.type === 'checkbox'">
|
||||
<el-checkbox-group v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-checkbox v-bind="option" />
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<!-- Input Number 数字输入框 -->
|
||||
<template v-else-if="item.type === 'input-number'">
|
||||
<el-input-number v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- TreeSelect 树形选择 -->
|
||||
<template v-else-if="item.type === 'tree-select'">
|
||||
<el-tree-select v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- DatePicker 日期选择器 -->
|
||||
<template v-else-if="item.type === 'date-picker'">
|
||||
<el-date-picker v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Text 文本 -->
|
||||
<template v-else-if="item.type === 'text'">
|
||||
<el-text v-bind="item.attrs">
|
||||
{{ formData[item.prop] }}
|
||||
</el-text>
|
||||
</template>
|
||||
<!-- 自定义 -->
|
||||
<template v-else-if="item.type === 'custom'">
|
||||
<slot
|
||||
:name="item.slotName ?? item.prop"
|
||||
:prop="item.prop"
|
||||
:formData="formData"
|
||||
:attrs="item.attrs"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<!-- 弹窗底部操作按钮 -->
|
||||
<template #footer>
|
||||
<div v-if="!formDisable">
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
<!-- dialog -->
|
||||
<template v-else>
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:align-center="true"
|
||||
:append-to-body="true"
|
||||
width="70vw"
|
||||
v-bind="modalConfig.dialog"
|
||||
style="padding-right: 0"
|
||||
@close="handleCloseModal"
|
||||
>
|
||||
<!-- 滚动 -->
|
||||
<el-scrollbar max-height="60vh">
|
||||
<!-- 表单 -->
|
||||
<el-form
|
||||
ref="formRef"
|
||||
label-width="auto"
|
||||
v-bind="modalConfig.form"
|
||||
style="padding-right: var(--el-dialog-padding-primary)"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<template v-for="item in formItems" :key="item.prop">
|
||||
<el-col v-show="!item.hidden" v-bind="item.col">
|
||||
<el-form-item :label="item.label" :prop="item.prop">
|
||||
<!-- Label -->
|
||||
<template v-if="item.tips" #label>
|
||||
<span>
|
||||
{{ item.label }}
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
:content="item.tips"
|
||||
:raw-content="true"
|
||||
>
|
||||
<el-icon style="vertical-align: -0.15em" size="16">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Input 输入框 -->
|
||||
<template v-if="item.type === 'input' || item.type === undefined">
|
||||
<el-input v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Select 选择器 -->
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<el-select v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-option v-bind="option" />
|
||||
</template>
|
||||
</el-select>
|
||||
</template>
|
||||
<!-- Radio 单选框 -->
|
||||
<template v-else-if="item.type === 'radio'">
|
||||
<el-radio-group v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-radio v-bind="option" />
|
||||
</template>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<!-- switch 开关 -->
|
||||
<template v-else-if="item.type === 'switch'">
|
||||
<el-switch v-model="formData[item.prop]" inline-prompt v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Checkbox 多选框 -->
|
||||
<template v-else-if="item.type === 'checkbox'">
|
||||
<el-checkbox-group v-model="formData[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-checkbox v-bind="option" />
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<!-- Input Number 数字输入框 -->
|
||||
<template v-else-if="item.type === 'input-number'">
|
||||
<el-input-number v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- TreeSelect 树形选择 -->
|
||||
<template v-else-if="item.type === 'tree-select'">
|
||||
<el-tree-select v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- DatePicker 日期选择器 -->
|
||||
<template v-else-if="item.type === 'date-picker'">
|
||||
<el-date-picker v-model="formData[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- Text 文本 -->
|
||||
<template v-else-if="item.type === 'text'">
|
||||
<el-text v-bind="item.attrs">
|
||||
{{ formData[item.prop] }}
|
||||
</el-text>
|
||||
</template>
|
||||
<!-- 自定义 -->
|
||||
<template v-else-if="item.type === 'custom'">
|
||||
<slot
|
||||
:name="item.slotName ?? item.prop"
|
||||
:prop="item.prop"
|
||||
:formData="formData"
|
||||
:attrs="item.attrs"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</template>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
<!-- 弹窗底部操作按钮 -->
|
||||
<template #footer>
|
||||
<div style="padding-right: var(--el-dialog-padding-primary)">
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button @click="handleClose">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useThrottleFn } from "@vueuse/core";
|
||||
import type { FormInstance, FormRules } from "element-plus";
|
||||
import { nextTick, reactive, ref, watch, watchEffect } from "vue";
|
||||
import type { IModalConfig, IObject } from "./types";
|
||||
|
||||
// 定义接收的属性
|
||||
const props = defineProps<{
|
||||
modalConfig: IModalConfig;
|
||||
}>();
|
||||
// 自定义事件
|
||||
const emit = defineEmits<{
|
||||
submitClick: [];
|
||||
}>();
|
||||
|
||||
const pk = props.modalConfig.pk ?? "id";
|
||||
const modalVisible = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const formItems = reactive(props.modalConfig.formItems);
|
||||
const formData = reactive<IObject>({});
|
||||
const formRules: FormRules = {};
|
||||
const formDisable = ref(false);
|
||||
const prepareFuncs = [];
|
||||
for (const item of formItems) {
|
||||
item.initFn && item.initFn(item);
|
||||
formData[item.prop] = item.initialValue ?? "";
|
||||
formRules[item.prop] = item.rules ?? [];
|
||||
|
||||
if (item.watch !== undefined) {
|
||||
prepareFuncs.push(() => {
|
||||
watch(
|
||||
() => formData[item.prop],
|
||||
(newValue, oldValue) => {
|
||||
item.watch && item.watch(newValue, oldValue, formData, formItems);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (item.computed !== undefined) {
|
||||
prepareFuncs.push(() => {
|
||||
watchEffect(() => {
|
||||
item.computed && (formData[item.prop] = item.computed(formData));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (item.watchEffect !== undefined) {
|
||||
prepareFuncs.push(() => {
|
||||
watchEffect(() => {
|
||||
item.watchEffect && item.watchEffect(formData);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
prepareFuncs.forEach((func) => func());
|
||||
|
||||
// 获取表单数据
|
||||
function getFormData(key?: string) {
|
||||
return key === undefined ? formData : (formData[key] ?? undefined);
|
||||
}
|
||||
|
||||
// 设置表单值
|
||||
function setFormData(data: IObject) {
|
||||
for (const key in formData) {
|
||||
if (formData.hasOwnProperty(key) && key in data) {
|
||||
formData[key] = data[key];
|
||||
}
|
||||
}
|
||||
if (data?.hasOwnProperty(pk)) {
|
||||
formData[pk] = data[pk];
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单项值
|
||||
function setFormItemData(key: string, value: any) {
|
||||
formData[key] = value;
|
||||
}
|
||||
|
||||
// 显示modal
|
||||
function setModalVisible(data: IObject = {}) {
|
||||
modalVisible.value = true;
|
||||
// nextTick解决赋值后重置表单无效问题
|
||||
nextTick(() => {
|
||||
Object.values(data).length > 0 && setFormData(data);
|
||||
});
|
||||
}
|
||||
|
||||
// 表单提交
|
||||
const handleSubmit = useThrottleFn(() => {
|
||||
formRef.value?.validate((valid: boolean) => {
|
||||
if (valid) {
|
||||
if (typeof props.modalConfig.beforeSubmit === "function") {
|
||||
props.modalConfig.beforeSubmit(formData);
|
||||
}
|
||||
props.modalConfig.formAction(formData).then(() => {
|
||||
let msg = "操作成功";
|
||||
if (props.modalConfig.component === "drawer") {
|
||||
if (props.modalConfig.drawer?.title) {
|
||||
msg = `${props.modalConfig.drawer?.title}成功`;
|
||||
}
|
||||
} else {
|
||||
if (props.modalConfig.dialog?.title) {
|
||||
msg = `${props.modalConfig.dialog?.title}成功`;
|
||||
}
|
||||
}
|
||||
ElMessage.success(msg);
|
||||
emit("submitClick");
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
// 隐藏弹窗
|
||||
function handleClose() {
|
||||
modalVisible.value = false;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function handleCloseModal() {
|
||||
formRef.value?.resetFields();
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate();
|
||||
});
|
||||
}
|
||||
|
||||
// 禁用表单--用于详情时候用
|
||||
function handleDisabled(disable: boolean) {
|
||||
formDisable.value = disable;
|
||||
props.modalConfig.formItems.forEach((item) => {
|
||||
if (item) {
|
||||
if (item.attrs) {
|
||||
item.attrs.disabled = disable;
|
||||
} else {
|
||||
item.attrs = { disabled: disable };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露的属性和方法
|
||||
defineExpose({ setModalVisible, getFormData, setFormData, setFormItemData, handleDisabled });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
239
src/components/CURD/PageSearch.vue
Normal file
239
src/components/CURD/PageSearch.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<el-card
|
||||
v-show="visible"
|
||||
v-hasPerm="[`${searchConfig.pageName}:query`]"
|
||||
shadow="never"
|
||||
class="mb-[10px]"
|
||||
>
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<template v-for="(item, index) in formItems" :key="item.prop">
|
||||
<el-form-item
|
||||
v-show="isExpand ? true : index < showNumber"
|
||||
:label="item.label"
|
||||
:prop="item.prop"
|
||||
>
|
||||
<!-- Label -->
|
||||
<template v-if="item.tips" #label>
|
||||
<span>
|
||||
{{ item.label }}
|
||||
<el-tooltip
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
:content="item.tips"
|
||||
:raw-content="true"
|
||||
>
|
||||
<el-icon style="vertical-align: -0.15em" size="16">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<!-- Input 输入框 -->
|
||||
<template v-if="item.type === 'input' || item.type === undefined">
|
||||
<el-input
|
||||
v-model="queryParams[item.prop]"
|
||||
v-bind="item.attrs"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</template>
|
||||
<!-- InputTag 标签输入框 -->
|
||||
<template v-if="item.type === 'input-tag'">
|
||||
<div class="flex-center">
|
||||
<el-tag
|
||||
v-for="tag in inputTagMap[item.prop].data"
|
||||
:key="tag"
|
||||
class="mr-2"
|
||||
:closable="true"
|
||||
v-bind="inputTagMap[item.prop].tagAttrs"
|
||||
@close="handleCloseTag(item.prop, tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<template v-if="inputTagMap[item.prop].inputVisible">
|
||||
<el-input
|
||||
:ref="(el: HTMLElement) => (inputTagMap[item.prop].inputRef = el)"
|
||||
v-model="inputTagMap[item.prop].inputValue"
|
||||
v-bind="inputTagMap[item.prop].inputAttrs"
|
||||
@keyup.enter="handleInputConfirm(item.prop)"
|
||||
@blur="handleInputConfirm(item.prop)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
v-bind="inputTagMap[item.prop].buttonAttrs"
|
||||
@click="handleShowInput(item.prop)"
|
||||
>
|
||||
{{ inputTagMap[item.prop].buttonAttrs.btnText }}
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Select 选择器 -->
|
||||
<template v-else-if="item.type === 'select'">
|
||||
<el-select v-model="queryParams[item.prop]" v-bind="item.attrs">
|
||||
<template v-for="option in item.options" :key="option.value">
|
||||
<el-option :label="option.label" :value="option.value" />
|
||||
</template>
|
||||
</el-select>
|
||||
</template>
|
||||
<!-- TreeSelect 树形选择 -->
|
||||
<template v-else-if="item.type === 'tree-select'">
|
||||
<el-tree-select v-model="queryParams[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
<!-- DatePicker 日期选择器 -->
|
||||
<template v-else-if="item.type === 'date-picker'">
|
||||
<el-date-picker v-model="queryParams[item.prop]" v-bind="item.attrs" />
|
||||
</template>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="refresh" @click="handleReset">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<el-link
|
||||
v-if="isExpandable && formItems.length > showNumber"
|
||||
class="ml-2"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="isExpand = !isExpand"
|
||||
>
|
||||
<template v-if="isExpand">
|
||||
收起
|
||||
<el-icon>
|
||||
<ArrowUp />
|
||||
</el-icon>
|
||||
</template>
|
||||
<template v-else>
|
||||
展开
|
||||
<el-icon>
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { reactive, ref } from "vue";
|
||||
import type { IObject, ISearchConfig } from "./types";
|
||||
|
||||
// 定义接收的属性
|
||||
const props = defineProps<{
|
||||
searchConfig: ISearchConfig;
|
||||
}>();
|
||||
// 自定义事件
|
||||
const emit = defineEmits<{
|
||||
queryClick: [queryParams: IObject];
|
||||
resetClick: [queryParams: IObject];
|
||||
}>();
|
||||
|
||||
const queryFormRef = ref<FormInstance>();
|
||||
// 是否显示
|
||||
const visible = ref(true);
|
||||
// 响应式的formItems
|
||||
const formItems = reactive(props.searchConfig.formItems);
|
||||
// 是否可展开/收缩
|
||||
const isExpandable = ref(props.searchConfig.isExpandable ?? true);
|
||||
// 是否已展开
|
||||
const isExpand = ref(false);
|
||||
// 表单项展示数量,若可展开,超出展示数量的表单项隐藏
|
||||
const showNumber = computed(() => {
|
||||
if (isExpandable.value === true) {
|
||||
return props.searchConfig.showNumber ?? 3;
|
||||
} else {
|
||||
return formItems.length;
|
||||
}
|
||||
});
|
||||
// 搜索表单数据
|
||||
const queryParams = reactive<IObject>({});
|
||||
const inputTagMap = reactive<IObject>({});
|
||||
for (const item of formItems) {
|
||||
item.initFn && item.initFn(item);
|
||||
if (item.type === "input-tag") {
|
||||
inputTagMap[item.prop] = {
|
||||
data: Array.isArray(item.initialValue) ? item.initialValue : [],
|
||||
inputVisible: false,
|
||||
inputValue: "",
|
||||
inputRef: null,
|
||||
buttonAttrs: {
|
||||
size: item.attrs?.size ?? "default",
|
||||
btnText: item.attrs?.btnText ?? "+ New Tag",
|
||||
style: "color: #b0b2b7",
|
||||
},
|
||||
inputAttrs: {
|
||||
size: item.attrs?.size ?? "default",
|
||||
clearable: item.attrs?.clearable ?? false,
|
||||
style: "width: 150px",
|
||||
},
|
||||
tagAttrs: {
|
||||
size: item.attrs?.size ?? "default",
|
||||
},
|
||||
};
|
||||
queryParams[item.prop] = computed({
|
||||
get() {
|
||||
return typeof item.attrs?.join === "string"
|
||||
? inputTagMap[item.prop].data.join(item.attrs.join)
|
||||
: inputTagMap[item.prop].data;
|
||||
},
|
||||
set(value) {
|
||||
// resetFields时会被调用
|
||||
inputTagMap[item.prop].data =
|
||||
typeof item.attrs?.join === "string"
|
||||
? value.split(item.attrs.join).filter((item: any) => item !== "")
|
||||
: value;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
queryParams[item.prop] = item.initialValue ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// 重置操作
|
||||
function handleReset() {
|
||||
queryFormRef.value?.resetFields();
|
||||
emit("resetClick", queryParams);
|
||||
}
|
||||
|
||||
// 查询操作
|
||||
function handleQuery() {
|
||||
emit("queryClick", queryParams);
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
function getQueryParams() {
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
// 显示/隐藏 SearchForm
|
||||
function toggleVisible() {
|
||||
visible.value = !visible.value;
|
||||
}
|
||||
|
||||
// 关闭标签
|
||||
function handleCloseTag(prop: string, tag: string) {
|
||||
inputTagMap[prop].data.splice(inputTagMap[prop].data.indexOf(tag), 1);
|
||||
}
|
||||
// 添加标签
|
||||
function handleInputConfirm(prop: string) {
|
||||
if (inputTagMap[prop].inputValue) {
|
||||
inputTagMap[prop].data.push(inputTagMap[prop].inputValue);
|
||||
}
|
||||
inputTagMap[prop].inputVisible = false;
|
||||
inputTagMap[prop].inputValue = "";
|
||||
}
|
||||
// 显示标签输入框
|
||||
function handleShowInput(prop: string) {
|
||||
inputTagMap[prop].inputVisible = true;
|
||||
nextTick(() => {
|
||||
inputTagMap[prop].inputRef.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露的属性和方法
|
||||
defineExpose({ getQueryParams, toggleVisible });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
272
src/components/CURD/types.ts
Normal file
272
src/components/CURD/types.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import type {
|
||||
DialogProps,
|
||||
DrawerProps,
|
||||
FormItemRule,
|
||||
FormProps,
|
||||
PaginationProps,
|
||||
TableProps,
|
||||
ColProps,
|
||||
} from "element-plus";
|
||||
import type PageContent from "./PageContent.vue";
|
||||
import type PageForm from "./PageForm.vue";
|
||||
import type PageModal from "./PageModal.vue";
|
||||
import type PageSearch from "./PageSearch.vue";
|
||||
|
||||
export type PageSearchInstance = InstanceType<typeof PageSearch>;
|
||||
export type PageContentInstance = InstanceType<typeof PageContent>;
|
||||
export type PageModalInstance = InstanceType<typeof PageModal>;
|
||||
export type PageFormInstance = InstanceType<typeof PageForm>;
|
||||
|
||||
export type IObject = Record<string, any>;
|
||||
|
||||
export interface IOperatData {
|
||||
name: string;
|
||||
row: IObject;
|
||||
column: IObject;
|
||||
$index: number;
|
||||
}
|
||||
|
||||
export interface ISearchConfig {
|
||||
// 页面名称(参与组成权限标识,如sys:user:xxx)
|
||||
pageName: string;
|
||||
// 表单项
|
||||
formItems: Array<{
|
||||
// 组件类型(如input,select等)
|
||||
type?: "input" | "select" | "tree-select" | "date-picker" | "input-tag";
|
||||
// 标签文本
|
||||
label: string;
|
||||
// 标签提示
|
||||
tips?: string;
|
||||
// 键名
|
||||
prop: string;
|
||||
// 组件属性(input-tag组件支持join,btnText,size属性)
|
||||
attrs?: IObject;
|
||||
// 初始值
|
||||
initialValue?: any;
|
||||
// 可选项(适用于select组件)
|
||||
options?: { label: string; value: any }[];
|
||||
// 初始化数据函数扩展
|
||||
initFn?: (formItem: IObject) => void;
|
||||
}>;
|
||||
// 是否开启展开和收缩
|
||||
isExpandable?: boolean;
|
||||
// 默认展示的表单项数量
|
||||
showNumber?: number;
|
||||
}
|
||||
|
||||
export interface IContentConfig<T = any> {
|
||||
// 页面名称(参与组成权限标识,如sys:user:xxx)
|
||||
pageName: string;
|
||||
// table组件属性
|
||||
table?: Omit<TableProps<any>, "data">;
|
||||
// pagination组件属性
|
||||
pagination?:
|
||||
| boolean
|
||||
| Partial<
|
||||
Omit<
|
||||
PaginationProps,
|
||||
"v-model:page-size" | "v-model:current-page" | "total" | "currentPage"
|
||||
>
|
||||
>;
|
||||
// 列表的网络请求函数(需返回promise)
|
||||
indexAction: (queryParams: T) => Promise<any>;
|
||||
// 默认的分页相关的请求参数
|
||||
request?: {
|
||||
pageName: string;
|
||||
limitName: string;
|
||||
};
|
||||
// 数据格式解析的回调函数
|
||||
parseData?: (res: any) => {
|
||||
total: number;
|
||||
list: IObject[];
|
||||
[key: string]: any;
|
||||
};
|
||||
// 修改属性的网络请求函数(需返回promise)
|
||||
modifyAction?: (data: {
|
||||
[key: string]: any;
|
||||
field: string;
|
||||
value: boolean | string | number;
|
||||
}) => Promise<any>;
|
||||
// 删除的网络请求函数(需返回promise)
|
||||
deleteAction?: (ids: string) => Promise<any>;
|
||||
// 后端导出的网络请求函数(需返回promise)
|
||||
exportAction?: (queryParams: T) => Promise<any>;
|
||||
// 前端全量导出的网络请求函数(需返回promise)
|
||||
exportsAction?: (queryParams: T) => Promise<IObject[]>;
|
||||
// 导入模板
|
||||
importTemplate?: string | (() => Promise<any>);
|
||||
// 后端导入的网络请求函数(需返回promise)
|
||||
importAction?: (file: File) => Promise<any>;
|
||||
// 前端导入的网络请求函数(需返回promise)
|
||||
importsAction?: (data: IObject[]) => Promise<any>;
|
||||
// 主键名(默认为id)
|
||||
pk?: string;
|
||||
// 表格工具栏(默认支持add,delete,export,也可自定义)
|
||||
toolbar?: Array<
|
||||
| "add"
|
||||
| "delete"
|
||||
| "import"
|
||||
| "export"
|
||||
| {
|
||||
auth: string;
|
||||
icon?: string;
|
||||
name: string;
|
||||
text: string;
|
||||
type?: "primary" | "success" | "warning" | "danger" | "info";
|
||||
}
|
||||
>;
|
||||
// 表格工具栏右侧图标
|
||||
defaultToolbar?: Array<
|
||||
| "refresh"
|
||||
| "filter"
|
||||
| "imports"
|
||||
| "exports"
|
||||
| "search"
|
||||
| {
|
||||
name: string;
|
||||
icon: string;
|
||||
title?: string;
|
||||
auth?: string;
|
||||
}
|
||||
>;
|
||||
// table组件列属性(额外的属性templet,operat,slotName)
|
||||
cols: Array<{
|
||||
type?: "default" | "selection" | "index" | "expand";
|
||||
label?: string;
|
||||
prop?: string;
|
||||
width?: string | number;
|
||||
align?: "left" | "center" | "right";
|
||||
columnKey?: string;
|
||||
reserveSelection?: boolean;
|
||||
// 列是否显示
|
||||
show?: boolean;
|
||||
// 模板
|
||||
templet?:
|
||||
| "image"
|
||||
| "list"
|
||||
| "url"
|
||||
| "switch"
|
||||
| "input"
|
||||
| "price"
|
||||
| "percent"
|
||||
| "icon"
|
||||
| "date"
|
||||
| "tool"
|
||||
| "custom";
|
||||
// image模板相关参数
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
// list模板相关参数
|
||||
selectList?: IObject;
|
||||
// switch模板相关参数
|
||||
activeValue?: boolean | string | number;
|
||||
inactiveValue?: boolean | string | number;
|
||||
activeText?: string;
|
||||
inactiveText?: string;
|
||||
// input模板相关参数
|
||||
inputType?: string;
|
||||
// price模板相关参数
|
||||
priceFormat?: string;
|
||||
// date模板相关参数
|
||||
dateFormat?: string;
|
||||
// tool模板相关参数
|
||||
operat?: Array<
|
||||
| "edit"
|
||||
| "delete"
|
||||
| {
|
||||
auth?: string;
|
||||
icon?: string;
|
||||
name: string;
|
||||
text: string;
|
||||
type?: "primary" | "success" | "warning" | "danger" | "info";
|
||||
render?: (row: IObject) => boolean;
|
||||
}
|
||||
>;
|
||||
// filter值拼接符
|
||||
filterJoin?: string;
|
||||
[key: string]: any;
|
||||
// 初始化数据函数
|
||||
initFn?: (item: IObject) => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface IModalConfig<T = any> {
|
||||
// 页面名称
|
||||
pageName?: string;
|
||||
// 主键名(主要用于编辑数据,默认为id)
|
||||
pk?: string;
|
||||
// 组件类型
|
||||
component?: "dialog" | "drawer";
|
||||
// dialog组件属性
|
||||
dialog?: Partial<Omit<DialogProps, "modelValue">>;
|
||||
// drawer组件属性
|
||||
drawer?: Partial<Omit<DrawerProps, "modelValue">>;
|
||||
// form组件属性
|
||||
form?: IForm;
|
||||
// 表单项
|
||||
formItems: IFormItems<T>;
|
||||
// 提交之前处理
|
||||
beforeSubmit?: (data: T) => void;
|
||||
// 提交的网络请求函数(需返回promise)
|
||||
formAction: (data: T) => Promise<any>;
|
||||
}
|
||||
|
||||
export type IForm = Partial<Omit<FormProps, "model" | "rules">>;
|
||||
|
||||
// 表单项
|
||||
export type IFormItems<T = any> = Array<{
|
||||
// 组件类型(如input,select,radio,custom等,默认input)
|
||||
type?:
|
||||
| "input"
|
||||
| "select"
|
||||
| "radio"
|
||||
| "switch"
|
||||
| "checkbox"
|
||||
| "tree-select"
|
||||
| "date-picker"
|
||||
| "input-number"
|
||||
| "text"
|
||||
| "custom";
|
||||
// 组件属性
|
||||
attrs?: IObject;
|
||||
// 组件可选项(适用于select,radio,checkbox组件)
|
||||
options?: Array<{
|
||||
label: string;
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
[key: string]: any;
|
||||
}>;
|
||||
// 插槽名(适用于组件类型为custom)
|
||||
slotName?: string;
|
||||
// 标签文本
|
||||
label: string;
|
||||
// 标签提示
|
||||
tips?: string;
|
||||
// 键名
|
||||
prop: string;
|
||||
// 验证规则
|
||||
rules?: FormItemRule[];
|
||||
// 初始值
|
||||
initialValue?: any;
|
||||
// 是否隐藏
|
||||
hidden?: boolean;
|
||||
// layout组件Col属性
|
||||
col?: Partial<ColProps>;
|
||||
// 监听函数
|
||||
watch?: (newValue: any, oldValue: any, data: T, items: IObject[]) => void;
|
||||
// 计算属性函数
|
||||
computed?: (data: T) => any;
|
||||
// 监听收集函数
|
||||
watchEffect?: (data: T) => void;
|
||||
// 初始化数据函数扩展
|
||||
initFn?: (item: IObject) => void;
|
||||
}>;
|
||||
|
||||
export interface IPageForm {
|
||||
// 主键名(主要用于编辑数据,默认为id)
|
||||
pk?: string;
|
||||
// form组件属性
|
||||
form?: IForm;
|
||||
// 表单项
|
||||
formItems: IFormItems;
|
||||
}
|
||||
68
src/components/CURD/usePage.ts
Normal file
68
src/components/CURD/usePage.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { ref } from "vue";
|
||||
import type { IObject, PageContentInstance, PageModalInstance, PageSearchInstance } from "./types";
|
||||
|
||||
function usePage() {
|
||||
const searchRef = ref<PageSearchInstance>();
|
||||
const contentRef = ref<PageContentInstance>();
|
||||
const addModalRef = ref<PageModalInstance>();
|
||||
const editModalRef = ref<PageModalInstance>();
|
||||
|
||||
// 搜索
|
||||
function handleQueryClick(queryParams: IObject) {
|
||||
const filterParams = contentRef.value?.getFilterParams();
|
||||
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
|
||||
}
|
||||
// 重置
|
||||
function handleResetClick(queryParams: IObject) {
|
||||
const filterParams = contentRef.value?.getFilterParams();
|
||||
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
|
||||
}
|
||||
// 新增
|
||||
function handleAddClick() {
|
||||
//显示添加表单
|
||||
addModalRef.value?.setModalVisible();
|
||||
}
|
||||
// 编辑
|
||||
function handleEditClick(row: IObject) {
|
||||
//显示编辑表单 根据数据进行填充
|
||||
editModalRef.value?.setModalVisible(row);
|
||||
}
|
||||
// 表单提交
|
||||
function handleSubmitClick() {
|
||||
//根据检索条件刷新列表数据
|
||||
const queryParams = searchRef.value?.getQueryParams();
|
||||
contentRef.value?.fetchPageData(queryParams, true);
|
||||
}
|
||||
// 导出
|
||||
function handleExportClick() {
|
||||
// 根据检索条件导出数据
|
||||
const queryParams = searchRef.value?.getQueryParams();
|
||||
contentRef.value?.exportPageData(queryParams);
|
||||
}
|
||||
// 搜索显隐
|
||||
function handleSearchClick() {
|
||||
searchRef.value?.toggleVisible();
|
||||
}
|
||||
// 涮选数据
|
||||
function handleFilterChange(filterParams: IObject) {
|
||||
const queryParams = searchRef.value?.getQueryParams();
|
||||
contentRef.value?.fetchPageData({ ...queryParams, ...filterParams }, true);
|
||||
}
|
||||
|
||||
return {
|
||||
searchRef,
|
||||
contentRef,
|
||||
addModalRef,
|
||||
editModalRef,
|
||||
handleQueryClick,
|
||||
handleResetClick,
|
||||
handleAddClick,
|
||||
handleEditClick,
|
||||
handleSubmitClick,
|
||||
handleExportClick,
|
||||
handleSearchClick,
|
||||
handleFilterChange,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePage;
|
||||
Reference in New Issue
Block a user