编辑类表单
AddEditFormComponent.vue
<template>
<el-form ref="addEditFormRef" :model="form" :rules="rules" class="addEdit_form" :label-width="labelWidth" :disabled="disabled">
<template v-for="item in formItemList" :key="item.prop">
<el-form-item :prop="item.prop" :label="item.label" :label-width="item.labelWidth" :class="{ 'required ': item.isRequired }">
<slot :name="item.prop" :value="form[item.prop]" :form="form">
<template v-if="item.type === 'input'">
<el-input v-model.trim="form[item.prop]" :placeholder="item.placeholder || '请输入'" clearable :maxlength="item.maxlength || null">
<template #suffix>
<span v-if="item.suffix">{{ item.suffix }}</span>
</template>
</el-input>
</template>
<template v-else-if="item.type === 'textarea'">
<el-input
v-model="form[item.prop]"
:placeholder="item.placeholder || '请输入内容'"
clearable
:autosize="{ minRows: 3 }"
type="textarea"
:disabled="item.disabled || false"
:maxlength="item.maxlength || null"
show-word-limit
/>
</template>
<template v-else-if="item.type === 'text'">
<span class="form-text">{{ item.formatter ? item.formatter(form[item.prop]) : form[item.prop] }}</span>
</template>
<template v-else-if="item.type === 'select'">
<el-select v-model="form[item.prop]" :placeholder="item.placeholder || '请选择'" clearable :filterable="item.filterable" :disabled="item.disabled || false">
<el-option v-for="option in item.options || []" :key="item.prop + '_option_' + option.value" :label="option.name" :value="option.value" />
</el-select>
</template>
<template v-else-if="item.type === 'radio'">
<el-radio-group v-model="form[item.prop]" :disabled="item.disabled || false">
<el-radio v-for="option in item.options || []" :key="item.prop + '_option_' + option.value" :label="option.value">
{{ option.name }}
</el-radio>
</el-radio-group>
</template>
<template v-else-if="item.type === 'checkbox'">
<el-checkbox-group v-model="form[item.prop]" :disabled="item.disabled || false">
<el-checkbox v-for="option in item.options || []" :key="item.prop + '_option_' + option.value" :label="option.value">
{{ option.name }}
</el-checkbox>
</el-checkbox-group>
</template>
<template v-else-if="item.type === 'daterange'">
<el-date-picker v-model="form[item.prop]" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" clearable :disabled="item.disabled || false" />
</template>
<template v-else-if="item.type === 'date'">
<el-date-picker v-model="form[item.prop]" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" clearable :disabled="item.disabled || false" />
</template>
<template v-else-if="item.type === 'yearrange'">
<date-picker v-model="form[item.prop]" type="yearrange" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY" clearable :disabled="item.disabled || false" />
</template>
<template v-else-if="item.type === 'year'"><el-date-picker v-model="form[item.prop]" type="year" value-format="YYYY" placeholder="请选择"></el-date-picker></template>
<template v-else-if="item.type === 'upload'">
<BasicUploadComponent v-model:fileList="form[item.prop]" :maxLength="item.maxLength" @success="onUploadSuccess(item.prop)"></BasicUploadComponent>
</template>
</slot>
</el-form-item>
</template>
</el-form>
</template>
<script setup>
const { $deepCopy } = getCurrentInstance().appContext.config.globalProperties;
import { validateFormsByFormRefs } from '@/utils/validateForms';
import BasicUploadComponent from './BasicUploadComponent.vue';
const props = defineProps({
rules: {
type: Object,
default: () => {},
},
labelWidth: {
type: String,
default: '80px',
},
formItemList: {
type: Array,
default: () => [],
},
formData: {
type: Object,
default: () => {},
},
disabled: {
type: Boolean,
default: false,
},
});
const form = ref({});
const addEditFormRef = ref(null);
watch(
props.formData,
(newVal) => {
console.log('watch---formData', newVal);
form.value = $deepCopy(newVal);
},
{ deep: true, immediate: true }
);
const onUploadSuccess = (prop) => {
addEditFormRef.value.clearValidate(prop);
};
const submitForm = async () => {
await Promise.all(validateFormsByFormRefs([addEditFormRef.value])).catch((err) => {
console.log('err', err);
return Promise.reject(err);
});
return $deepCopy(form.value);
};
const resetFields = () => {
addEditFormRef.value && addEditFormRef.value.resetFields();
};
defineExpose({
resetFields,
submitForm,
});
</script>
<style lang="scss" scoped>
.addEdit_form {
::v-deep .required {
.el-form-item__label {
&::before {
content: '*';
color: #ff4d4f;
margin-right: 4px;
}
}
}
}
</style>
示例
<template>
<h2>addEditFormView</h2>
<AddEditFormComponent style="width: 600px" ref="addEditFormComponentRef" :formData="formData" :formItemList="formItemList" :rules="rules" labelWidth="160px"></AddEditFormComponent>
<el-button @click="onSubmit" type="primary">提交</el-button>
</template>
<script setup>
import AddEditFormComponent from '@/components/AddEditFormComponent.vue';
const formData = reactive({
artist: 'bwf',
type: 2,
creationYear: '',
artworkDescribe: '',
imgs: [],
});
const formItemList = ref([
{ prop: 'artist', label: '艺术家', type: 'input' },
{ prop: 'type', type: 'select', label: '类型:', options: [] },
{ prop: 'creationYear', type: 'year', label: '创作年代:' },
{ prop: 'artworkDescribe', label: '艺术品描述:', isRequired: true, type: 'textarea' },
{ prop: 'imgs', label: '艺术家图片', type: 'upload', maxLength: 1 },
]);
const setTypeOptions = async () => {
const res = await Promise.resolve([
{ name: '类型1', value: 1 },
{ name: '类型2', value: 2 },
{ name: '类型3', value: 3 },
]);
formItemList.value.find((item) => item.prop === 'type').options = res;
};
setTimeout(() => {
setTypeOptions();
}, 500);
const rules = {
artist: [{ required: true, message: '请输入艺术家' }],
type: [{ required: true, message: '请选择类型' }],
creationYear: [{ required: true, message: '请选择创作年代' }],
artworkDescribe: [{ required: true, message: '请输入艺术品描述' }],
imgs: [{ required: true, message: '请上传艺术家图片' }],
};
const addEditFormComponentRef = ref(null);
const onSubmit = async () => {
const formData = await addEditFormComponentRef.value.submitForm();
console.log('onSubmit', formData);
};
</script>
<style scoped></style>
上传组件
BasicUploadComponent.vue
<template>
<!-- 支持单张以及批量 手动上传 -->
<el-upload
action=""
:class="{ hidden: isDisabled || fileList.length >= maxLength }"
multiple
:file-list="fileList"
class="upload_wrapper"
:style="{ width: uploadBoxWidth }"
:limit="maxLength"
:on-exceed="onExceed"
:before-upload="beforeUpload"
list-type="picture-card"
:http-request="handleUpload"
:on-remove="onRemove"
:on-success="onSuccess"
:on-preview="onPreview"
>
<el-icon><Plus /></el-icon>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</template>
<script setup>
import { Plus } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
fileList: {
type: Array,
default: () => [],
},
maxLength: {
type: Number,
default: 10,
},
uploadBoxWidth: {
type: String,
},
isDisabled: {
type: Boolean,
default: false,
},
allowTypes: {
type: Array,
default: () => ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp', 'image/gif'],
},
maxSize: {
type: Number,
default: 5,
},
});
const emits = defineEmits(['update:fileList', 'success']);
const dialogImageUrl = ref('');
const dialogVisible = ref(false);
const onPreview = (file) => {
console.log('onPreview', file);
dialogImageUrl.value = file.url;
dialogVisible.value = true;
};
const onRemove = (file, fileList) => {
console.log('onRemove', file, fileList);
emits('update:fileList', fileList);
};
const onExceed = () => {
ElMessage({
message: `最大允许上传${props.maxLength}张图片`,
type: 'warning',
});
};
const beforeUpload = (file) => {
console.log('beforeUpload', file);
const typePassed = props.allowTypes.includes(file.type);
const sizePassed = file.size / 1024 / 1024 < props.maxSize;
if (!typePassed) {
ElMessage({
message: `文件格式不正确!`,
type: 'error',
});
return false;
}
if (!sizePassed) {
ElMessage({
message: `上传文件大小不能超过 ${props.maxSize}MB!`,
type: 'error',
});
return false;
}
return true;
};
const handleUpload = async (fileInfo) => {
console.log('handleUpload', fileInfo);
const file = fileInfo.file;
const formData = new FormData();
formData.append('file', file);
const imgTypes = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp', 'image/gif'];
const fileTypes = ['application/pdf'];
let res = {};
if (imgTypes.includes(file.type)) {
res = await new Promise((resolve) => {
setTimeout(() => {
resolve({
url: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
fileId: `${new Date().getTime()}`,
});
}, 800);
});
} else if (fileTypes.includes(fileInfo.file.type)) {
res = await bigFileUploadRequest(formData);
}
return res;
};
const onSuccess = (res, file, fileList) => {
console.log('onSuccess', fileList);
const files = fileList.map((item) => {
const file = { ...(item.response ?? item), uid: item.uid };
return file;
});
emits('update:fileList', files);
emits('success', files);
};
</script>
<style lang="scss" scoped>
.upload_wrapper {
&.hidden {
::v-deep .el-upload {
display: none;
}
}
}
</style>
示例
<template>
<h2>basicUploadView</h2>
<BasicUploadComponent v-model:fileList="fileList" :maxLength="1"></BasicUploadComponent>
<el-button @click="onSubmit" type="primary">提交</el-button>
</template>
<script setup>
import BasicUploadComponent from '@/components/BasicUploadComponent.vue';
const fileList = ref([]);
const onSubmit = () => {
console.log('onSubmit', fileList.value);
};
</script>
<style scoped></style>
筛选组件
FilterFormComponent.vue
<template>
<el-form ref="filterFormRef" class="filter_form" :model="filterModel" :inline="isInline" :label-width="labelWidth" :label-position="labelPosition">
<div class="filter_wrapper">
<el-form-item v-for="item in filters" :key="item.prop" :label="item.label + ':'" :prop="item.prop">
<slot :name="item.prop">
<template v-if="item.type === 'input'">
<el-input v-model.trim="filterModel[item.prop]" :placeholder="item.placeholder || '请输入'" clearable :style="{ width: item.width }"> </el-input>
</template>
<template v-if="item.type === 'select'">
<el-select v-model="filterModel[item.prop]" :placeholder="item.placeholder || '请选择'" clearable :multiple="item.multiple || false" collapse-tags :style="{ width: item.width }">
<el-option v-for="option in item.options || []" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</template>
<template v-if="item.type === 'daterange'">
<el-date-picker
v-model="filterModel[item.prop]"
type="daterange"
:value-format="item.valueFormat || 'YYYY-MM-DD'"
start-placeholder="开始日期"
end-placeholder="结束日期"
:placeholder="item.placeholder || '请选择'"
clearable
:style="{ width: item.width }"
/>
</template>
<template v-if="item.type === 'yearrange'">
<date-picker
v-model="filterModel[item.prop]"
type="yearrange"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY"
:placeholder="item.placeholder || '请选择'"
clearable
:style="{ width: item.width }"
/>
</template>
</slot>
</el-form-item>
</div>
<div class="handle_wrapper">
<el-form-item>
<el-button type="primary" @click="onSubmit">搜索</el-button>
<el-button @click="onReset">重置</el-button>
<slot name="exOperation"></slot>
</el-form-item>
</div>
</el-form>
</template>
<script setup>
const props = defineProps({
labelPosition: {
type: String,
default: 'right',
},
isInline: {
type: Boolean,
default: true,
},
filters: {
type: Array,
default: () => [],
},
labelWidth: {
type: String,
},
});
const emits = defineEmits(['submit', 'reset']);
const filterFormRef = ref(null);
const filterModel = reactive({});
const onSubmit = () => {
console.log('onSubmit', filterModel);
emits('submit', filterModel);
};
const onReset = () => {
console.log('onReset');
filterFormRef.value.resetFields();
};
</script>
<style lang="scss" scoped>
.filter_form {
padding: 0 24px;
display: flex;
align-items: flex-end;
.filter_wrapper {
flex: 1;
}
}
</style>
分页
PaginationComponent.vue
<template>
<div class="pagination_wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="sizesOptions"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup>
import { paginationKey } from '@/keys';
const { currentPage, pageSize, total, updateCurrentPage, updatePagesize, paginationChange, sizesOptions = [10, 20, 50, 100] } = inject(paginationKey);
const handleSizeChange = (val) => {
updateCurrentPage(1);
updatePagesize(val);
paginationChange();
};
const handleCurrentChange = (val) => {
updateCurrentPage(val);
paginationChange();
};
</script>
<style lang="scss" scoped>
.pagination_wrapper {
margin-top: 10px;
display: flex;
justify-content: flex-end;
padding: 0 24px;
}
</style>
表格
BasicTableComponent.vue
<template>
<el-table ref="basicTableComponentRef" @sort-change="onSortChange" :data="tableDatas" style="width: 100%" stripe @selection-change="onSelectionChange" v-bind="$attrs" v-loading="loading">
<template v-for="item in columns">
<el-table-column
v-if="['index', 'selection'].includes(item.type)"
:key="item.type"
:type="item.type || ''"
:index="item.index || columnIndex"
:label="item.label"
:align="item.align || 'center'"
v-bind="item.attrs || {}"
/>
<el-table-column
v-else
:sortable="item.sortable || false"
:key="item.prop"
:prop="item.prop"
:label="item.label"
:align="item.align || 'center'"
:show-overflow-tooltip="item.showOverflowTooltip"
v-bind="item.attrs || {}"
>
<template #default="{ row, column }">
<slot :name="item.prop" :row="row" :column="column">
{{ item.formatter ? item.formatter(row[column.property], row) : row[column.property] }}
</slot>
</template>
</el-table-column>
</template>
</el-table>
<PaginationComponent v-if="showPagination"></PaginationComponent>
</template>
<script setup>
import PaginationComponent from './PaginationComponent.vue';
const props = defineProps({
tableDatas: {
type: Array,
default: () => [],
},
columns: {
type: Array,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
showPagination: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(['sortChange']);
const multipleSelection = ref([]);
const columnIndex = (index) => {
return index + 1;
};
const onSelectionChange = (val) => {
multipleSelection.value = val;
console.log('multipleSelection', multipleSelection.value);
};
const onSortChange = ({ column, prop, order }) => {
console.log('onSortChange', column, prop, order);
emits('sortChange', { prop, order });
};
defineExpose({
multipleSelection,
});
</script>
<style scoped></style>
示例
<template>
<h2>basicTableView</h2>
<FilterFormComponent :filters="filters" @submit="onSubmit"></FilterFormComponent>
<p style="text-align: right">
<el-button type="primary" @click="onBatchDel">批量删除</el-button>
</p>
<BasicTableComponent ref="tableComponentRef" :tableDatas="tableDatas" :columns="columns" @sortChange="onSortChange">
<template #operation="{ row }">
<el-button type="primary" @click="onEdit(row)">编辑</el-button>
</template>
</BasicTableComponent>
</template>
<script setup>
import BasicTableComponent from '@/components/BasicTableComponent.vue';
import FilterFormComponent from '@/components/FilterFormComponent.vue';
import { paginationKey } from '@/keys';
import usePaginations from '@/composables/usePaginations';
const { paginations, updateCurrentPage, updatePagesize } = usePaginations();
const filters = reactive([
{
prop: 'name',
label: '姓名',
type: 'input',
width: '200px',
},
{
prop: 'date',
label: '日期',
type: 'daterange',
},
{
prop: 'gender',
label: '性别',
type: 'select',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
],
},
]);
const tableComponentRef = ref(null);
const tableDatas = ref([
]);
const columns = [
{ type: 'selection', label: '' },
{ prop: 'date', label: '日期' },
{ prop: 'name', label: '姓名' },
{ prop: 'age', label: '年龄' },
{
prop: 'gender',
label: '性别',
formatter: (val, row) => {
if (val === 'female') return '女';
return '男';
},
},
{ prop: 'address', label: '地址' },
{ prop: 'operation', label: '操作' },
];
const onSubmit = (filters) => {
console.log('onSubmit', filters);
};
const paginationChange = () => {
console.log('pagination---chage', paginations);
};
provide(paginationKey, {
...toRefs(paginations),
updateCurrentPage,
updatePagesize,
paginationChange,
});
const onEdit = (row) => {
console.log('onEdit---row', row);
};
const onBatchDel = () => {
console.log(
'onBatchDel',
tableComponentRef.value.multipleSelection.map((item) => item.id)
);
};
const gettableDatass = async () => {
const datas = await Promise.resolve([
{
id: '1',
date: '2016-05-03',
name: 'Tom',
age: 18,
gender: 'female',
address: 'No. 189, Grove St, Los Angeles',
},
{
id: '2',
date: '2018-05-03',
name: 'Alice',
age: 23,
gender: 'female',
address: 'No. 189, Grove St, Los Angeles',
},
{
id: '3',
date: '2015-05-03',
name: 'Jack',
age: 20,
gender: 'male',
address: 'No. 189, Grove St, Los Angeles',
},
]);
tableDatas.value = datas;
paginations.total = 200;
};
gettableDatass();
</script>
<style lang="scss" scoped></style>
编辑性表格
FormTableComponent.vue
<template>
<div class="form_table_component">
<slot name="addRow">
<div style="text-align: right">
<el-button type="primary" style="text-align: right" @click="onAddRow(tableName)">添加</el-button>
</div>
</slot>
<el-form :model="form" ref="formRef">
<el-table :data="form[tableName]" border style="width: 100%; margin: 10px 0 20px">
<el-table-column v-for="item in columns" :key="item.prop" :label="item.label" :width="item.width" :align="item.align || 'center'">
<template #default="{ $index, row }">
<el-form-item :prop="tableName + '.' + $index + '.' + item.prop" :rules="item.rules">
<el-input v-if="item.type == 'input'" v-model="row[item.prop]" :placeholder="item.placeholder || '请输入'"></el-input>
<el-select v-if="item.type == 'select'" v-model="row[item.prop]" :style="{ width: '100%', display: 'flex' }" @change="(e) => item.change && item.change(e, row)">
<el-option :label="option.label" :value="option.value" v-for="option in item.options || row[item.affectedOptions]" :key="option.value"></el-option>
</el-select>
<el-date-picker
v-if="item.type == 'date'"
type="date"
:placeholder="item.placeholder || '请选择'"
:style="{ width: '100%', display: 'flex' }"
:value-format="item.valueFormat || 'YYYY-MM-DD'"
v-model="row[item.prop]"
></el-date-picker>
</el-form-item>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" :width="handlerColumnWidth" align="center">
<template #default="scope">
<slot name="handlerColumn" :scope="scope" :tableName="tableName">
<el-button @click="onDelRow(scope.$index)">删除</el-button>
</slot>
</template>
</el-table-column>
</el-table>
</el-form>
</div>
</template>
<script setup>
const props = defineProps({
form: {
type: Object,
default: () => {},
},
tableName: {
type: String,
},
columns: {
type: Array,
default: () => [],
},
handlerColumnWidth: {
type: Number,
default: 100,
},
});
const onAddRow = (tableName) => {
props.form[tableName].push({});
};
const onDelRow = (index) => {
props.form[props.tableName].splice(index, 1);
};
defineExpose({
onAddRow,
onDelRow,
});
</script>
<style lang="scss" scoped>
.form_table_component {
::v-deep .select-trigger {
flex: 1;
}
}
</style>
示例
<template>
<h2>formTableView</h2>
<el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="活动名称" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="form.region" placeholder="请选择">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
</el-form>
<FormTableComponent ref="formTableComponentRef" :form="form" tableName="users" :columns="columns" />
<FormTableComponent ref="formTableComponentRef2" :form="form" tableName="users2" :columns="columns2" />
<p>
<el-button type="primary" @click="onSubmit">提交</el-button>
</p>
</template>
<script setup>
import FormTableComponent from '@/components/FormTableComponent.vue';
import { validateFormsByFormRefs } from '@/utils/validateForms';
import { nameCnValidator } from '@/utils/validateFormItem';
import { reactive } from 'vue';
const form = reactive({
name: 'Hello',
region: '',
users: [],
users2: [],
});
const rules = reactive({
name: [
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
region: [
{
required: true,
message: 'Please select Activity zone',
trigger: 'change',
},
],
});
const columns = [
{ prop: 'birthDate', label: '出生日期', type: 'date' },
{
prop: 'name',
label: '姓名',
type: 'input',
rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }, { validator: nameCnValidator }],
},
{ prop: 'age', label: '年龄', type: 'input' },
{
prop: 'gender',
label: '性别',
type: 'select',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
],
},
];
const getCityOptions = async (pVal) => {
const cityOptions = await new Promise((resolve) => {
setTimeout(() => {
if (pVal === 'shanghai') {
return resolve([
{ label: '浦东', value: 'pudong' },
{ label: '徐汇', value: 'xuhui' },
]);
} else {
resolve([
{ label: '武汉', value: 'wuhan' },
{ label: '黄冈', value: 'huanggang' },
]);
}
}, 300);
});
return cityOptions;
};
const columns2 = ref([
{
prop: 'name',
label: '姓名',
type: 'input',
rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }, { validator: nameCnValidator }],
},
{
prop: 'province',
label: '省份',
type: 'select',
options: [
{ label: '上海', value: 'shanghai' },
{ label: '湖北', value: 'hubei' },
],
async change(val, row) {
row.city = '';
const cityOptions = await getCityOptions(val);
row.cityOptions = cityOptions;
},
},
{
prop: 'city',
label: '城市',
type: 'select',
affectedOptions: 'cityOptions',
},
]);
const ruleFormRef = ref(null);
const formTableComponentRef = ref(null);
const formTableComponentRef2 = ref(null);
const onSubmit = async () => {
const formRef = formTableComponentRef.value.$refs.formRef;
const formRef2 = formTableComponentRef2.value.$refs.formRef;
await Promise.all(validateFormsByFormRefs([ruleFormRef.value, formRef, formRef2]));
console.log('onSubmit', form);
};
</script>
<style scoped></style>
筛选表单+表格+分页
FilterFormTablePigination
<template>
<div class="app-container component-formTable">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-if="formColumns.length" v-show="showSearch">
<template v-for="item in formColumns" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop" v-if="item.type == 'select'">
<el-select
v-model="queryParams[item.prop]"
:placeholder="item.placeholder || '请选择'"
style="width: 200px"
:clearable="!item.hideClearable"
:multiple="item.multiple"
collapse-tags
collapse-tags-tooltip
>
<el-option v-if="item.needAll" label="全部" :value="item.defaultAllVal ?? ''" />
<DictOptions v-if="item.dictType" :dict-type="item.dictType" />
<template v-else>
<el-option v-for="opt in item.options" :key="opt.value" :label="opt.label" :value="opt.value" />
</template>
</el-select>
</el-form-item>
<el-form-item :label="item.label" :prop="item.prop" v-else-if="item.type === 'daterange'">
<el-date-picker
v-model="queryParams[item.prop]"
type="daterange"
range-separator="—"
start-placeholder="开始日期"
end-placeholder="结束日期"
:value-format="item.valueFormat ?? 'YYYY-MM-DD'"
/>
</el-form-item>
<el-form-item :label="item.label" :prop="item.prop" v-else>
<el-input
v-model="queryParams[item.prop]"
:placeholder="item.placeholder || '请输入' + item.label"
:clearable="!item.hideClearable"
:maxlength="item.maxlength || 999999"
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleResetQuery">重置</el-button>
<slot name="filterFormBtns"></slot>
</el-form-item>
<slot name="formItemAfter"></slot>
</el-form>
<div class="tableTools-wrap">
<slot name="tableTools"></slot>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getListRequest" :columns="tableColumns"></right-toolbar>
</div>
<slot name="tableBefore" :data="dataList"></slot>
<el-table v-loading="loading" :data="dataList" style="width: 100%" :row-key="rowKey">
<el-table-column label="序号" width="50" type="index" align="center" fixed>
<template #default="scope">
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<template v-for="item in visibleTableColumns" :key="item.prop">
<el-table-column
:label="item.label"
:align="item.align || 'center'"
:prop="item.prop"
:show-overflow-tooltip="item.tooltip || true"
:width="item.width || 'auto'"
:formatter="item.formatter"
:fixed="item.fixed || false"
>
<template #default="scope" v-if="!item.formatter">
<template v-if="item.format">
<span v-html="item.format(scope.row[item.prop], scope.row)"></span>
</template>
<template v-else-if="item.useDict">{{ dictColumnHandler(item.useDict, scope.row[item.prop]) }}</template>
<template v-else-if="item.useSlot">
<slot :name="item.prop" :row="scope.row"></slot>
</template>
<template v-else>
{{ scope.row[item.prop] || '-' }}
</template>
</template>
</el-table-column>
</template>
<slot name="tableOperate"></slot>
</el-table>
<slot name="tableAfter" :data="dataList"></slot>
<pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" @pagination="getListRequest" />
</div>
</template>
<script setup name="FormTable">
import DictOptions from '../DictOptions/index.vue';
import useDict from '@/utils/dict.js';
const { proxy } = getCurrentInstance();
const showSearch = ref(true);
const props = defineProps({
formColumns: {
type: Array,
default: [],
},
tableColumns: {
type: Array,
default: [],
},
initForm: {
type: Object,
default: {},
},
initData: {
type: Object,
default: {},
},
apiRequest: {
type: Function,
default: () => Promise.resolve({}),
},
rowKey: {
type: [Function, String],
default: 'id',
},
handleData: {
type: Function,
default: null,
},
queryParamsHandler: {
type: Function,
default: () => {},
},
});
const loading = ref(true);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(10);
const dataList = ref([]);
const queryParams = ref({ ...props.initForm });
const visibleTableColumns = computed(() => props.tableColumns.filter(column => !column.hide));
function dictColumnHandler(dictType, val) {
if (!val && val !== 0) {
return '-';
}
const dicts = useDict(dictType)[dictType]?.value ?? [];
return dicts.find(dict => dict.value === val)?.label ?? '-';
}
async function getListRequest() {
loading.value = true;
await props.queryParamsHandler(queryParams.value);
const params = {
...props.initData,
...queryParams.value,
pageNum: pageNum.value,
pageNo: pageNum.value,
pageSize: pageSize.value,
};
console.log('getListRequest---params', params);
props
.apiRequest(params)
.then(response => {
let rows = response.rows || response.data || [];
if (typeof props.handleData == 'function') {
rows = props.handleData(rows);
}
dataList.value = rows;
total.value = response.total || 0;
loading.value = false;
})
.catch(err => {
dataList.value = [];
loading.value = false;
});
}
function handleQuery() {
pageNum.value = 1;
getListRequest();
}
function handleResetQuery() {
proxy.resetForm('queryRef');
handleQuery();
}
getListRequest();
defineExpose({
getListRequest,
getFilterParams: () => ({
...props.initData,
...queryParams.value,
}),
getDataList: () => dataList.value,
handleQuery,
});
</script>
<style scoped lang="scss">
.component-formTable {
.tableTools-wrap {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
.top-right-btn {
margin-left: 12px;
}
}
}
</style>
DictOptions
<template>
<el-option v-for="opt in options" :key="opt.value" :label="opt.label" :value="opt.value" />
</template>
<script setup>
import useDict from '@/utils/dict.js';
const props = defineProps({
dictType: {
type: String,
default: '',
},
});
const options = computed(() => {
if (!props.dictType) return [];
const dicts = useDict(props.dictType);
try {
return dicts[props.dictType].value;
} catch (error) {
return [];
}
});
</script>
<style scoped>
.el-tag + .el-tag {
margin-left: 10px;
}
</style>
utils/dict.js
import useDictStore from '@/store/modules/dict';
import { getDicts } from '@/api/system/dict/data';
export default function useDict(...args) {
const res = ref({});
return (() => {
args.forEach((dictType, index) => {
res.value[dictType] = [];
const dicts = useDictStore().getDict(dictType);
if (dicts) {
res.value[dictType] = dicts;
} else {
useDictStore().setDict(dictType, []);
getDicts(dictType).then(resp => {
res.value[dictType] = resp.data.map(p => ({
label: p.dictLabel,
value: p.dictValue,
elTagType: p.listClass,
elTagClass: p.cssClass,
}));
useDictStore().setDict(dictType, res.value[dictType]);
});
}
});
return toRefs(res.value);
})();
}
@/store/modules/dict
const useDictStore = defineStore('dict', {
state: () => ({
dict: [],
}),
actions: {
getDict(_key) {
if (_key == null && _key == '') {
return null;
}
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
return this.dict[i].value;
}
}
} catch (e) {
return null;
}
},
setDict(_key, value) {
if (_key !== null && _key !== '') {
const index = this.dict.findIndex(item => item.key === _key);
if (index !== -1) {
this.dict.splice(index, 1);
}
this.dict.push({
key: _key,
value,
});
}
},
removeDict(_key) {
let bln = false;
try {
for (let i = 0; i < this.dict.length; i++) {
if (this.dict[i].key == _key) {
this.dict.splice(i, 1);
return true;
}
}
} catch (e) {
bln = false;
}
return bln;
},
cleanDict() {
this.dict = [];
},
initDict() {},
},
});
export default useDictStore;
RightToolbar
<template>
<div class="top-right-btn" :style="style">
<el-row>
<el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
<el-button circle :icon="showSearch ? 'CaretTop' : 'CaretBottom'" @click="toggleSearch()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="刷新当前页" placement="top">
<el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="显示隐藏表格列" placement="top" v-if="columns">
<el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'" />
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button circle icon="Menu" />
<template #dropdown>
<el-dropdown-menu>
<template v-for="item in columns" :key="item.key">
<el-dropdown-item>
<el-checkbox :checked="!item.hide" @change="checkboxChange($event, item.label)" :label="item.label" />
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</el-row>
<el-dialog :title="title" v-model="open" append-to-body>
<el-transfer :titles="['显示', '隐藏']" v-model="value" :data="columns" @change="dataChange"></el-transfer>
</el-dialog>
</div>
</template>
<script setup>
const props = defineProps({
showSearch: {
type: Boolean,
default: true,
},
columns: {
type: Array,
},
search: {
type: Boolean,
default: true,
},
showColumnsType: {
type: String,
default: 'checkbox',
},
gutter: {
type: Number,
default: 10,
},
});
const emits = defineEmits(['update:showSearch', 'queryTable']);
const value = ref([]);
const title = ref('显示/隐藏');
const open = ref(false);
const style = computed(() => {
const ret = {};
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`;
}
return ret;
});
function toggleSearch() {
emits('update:showSearch', !props.showSearch);
}
function refresh() {
emits('queryTable');
}
function dataChange(data) {
for (const item in props.columns) {
const { key } = props.columns[item];
props.columns[item].hide = !data.includes(key);
}
}
function showColumn() {
open.value = true;
}
if (props.showColumnsType == 'transfer') {
for (const item in props.columns) {
if (props.columns[item].hide === true) {
value.value.push(parseInt(item));
}
}
}
function checkboxChange(event, label) {
props.columns.filter(item => item.label == label)[0].hide = !event;
}
</script>
<style lang="scss" scoped>
:deep(.el-transfer__button) {
border-radius: 50%;
display: block;
margin-left: 0px;
}
:deep(.el-transfer__button:first-child) {
margin-bottom: 10px;
}
:deep(.el-dropdown-menu__item) {
line-height: 30px;
padding: 0 17px;
}
</style>
Pagination
<template>
<div :class="{ hidden: hidden }" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup>
import scrollTo from '@/utils/scroll-to';
const props = defineProps({
total: {
required: true,
type: Number,
},
page: {
type: Number,
default: 1,
},
limit: {
type: Number,
default: 20,
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50];
},
},
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7,
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper',
},
background: {
type: Boolean,
default: true,
},
autoScroll: {
type: Boolean,
default: true,
},
hidden: {
type: Boolean,
default: false,
},
});
const emit = defineEmits();
const currentPage = computed({
get() {
return props.page;
},
set(val) {
emit('update:page', val);
},
});
const pageSize = computed({
get() {
return props.limit;
},
set(val) {
emit('update:limit', val);
},
});
function handleSizeChange(val) {
if (currentPage.value * val > props.total) {
currentPage.value = 1;
}
emit('pagination', { page: currentPage.value, limit: val });
if (props.autoScroll) {
scrollTo(0, 800);
}
}
function handleCurrentChange(val) {
emit('pagination', { page: val, limit: pageSize.value });
if (props.autoScroll) {
scrollTo(0, 800);
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
padding: 16px 20px;
height: initial;
line-height: initial;
}
.pagination-container.hidden {
display: none;
}
</style>
使用示例
<template>
<div class="page-channelAgencyInfo-ppl">
<FormTable
ref="formTableRef"
:formColumns="formColumnsClone"
:tableColumns="tableColumnsClone"
:apiRequest="clueNewestPPLListRequest"
:queryParamsHandler="queryParamsHandler"
>
<template #tableTools>
<el-button type="primary" @click="onExport" icon="Download">导出</el-button>
</template>
<template #tableOperate>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" :fixed="'right'">
<template #default="{ row }">
<el-button link type="primary" @click="onShowPPLHistoryDialog(row)">更新</el-button>
</template>
</el-table-column>
</template>
</FormTable>
<PPLHistoryDialog ref="pplHistoryDialogRef" @handleQuery="handleQuery"></PPLHistoryDialog>
</div>
</template>
<script setup>
import { formColumns, tableColumns } from './ppl.js';
import { deepClone } from '@/utils/index.js';
import { clueNewestPPLListRequest } from '@/api/channel-agency/ppl.js';
import PPLHistoryDialog from './ppl-component/pplHistoryDialog/index.vue';
const { proxy } = getCurrentInstance();
const formColumnsClone = deepClone(formColumns);
const tableColumnsClone = ref(deepClone(tableColumns));
const pplHistoryDialogRef = ref(null);
const formTableRef = ref(null);
const queryParamsHandler = queryParams => {
if (Array.isArray(queryParams.createdDate)) {
queryParams.startTime = queryParams.createdDate[0];
queryParams.endTime = queryParams.createdDate[1];
} else {
delete queryParams.startTime;
delete queryParams.endTime;
}
};
const onShowPPLHistoryDialog = (row = {}) => {
pplHistoryDialogRef.value.showPPLHistoryDialog(row);
};
const onExport = async () => {
const filterParams = formTableRef.value.getFilterParams();
await proxy.download('tamper/institution/clue/newestExport', filterParams, `ppl.xlsx`);
};
const handleQuery = async () => {
formTableRef.value.handleQuery();
};
</script>
<style lang="scss"></style>
formColumns tableColumns
export const clueStatusOptions = [
{ label: '全部', value: '' },
{ label: '待审核', value: '0' },
{ label: '正常', value: '1' },
];
export const clueTypeOptions = [
{ label: '线索', value: '0' },
{ label: '机构', value: '1' },
];
export const formColumns = [
{
label: '渠道名称',
prop: 'channelName',
type: 'text',
},
{
label: '机构真实名称',
prop: 'institutionName',
type: 'text',
},
{
label: '区域经理',
prop: 'regionalManager',
type: 'text',
},
{
label: '状态',
prop: 'clueStatus',
type: 'select',
options: clueStatusOptions,
},
{
label: '机构编码',
prop: 'channelCode',
type: 'text',
},
{
label: '最后跟进时间',
prop: 'createdDate',
type: 'daterange',
},
{
label: '合作进度',
prop: 'progressList',
type: 'select',
multiple: true,
dictType: 'cooperation_progress',
},
];
export const tableColumns = [
{
label: '渠道编码',
prop: 'channelCode',
formatter(row) {
return row.channelCode || '未生成';
},
},
{
label: '渠道线索名称',
prop: 'channelName',
},
{
label: '机构真实名称',
prop: 'institutionName',
formatter(row) {
return row.institutionName || '待补充';
},
},
{
label: '区域经理',
prop: 'regionalManager',
},
{
label: '工号',
prop: 'managerId',
},
{
label: '创建时间',
prop: 'createTime',
},
{
label: '合作进度',
prop: 'cooperationProgress',
useDict: 'cooperation_progress',
},
{
label: '最后跟进时间',
prop: 'lastUpdateTime',
},
];
export const tableColumns = [
{
label: '渠道编码',
prop: 'channelCode',
formatter(row) {
return row.channelCode || '未生成';
},
},
{
label: '渠道线索名称',
prop: 'channelName',
},
{
label: '机构真实名称',
prop: 'institutionName',
formatter(row) {
return row.institutionName || '待补充';
},
},
{
label: '区域经理',
prop: 'regionalManager',
},
{
label: '工号',
prop: 'managerId',
},
{
label: '合作进度',
prop: 'cooperationProgress',
useDict: 'cooperation_progress',
},
{
label: '最新PPL',
prop: 'newestPPL',
},
{
label: '最后更新时间',
prop: 'lastUpdateTime',
},
{
label: '备注',
prop: 'remark',
},
];
便捷性表单
MyForm
<template>
<el-form status-icon ref="formRef" :model="formData" :rules="rules" class="component-form" :label-width="labelWidth" :disabled="disabled">
<el-row>
<template v-for="item in formItemList" :key="item.prop">
<el-col :span="item.colSpan ?? 24" v-if="!item.hide">
<el-form-item :prop="item.prop" :label="item.label" :label-width="item.labelWidth">
<slot :name="item.prop" :value="formData[item.prop]" :formData="formData">
<el-input
v-model.number="formData[item.prop]"
type="number"
v-if="['number'].includes(item.type)"
:placeholder="item.placeholder || '请输入数值'"
/>
<span v-else-if="['text'].includes(item.type)">{{ formData[item.prop] }}</span>
<component
v-else
:disabled="item.disabled"
:is="typeComponentMap.get(item.type).is"
v-bind="typeComponentMap.get(item.type).attrs"
v-model.trim="formData[item.prop]"
:clearable="!item.hideClearable"
:multiple="['select'].includes(item.type) && item.multiple"
@change="val => item.changeCb && item.changeCb(val)"
:placeholder="item.placeholder || (['input', 'textarea'].includes(item.type) ? '请输入' : '请选择') + item.label"
>
<template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
<DictOptions v-if="item.dictType" :dict-type="item.dictType" />
<component
v-else
:is="typeComponentMap.get(item.type).child"
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
></component>
</template>
</component>
</slot>
</el-form-item>
</el-col>
</template>
</el-row>
<el-form-item>
<slot name="btns" :formData="formData" :onSubmit="onSubmit">
</slot>
</el-form-item>
</el-form>
</template>
<script>
export default {
components: { ElInput, ElSelect, ElRadioGroup, ElCheckboxGroup, ElDatePicker, ElOption, ElRadio, ElCheckbox },
};
</script>
<script setup>
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
labelWidth: {
type: String,
default: '140px',
},
formItemList: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
initFormData: {
type: Object,
default: () => ({}),
},
});
const formData = reactive(props.initFormData);
const formRef = ref(null);
const typeComponentMap = new Map([
['input', { is: 'el-input', attrs: {} }],
['select', { is: 'el-select', attrs: {}, child: 'el-option' }],
['radio', { is: 'el-radio-group', attrs: {}, child: 'el-radio' }],
['checkbox', { is: 'el-checkbox-group', attrs: {}, child: 'el-checkbox' }],
['daterange', { is: 'el-date-picker', attrs: { type: 'daterange', 'value-format': 'YYYY-MM-DD' } }],
['year', { is: 'el-date-picker', attrs: { type: 'year', 'value-format': 'YYYY' } }],
['textarea', { is: 'el-input', attrs: { type: 'textarea' } }],
['datetime', { is: 'el-date-picker', attrs: { type: 'datetime', 'value-format': 'YYYY-MM-DD HH:mm:ss' } }],
]);
const onSubmit = async () => {
console.log('formData', formData);
await formRef.value.validate();
console.log('passed');
return { ...formData };
};
const resetFields = () => {
formRef.value?.resetFields();
};
defineExpose({
onSubmit,
resetFields,
});
</script>
<style lang="scss">
.component-form {
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}
}
</style>
使用示例
<template>
<el-dialog v-model="createTalkDialogVisible" title="新增沟通跟进" width="900">
<h3>基本信息</h3>
<p class="header-p">
<span>
<em>渠道名称:</em>
{{ rowInfo.channelName }}
</span>
<span>
<em>机构真实名称:</em>
{{ rowInfo.institutionName || '未补充' }}
</span>
<span>
<em>机构编码:</em>
{{ rowInfo.channelCode || '未生成' }}
</span>
</p>
<h3>沟通信息</h3>
<MyForm :formItemList="formItemList" :rules="formRules" ref="formRef" :initFormData="initFormData"></MyForm>
<footer>
<el-button type="primary" @click="onSubmit">确认新增</el-button>
</footer>
</el-dialog>
</template>
<script setup>
import { formItemConfigs, formRules } from './index.js';
import { clueInsertCommunicateRequest } from '@/api/channel-agency/talk.js';
import useUserStore from '@/store/modules/user';
const emit = defineEmits(['handleQuery']);
const userStore = useUserStore();
const formRef = ref(null);
const createTalkDialogVisible = ref(false);
const rowInfo = reactive({});
const formItemList = ref(formItemConfigs);
const initFormData = reactive({});
const handleQuery = () => {
createTalkDialogVisible.value = false;
emit('handleQuery');
};
const showCreateTalkDialog = async row => {
Object.assign(rowInfo, row);
createTalkDialogVisible.value = true;
formRef.value?.resetFields();
};
const onSubmit = async () => {
const formData = await formRef.value.onSubmit();
await clueInsertCommunicateRequest({ clueId: rowInfo.clueId, ...formData, creator: userStore.nickName });
formRef.value?.resetFields();
handleQuery();
};
defineExpose({
showCreateTalkDialog,
});
</script>
<style lang="scss" scoped>
.header-p {
span {
margin-right: 40px;
}
}
footer {
display: flex;
justify-content: center;
}
</style>
配置项
import { chineseEnglishNumRule, mobileRule, lengthLimitRule } from '@/utils/formRules.js';
export const communicateMethodOptions = [
{ label: '面谈', value: 'face' },
{ label: '电话沟通', value: 'tel' },
];
export const formItemConfigs = [
{ prop: 'communicateTime', label: '沟通时间', type: 'datetime', colSpan: 12 },
{ prop: 'communicateMethod', label: '沟通方式', type: 'select', colSpan: 12, options: communicateMethodOptions },
{ prop: 'communicateObj', label: '沟通对象', type: 'input', colSpan: 8 },
{ prop: 'communicatePhone', label: '沟通对象手机号', type: 'input', colSpan: 8 },
{ prop: 'communicatePost', label: '沟通对象岗位', type: 'input', colSpan: 8 },
{ prop: 'communicateContent', label: '沟通记录', type: 'textarea' },
{ prop: 'requiredHelp', label: '所需支持', type: 'textarea' },
];
export const formRules = {
communicateTime: [{ required: true, message: '请选择', trigger: 'blur' }],
communicateMethod: [{ required: true, message: '请选择', trigger: 'blur' }],
openingBankAccount: [{ required: true, message: '请输入', trigger: 'blur' }],
communicatePhone: [mobileRule()],
communicateObj: [{ required: true, message: '请输入', trigger: 'blur' }, chineseEnglishNumRule(), lengthLimitRule({ maxLength: 50 })],
communicatePost: [{ required: true, message: '请输入', trigger: 'blur' }, chineseEnglishNumRule(), lengthLimitRule({ maxLength: 50 })],
communicateContent: [{ required: true, message: '请输入', trigger: 'blur' }, lengthLimitRule()],
requiredHelp: [lengthLimitRule()],
};