feat:通用表单

main
向文可 3 years ago
parent 376f17fe29
commit ccbd7f96a0

@ -0,0 +1,40 @@
const mock = (data) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(data);
}, Math.random() * 1500 + 500);
});
export const findUserList = (data) => {
return mock({
content: [
{
id: 1,
username: 'user001',
nickname: '张三',
sex: 1,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: true,
},
{
id: 2,
username: 'user003',
nickname: '李四',
sex: 0,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: true,
},
{
id: 3,
username: 'user003',
nickname: '王五',
sex: 1,
avatar: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2Ffd%2Ff1%2Fda%2Ffdf1dacb8ff0b8f13ed29bcbee42f328.jpeg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1650201540&t=ba213738d8f11e79302fab71602856f2',
loginTime: Date.now(),
enabled: false,
},
],
totalElements: 3,
});
};

@ -0,0 +1,621 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import SortableTable from './SortableTable.vue';
import { ElTable, ElTableColumn } from 'element-plus/es/components/table/index';
import 'element-plus/es/components/table/style/css';
const props = defineProps({
code: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
sortable: {
type: Boolean,
default: false,
},
config: {
type: Object,
required: true,
},
operation: {
type: Array,
default() {
return ['create', 'remove', 'search'];
},
},
total: {
type: Number,
default: 0,
},
});
const emits = defineEmits([
'create',
'save',
'remove',
'search',
'select',
'selectAll',
'selectionChange',
'currentChange',
'reset',
'template',
'import',
'export',
]);
const attrs = useAttrs();
const slots = useSlots();
const { proxy } = getCurrentInstance();
const store = useStore();
//
const getObjectProps = (obj) => {
return _.clone(Object.fromEntries(Object.entries(obj).filter((entry) => typeof entry[1] !== 'object')));
};
//
const settings = computed(() => store.state.local.listSettings);
const handleInit = (clear = false) => {
//
if (props.code && clear) {
store.commit('local/setListSettings', {
...unref(settings),
[props.code]: null,
});
console.info('[list] reset:' + props.code);
}
let config = null;
if (
!props.code ||
!unref(settings) ||
!unref(settings)[props.code] ||
!(unref(settings)[props.code] instanceof Array)
) {
//
config = props.config.columns
.map((item, index) => {
return {
_sort: index + 1,
_index: index,
_visible: true,
width: '',
fixed: false,
...getObjectProps(item),
};
})
.sort((prev, next) => prev._sort - next._sort);
if (props.code) {
//
store.commit('local/setListSettings', {
...unref(settings),
[props.code]: config,
});
}
} else {
config = unref(settings)[props.code];
}
//
let needReset = false;
config = props.config.columns.map((item, index) => {
//
let old = config.find((temp) => temp._index === index);
if (
old &&
Object.entries(getObjectProps(item)).every((entry) => {
return old[entry[0]] === entry[1];
})
) {
item = {
...old,
...item,
};
} else {
needReset = true;
}
return item;
});
return needReset ? handleInit(true) : config;
};
const setting = ref(handleInit());
console.info('[list] setting:' + props.code, unref(setting));
//
let rollback = ref([]);
const settingVisible = ref(false);
const handleSetting = () => {
rollback.value = _.cloneDeep(unref(setting));
settingVisible.value = true;
};
const handleCancel = () => {
settingVisible.value = false;
};
const handleSave = () => {
rollback.value = null;
store.commit('local/setListSettings', {
...unref(settings),
[props.code]: unref(setting).map((item) => getObjectProps(item)),
});
settingVisible.value = false;
};
const handleCloseSetting = () => {
if (unref(rollback)) {
setting.value = unref(rollback);
}
};
//
const refsUpload = ref(null);
const importVisible = ref(false);
const handleShowImport = () => {
importVisible.value = true;
};
const handleCancelImport = () => {
importVisible.value = false;
};
const handleSaveImport = (file) => {
const files = unref(refsUpload).refsUpload.uploadFiles;
if (!files.length) {
proxy.$baseMessage('请上传要导入的文件', 'error');
} else {
if (file === false) {
unref(refsUpload).refsUpload.submit();
} else {
handleImport(file);
importVisible.value = false;
}
}
};
//
const refTable = ref(null);
const checked = ref(null);
const selection = ref([]);
const handleSelect = (arr, row) => {
setTimeout(() => {
let res = [],
selected = arr.indexOf(row) !== -1;
(function deep(item) {
res.push(item);
(item.children || []).forEach(deep);
})(row);
res.forEach((item) => {
toggleRowExpansion(item, selected);
toggleRowSelection(item, selected);
});
}, 0);
emits('select', arr, row);
};
//
const pages = computed(() => store.state.local.listPage);
const search = ref(
props.code
? store.state.local.listPage[props.code]
: {
pageIndex: 1,
length: 10,
}
);
const resetPage = (
page = {
pageIndex: 1,
length: 10,
}
) => {
if (props.code) {
store.commit('local/setListPage', {
...unref(pages),
[props.code]: page,
});
}
search.value = page;
};
if (!unref(search)) {
resetPage();
}
watch(
() => props.total,
(value) => {
if (!value) {
resetPage();
}
}
);
//
const handleSearch = () => {
emits('search', unref(search));
};
const handleReset = () => {
emits('reset');
search.value = { pageIndex: 1, length: 10 };
handleSearch();
};
const handleTemplate = () => {
emits('template');
};
const handleImport = (files) => {
emits('import', files);
};
const handleExport = () => {
emits('export');
};
const handleCreate = () => {
emits('create', unref(checked));
};
const handleUpdate = () => {
emits('save', unref(selection));
};
const handleRemove = () => {
emits('remove', unref(selection));
};
const listeners = {
onSelect: handleSelect,
onSelectAll: (arr) => {
setTimeout(() => {
attrs.data.forEach((item) => handleSelect(arr, item));
}, 0);
emits('selectAll', arr);
},
onSelectionChange: (arr) => {
selection.value = arr;
emits('selectionChange', arr);
},
onCurrentChange: (row) => {
checked.value = row;
emits('currentChange', row);
},
};
//
watch(
search,
(value) => {
if (value && props.code) {
resetPage(value);
}
handleSearch();
},
{ deep: true, immediate: props.config.autoSearch !== false }
);
//
const handleProxy = (fnName, args) => {
return unref(refTable)[fnName]?.apply(unref(refTable), args);
};
const clearSelection = function () {
return handleProxy('clearSelection', arguments);
};
const toggleRowSelection = function () {
return handleProxy('toggleRowSelection', arguments);
};
const toggleAllSelection = function () {
return handleProxy('toggleAllSelection', arguments);
};
const toggleRowExpansion = function () {
return handleProxy('toggleRowExpansion', arguments);
};
const setCurrentRow = function () {
return handleProxy('setCurrentRow', arguments);
};
const clearSort = function () {
return handleProxy('clearSort', arguments);
};
const clearFilter = function () {
return handleProxy('clearFilter', arguments);
};
const doLayout = function () {
return handleProxy('doLayout', arguments);
};
const sort = function () {
return handleProxy('sort', arguments);
};
//
defineExpose({
search: handleSearch,
selection,
refTable,
clearSelection,
toggleRowSelection,
toggleAllSelection,
toggleRowExpansion,
setCurrentRow,
clearSort,
clearFilter,
doLayout,
sort,
});
const Component = props.sortable ? SortableTable : ElTable;
const render = () => (
<div class="common-list">
{slots.search ? <div class="search-box">{slots.search()}</div> : ''}
<div class="operation-box">
{props.operation.includes('create') ? (
<ElButton type="primary" onClick={() => handleCreate()}>
<ElIcon name="Plus" />
<span>新增{props.title}</span>
</ElButton>
) : (
''
)}
{props.operation.includes('save') ? (
<ElButton type="primary" onClick={() => handleUpdate()}>
<ElIcon name="Tickets" />
<span>批量保存</span>
</ElButton>
) : (
''
)}
{props.operation.includes('remove') ? (
<ElButton type="danger" onClick={() => handleRemove()}>
<ElIcon name="Delete" />
<span>批量移除</span>
</ElButton>
) : (
''
)}
{props.operation.includes('search') ? (
<ElButton type="success" onClick={() => handleSearch()}>
<ElIcon name="Search" />
<span>查询</span>
</ElButton>
) : (
''
)}
{props.operation.includes('search') ? (
<ElButton onClick={() => handleReset()}>
<ElIcon name="MagicStick" />
<span>重置</span>
</ElButton>
) : (
''
)}
{props.operation.includes('import') ? (
<ElButton type="warning" onClick={() => handleShowImport()}>
<ElIcon name="Document" />
<span>模板导入</span>
</ElButton>
) : (
''
)}
{props.operation.includes('export') ? (
<ElButton type="warning" onClick={() => handleExport()}>
<ElIcon name="Download" />
<span>导出</span>
</ElButton>
) : (
''
)}
{slots.operation?.()}
{props.operation.includes('import') ? (
<ElDialog
title={'模板导入 - ' + props.title}
v-model={importVisible.value}
width="765px"
v-slots={{
footer: () => {
return (
<div>
<ElButton type="danger" onClick={() => handleCancelImport()}>
取消
</ElButton>
<ElButton type="primary" onClick={() => handleSaveImport(false)}>
导入
</ElButton>
</div>
);
},
}}
>
<ElButton icon="Document" type="warning" onClick={() => handleTemplate()}>
下载模板
</ElButton>
<br />
<br />
<ElUpload
ref={refsUpload}
auto-upload={false}
before-upload={(e) => {
handleSaveImport(e);
return false;
}}
/>
</ElDialog>
) : (
''
)}
{props.code ? (
<ElButton class="setting-btn" icon="setting" type="text" onClick={() => handleSetting()}>
设置
</ElButton>
) : (
''
)}
<ElDialog
title={'表格设置 - ' + props.code}
v-model={settingVisible.value}
width="765px"
onClose={() => handleCloseSetting()}
v-slots={{
footer: () => {
return (
<div>
<ElButton type="danger" onClick={() => handleCancel()}>
取消
</ElButton>
<ElButton type="primary" onClick={() => handleSave()}>
保存
</ElButton>
</div>
);
},
}}
>
<p>修改后如果显示异常刷新页面即可恢复正常</p>
<br />
<ElTable border stripe highlightCurrentRow data={setting.value} height="50vh">
<ElTableColumn
headerAlign="center"
align="center"
label="排序"
prop="_sort"
width="72px"
v-slots={{
default: ({ row }) => (
<ElInputNumber controls={false} v-model={row._sort} style="width:48px;" />
),
}}
/>
<ElTableColumn
headerAlign="center"
align="center"
label="列名"
prop="label"
width="200px"
v-slots={{
default: ({ row }) =>
row.type === 'index' ? (
'序号'
) : row.type === 'selection' ? (
'选择框'
) : (
<ElInput
v-model={row.label}
placeholder={props.config.columns[row._index].label}
/>
),
}}
/>
<ElTableColumn
headerAlign="center"
align="center"
label="宽度"
prop="width"
width="140px"
v-slots={{
default: ({ row }) => <ElInput v-model={row.width} style="width:104px;" />,
}}
/>
<ElTableColumn
headerAlign="center"
align="center"
label="固定"
prop="fixed"
width="240px"
v-slots={{
default: ({ row }) => (
<ElRadioGroup
v-model={row.fixed}
button
opts={[
{ label: '左侧', value: 'left' },
{ label: '取消', value: false },
{ label: '右侧', value: 'right' },
]}
></ElRadioGroup>
),
}}
/>
<ElTableColumn
headerAlign="center"
align="center"
label="显示"
prop="_visible"
width="72px"
v-slots={{
default: ({ row }) => <ElSwitch v-model={row._visible} />,
}}
/>
</ElTable>
</ElDialog>
</div>
<div class="content-box">
<Component
ref={refTable}
border
stripe
highlightCurrentRow={false}
rowKey="id"
height="100%"
{...listeners}
{...attrs}
{...props.config.table}
v-slots={{
empty: () => (
<div class="empty-content">
<ElEmpty description="没有查询到符合条件的数据" />
</div>
),
...props.config.table?.slots,
}}
>
{unref(setting).map((col) =>
col._visible ? (
<ElTableColumn
showOverflowTooltip={!col.slots?.default}
headerAlign="center"
align="center"
selectable={(row) => !row.baseData}
v-slots={{ ...col.slots }}
{...col}
></ElTableColumn>
) : (
''
)
)}
</Component>
</div>
<div class="pagination-box">
{props.config.page === false ? (
''
) : (
<ElPagination
background
hide-on-single-page={false}
page-sizes={props.config.page?.sizes || [10, 15, 20, 30, 50, 100]}
layout={props.config.page?.layout || 'total, sizes, prev, pager, next, jumper'}
total={props.total}
v-model:current-page={unref(search).pageIndex}
v-model:page-size={unref(search).length}
/>
)}
</div>
</div>
);
</script>
<style lang="less" scoped>
.common-list {
width: 100%;
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
:deep(.search-box) {
border-radius: 5px;
}
:deep(.operation-box) {
border-radius: 5px;
padding-bottom: 10px;
display: flex;
align-items: center;
.setting-btn {
margin-left: auto;
}
}
:deep(.content-box) {
flex: 1;
overflow: hidden;
margin-bottom: 10px;
display: flex;
flex-direction: column;
.el-table {
max-height: 100%;
flex: 1;
}
}
:deep(.pagination-box) {
display: flex;
justify-content: center;
.el-pagination {
margin: 0;
}
}
}
</style>

