updated frontend

This commit is contained in:
marvzhang
2021-07-15 21:37:37 +08:00
parent 058531b267
commit caf2380f99
550 changed files with 27134 additions and 49444 deletions

View File

@@ -0,0 +1,262 @@
<template>
<div ref="tableWrapperRef" class="table">
<!-- Table Body -->
<el-table
v-if="selectedColumns.length > 0"
ref="tableRef"
:data="tableData"
:fit="false"
:row-key="rowKey"
border
size="small"
@selection-change="onSelectionChange"
>
<el-table-column
v-if="selectable"
align="center"
reserve-selection
type="selection"
width="40"
fixed="left"
:selectable="selectableFunction"
/>
<el-table-column
v-for="c in selectedColumns"
:key="c.key"
:column-key="c.key"
:align="c.align"
:fixed="c.fixed ? c.fixed : false"
:label="c.label"
:width="c.width"
:min-width="c.minWidth || c.width"
:sortable="c.sortable"
:index="c.index"
:resizable="c.resizable === undefined ? true : c.resizable"
:class-name="c.className"
>
<template #header="scope">
<TableHeader :column="c" :index="scope.$index" @change="onHeaderChange"/>
</template>
<template #default="scope">
<TableCell :column="c" :row="scope.row" :row-index="scope.$index"/>
</template>
</el-table-column>
</el-table>
<!-- ./Table Body-->
<!-- Table Footer-->
<div v-if="!hideFooter" class="table-footer">
<TableActions
:selection="internalSelection"
@delete="onDelete"
@edit="onEdit"
@export="onExport"
@customize-columns="onShowColumnsTransfer"
>
<template #prefix>
<slot name="actions-prefix"></slot>
</template>
<template #suffix>
<slot name="actions-suffix"></slot>
</template>
</TableActions>
<el-pagination
:current-page="page"
:page-size="pageSize"
:total="total"
class="pagination"
layout="total, sizes, prev, pager, next"
@current-change="onCurrentChange"
@size-change="onSizeChange"
/>
</div>
<!-- ./Table Footer-->
<!-- Table Columns Transfer -->
<TableColumnsTransfer
:columns="columns"
:selected-column-keys="internalSelectedColumnKeys"
:visible="columnsTransferVisible"
@apply="onColumnsChange"
@close="onHideColumnsTransfer"
/>
<!-- ./Table Columns Transfer -->
</div>
</template>
<script lang="ts">
import {defineComponent, inject, PropType, ref, SetupContext} from 'vue';
import {Table} from 'element-plus/lib/el-table/src/table.type';
import TableCell from '@/components/table/TableCell.vue';
import TableHeader from '@/components/table/TableHeader.vue';
import TableColumnsTransfer from '@/components/table/TableColumnsTransfer.vue';
import useColumn from '@/components/table/column';
import useHeader from '@/components/table/header';
import useData from '@/components/table/data';
import TableActions from '@/components/table/TableActions.vue';
import useAction from '@/components/table/action';
import usePagination from '@/components/table/pagination';
export default defineComponent({
name: 'Table',
components: {
TableActions,
TableColumnsTransfer,
TableCell,
TableHeader,
},
props: {
data: {
type: Array as PropType<TableData>,
required: true,
default: () => {
return [];
},
},
columns: {
type: Array as PropType<TableColumn[]>,
required: true,
default: () => {
return [];
},
},
selectedColumnKeys: {
type: Array as PropType<string[]>,
required: true,
default: () => {
return [];
},
},
total: {
type: Number,
default: 0,
},
page: {
type: Number,
default: 1,
},
pageSize: {
type: Number,
default: 10,
},
rowKey: {
type: String,
default: '_id',
},
selectable: {
type: Boolean,
default: false,
},
visibleButtons: {
type: Array as PropType<BuiltInTableActionButtonName[]>,
required: false,
default: () => {
return [];
}
},
hideFooter: {
type: Boolean,
default: false,
},
selectableFunction: {
type: Function as PropType<TableSelectableFunction>,
default: () => true,
}
},
emits: [
'edit',
'delete',
'export',
'header-change',
'pagination-change',
'selection-change',
],
setup(props: TableProps, ctx: SetupContext) {
const tableWrapperRef = ref();
const tableRef = ref<Table>();
const {
tableData,
} = useData(props, ctx);
const {
internalSelectedColumnKeys,
columnsTransferVisible,
selectedColumns,
onShowColumnsTransfer,
onHideColumnsTransfer,
onColumnsChange,
} = useColumn(props, ctx, tableRef, tableWrapperRef);
const {
onHeaderChange,
} = useHeader(props, ctx);
// inject action functions
const actionFunctions = inject<ListLayoutActionFunctions>('action-functions');
const {
selection: internalSelection,
onSelectionChange,
onAdd,
onEdit,
onDelete,
onExport,
} = useAction(props, ctx, tableRef, actionFunctions as ListLayoutActionFunctions);
const {
onCurrentChange,
onSizeChange,
} = usePagination(props, ctx);
return {
tableWrapperRef,
tableRef,
tableData,
internalSelectedColumnKeys,
columnsTransferVisible,
selectedColumns,
onHeaderChange,
onShowColumnsTransfer,
onHideColumnsTransfer,
onColumnsChange,
onExport,
internalSelection,
onSelectionChange,
onAdd,
onEdit,
onDelete,
onCurrentChange,
onSizeChange,
};
},
});
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.table {
background-color: $containerWhiteBg;
.el-table {
width: 100%;
}
.table-footer {
display: flex;
justify-content: space-between;
padding: 10px;
.pagination {
text-align: right;
}
}
}
</style>
<style scoped>
.el-table >>> th > .cell {
line-height: 1.5;
word-break: normal;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="table-actions">
<slot name="prefix"></slot>
<!-- <FaIconButton-->
<!-- v-if="showButton(ACTION_ADD)"-->
<!-- :icon="['fa', 'plus']"-->
<!-- class="action-btn"-->
<!-- size="mini"-->
<!-- tooltip="Add"-->
<!-- type="success"-->
<!-- :disabled="selection.length === 0"-->
<!-- @click="onAdd"-->
<!-- />-->
<FaIconButton
v-if="showButton(ACTION_EDIT)"
:disabled="selection.length === 0"
:icon="['fa', 'edit']"
class="action-btn"
size="mini"
tooltip="Edit Selected"
type="warning"
@click="onEdit"
/>
<FaIconButton
v-if="showButton(ACTION_DELETE)"
:disabled="selection.length === 0"
:icon="['fa', 'trash-alt']"
class="action-btn"
size="mini"
tooltip="Delete Selected"
type="danger"
@click="onDelete"
/>
<FaIconButton
v-if="showButton(TABLE_ACTION_EXPORT)"
:icon="['fa', 'file-export']"
class="action-btn"
size="mini"
tooltip="Export"
type="primary"
@click="onExport"
/>
<FaIconButton
v-if="showButton(TABLE_ACTION_CUSTOMIZE_COLUMNS)"
:icon="['fa', 'arrows-alt']"
class="action-btn"
size="mini"
tooltip="Customize Columns"
type="primary"
@click="onCustomizeColumns"
/>
<slot name="suffix"></slot>
</div>
</template>
<script lang="ts">
import {defineComponent, PropType} from 'vue';
import FaIconButton from '@/components/button/FaIconButton.vue';
import {ACTION_ADD, ACTION_DELETE, ACTION_EDIT,} from '@/constants/action';
import {TABLE_ACTION_CUSTOMIZE_COLUMNS, TABLE_ACTION_EXPORT,} from '@/constants/table';
export default defineComponent({
name: 'TableActions',
components: {
FaIconButton,
},
emits: [
'edit',
'delete',
'export',
'customize-columns'
],
props: {
selection: {
type: Array as PropType<TableData>,
required: false,
default: () => {
return [];
},
},
visibleButtons: {
type: Array as PropType<BuiltInTableActionButtonName[]>,
required: false,
default: () => {
return [];
}
}
},
setup(props: TableActionsProps, {emit}) {
// const onAdd = () => {
// emit('click-add');
// };
const onEdit = () => {
emit('edit');
};
const onDelete = async () => {
emit('delete');
};
const onExport = () => {
emit('export');
};
const onCustomizeColumns = () => {
emit('customize-columns');
};
const showButton = (name: string): boolean => {
const {visibleButtons} = props;
if (visibleButtons.length === 0) return true;
return visibleButtons.includes(name);
};
return {
ACTION_ADD,
ACTION_EDIT,
ACTION_DELETE,
TABLE_ACTION_EXPORT,
TABLE_ACTION_CUSTOMIZE_COLUMNS,
// onAdd,
onEdit,
onDelete,
onExport,
onCustomizeColumns,
showButton,
};
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import {defineComponent, h} from 'vue';
import FaIconButton from '@/components/button/FaIconButton.vue';
export default defineComponent({
name: 'TableCell',
props: {
column: {
type: Object,
required: true,
},
row: {
type: Object,
required: true,
},
rowIndex: {
type: Number,
required: true,
}
},
emits: [
'click',
],
setup: function (props) {
const getChildren = () => {
const {row, column, rowIndex} = props as TableCellProps;
const {value, buttons} = column;
// value
if (value !== undefined) {
if (typeof value === 'function') {
return [value(row, rowIndex, column)];
} else {
return value;
}
}
// buttons
if (buttons) {
let _buttons: TableColumnButton[] = [];
if (typeof buttons === 'function') {
_buttons = buttons(row);
} else if (Array.isArray(buttons) && buttons.length > 0) {
_buttons = buttons;
}
return _buttons.map(btn => {
const {tooltip, type, size, icon, disabled, onClick} = btn;
const props = {
key: JSON.stringify({tooltip, type, size, icon}),
tooltip: typeof tooltip === 'function' ? tooltip(row) : tooltip,
type,
size,
icon,
disabled: disabled?.(row),
onClick: () => {
onClick?.(row, rowIndex, column);
},
};
// FIXME: use "as any" to fix type errors temporarily
return h(FaIconButton, props as any);
});
}
// plain text
return [row[column.key]];
};
return () => h('div', getChildren());
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,103 @@
<template>
<el-dialog
:before-close="onClose"
:model-value="visible"
title="Table Columns Customization">
<div class="table-columns-transfer-content">
<Transfer
:data="computedData"
:titles="['Available', 'Selected']"
:value="internalSelectedColumnKeys"
@change="onChange"
/>
</div>
<template #footer>
<Button plain size="mini" type="info" @click="onClose">Cancel</Button>
<Button size="mini" @click="onApply">Apply</Button>
</template>
</el-dialog>
</template>
<script lang="ts">
import {computed, defineComponent, onBeforeMount, ref} from 'vue';
import Transfer from '@/components/transfer/Transfer.vue';
import Button from '@/components/button/Button.vue';
import {DataItem} from 'element-plus/lib/el-transfer/src/transfer';
export default defineComponent({
name: 'TableColumnsTransfer',
components: {Button, Transfer},
props: {
visible: {
type: Boolean,
default: false,
},
columns: {
type: Array,
default: () => {
return [];
},
},
selectedColumnKeys: {
type: Array,
default: () => {
return [];
}
},
},
emits: [
'close',
'change',
'sort',
'apply',
],
setup(props, {emit}) {
const internalSelectedColumnKeys = ref<string[]>([]);
const computedData = computed<DataItem[]>(() => {
const {columns} = props as TableColumnsTransferProps;
return columns.map(d => {
const {key, label, disableTransfer} = d;
return {
key,
label,
disabled: disableTransfer || false,
};
});
});
const onClose = () => {
emit('close');
};
const onChange = (value: string[]) => {
internalSelectedColumnKeys.value = value;
};
const onApply = () => {
emit('apply', internalSelectedColumnKeys.value);
emit('close');
};
onBeforeMount(() => {
const {selectedColumnKeys} = props as TableColumnsTransferProps;
internalSelectedColumnKeys.value = selectedColumnKeys || [];
});
return {
internalSelectedColumnKeys,
computedData,
onClose,
onChange,
onApply,
};
},
});
</script>
<style lang="scss" scoped>
.table-columns-transfer-content {
display: flex;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,284 @@
<template>
<div class="table-header">
<span :class="[column.required ? 'required' : '']" class="label">
<span v-if="column.icon" class="label-icon">
<Icon :icon="column.icon"/>
</span>
{{ column.label }}
</span>
<TableHeaderDialog
v-if="hasDialog"
:action-status-map="actionStatusMap"
:column="column"
:visible="dialogVisible"
:filter="filterData"
:sort="sortData"
@apply="onDialogApply"
@clear="onDialogClear"
@cancel="onDialogCancel"
>
<template #reference>
<div class="actions">
<TableHeaderAction
v-for="{key, tooltip, isHtml, icon, onClick} in actions"
:key="key + JSON.stringify(icon)"
:icon="icon"
:status="actionStatusMap[key]"
:tooltip="tooltip"
:is-html="isHtml"
@click="onClick"
/>
</div>
</template>
</TableHeaderDialog>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, PropType, reactive, ref} from 'vue';
import TableHeaderDialog from '@/components/table/TableHeaderDialog.vue';
import TableHeaderAction from '@/components/table/TableHeaderAction.vue';
import {conditionTypesMap} from '@/components/filter/FilterCondition.vue';
import {ASCENDING, DESCENDING} from '@/constants/sort';
import variables from '@/styles/variables.scss';
import {FILTER_OP_NOT_SET} from '@/constants/filter';
import Icon from '@/components/icon/Icon.vue';
export default defineComponent({
name: 'TableHeader',
components: {
Icon,
TableHeaderAction,
TableHeaderDialog,
},
props: {
column: {
type: Object as PropType<TableColumn>,
required: true,
},
index: {
type: Number,
required: false,
},
},
emits: [
'change',
],
setup(props, {emit}) {
const dialogVisible = ref<boolean>(false);
const actionStatusMap = reactive<TableHeaderActionStatusMap>({
filter: {active: false, focused: false},
sort: {active: false, focused: false},
});
const sortData = ref<SortData>();
const filterData = ref<TableHeaderDialogFilterData>();
const filterItemsMap = computed<Map<any, string | undefined>>(() => {
const map = new Map<any, string | undefined>();
const {column} = props;
const {filterItems} = column;
if (!filterItems) return map;
filterItems.forEach(d => {
const {label, value} = d;
map.set(value, label);
});
return map;
});
const actions = computed<TableColumnButton[]>(() => {
const {column} = props;
// sort icon and tooltip
let sortIcon = ['fa', 'sort-amount-down-alt'];
let sortTooltip = 'Sort';
if (sortData.value?.d === ASCENDING) {
sortIcon = ['fa', 'sort-amount-up'];
sortTooltip = 'Sorted Ascending';
} else if (sortData.value?.d === DESCENDING) {
sortIcon = ['fa', 'sort-amount-down-alt'];
sortTooltip = 'Sorted Descending';
}
// filter tooltip
let filterTooltip = 'Filter';
let filterIsHtml = false;
if (filterData.value) {
const {searchString, conditions, items} = filterData.value;
// search string
if (searchString) {
filterTooltip += `<br><span style="color: ${variables.primaryColor}">Search:</span> <span style="color: ${variables.warningColor};">"${searchString}"</span>`;
filterIsHtml = true;
}
// filter conditions
if (conditions && conditions.length > 0) {
filterTooltip += '<br>' + conditions.filter(d => d.op !== FILTER_OP_NOT_SET)
.map(d => `<span style="color: ${variables.primaryColor};margin-right: 5px">${conditionTypesMap[d.op || '']}:</span> <span style="color: ${variables.warningColor};">"${d.value}"</span>`)
.join('<br>');
filterIsHtml = true;
}
// filter items
if (items && items.length > 0) {
const itemsStr = items.map(value => filterItemsMap.value.get(value)).join(', ');
filterTooltip += `<br><span style="color: ${variables.primaryColor};margin-right: 5px">Include:</span><span style="color: ${variables.warningColor}">` + itemsStr + '</span>';
filterIsHtml = true;
}
}
// tooltip items
const items = [];
if (column.hasSort) {
items.push({
key: 'sort',
tooltip: sortTooltip,
icon: sortIcon,
onClick: () => {
dialogVisible.value = true;
actionStatusMap.sort.focused = true;
}
});
}
if (column.hasFilter) {
items.push({
key: 'filter',
tooltip: filterTooltip,
isHtml: filterIsHtml,
icon: ['fa', 'filter'],
onClick: () => {
dialogVisible.value = true;
actionStatusMap.filter.focused = true;
}
});
}
return items;
});
const hideDialog = () => {
dialogVisible.value = false;
actionStatusMap.filter.focused = false;
actionStatusMap.sort.focused = false;
};
const clearDialog = () => {
const {column} = props as TableHeaderProps;
// set status
actionStatusMap.filter.active = false;
actionStatusMap.sort.active = false;
// set data
sortData.value = undefined;
filterData.value = undefined;
// hide
hideDialog();
// emit
emit('change', column, undefined, undefined);
};
const onDialogCancel = () => {
hideDialog();
};
const onDialogClear = () => {
clearDialog();
};
const onDialogApply = (value: TableHeaderDialogValue) => {
const {column} = props as TableHeaderProps;
const {sort, filter} = value;
// set status
if (sort) actionStatusMap.sort.active = true;
if (filter) actionStatusMap.filter.active = true;
// set data
sortData.value = sort;
filterData.value = filter;
// if no data set, clear
if (!sortData.value && !filterData.value) {
clearDialog();
return;
}
// hide
hideDialog();
// emit
emit('change', column, sortData.value, filterData.value);
};
const hasDialog = computed<boolean>(() => {
const {
hasSort,
hasFilter,
} = props.column;
return !!hasSort || !!hasFilter;
});
return {
dialogVisible,
actionStatusMap,
actions,
sortData,
filterData,
onDialogCancel,
onDialogClear,
onDialogApply,
hasDialog,
};
},
});
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.table-header {
display: flex;
position: relative;
.label {
display: flex;
align-items: center;
&.required:before {
content: "*";
color: $red;
margin-right: 4px;
}
.label-icon {
color: $infoMediumLightColor;
font-size: 8px;
margin-right: 5px;
}
}
.actions {
position: absolute;
right: 0;
height: 100%;
align-items: center;
cursor: pointer;
}
}
</style>
<style scoped>
.table-header:hover .actions >>> .action {
display: inline;
}
.table-header .actions >>> .action {
display: none;
}
</style>

View File

@@ -0,0 +1,86 @@
<template>
<span :class="classes" class="action" @click="onClick">
<el-tooltip :content="tooltip">
<template v-if="isHtml" #content>
<div v-html="tooltip"/>
</template>
<font-awesome-icon :icon="icon"/>
</el-tooltip>
</span>
</template>
<script lang="ts">
import {computed, defineComponent} from 'vue';
export default defineComponent({
name: 'TableHeaderAction',
props: {
tooltip: {
type: [String, Object],
required: false,
},
isHtml: {
type: Boolean,
required: false,
default: false,
},
icon: {
type: [Array, String],
required: true,
},
status: {
type: Object,
required: false,
default: () => {
return {active: false, focused: false};
}
}
},
emits: [
'click',
],
setup(props, {emit}) {
const classes = computed<string[]>(() => {
const {status} = props as TableHeaderActionProps;
if (!status) return [];
const {active, focused} = status;
const cls = [];
if (active) cls.push('active');
if (focused) cls.push('focused');
return cls;
});
const onClick = () => {
emit('click');
};
return {
classes,
onClick,
};
},
});
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.action {
margin-left: 3px;
font-size: 10px;
&:hover {
color: $primaryColor;
}
&.focused {
display: inline !important;
color: $primaryColor;
}
&.active {
display: inline !important;
color: $warningColor;
}
}
</style>

View File

@@ -0,0 +1,290 @@
<template>
<el-popover
:show-arrow="false"
:visible="visible"
class="table-header-dialog"
popper-class="table-header-popper"
trigger="manual"
>
<template #reference>
<div>
<slot name="reference"/>
</div>
</template>
<div v-click-outside="onClickOutside" class="content">
<div class="header">
<div class="title">{{ column.label }}</div>
</div>
<span class="close" @click="onCancel">
<el-icon name="close"></el-icon>
</span>
<div class="body">
<div class="list">
<div v-if="column.hasSort" class="item sort">
<TableHeaderDialogSort :value="internalSort?.d" @change="onSortChange"/>
</div>
<div v-if="column.hasFilter" class="item filter">
<TableHeaderDialogFilter
:column="column"
:conditions="conditions"
:search-string="searchString"
@change="onFilterChange"
@enter="onFilterEnter"
/>
</div>
</div>
</div>
<div class="footer">
<Button plain size="mini" tooltip="Cancel" type="info" @click="onCancel">Cancel</Button>
<Button plain size="mini" tooltip="Clear" type="warning" @click="onClear">Clear</Button>
<Button
:disabled="isApplyDisabled"
size="mini"
tooltip="Apply"
type="primary"
@click="onApply"
>
Apply
</Button>
</div>
</div>
</el-popover>
</template>
<script lang="ts">
import {computed, defineComponent, PropType, ref, watch} from 'vue';
import Button from '@/components/button/Button.vue';
import TableHeaderDialogFilter from '@/components/table/TableHeaderDialogFilter.vue';
import TableHeaderDialogSort from '@/components/table/TableHeaderDialogSort.vue';
import variables from '@/styles/variables.scss';
import {plainClone} from '@/utils/object';
import {FILTER_OP_NOT_SET} from '@/constants/filter';
import {ClickOutside} from 'element-plus/lib/directives';
export default defineComponent({
name: 'TableHeaderFilter',
components: {
TableHeaderDialogSort,
TableHeaderDialogFilter,
Button,
},
directives: {
ClickOutside,
},
props: {
visible: {
type: Boolean,
required: false,
default: false,
},
column: {
type: Object as PropType<TableColumn>,
required: true,
},
actionStatusMap: {
type: Object,
required: true,
},
sort: {
type: Object as PropType<SortData>,
required: false,
},
filter: {
type: Object,
required: false,
},
},
emits: [
'click',
'cancel',
'clear',
'apply',
],
setup(props, {emit}) {
const defaultInternalSort = {key: props.column.key} as SortData;
const internalSort = ref<SortData>();
const internalFilter = ref<TableHeaderDialogFilterData>();
const searchString = computed<string | undefined>(() => internalFilter.value?.searchString);
const conditions = computed<FilterConditionData[]>(() => internalFilter.value?.conditions || []);
const items = computed<string[]>(() => internalFilter.value?.items || []);
const trueConditions = computed<FilterConditionData[]>(() => {
return conditions.value?.filter(d => d.op !== FILTER_OP_NOT_SET);
});
const isEmptyFilter = computed<boolean>(() => {
return !searchString.value && trueConditions.value.length == 0 && items.value.length === 0;
});
const isApplyDisabled = computed<boolean>(() => {
for (const cond of trueConditions.value) {
if (!cond.value) {
return true;
}
}
return false;
});
const cancel = () => {
emit('cancel');
};
const clear = () => {
if (!internalSort.value) internalSort.value = plainClone(defaultInternalSort);
internalSort.value.d = undefined;
internalFilter.value = undefined;
emit('clear');
};
const apply = () => {
if (!internalSort.value && isEmptyFilter.value) {
clear();
return;
}
const value: TableHeaderDialogValue = {
sort: internalSort.value,
filter: internalFilter.value,
};
emit('apply', value);
};
const onClickOutside = () => {
const {visible} = props;
if (!visible) return;
cancel();
};
const onCancel = () => {
cancel();
};
const onClear = () => {
clear();
};
const onApply = () => {
apply();
};
const onSortChange = (value: string) => {
if (!internalSort.value) internalSort.value = plainClone(defaultInternalSort);
internalSort.value.d = value;
};
const onFilterChange = (value: TableHeaderDialogFilterData) => {
internalFilter.value = value;
};
const onFilterEnter = () => {
apply();
};
watch(() => {
const {visible} = props as TableHeaderDialogProps;
return visible;
}, () => {
const {sort, filter, visible} = props as TableHeaderDialogProps;
if (visible) {
internalSort.value = (sort ? plainClone(sort) : plainClone(defaultInternalSort)) as SortData;
internalFilter.value = plainClone(filter) as TableHeaderDialogFilterData;
}
});
return {
variables,
internalSort,
searchString,
conditions,
isApplyDisabled,
onClickOutside,
onCancel,
onClear,
onApply,
onSortChange,
onFilterChange,
onFilterEnter,
};
},
});
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.content {
position: relative;
min-width: 100%;
flex: 1;
display: flex;
flex-direction: column;
.close {
position: absolute;
top: 0;
right: 0;
cursor: pointer;
}
.header {
.title {
font-size: 16px;
font-weight: 900;
color: $infoMediumColor;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid $infoBorderColor;
}
}
.body {
flex: 1;
display: flex;
flex-direction: column;
.list {
flex: 1;
min-height: 100%;
display: flex;
flex-direction: column;
.item {
padding: 10px 0;
border-bottom: 1px solid $infoBorderColor;
&:first-child {
padding-top: 0;
}
&:last-child {
border-bottom: none;
}
&.sort {
flex-basis: 100%;
}
&.filter {
flex: 1;
display: flex;
flex-direction: column;
}
}
}
}
.footer {
height: 30px;
text-align: right;
}
}
</style>
<style>
.table-header-popper {
min-width: 320px !important;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,236 @@
<template>
<div class="table-header-dialog-filter">
<div class="title">
<span>Filter</span>
<el-input
v-if="column.allowFilterSearch"
:model-value="internalSearchString"
class="search"
placeholder="Search"
prefix-icon="el-icon-search"
size="mini"
@input="onSearch"
@clear="onClear"
@keyup.enter="onEnter"
/>
<!-- <el-tooltip content="Add Condition">-->
<!-- <span class="icon" @click="onAddCondition">-->
<!-- <el-icon name="circle-plus-outline"/>-->
<!-- </span>-->
<!-- </el-tooltip>-->
</div>
<!-- <el-form>-->
<!-- <FilterConditionList :conditions="internalConditions" @change="onConditionsChange"/>-->
<!-- </el-form>-->
<div v-if="column.allowFilterItems" class="items">
<template v-if="filteredItems.length > 0">
<el-checkbox-group v-model="internalItems" class="item-list" @change="onItemsChange">
<el-checkbox
v-for="(item, $index) in filteredItems"
:key="$index"
:label="item.value"
class="item"
>
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</template>
<template v-else>
<Empty description="No data available"></Empty>
</template>
</div>
</div>
</template>
<script lang="ts">
import {computed, defineComponent, PropType, ref, watch} from 'vue';
import Empty from '@/components/empty/Empty.vue';
import {getDefaultFilterCondition} from '@/components/filter/FilterCondition.vue';
// import FilterConditionList from '@/components/filter/FilterConditionList.vue';
import {debounce} from '@/utils/debounce';
export default defineComponent({
name: 'TableHeaderDialogFilter',
components: {
// FilterConditionList,
Empty,
},
props: {
column: {
type: Object as PropType<TableColumn>,
required: false,
},
searchString: {
type: String,
required: false,
},
conditions: {
type: Array as PropType<FilterConditionData[]>,
required: false,
default: () => {
return [];
}
},
},
emits: [
'change',
'enter',
],
setup(props, {emit}) {
const internalConditions = ref<FilterConditionData[]>([getDefaultFilterCondition()]);
const internalSearchString = ref<string>();
const internalItems = ref<string[]>([]);
const filterData = computed<TableHeaderDialogFilterData>(() => {
return {
searchString: internalSearchString.value,
conditions: internalConditions.value,
items: internalItems.value,
};
});
const filteredItems = computed<SelectOption[]>(() => {
const {column} = props as TableHeaderDialogFilterProps;
const items = column?.filterItems;
// undefined items
if (!items) {
// console.log('undefined items');
return [];
}
// invalid type of items or empty items
if (!Array.isArray(items) || items.length === 0) {
// console.log('invalid type of items or empty items');
return [];
}
// items as an array of SelectOption
// console.log('items as an array of SelectOption');
return items.filter(d => filterData.value.searchString ? d.label?.toLowerCase()?.includes(filterData.value.searchString) : true);
});
const onAddCondition = () => {
internalConditions.value.push(getDefaultFilterCondition());
};
const onConditionsChange = (newConditions: FilterConditionData[]) => {
internalConditions.value = newConditions;
emit('change', filterData.value);
};
const onItemsChange = (newItems: string[]) => {
internalItems.value = newItems;
emit('change', filterData.value);
};
const search = debounce(() => {
if (internalSearchString.value) {
internalItems.value = filteredItems.value.map(d => d.value);
} else {
internalItems.value = [];
}
emit('change', filterData.value);
}, {delay: 100});
const onSearch = (value?: string) => {
internalSearchString.value = value;
search();
};
const onEnter = () => {
emit('enter');
};
watch(() => {
const {searchString} = props as TableHeaderDialogFilterProps;
return searchString;
}, (value) => {
internalSearchString.value = value;
});
watch(() => {
const {conditions} = props as TableHeaderDialogFilterProps;
return conditions;
}, (value) => {
if (value) {
internalConditions.value = value;
if (internalConditions.value.length === 0) {
internalConditions.value.push(getDefaultFilterCondition());
}
}
});
return {
internalSearchString,
internalConditions,
internalItems,
filteredItems,
onAddCondition,
onConditionsChange,
onItemsChange,
onSearch,
onEnter,
};
},
});
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.table-header-dialog-filter {
flex: 1;
display: flex;
flex-direction: column;
.title {
font-size: 14px;
font-weight: 900;
margin-bottom: 10px;
color: $infoMediumColor;
display: flex;
align-items: center;
.search {
margin-left: 5px;
flex: 1;
}
.icon {
cursor: pointer;
margin-left: 5px;
}
}
.items {
overflow: auto;
flex: 1;
border: 1px solid $infoBorderColor;
padding: 10px;
.item-list {
list-style: none;
padding: 0;
margin: 0;
height: 100%;
max-height: 240px;
display: flex;
flex-direction: column;
.item {
cursor: pointer;
&:hover {
text-decoration: underline;
}
.label {
margin-left: 5px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="table-header-dialog-sort">
<div class="title">
<span>Sort</span>
<el-tooltip v-if="value" content="Clear sort">
<span class="icon" @click="onClear">
<el-icon name="circle-close"/>
</span>
</el-tooltip>
</div>
<el-radio-group :model-value="value" size="mini" type="primary" @change="onChange">
<el-radio-button :label="ASCENDING" class="sort-btn">
<font-awesome-icon :icon="['fa', 'sort-amount-up']"/>
Ascending
</el-radio-button>
<el-radio-button :label="DESCENDING" class="sort-btn">
<font-awesome-icon :icon="['fa', 'sort-amount-down-alt']"/>
Descending
</el-radio-button>
</el-radio-group>
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue';
import {ASCENDING, DESCENDING, UNSORTED} from '@/constants/sort';
export default defineComponent({
name: 'TableHeaderDialogSort',
props: {
value: {
type: String,
required: false,
},
},
emits: [
'change'
],
setup(props, {emit}) {
const onChange = (value: SortDirection) => {
if (value === UNSORTED) {
emit('change', undefined);
return;
}
emit('change', value);
};
const onClear = () => {
emit('change');
};
return {
onChange,
onClear,
UNSORTED,
ASCENDING,
DESCENDING,
};
},
});
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
.table-header-dialog-sort {
.title {
font-size: 14px;
font-weight: 900;
margin-bottom: 10px;
color: $infoMediumColor;
display: flex;
align-items: center;
.icon {
cursor: pointer;
margin-left: 5px;
}
}
.el-radio-group {
width: 100%;
display: flex;
.sort-btn.el-radio-button {
&:not(.unsorted) {
flex: 1;
}
&.unsorted {
flex-basis: 20px;
}
}
}
}
</style>
<style scoped>
.table-header-dialog-sort >>> .el-radio-group .el-radio-button .el-radio-button__inner {
width: 100%;
}
</style>

View File

@@ -0,0 +1,68 @@
import {inject, Ref, ref, SetupContext} from 'vue';
import {Table} from 'element-plus/lib/el-table/src/table.type';
import {ElMessageBox} from 'element-plus';
const useAction = (props: TableProps, ctx: SetupContext, table: Ref<Table | undefined>, actionFunctions: ListLayoutActionFunctions) => {
const {emit} = ctx;
// store context
const storeContext = inject<ListStoreContext<BaseModel>>('store-context');
const ns = storeContext?.namespace;
const store = storeContext?.store;
// table selection
const selection = ref<TableData>([]);
const onSelectionChange = (value: TableData) => {
selection.value = value;
emit('selection-change', value);
};
// action functions
const {
getList,
deleteList,
} = actionFunctions;
const onAdd = () => {
emit('add');
};
const onEdit = async () => {
emit('edit', selection.value);
if (storeContext) {
store?.commit(`${ns}/showDialog`, 'edit');
store?.commit(`${ns}/setIsSelectiveForm`, true);
store?.commit(`${ns}/setFormList`, selection.value);
}
};
const onDelete = async () => {
const res = await ElMessageBox.confirm('Are you sure to delete selected items?', 'Batch Delete', {
type: 'warning',
confirmButtonText: 'Delete',
confirmButtonClass: 'el-button--danger',
});
if (!res) return;
const ids = selection.value.map(d => d._id as string);
await deleteList(ids);
table.value?.store?.clearSelection();
await getList();
emit('delete', selection.value);
};
const onExport = () => {
emit('export');
};
return {
// public variables and methods
selection,
onSelectionChange,
onAdd,
onEdit,
onDelete,
onExport,
};
};
export default useAction;

View File

@@ -0,0 +1,136 @@
import {computed, onBeforeMount, onMounted, ref, Ref, SetupContext} from 'vue';
import {Table, TableColumnCtx} from 'element-plus/lib/el-table/src/table.type';
import {cloneArray, plainClone} from '@/utils/object';
import useStore from '@/components/table/store';
import {getColumnWidth, getTableWidth} from '@/utils/table';
const useColumns = (props: TableProps, ctx: SetupContext, table: Ref<Table | undefined>, wrapper: Ref) => {
const {columns} = props;
const {store} = useStore(props, ctx, table);
const columnsTransferVisible = ref<boolean>(false);
const internalSelectedColumnKeys = ref<string[]>([]);
const columnsMap = computed<TableColumnsMap>(() => {
const map = {} as TableColumnsMap;
columns.forEach(c => {
map[c.key] = c;
});
return map;
});
const columnsCtx = computed<TableColumnCtx[]>(() => {
return table.value?.store.states.columns.value || [];
});
const columnCtxMap = computed<TableColumnCtxMap>(() => {
const map = {} as TableColumnCtxMap;
columnsCtx.value.forEach(c => {
map[c.columnKey] = c;
});
return map;
});
const selectedColumns = computed<TableColumn[]>(() => {
return internalSelectedColumnKeys.value.map(key => columnsMap.value[key]);
});
const defaultSelectedColumns = computed<TableColumn[]>(() => {
return columns.filter(d => !d.defaultHidden);
});
const onShowColumnsTransfer = () => {
columnsTransferVisible.value = true;
};
const onHideColumnsTransfer = () => {
columnsTransferVisible.value = false;
};
const isColumnsEqual = (columnKeys: string[]) => {
const columnKeysSorted = cloneArray(columnKeys).sort().join(',');
const internalSelectedColumnKeysSorted = cloneArray(internalSelectedColumnKeys.value).sort().join(',');
return columnKeysSorted === internalSelectedColumnKeysSorted;
};
const updateColumns = (columnKeys?: string[]) => {
if (!store.value) return;
if (!columnKeys) {
columnKeys = selectedColumns.value.map(d => d.key);
}
// selection column keys
const selectionColumnKeys = columnsCtx.value.filter(d => d.type === 'selection').map(d => d.columnKey);
// table width
const tableWidth = getTableWidth();
// table width
let tableFixedTotalWidth = 0;
columns.map((d) => getColumnWidth(d) as number).filter(w => !!w).forEach((w: number) => {
tableFixedTotalWidth += w;
});
// auto width
const autoWidth = tableWidth ? (tableWidth - tableFixedTotalWidth - 40 - 12) : 0;
// columns to update
const columnsToUpdate = selectionColumnKeys.concat(columnKeys).map(key => {
const columnCtx = columnCtxMap.value[key];
const column = columnsMap.value[key];
if (column && column.width === 'auto') {
if (autoWidth) {
columnCtx.width = autoWidth > 400 ? autoWidth : 400;
}
}
return columnCtx;
});
// update columns
if (isColumnsEqual(columnKeys)) {
store.value?.commit('setColumns', columnsToUpdate);
store.value?.updateColumns();
}
internalSelectedColumnKeys.value = columnKeys;
// set table width to 100%
// wrapper.value.querySelectorAll('.el-table__body').forEach((el: HTMLTableElement) => {
// el.setAttribute('style', 'width: 100%');
// });
};
const onColumnsChange = (value: string[]) => {
updateColumns(value);
};
const initColumns = () => {
if (defaultSelectedColumns.value.length < columns.length) {
internalSelectedColumnKeys.value = plainClone(defaultSelectedColumns.value.map(d => d.key));
} else {
internalSelectedColumnKeys.value = cloneArray(columns.map(d => d.key));
}
};
onBeforeMount(() => {
initColumns();
});
onMounted(() => {
setTimeout(updateColumns, 0);
});
return {
internalSelectedColumnKeys,
columnsMap,
columnsTransferVisible,
selectedColumns,
onShowColumnsTransfer,
onHideColumnsTransfer,
onColumnsChange,
};
};
export default useColumns;

View File

@@ -0,0 +1,14 @@
import {computed, SetupContext} from 'vue';
const useData = (props: TableProps, ctx: SetupContext) => {
const tableData = computed(() => {
const {data} = props;
return data;
});
return {
tableData,
};
};
export default useData;

View File

@@ -0,0 +1,16 @@
import {SetupContext} from 'vue';
const useHeader = (props: TableProps, ctx: SetupContext) => {
const {emit} = ctx;
const onHeaderChange = (column: TableColumn, sort: SortData, filter: FilterConditionData[]) => {
emit('header-change', column, sort, filter);
};
return {
// public variables and methods
onHeaderChange,
};
};
export default useHeader;

View File

@@ -0,0 +1,28 @@
import {SetupContext} from 'vue';
const usePagination = (props: TableProps, ctx: SetupContext) => {
const {emit} = ctx;
const onCurrentChange = (page: number) => {
const {pageSize} = props;
emit('pagination-change', {
page,
size: pageSize,
} as TablePagination);
};
const onSizeChange = (size: number) => {
const {page} = props;
emit('pagination-change', {
page,
size,
} as TablePagination);
};
return {
onCurrentChange,
onSizeChange,
};
};
export default usePagination;

View File

@@ -0,0 +1,22 @@
import {computed, Ref, SetupContext} from 'vue';
import {Table, TableColumnCtx} from 'element-plus/lib/el-table/src/table.type';
const useStore = (props: TableProps, ctx: SetupContext, table: Ref<Table | undefined>) => {
const setColumns = (states: TableStoreStates, columns: TableColumnCtx[]) => {
states._columns.value = columns;
};
const store = computed<TableStore | undefined>(() => {
const store = (table.value?.store as unknown) as TableStore;
if (!store) return;
store.mutations.setColumns = setColumns;
return store;
});
return {
// public variables and methods
store,
};
};
export default useStore;