vue3组合式api + ts + Elementplus 高度封装增删改查列表,(搜索、表格列表、新增、编辑、查看)
先展示功能
使用代码展示
test/index.vue
<script setup lang="ts">
import {
getTestList,
addTest,
updateTest,
deleteTest,
exportTest,
getTestType
}from'@/api/test'
import { tSettings, tHeader, sFormData, fStructure, dConfig, fConfig, fModel, fItem } from './config'
import { getQueryVO } from '@/api/test/types';
import { ComponentInternalInstance } from "vue";
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const searchFormData = ref({ ...sFormData });
const formStructure = ref<FormStructure[]>([...fStructure])
const tableData = ref<getQueryVO[]>([]);
const tableHeader = ref<TableHeader[]>([...tHeader]);
const tableSettings = ref<Settings>({ ...tSettings });
const dialogConfig = reactive({ ...dConfig })
const dialogVisible = ref(false)
const formModel = ref<FormModel>({ ...fModel })
const formConfig = reactive<FormConfig>({ ...fConfig })
const formItem = ref<FormItem[]>([...fItem])
// dialogForm确认按钮状态
const verifyLoading = ref(false);
const getList = async () => {
tableSettings.value.isLoading = true;
let params = {
pageSize: tableSettings.value.pageSize,
pageNum: tableSettings.value.pageNum,
...searchFormData.value
};
const res = await getTestList(params);
tableData.value = res.rows;
tableSettings.value.total = res.total;
tableSettings.value.isLoading = false;
};
const exportFile = async () => {
await exportTest({
...searchFormData.value
}, 'test列表.xlsx');
};
const addData = () => {
dialogConfig.dialogTitle = '新增test信息'
formConfig.type = 'edit'
formModel.value = { ...fModel }
dialogConfig.dialogFooterSaveBtn = true
dialogVisible.value = true
}
const editData = (row: getQueryVO) => {
dialogConfig.dialogTitle = '编辑test信息'
formConfig.type = 'edit'
formModel.value = { ...row }
dialogConfig.dialogFooterSaveBtn = true
dialogVisible.value = true
}
const viewData = (row: getQueryVO) => {
dialogConfig.dialogTitle = '查看test信息'
formConfig.type = 'view'
formModel.value = { ...row }
dialogConfig.dialogFooterSaveBtn = false
dialogVisible.value = true
}
const debouncedRefresh = async (ids: string) => {
const res = await deleteTest(ids);
proxy?.$modal.msgSuccess(res.msg);
getList()
}
const submitForm = async (data: getQueryVO) => {
try {
verifyLoading.value = true
const url = data.id ? updateTest : addTest
const res = await url(data)
proxy?.$modal.msgSuccess(res.msg);
verifyLoading.value = false
dialogVisible.value = false
getList()
} catch (error) {
verifyLoading.value = false
}
}
// 获取类型Opitons
const getUserList = async () => {
const res = await getTestType()
const list:Options[] = []
res.data.forEach((e:any)=>{
list.push({
label: e.label,
value: e.id
})
})
formStructure.value[0].options = list
formItem.value[0].options = list
}
onMounted(() => {
getUserList()
getList();
});
</script>
<template>
<div class="page-container">
<div class="page-header">
<form-search v-model:searchFormData="searchFormData" :formStructure="formStructure" @search="getList" />
</div>
<div class="page-body">
<div class="page-table-header">
<div class="page-table-title">test列表({{ tableSettings.total }})</div>
<div>
<el-button type="primary" @click="addData()" icon="CirclePlus"> 新增 </el-button>
<el-button @click="exportFile()" icon="Download" v-hasPermi="['hw:warehouse:add']"> 导出 </el-button>
</div>
</div>
<div style="flex: 1;">
<custom-table :tableData="tableData" :tableHeader="tableHeader" v-model:settings="tableSettings" @pageChange="getList()">
<template #controls="{ data }">
<div class="table-controls">
<div class="table-controls-item" @click="viewData(data)">详情</div>
<div class="table-controls-item" @click="editData(data)">编辑</div>
<div class="table-controls-item" style="color:#f25555">
<el-popconfirm title="确认删除吗?" @confirm="debouncedRefresh(data.id + '')">
<template #reference> 删除 </template>
</el-popconfirm>
</div>
</div>
</template>
</custom-table>
</div>
</div>
<dialog-form
v-model:dialogVisible="dialogVisible"
:dialogConfig="dialogConfig"
:verifyLoading="verifyLoading"
:formModel="formModel"
:formItem="formItem"
:formConfig="formConfig"
@submitForm="submitForm"
/>
</div>
</template>
<style lang="scss" scoped></style>
test/config.ts
/**
* 搜索组件初始值
*/
export const sFormData = {
type: undefined,
code: undefined,
name: undefined
}
/**
* @description 表单配置文件
*/
export const fStructure: FormStructure[] = [
{
type: 'select',
label: 'test类型',
prop: 'type',
options: []
},
{
type: 'input',
label: 'test编号',
prop: 'code',
},
{
type: 'input',
label: 'test名称',
prop: 'name',
},
]
/**
* @description 表格配置文件
*/
export const tSettings: Settings = {
isIndex: true,
align: 'center',
isPagination: true,
total: 0,
pageNum: 1,
pageSize: 10,
isLoading: true
}
/**
* @description: 表头、数据配置
*/
export const tHeader: TableHeader[] = [
{
label: 'test类型',
prop: 'type'
},
{
label: 'test编号',
prop: 'code'
},
{
label: 'test名称',
prop: 'name'
},
{
label: 'test责任人',
prop: 'people'
},
{
label: '启用时间',
prop: 'createdTime'
},
{
label: 'test环境状态',
prop: 'environmentalState'
},
{
label: 'test状态',
prop: 'warehouseStatus'
},
{
label: 'test容量(吨)',
prop: 'capacity'
},
{
label: '操作',
prop: 'controls',
fixed: 'right',
width: 250,
isTemplate: true
},
]
/* 弹窗配置 */
export const dConfig: DialogConfig = {
dialogTitle: "",
dialogWidth: "800px",
dialogModal: true,
dialogTop: '20px',
dialogFullscreen: false,
dialogClickModalClose: false,
dialogESCModalClose: false,
dialogDraggable: false,
dialogFooterBtn: true,
dialogFooterSaveBtn: true,
dialogDestroyOnClose: false
}
/* 表单配置 */
export const fConfig: FormConfig = {
type: 'edit',
inline: true,
labelWidth: '120px'
}
/**
* @description: 表单模型
*/
export const fModel: FormModel = {
id: undefined,
type: undefined,
code: undefined,
name: undefined,
createdTime: undefined,
people: undefined,
environmentalState: undefined,
status: undefined,
capacity: undefined
}
/**
* @description: 表单配置项
*/
export const fItem: FormItem[] = [
{
componentType: 'select',
label: 'test类型',
prop: 'type',
isRules: true,
options: []
},
{
componentType: 'input',
type: 'text',
label: 'test编号',
prop: 'code',
isRules: true
},
{
componentType: 'input',
type: 'text',
label: 'test名称',
prop: 'name',
isRules: true,
maxlength: 255
},
{
componentType: 'input',
type: 'text',
label: 'test责任人',
prop: 'people',
isRules: true,
maxlength: 10
},
{
componentType: 'date',
type: 'date',
label: '启用时间',
prop: 'createdTime',
isRules: true,
},
{
componentType: 'input',
type: 'number',
label: 'test总容量(吨)',
prop: 'capacity',
isRules: true,
maxlength: 10
},
{
componentType: 'input',
type: 'text',
label: 'test环境状态',
prop: 'warehouseEnvironmentalStatus',
isRules: true,
maxlength: 10
},
{
componentType: 'input',
type: 'text',
label: 'test状态',
prop: 'warehouseStatus',
isRules: true,
maxlength: 10
},
]
代码展示
1、所涉及的代码层级
├── src
│ └── api
│ └── test
│ └── index.ts
│ └── types.ts
│ └── components
│ └── CustomDialog
│ └── index.vue
│ └── CustomTable
│ └── index.vue
│ └── DialogForm
│ └── index.vue
│ └── FileUpload
│ └── index.vue
│ └── FormSearch
│ └── index.vue
│ └── types
│ └── global.d.ts
│ └── views
│ └── test
│ └── config.ts
│ └── index.vue
│ └── main.ts
├── .env.development
├── .eslintrc.js
├── .tsconfig.json
2、搜索区域封装
路径 /src/components/FormSearch/index.vue
<script setup lang="ts">
/**
调用方式
<form-search
v-model:searchFormData="searchFormData"
:formStructure="formStructure"
@search="getList"
/>
*/
interface SearchFormData {
[key: string]: any
}
interface SearchProps {
formStructure?: FormStructure[],
searchFormData: SearchFormData
}
const props = withDefaults(defineProps<SearchProps>(), {
searchFormData: () => ({}),
formStructure: () => ([])
})
const initialValue = ref(JSON.parse(JSON.stringify(props.searchFormData)))
const searchFormData = ref(JSON.parse(JSON.stringify(props.searchFormData)))
const formStructure = ref(JSON.parse(JSON.stringify(props.formStructure)))
const emit = defineEmits(['search', 'update:searchFormData'])
const search = () => {
emit('update:searchFormData', searchFormData.value)
emit('search')
}
const reset = () => {
emit('update:searchFormData', initialValue.value)
emit('search')
}
watch(() => props.searchFormData, (val) => {
searchFormData.value = JSON.parse(JSON.stringify(val))
})
watch(() => props.formStructure, (val) => {
formStructure.value = JSON.parse(JSON.stringify(val))
})
</script>
<template>
<div style="overflow: auto;">
<el-form
:model="searchFormData"
:inline="true"
>
<el-form-item
v-for="(item, index) in formStructure"
:key="index"
:label="item.label"
:prop="item.prop"
:label-width="item.labelWidth ? item.labelWidth + 'px' : '120px'"
>
<el-input
v-if="item.type === 'input'"
v-model="searchFormData[item.prop]"
:placeholder="item.placeholder || '请输入' + item.label"
:maxlength="item.length || '99'"
:clearable="item.clearable || true"
:style="item.style || 'width: 250px'"
/>
<el-select
v-else-if="item.type === 'select'"
:disabled="item.disabled"
v-model="searchFormData[item.prop]"
:style="item.style || 'width: 250px'"
:clearable="item.clearable || true"
:multiple="item.multiple"
:filterable="item.filterable || true"
:placeholder="item.placeholder || '请选择' + item.label"
>
<el-option
v-for="(option, index2) in item.options || []"
:key="index2"
:label="option['label']"
:value="option['value']"
/>
</el-select>
<el-date-picker
v-else-if="item.type === 'date' || item.type === 'datetime'"
v-model="item.value"
:type="item.type"
:placeholder="item.placeholder || '请选择' + item.label"
:style="item.style || 'width: 250px'"
/>
</el-form-item>
<el-form-item
prop=" "
label-width="120px"
>
<el-input style="width: 170px;visibility: hidden;" />
</el-form-item>
</el-form>
<div class="search-btn">
<el-button
type="primary"
@click="search()"
icon="search"
>
查询
</el-button>
<el-button
@click="reset()"
icon="RefreshLeft"
>
重置
</el-button>
</div>
</div>
</template>
<style lang='scss' scoped>
.search-btn {
float: right;
height: 0px;
height: 0;
position: relative;
top: -50px;
}
</style>
3、新增、编辑、查看Dialog封装
路径 /src/components/CustomDialog/index.vue
<script lang="ts" setup>
interface Props {
dialogVisible: boolean,
verifyLoading?: boolean,
dialogConfig: DialogConfig
}
const props = withDefaults(defineProps<Props>(), {
dialogVisible: false,
verifyLoading: false,
dialogConfig: () => {
return {
dialogTitle: "",
dialogWidth: "800px",
dialogModal: true,
dialogTop: '5vh',
dialogFullscreen: false,
dialogClickModalClose: false,
dialogESCModalClose: false,
dialogDraggable: false,
dialogFooterBtn: true,
dialogFooterSaveBtn: true,
dialogDestroyOnClose: false
}
}
})
const dialog = ref<boolean>(props.dialogVisible)
const emit = defineEmits(['verify', 'close'])
// 保存提交回调函数
const verifySubmit = () => {
emit('verify') //emit方法供父级组件调用
}
const uploadVisible = () => {
emit('close')
}
// watch监听
watch(() => props.dialogVisible, (newValue, oldValue) => {
dialog.value = newValue
}, { deep: true, immediate: true })
</script>
<template>
<el-dialog
v-model="dialog"
append-to-body
:modal="props.dialogConfig.dialogModal"
:close-on-click-modal="props.dialogConfig.dialogClickModalClose"
:close-on-press-escape="props.dialogConfig.dialogESCModalClose"
:draggable="props.dialogConfig.dialogDraggable"
:show-close="false"
:width="props.dialogConfig.dialogWidth"
:top="props.dialogConfig.dialogTop"
:destroy-on-close="props.dialogConfig.dialogDestroyOnClose"
class="custom-dialog"
>
<template #header>
<div class="custom-dialog-header">
<div style="font-size: 20px;font-weight: 600;">{{ props.dialogConfig.dialogTitle }}</div>
<div style="cursor: pointer;" @click="uploadVisible()">
<el-icon>
<CloseBold />
</el-icon>
</div>
</div>
</template>
<div style="max-height: 540px;overflow: auto;">
<slot></slot>
</div>
<template #footer v-if="props.dialogConfig.dialogFooterBtn">
<div style="border-top: 1px solid #f0f0f0;padding: 12px;display: flex;justify-content: center;">
<el-button
v-if="props.dialogConfig.dialogFooterSaveBtn"
type="primary"
@click="verifySubmit"
:disabled="props.verifyLoading"
:icon="props.verifyLoading ? 'Loading' : ''"
>确认</el-button
>
<el-button type="info" @click="uploadVisible()">取消</el-button>
</div>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.custom-dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 55px;
border-bottom: 1px solid #f0f0f0;
padding: 0 24px;
}
:deep(.el-dialog__footer) {
padding: 0 !important;
}
</style>
路径 /src/components/DialogForm/index.vue
<script lang="ts" setup>
import type { FormRules } from 'element-plus'
import { parseTime } from '@/utils/ruoyi'
import FileUpload from '@/components/FileUpload/index.vue'
interface Props {
dialogVisible: boolean,
verifyLoading: boolean,
dialogConfig: DialogConfig,
formModel: FormModel,
formItem: FormItem[],
formConfig: FormConfig
}
const props = withDefaults(defineProps<Props>(), {})
const formRules = ref<FormRules>({})
const verifyLoading = ref(props.verifyLoading)
const formModel = ref({ ...props.formModel })
const formRef = ref<any>(null)
const emit = defineEmits(['update:dialogVisible', 'submitForm'])
const messageStr = (type: ComponentType) => {
switch (type) {
case 'input':
return { message: '请输入', trigger: 'blur' }
case 'select':
return { message: '请选择', trigger: 'change' }
case 'date':
return { message: '请选择', trigger: 'change' }
case 'datetime':
return { message: '请选择', trigger: 'change' }
case 'radio':
return { message: '请选择', trigger: 'change' }
case 'checkbox':
return { message: '请选择', trigger: 'change' }
case 'upload':
return { message: '请上传', trigger: 'change' }
default:
return { message: '请输入', trigger: 'blur' }
}
}
props.formItem.forEach(e => {
if (e.isRules) {
formRules.value[e.prop] = [
{
required: true,
message: `${messageStr(e.componentType).message}${e.label}`,
trigger: messageStr(e.componentType).trigger
}
]
}
})
const closeDialog = () => {
emit('update:dialogVisible', false)
}
const dialogVerify = async () => {
await formRef.value?.validate((valid: boolean) => {
if (valid) {
const formData = { ...formModel.value }
// 日期自动格式化提交
props.formItem.forEach(e => {
if (e.type == 'date') {
formData[e.prop] = parseTime(formData[e.prop], '{y}-{m}-{d}')
}
if (e.type == 'datetime') {
formData[e.prop] = parseTime(formData[e.prop], '{y}-{m}-{d} {h}:{i}:{s}')
}
})
emit('submitForm', formData)
}
})
}
watch(() => props.dialogVisible, (newValue, oldValue) => {
if (newValue === true && formRef.value) {
formRef.value.resetFields()
}
if (newValue === false) {
closeDialog()
}
}, { deep: true, immediate: true })
watch(() => props.verifyLoading, (newValue, oldValue) => {
verifyLoading.value = newValue
}, { deep: true, immediate: true })
watch(() => props.formModel, (newValue, oldValue) => {
formModel.value = newValue
}, { deep: true, immediate: true })
</script>
<template>
<custom-dialog
:dialogVisible="props.dialogVisible"
:dialogConfig="props.dialogConfig"
:verifyLoading="verifyLoading"
@verify="dialogVerify"
@close="closeDialog"
>
<el-form :model="formModel" :rules="formRules" :inline="formConfig.inline" :label-width="formConfig.labelWidth || '100px'" ref="formRef">
<el-form-item v-for="(item, index) in formItem" :key="index" :label="item.label" :prop="item.prop">
<!-- 默认输入框 -->
<el-input
v-if="item.componentType === 'input'"
v-model="formModel[item.prop]"
:type="item.type ?? 'text'"
:placeholder="item.placeholder || `请输入${item.label}`"
:readonly="formConfig.type === 'view' ? true : item.disabled ? true : false"
:clearable="item.clearable"
:maxlength="item.maxlength !== undefined ? item.maxlength : 100"
:style="{ width: item.width ? item.width : '214px' }"
/>
<!-- 下拉框 -->
<el-select
v-else-if="item.componentType === 'select'"
v-model="formModel[item.prop]"
:placeholder="item.placeholder || `请选择${item.label}`"
:disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
:clearable="item.clearable"
:style="{ width: item.width ? item.width : '214px' }"
>
<el-option v-for="s in item.options" :key="s.value" :label="s.label" :value="s.value" />
</el-select>
<!-- 日期选择 -->
<el-date-picker
v-else-if="item.componentType === 'date'"
v-model="formModel[item.prop]"
type="date"
:placeholder="item.placeholder || `请选择${item.label}`"
:disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
:style="{ width: item.width ? item.width : '214px' }"
/>
<!-- 日期时间选择选择 -->
<el-date-picker
v-else-if="item.componentType === 'datetime'"
v-model="formModel[item.prop]"
type="datetime"
:placeholder="item.placeholder || `请选择${item.label}`"
:disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
:style="{ width: item.width ? item.width : '214px' }"
/>
<!-- 单选框 -->
<el-radio-group
v-else-if="item.componentType === 'radio'"
v-model="formModel[item.prop]"
:disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
:placeholder="item.placeholder || `请选择${item.label}`"
:style="{ width: item.width ? item.width : '214px' }"
>
<el-radio-group size="large" v-for="radio in item.options" :key="radio.value" :label="radio.label">
{{ radio.label }}.value
</el-radio-group>
</el-radio-group>
<!-- 复选框 -->
<el-checkbox-group
v-else-if="item.componentType === 'checkbox'"
v-model="formModel[item.prop]"
:disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
:placeholder="item.placeholder || `请选择${item.label}`"
:style="{ width: item.width ? item.width : '214px' }"
>
<el-checkbox v-for="checkbox in item.options" :key="checkbox.value" :label="checkbox.label">{{ checkbox.label }}</el-checkbox>
</el-checkbox-group>
<file-upload
v-if="item.componentType === 'upload'"
v-model:modelValue="formModel[item.prop]"
:limit="item.limit ? item.limit : 5"
:fileSize="item.fileSize ? item.fileSize : 5"
:fileType="item.fileType ? item.fileType : undefined"
:disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
/>
</el-form-item>
</el-form>
</custom-dialog>
</template>
<style lang="scss" scoped></style>
路径 /src/components/FileUpload/index.vue
<script setup lang="ts">
import { getToken } from "@/utils/auth";
import { ComponentInternalInstance } from "vue";
import { ElUpload, UploadFile } from "element-plus";
const VITE_APP_BASE_API = ref(import.meta.env.VITE_APP_BASE_API)
const props = defineProps({
modelValue: [String, Object, Array],
// 数量限制
limit: {
type: Number,
default: 10,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 10,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ['jpeg', 'jpg', 'png', 'doc', 'docx', 'xls', 'xlsx', 'pdf'],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue', 'uploadSuccess']);
const number = ref(0);
const uploadList = ref<any[]>([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + "/common/upload"); // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref<any[]>([]);
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
const fileUploadRef = ref(ElUpload);
watch(() => props.modelValue, async val => {
if (val) {
let temp = 1;
// 首先将值转为数组
let list = [];
if (Array.isArray(val)) {
list = val;
} else {
list = val.split(',');
}
// 然后将数组转为对象数组
fileList.value = list.map((item: any) => {
item = { name: item, url: item, ossId: item };
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
} else {
fileList.value = [];
return [];
}
}, { deep: true, immediate: true });
// 上传前校检格式和大小
const handleBeforeUpload = (file: any) => {
// 校检文件类型
if (props.fileType.length) {
const fileName = file.name.split('.');
const fileExt = fileName[fileName.length - 1];
const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
if (!isTypeOk) {
proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
return false;
}
}
// 校检文件大小
if (props.fileSize) {
const isLt = file.size / 1024 / 1024 < props.fileSize;
if (!isLt) {
proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
return false;
}
}
proxy?.$modal.loading("正在上传文件,请稍候...");
number.value++;
return true;
}
// 文件个数超出
const handleExceed = () => {
proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}
// 上传失败
const handleUploadError = () => {
proxy?.$modal.msgError("上传文件失败");
proxy?.$modal.closeLoading();
}
// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
if (res.code === 200) {
uploadList.value.push({ name: res.originalFilename, url: res.fileName, ossId: res.fileName });
emit('uploadSuccess', file, file, uploadList.value)
uploadedSuccessfully();
} else {
number.value--;
proxy?.$modal.closeLoading();
proxy?.$modal.msgError(res.msg);
fileUploadRef.value.handleRemove(file);
uploadedSuccessfully();
}
}
// 删除文件
const handleDelete = (index: number) => {
let ossId = fileList.value[index].ossId;
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
}
// 上传结束处理
const uploadedSuccessfully = () => {
if (number.value > 0 && uploadList.value.length === number.value) {
fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
uploadList.value = [];
number.value = 0;
emit("update:modelValue", listToString(fileList.value));
proxy?.$modal.closeLoading();
}
}
// 获取文件名称
const getFileName = (name: string) => {
// 如果是url那么取最后的名字 如果不是直接返回
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return name;
}
}
// 对象转成指定字符串分隔
const listToString = (list: any[], separator?: string) => {
let strs = "";
separator = separator || ",";
list.forEach(item => {
if (item.ossId) {
strs += item.ossId + separator;
}
})
return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
</script>
<template>
<div class="upload-file">
<el-upload multiple :action="uploadFileUrl" :before-upload="handleBeforeUpload" :file-list="fileList" :limit="limit"
:on-error="handleUploadError" :on-exceed="handleExceed" :on-success="handleUploadSuccess" :show-file-list="false"
:headers="headers" :accept="fileType ? fileType.join(',') : ''" class="upload-file-uploader" ref="fileUploadRef"
:disabled="disabled">
<!-- 上传按钮 -->
<el-button type="primary" :disabled="disabled">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" style="line-height: 15px" v-if="showTip">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件
</div>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${VITE_APP_BASE_API + file.url}`" :underline="false" target="_blank"
style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" class="el-icon-document"> {{
getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action" style="flex-shrink: 0;">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
</div>
</li>
</transition-group>
</div>
</template>
<style scoped lang="scss">
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
justify-content: space-between;
align-items: center;
color: inherit;
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
:deep(.el-link__inner) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
4、表格区域封装
路径 /src/components/CustomTable/index.vue
<script setup lang="ts">
/*传值说明:
settings:{ 相关配置
isLoading 加载数据时显示动效
height 表格高度
isSelection; 是否有多选
isIndex 是否需要序列号,默认不需要
isBorder: 是否加框线,默认添加,
isPagination: 是否添加分页,默认false,
pageSize 每页多少条
paginationHeight分页高度
total: 列表总条数
pageNum 当前页数
align 数据对齐方式menu[left,center,right]
}
tableData: 表格数据
tableHeader:{ 表头数据
label 表头文本
prop tableData对应字段
isTemplate 是否使用插槽
type 对应列的类型。'default' | 'selection' | 'index' | 'expand'
width
fixed 列是否固定在左侧或者右侧。 true 表示固定在左侧
}
事件说明:
pageSize pageSize发生变化
pageNum pageNum发生变化
handleSelect 勾选发生变化
调用方式
<custom-table
:tableData="tableData"
:tableHeader="tableHeader"
v-model:settings="tableSettings"
@pageChange="getList()"
></custom-table>
*/
export interface TableProps {
tableHeader: TableHeader[],
settings: Settings,
tableData: any[]
}
const props = withDefaults(defineProps<TableProps>(), {
tableHeader: () => { return [] },
tableData: () => { return [] },
settings: () => {
return {
height: 60,
isBorder: true,
isLoading: false,
isIndex: false,
isSelection: false,
isPagination: false,
paginationHeight: 75,
pageNum: 1,
pageSize: 10,
pageSizes: [5, 10, 15],
total: 0,
align: 'center',
}
},
})
// pageChange
const emit = defineEmits(['pageChange', 'handleSelect', 'update:settings'])
const customTable = ref<any>(null)
const pageChange = (type: 'pageSize' | 'pageNum', num: number) => {
const settingsData = {
...props.settings,
[type]: num
}
emit('update:settings', settingsData)
emit('pageChange')
}
const headerCellStyle = () => {
return {
'background-color': '#e9f0ee !important',
'font-size': '14px',
'font-weight': 400,
'color': '#333333',
'border': '1px solid #c2cccb',
'border-right': '0px',
'height': '64px !important'
}
}
const cellStyle = () => {
return {
'font-weight': 400,
'color': '#000000',
'font-size': '14px',
// 'border': '1px solid #d7e6e5',
'height': '50px !important',
'padding': '0 !important'
}
}
const height = computed(() => {
let height: number = 0
if (customTable.value) {
height = customTable.value.offsetHeight
if (props.settings.isPagination) {
height = height - (props.settings.paginationHeight as number ? props.settings.paginationHeight as number : 75)
}
}
return height ? height : undefined
})
</script>
<template>
<!-- 添加一层定位,方式flex布局时flex为1时宽度自动变宽 -->
<div style="width: 100%;height: 100%;position: relative;">
<div class="custom-table" ref="customTable">
<el-table
:height="height"
v-loading="settings.isLoading || false"
@selection-change="(e: any) => emit('handleSelect', e)"
:data="tableData"
style="width: 100%;border: 1px solid #f0f0f0;"
row-key="id"
:indent="0"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
stripe
:header-cell-style="headerCellStyle"
:cell-style="cellStyle"
:default-expand-all="false"
border
>
<el-table-column v-if="settings.isSelection" width="55" type="selection" fixed :align="settings.align"></el-table-column>
<el-table-column
v-if="settings.isIndex"
type="index"
:index="1"
fixed
label="序号"
width="55"
:align="settings.align"
style="color: #007EFF;"
></el-table-column>
<template v-for="item in tableHeader">
<template v-if="!item.isTemplate">
<el-table-column
:key="item.prop"
:type="item.type"
:prop="item.prop"
:label="item.label"
:width="item.width ? item.width : tableHeader.length > 12 ? 130 : undefined"
:fixed="item.fixed"
:show-overflow-tooltip="true"
:align="settings.align"
></el-table-column>
</template>
<!-- 使用插槽自定义 -->
<template v-else>
<el-table-column
:key="item.prop"
:type="item.type"
:label="item.label"
:width="item.width"
:fixed="item.fixed"
:show-overflow-tooltip="true"
:align="settings.align"
>
<template v-slot="{ row }">
<slot :name="item.prop" :data="row" />
</template>
</el-table-column>
</template>
</template>
<slot name="action"></slot>
<template #empty>
<div class="table-null" v-if="!settings.isLoading">
<img src="@/assets/images/public/zanwushuju.png" alt="" style="width: 150px;height: 150px;" />
<div class="empty-text">暂无数据</div>
</div>
</template>
</el-table>
<div class="tab-footer-pagination" :style="'height:' + props.settings.paginationHeight + 'px'" v-if="settings.isPagination">
<el-pagination
background
style="text-align:right;padding:25px 15px 25px 0;overflow-x: auto;overflow-y:hidden"
@size-change="(e: any) => pageChange('pageSize', e)"
@current-change="(e: any) => pageChange('pageNum', e)"
:current-page="settings.pageNum"
:page-sizes="settings.pageSizes"
:page-size="settings.pageSize"
layout=" prev, pager, next, sizes,jumper"
:total="settings.total"
></el-pagination>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.custom-table {
width: 100%;
// flex: 1;
height: 100%;
box-sizing: border-box;
box-sizing: border-box;
background-color: #ffffff;
position: absolute;
}
.empty-text {
font-size: 18px;
color: #999;
font-weight: 400;
text-align: center;
height: 24px;
line-height: 24px;
}
.el-table .el-table__expand-icon {
display: none;
}
.el-table__row--level-1 {
background-color: #F0EDFF;
}
.el-table__body tr.current-row>td {
background-color: #fff !important;
}
.tab-footer-pagination {
display: flex;
justify-content: flex-end;
margin-right: 27px;
height: 75px;
background-color: #ffffff;
}
.table-null {
margin: auto;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* // 表格斑马自定义颜色 */
:deep(.el-table__row.warning-row) {
background: #f6fbfa;
}
/* // 修改高亮当前行颜色 */
:deep(.el-table tbody tr:hover>td) {
background: #ccf1e9 !important;
}
/* 全局样式或组件的<style>标签中 */
//强制显示横向滚动条
:deep(.el-scrollbar__bar.is-horizontal) {
display: block !important;
}
//滚动条颜色
:deep(.el-scrollbar__thumb) {
background: #d4d8de !important;
opacity: 1 !important;
}
//强制显示纵向滚动条
:deep(.el-scrollbar__bar.is-vertical) {
display: block !important;
}
</style>
5、其他代码
路径 /src/types/global.d.ts
import { FormRules } from 'element-plus';
declare global {
/**
* 界面字段隐藏属性
*/
interface FieldOption {
key: number;
label: string;
visible: boolean;
}
/**
* 弹窗属性
*/
interface DialogOption {
/**
* 弹窗标题
*/
title?: string;
/**
* 是否显示
*/
visible: boolean;
}
interface UploadOption {
/** 设置上传的请求头部 */
headers: { [key: string]: any };
/** 上传的地址 */
url: string;
}
/**
* 导入属性
*/
interface ImportOption extends UploadOption {
/** 是否显示弹出层 */
open: boolean;
/** 弹出层标题 */
title: string;
/** 是否禁用上传 */
isUploading: boolean;
/** 其他参数 */
[key: string]: any;
}
/**
* 字典数据 数据配置
*/
interface DictDataOption {
label: string;
value: string;
elTagType?: ElTagType;
elTagClass?: string;
}
interface BaseEntity {
createBy?: any;
createTime?: string;
updateBy?: any;
updateTime?: any;
}
/**
* 分页数据
* T : 表单数据
* D : 查询参数
*/
interface PageData<T, D> {
form: T;
queryParams: D;
rules: FormRules;
}
/**
* 分页查询参数
*/
interface PageQuery {
pageNum: number;
pageSize: number;
}
/**
* 表单结构
*/
interface FormStructure {
type: 'input' | 'select' | 'date' | 'datetime',
label: string,
prop: string,
placeholder?: string,
length?: string,
disabled?: boolean,
style?: string,
multiple?: boolean,
filterable?: boolean,
clearable?: boolean,
options?: { label: string; value: string | number; }[];
labelWidth?: string,
}
/**
* 表格头部
*/
interface TableHeader {
label: string;
prop: string;
type?: string,
isTemplate?: boolean;
width?: string | number;
fixed?: boolean | 'left' | 'right',
}
/**
* 表格数据
*/
interface Settings {
height?: number | string;
isBorder?: boolean;
isLoading?: boolean;
isIndex?: boolean;
isSelection?: boolean;
isPagination?: boolean;
paginationHeight?: number;
pageNum: number;
pageSize: number;
pageSizes?: number[];
total?: number;
align?: string;
}
/**
* 模态框配置
* @param dialogTitle: string; //模态框标题名称
* @param dialogWidth: string; //模态框弹窗宽度
* @param dialogModal: boolean; //是否需要模态框(遮罩层)
* @param dialogTop: string, //模态框距离顶部距离
* @param dialogFullscreen: boolean; //模态框是否为全屏
* @param dialogClickModalClose: any; //是否可以通过点击遮罩层关闭Dialog
* @param dialogESCModalClose: any; //是否可以通过按下ESC关闭Dialog
* @param dialogDraggable: any; //是否开启模态框拖拽功能
* @param dialogFooterBtn: any; //是否开启底部操作按钮
* @param dialogFooterSaveBtn: any; //是否开启底部确认按钮
* @param dialogDestroyOnClose: boolean; //是否在关闭时销毁
*/
interface DialogConfig {
dialogTitle: string; //模态框标题名称
dialogWidth: string; //模态框弹窗宽度
dialogModal: boolean; //是否需要模态框(遮罩层)
dialogTop: string, //模态框距离顶部距离
dialogFullscreen: boolean; //模态框是否为全屏
dialogClickModalClose: any; //是否可以通过点击遮罩层关闭Dialog
dialogESCModalClose: any; //是否可以通过按下ESC关闭Dialog
dialogDraggable: any; //是否开启模态框拖拽功能
dialogFooterBtn: any; //是否开启底部操作按钮
dialogFooterSaveBtn: any; //是否开启底部确认按钮
dialogDestroyOnClose: boolean; //是否在关闭时销毁
}
type ComponentType = 'input' | 'select' | 'date' | 'datetime' | 'radio' | 'checkbox' | 'upload'
type Types = 'text' | 'date' | 'datetime' | 'textarea' | 'number' | 'password'
/**
* 表单配置
* @param componentType 组件类型
* @param type 字段类型
* @param label 标签
* @param prop 字段名
* @param isRules 是否校验
* @param maxlength 最大长度
* @param options 选项
* @param clearable 是否可清除
* @param placeholder 占位符
* @param width 宽度
* @param limit 最大上传数量
* @param fileType 文件类型
* @param fileSize 文件大小
*/
interface FormItem {
componentType: ComponentType,
type?: Types,
label: string,
prop: string,
isRules: boolean,
maxlength?: number,
options?: { label: string, value: string }[],
clearable?: boolean,
placeholder?: string,
width?: string,
limit?: number,
fileType?: string[],
fileSize?: number,
disabled?: boolean
}
interface FormConfig {
type: 'edit' | 'view',
inline: boolean,
labelWidth: string | number
}
interface FormModel {
[key: string]: any
}
interface Options {
label: string,
value: string
}
mian.ts
...
// 全局组件
import CustomTable from '@/components/CustomTable/index.vue';
import FormSearch from '@/components/FormSearch/index.vue';
import CustomDialog from '@/components/CustomDialog/index.vue';
import DialogForm from '@/components/DialogForm/index.vue';
...
// 全局组件
app.component('custom-table', CustomTable)
app.component('form-search', FormSearch)
app.component('custom-dialog', CustomDialog)
app.component('dialog-form', DialogForm)
...
.env.development
...
# 开发环境
VITE_APP_BASE_API = '/dev-api'
.eslintrc.js
module.exports = {
...
include: [...其他规则...,'src/types/**/*.d.ts'],
...
}
tsconfig.json
{
...
"include": [...其他规则..., "src/types/**/*.d.ts"],
...
}