@ -0,0 +1,19 @@
<template>
<component :is="render" />
</template>
<script setup lang="jsx">
import { ElButton } from 'element-plus/es/components/button/index';
import 'element-plus/es/components/button/style/css';
const props = defineProps({});
const attrs = useAttrs();
const slots = useSlots();
const render = () => <ElButton {...props} {...attrs} v-slots={slots} />;
</script>
<style lang="less" scoped>
.el-button {
:deep(.el-icon) {
position: relative;
top: -2px;
}
}
</style>

@ -56,4 +56,7 @@
margin-left: @layout-space-small;
}
}
.el-button + .el-dropdown {
margin-left: 10px;
}
</style>

@ -47,8 +47,6 @@
font-size: v-bind(size);
color: v-bind(color);
line-height: 1;
position: relative;
top: 1px;
}
svg.x-icon {
width: v-bind(size);

@ -7,7 +7,7 @@
const props = defineProps({
src: {
type: String,
required: true,
default: 'none',
},
alt: {
type: String,

@ -1,7 +1,7 @@
import { createStore } from 'vuex';
const modules = Object.fromEntries(
Object.entries(import.meta.globEager('./modules/*.js')).map((entry) => {
Object.entries(import.meta.globEager('./**/*.js')).map((entry) => {
let arr = entry[0].split('/').pop().split('.');
arr.pop();
let moduleName = _.camelCase(arr.join('-'));

@ -1,11 +1,15 @@
const state = () => ({
lastSendMessageTime: 0,
token: null,
listSettings: {},
listPage: {},
});
const getters = {};
const mutations = {
sendMessage: (state) => (state.lastSendMessageTime = new Date().getTime()),
setToken: (state, data) => (state.token = data),
setListSettings: (state, data) => (state.listSettings = data),
setListPage: (state, data) => (state.listPage = data),
};
const actions = {};
export default {

@ -0,0 +1,39 @@
import * as api from '@/api/system/user.js';
import { ElMessage } from '@/plugins/element-plus';
const state = () => ({
loading: false,
list: [],
total: 0,
opts: {
sex: [
{ label: '男', value: 1 },
{ label: '女', value: 0 },
],
},
});
const getters = {};
const mutations = {
setLoading: (state, data) => (state.loading = data),
setList: (state, data) => (state.list = data),
setTotal: (state, data) => (state.total = data),
};
const actions = {
search: async ({ commit }, data) => {
commit('setLoading', true);
let res = await api.findUserList(data);
if (res) {
commit('setList', res.content);
commit('setTotal', res.totalElements);
} else {
ElMessage.error('查询用户失败');
commit('setList', []);
}
commit('setLoading', false);
},
};
export default {
state,
getters,
mutations,
actions,
};

@ -3,6 +3,8 @@ body,
#app {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
}
ul,
ol {

@ -1,7 +1,136 @@
<template>
<div>用户列表</div>
<TableList
v-loading="loading"
code="DemoList"
title="用户"
:data="list"
:total="total"
:config="config"
@search="handleSearch"
@create="handleCreate"
@remove="handleRemove"
>
<template #search>
<el-form inline>
<el-form-item prop="username" label="用户名">
<el-input v-model="state.condition.username" />
</el-form-item>
</el-form>
</template>
</TableList>
</template>
<script setup></script>
<script setup lang="jsx">
const store = useStore();
const loading = computed(() => store.state.user.loading);
const list = computed(() => store.state.user.list);
const total = computed(() => store.state.user.total);
const opts = computed(() => store.state.user.opts);
const state = reactive({
condition: {
username: null,
},
});
const handleSearch = (page) => {
store.dispatch('user/search', { ...page, ...state.condition });
};
const handleCreate = (row) => {
alert('create ' + row.id);
};
const handleUpdate = (row) => {
alert('update ' + row.id);
};
const handleRemove = (rows) => {
alert('delete ' + rows.map((item) => item.id).join(','));
};
const handleDetail = (row) => {
alert('detail ' + row.id);
};
const handleEnabled = (row, enabled) => {
alert((enabled ? 'enabled ' : 'disabled ') + row.id);
};
const config = reactive({
columns: [
{
type: 'selection',
fixed: 'left',
width: 60,
},
{
label: '用户名',
prop: 'username',
minWidth: 160,
fixed: 'left',
},
{
label: '昵称',
prop: 'nickname',
minWidth: 160,
},
{
label: '性别',
slots: {
default: ({ row }) => unref(opts).sex.find((item) => item.value === row.sex)?.label,
},
width: 100,
},
{
label: '头像',
slots: {
default: ({ row }) => <ElImage src={row.avatar} alt="用户头像" />,
},
width: 100,
},
{
label: '登录时间',
slots: {
default: ({ row }) => dayjs(row.loginTime).format('YYYY-MM-DD HH:mm:ss'),
},
width: 180,
},
{
label: '状态',
slots: {
default: ({ row }) => (
<ElSwitch
modelValue={row.enabled}
active-text="启用"
inactive-text="禁用"
active-value={true}
inactive-value={false}
onInput={(e) => handleEnabled(row, e)}
/>
),
},
width: 160,
},
{
label: '操作',
fixed: 'right',
slots: {
default: ({ row }) => (
<div>
<ElButton type="text" onClick={() => handleUpdate(row)}>
编辑
</ElButton>
<ElButton type="text" onClick={() => handleRemove([row])}>
删除
</ElButton>
<ElDropdown
opts={[
{
label: '详情',
onClick: () => handleDetail(row),
},
]}
/>
</div>
),
},
width: 200,
},
],
});
</script>
<style lang="less" scoped></style>

Loading…
Cancel
Save