|
|
<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>
|