Browse Source

feat(系统模块): 1、添加smart-table组件 2、添加功能管理页面

shizhongming 3 years ago
parent
commit
e24a338403
  1. 5
      src/components/Button/index.ts
  2. 5
      src/components/Button/src/BasicButton.vue
  3. 53
      src/components/Button/src/PopConfirmVxeButton.vue
  4. 3
      src/components/Icon/index.ts
  5. 3
      src/components/Layout/index.ts
  6. 23
      src/components/Layout/src/TableSearchLayout.less
  7. 48
      src/components/Layout/src/TableSearchLayout.tsx
  8. 1
      src/components/SmartTable/VXETablePluginAntd.ts
  9. 17
      src/components/SmartTable/index.ts
  10. 97
      src/components/SmartTable/src/SmartTable.less
  11. 434
      src/components/SmartTable/src/SmartTable.tsx
  12. 154
      src/components/SmartTable/src/components/SmartTableAddEditModal.tsx
  13. 375
      src/components/SmartTable/src/components/SmartTableColumnConfig.tsx
  14. 225
      src/components/SmartTable/src/components/SmartVxeTableAction.vue
  15. 215
      src/components/SmartTable/src/components/TableAction.vue
  16. 32
      src/components/SmartTable/src/const.ts
  17. 10
      src/components/SmartTable/src/defaultConfig.ts
  18. 124
      src/components/SmartTable/src/hooks/useLoadData.ts
  19. 24
      src/components/SmartTable/src/hooks/useLoading.ts
  20. 73
      src/components/SmartTable/src/hooks/usePagination.ts
  21. 174
      src/components/SmartTable/src/hooks/useSmartTable.ts
  22. 88
      src/components/SmartTable/src/hooks/useSmartTableCheckbox.ts
  23. 190
      src/components/SmartTable/src/hooks/useSmartTableColumn.tsx
  24. 48
      src/components/SmartTable/src/hooks/useSmartTableColumnConfig.ts
  25. 124
      src/components/SmartTable/src/hooks/useSmartTableDynamicClassStyle.ts
  26. 260
      src/components/SmartTable/src/hooks/useTableAjax.ts
  27. 195
      src/components/SmartTable/src/hooks/useTableDrag.ts
  28. 213
      src/components/SmartTable/src/hooks/useTableModalAddEdit.ts
  29. 83
      src/components/SmartTable/src/hooks/useTablePager.tsx
  30. 205
      src/components/SmartTable/src/hooks/useTableSearchForm.ts
  31. 358
      src/components/SmartTable/src/hooks/useTableToolbarConfig.ts
  32. 35
      src/components/SmartTable/src/hooks/useToolbarEvent.ts
  33. 22
      src/components/SmartTable/src/hooks/userSmartTableContext.ts
  34. 44
      src/components/SmartTable/src/props.ts
  35. 75
      src/components/SmartTable/src/renderer/VxeTableAntRenderer.css
  36. 998
      src/components/SmartTable/src/renderer/VxeTableAntdRenderer.ts
  37. 99
      src/components/SmartTable/src/renderer/VxeTableButtonRenderer.tsx
  38. 39
      src/components/SmartTable/src/style/SmartTableColumnConfig.less
  39. 53
      src/components/SmartTable/src/types/SmartSearchFormType.ts
  40. 31
      src/components/SmartTable/src/types/SmartTableActionType.ts
  41. 35
      src/components/SmartTable/src/types/SmartTableAddEditType.ts
  42. 30
      src/components/SmartTable/src/types/SmartTableAuthType.ts
  43. 75
      src/components/SmartTable/src/types/SmartTableButton.ts
  44. 83
      src/components/SmartTable/src/types/SmartTableColumnType.ts
  45. 11
      src/components/SmartTable/src/types/SmartTableToolbarConfigType.ts
  46. 159
      src/components/SmartTable/src/types/SmartTableType.ts
  47. 21
      src/components/SmartTable/src/utils.ts
  48. 21
      src/components/registerGlobComp.ts
  49. 20
      src/design/public.less
  50. 31
      src/hooks/setting/UseSizeSetting.ts
  51. 24
      src/hooks/web/useMessage.tsx
  52. 2
      src/locales/lang/en.ts
  53. 2
      src/locales/lang/zh_CN.ts
  54. 106
      src/modules/app/i18n/lang/en_US.ts
  55. 106
      src/modules/app/i18n/lang/zh_CN.ts
  56. 57
      src/modules/system/constants/SystemConstants.ts
  57. 81
      src/modules/system/views/function/FunctionListView.api.ts
  58. 303
      src/modules/system/views/function/FunctionListView.config.ts
  59. 284
      src/modules/system/views/function/FunctionListView.vue
  60. 75
      src/modules/system/views/function/lang/en_US.ts
  61. 45
      src/modules/system/views/function/lang/zh_CN.ts
  62. 2
      src/router/guard/permissionGuard.ts
  63. 21
      src/router/helper/routeHelper.ts
  64. 14
      src/settings/componentSetting.ts
  65. 6
      src/settings/projectSetting.ts
  66. 4
      src/store/modules/app.ts
  67. 70
      src/utils/auth/index.ts
  68. 7
      src/utils/http/axios/index.ts
  69. 9
      src/utils/is.ts
  70. 8
      types/config.d.ts
  71. 11
      types/utils.d.ts
  72. 2
      vite.config.ts

5
src/components/Button/index.ts

@ -1,9 +1,12 @@
import type { ButtonProps as AndtButtonProps } from 'ant-design-vue';
import { withInstall } from '@/utils';
import type { ExtractPropTypes } from 'vue';
import button from './src/BasicButton.vue';
import popConfirmButton from './src/PopConfirmButton.vue';
import { buttonProps } from './src/props';
import popConfirmVxeButton from './src/PopConfirmVxeButton.vue';
export const Button = withInstall(button);
export const PopConfirmButton = withInstall(popConfirmButton);
export declare type ButtonProps = Partial<ExtractPropTypes<typeof buttonProps>>;
export declare type ButtonProps = Partial<ExtractPropTypes<typeof buttonProps>> & AndtButtonProps;
export const PopConfirmVxeButton = withInstall(popConfirmVxeButton);

5
src/components/Button/src/BasicButton.vue

@ -1,10 +1,9 @@
<template>
<Button v-bind="getBindValue" :class="getButtonClass" @click="onClick">
<template #icon>
<slot name="icon"></slot>
<template v-if="preIcon" #icon>
<Icon :icon="preIcon" :size="iconSize" />
</template>
<template #default="data">
<Icon :icon="preIcon" v-if="preIcon" :size="iconSize" />
<slot v-bind="data || {}"></slot>
<Icon :icon="postIcon" v-if="postIcon" :size="iconSize" />
</template>

53
src/components/Button/src/PopConfirmVxeButton.vue

@ -0,0 +1,53 @@
<script lang="ts">
import { computed, defineComponent, h, unref, useAttrs } from 'vue';
import { Popconfirm } from 'ant-design-vue';
import { extendSlots } from '@/utils/helper/tsxHelper';
import { omit } from 'lodash-es';
import { useI18n } from '@/hooks/web/useI18n';
import { VxeButton } from 'vxe-table';
const props = {
/**
* Whether to enable the drop-down menu
* @default: true
*/
enable: {
type: Boolean,
default: true,
},
};
export default defineComponent({
name: 'PopConfirmVxeButton',
inheritAttrs: false,
props,
setup(props, { slots }) {
const { t } = useI18n();
const attrs = useAttrs();
// get inherit binding value
const getBindValues = computed(() => {
return Object.assign(
{
okText: t('common.okText'),
cancelText: t('common.cancelText'),
},
{ ...props, ...unref(attrs) },
);
});
return () => {
const bindValues = omit(unref(getBindValues), 'icon');
const btnBind = omit(bindValues, 'title') as Recordable;
if (btnBind.disabled) btnBind.color = '';
const Button = h(VxeButton, btnBind, extendSlots(slots));
// If it is not enabled, it is a normal button
if (!props.enable) {
return Button;
}
return h(Popconfirm, bindValues, { default: () => Button });
};
},
});
</script>

3
src/components/Icon/index.ts

@ -1,4 +1,5 @@
import SvgIcon from './src/SvgIcon.vue';
import IconPicker from './src/IconPicker.vue';
import Icon from './Icon.vue';
export { IconPicker, SvgIcon };
export { IconPicker, SvgIcon, Icon };

3
src/components/Layout/index.ts

@ -0,0 +1,3 @@
import TableSearchLayout from './src/TableSearchLayout';
export { TableSearchLayout };

23
src/components/Layout/src/TableSearchLayout.less

@ -0,0 +1,23 @@
.smart-table-search-layout {
display: flex;
flex-flow: column;
height: 100%;
}
.smart-search-container {
width: 100%;
margin-bottom: 6px;
padding: 12px 10px 0;
background-color: @component-background;
}
.smart-table-container {
flex: 1;
min-height: 1px;
padding: 0 10px 5px;
background-color: @component-background;
}
.smart-table-search-hidden {
display: none;
}

48
src/components/Layout/src/TableSearchLayout.tsx

@ -0,0 +1,48 @@
import { defineComponent, computed, unref, toRefs } from 'vue';
import { propTypes } from '@/utils/propTypes';
import './TableSearchLayout.less';
export default defineComponent({
name: 'TableSearchLayout',
props: {
/**
* 是否显示搜索栏
*/
showSearch: propTypes.bool.def(true),
},
setup(props, { slots }) {
const { showSearch: showSearchRef } = toRefs(props);
const { search, default: tableSlot } = slots;
const hasSearchForm = computed(() => search !== undefined);
// const getTableContainerStyle = computed((): StyleValue => {
// let height = '100%'
// if (unref(hasSearchForm) && unref(showSearchRef)) {
// height = 'calc(100% - 70px)'
// }
// return {
// height,
// }
// })
const getSearchContainerClass = computed(() => {
const classList = ['smart-search-container'];
if (!unref(showSearchRef)) {
classList.push('smart-table-search-hidden');
}
return classList;
});
return () => (
<div class="smart-table-search-layout">
{unref(hasSearchForm) ? (
<div class={unref(getSearchContainerClass)}>{search && search()}</div>
) : (
''
)}
<div class="smart-table-container">{tableSlot && tableSlot()}</div>
</div>
);
},
});

1
src/components/SmartTable/VXETablePluginAntd.ts

@ -0,0 +1 @@
export { default as VXETablePluginAntd } from './src/renderer/VxeTableAntdRenderer';

17
src/components/SmartTable/index.ts

@ -0,0 +1,17 @@
export { default as SmartTable } from './src/SmartTable';
export { default as TableAction } from './src/components/TableAction.vue';
export { default as SmartVxeTableAction } from './src/components/SmartVxeTableAction.vue';
export { useSmartTable } from './src/hooks/useSmartTable';
export * from './src/types/SmartTableType';
export * from './src/types/SmartSearchFormType';
export * from './src/types/SmartTableAddEditType';
export * from './src/types/SmartTableActionType';
export * from './src/types/SmartTableButton';
export * from './src/types/SmartTableColumnType';
export * from './src/types/SmartTableAuthType';
export * from './src/types/SmartTableToolbarConfigType';

97
src/components/SmartTable/src/SmartTable.less

@ -0,0 +1,97 @@
@import './style/SmartTableColumnConfig';
/* 表格样式 */
.smart-table {
.vxe-tools--wrapper {
margin-right: 12px;
}
/* 文本颜色 */
.text-color--success {
color: green;
}
.text-color--lime {
color: #a0d911;
}
.text-color--link {
color: #1890ff;
}
.text-color--warning {
color: #faad14;
}
.text-color--warning-add {
color: #fa8c16;
}
.text-color--danger {
color: #f5222d;
}
/* 文本颜色加粗 */
.text-color--success-bold {
color: green;
font-weight: bold;
}
.text-color--lime-bold {
color: #a0d911;
font-weight: bold;
}
.text-color--link-bold {
color: #1890ff;
font-weight: bold;
}
.text-color--warning-bold {
color: #faad14;
font-weight: bold;
}
.text-color--warning-add-bold {
color: #fa8c16;
font-weight: bold;
}
.text-color--danger-bold {
color: #f5222d;
font-weight: bold;
}
/* 背景色 */
.bg--success {
background: #7ce57cad;
}
.bg--lime {
background: #a0d911;
}
.bg--link {
background: #1890ff;
}
.bg--warning {
background: #faad14;
}
.bg--warning-add {
background: #fa8c16;
}
.bg--danger {
background: #f5222d;
}
}
/* 表单内label样式 */
.text-color--danger > label {
color: #f5222d;
}

434
src/components/SmartTable/src/SmartTable.tsx

@ -0,0 +1,434 @@
import type { SmartTableProps, TableActionType } from './types/SmartTableType';
import type { VxeGridInstance } from 'vxe-table';
import { computed, defineComponent, provide, Ref, ref, unref } from 'vue';
import { TableSearchLayout } from '@/components/Layout';
import { BasicForm } from '@/components/Form';
import { useModal } from '@/components/Modal';
import { useI18n } from '@/hooks/web/useI18n';
import { smartTableProps } from './props';
import { useTableSearchForm } from './hooks/useTableSearchForm';
import { useLoading } from './hooks/useLoading';
import { usePagination } from './hooks/usePagination';
import { useTableAjax } from './hooks/useTableAjax';
import { useTableToolbarConfig } from './hooks/useTableToolbarConfig';
import { useTableModalAddEditConfig } from './hooks/useTableModalAddEdit';
import { createTableContext } from './hooks/userSmartTableContext';
import { useTableRowDrag } from './hooks/useTableDrag';
import { useSmartTableColumn } from './hooks/useSmartTableColumn';
import SmartTableAddEditModal from './components/SmartTableAddEditModal';
import './renderer/VxeTableButtonRenderer';
import { error } from '@/utils/log';
import './SmartTable.less';
import { useToolbarEvent } from './hooks/useToolbarEvent';
import { useSmartTableCheckbox } from './hooks/useSmartTableCheckbox';
import { useSmartTableDynamicClassStyle } from './hooks/useSmartTableDynamicClassStyle';
import { useSmartTableColumnConfig } from '@/components/SmartTable/src/hooks/useSmartTableColumnConfig';
export default defineComponent({
name: 'SmartTable',
props: smartTableProps,
emits: [
'register',
'toolbar-tool-click',
'proxy-query',
'proxy-delete',
'add-edit-modal-show',
'cell-click',
],
setup(props, { emit, slots, attrs }) {
const { t } = useI18n();
const tableElRef = ref<VxeGridInstance>() as Ref<VxeGridInstance>;
const wrapRef = ref(null);
const innerPropsRef = ref<Partial<SmartTableProps>>();
/**
* 表格计算属性
*/
const getTableProps = computed(() => {
return {
...props,
...unref(innerPropsRef),
};
});
const setProps = (props: Partial<SmartTableProps>) => {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
};
// -------------- 分页 ---------------------------
const {
getPaginationInfo,
getPagination,
setPagination,
getShowPagination,
setShowPagination,
} = usePagination(getTableProps);
/**
* vxe-table函数
*/
const getTableInstance = () => unref(tableElRef);
provide('getTableInstance', getTableInstance);
const commitVxeProxy = (code, ...args) => getTableInstance()?.commitProxy(code, args);
const getCheckboxRecords = (isFull: boolean) =>
getTableInstance()?.getCheckboxRecords(isFull) || [];
const getRadioRecord = (isFull: boolean) => getTableInstance()?.getRadioRecord(isFull);
const setRadioRow = (row: any) => getTableInstance()!.setRadioRow(row);
const setCheckboxRow = (rows: any | any[], checked: boolean) =>
getTableInstance()!.setCheckboxRow(rows, checked);
/**
* 加载状态
*/
const { getLoading, setLoading } = useLoading(getTableProps);
// -------------- 搜索表单 ------------------------
const {
getSearchFormProps,
handleSearchInfoChange,
getSearchFormSlot,
getSearchFormColumnSlot,
registerSearchForm,
searchFormAction,
getSearchFormVisible,
} = useTableSearchForm(
getTableProps,
slots,
(params) => query(params),
getLoading,
setPagination,
);
// -------------- 加载函数 ------------------------
const {
reload,
query,
getProxyConfigRef,
deleteByRow,
deleteByCheckbox,
getProxyEvents,
useYnByCheckbox,
useYnByRow,
} = useTableAjax(getTableProps, tableElRef, emit, {
commitVxeProxy,
getSearchFormParameter: searchFormAction.getSearchFormParameter,
getCheckboxRecords,
setLoading,
});
// -------------- 添加修改操作 ---------------------
const [registerAddEditModal, { openModal: openAddEditModal }] = useModal();
const addEditModalRef = ref();
const getAddEditFieldsValue = () => unref(addEditModalRef).getFieldsValue();
const resetAddEditFields = () => unref(addEditModalRef).resetFields();
const setAddEditFieldsValue = (data: Recordable) => unref(addEditModalRef).setFieldsValue(data);
const validateAddEdit = () => unref(addEditModalRef).validate();
const validateAddEditFields = () => unref(addEditModalRef).validateFields();
const getAddEditForm = () => unref(addEditModalRef).getFormAction();
const {
getHasAddEdit,
showAddModal,
editByCheckbox,
editByRowModal,
getAddEditFormProps,
getAddEditModalProps,
getAddEditFormSlots,
} = useTableModalAddEditConfig(emit, getTableProps, slots, {
getCheckboxRecords,
openAddEditModal,
query,
});
const getCustomConfig = computed(() => {
const tableProps = unref(getTableProps);
if (tableProps.customConfig) {
return tableProps.customConfig;
}
return {
storage: {
visible: true,
resizable: true,
},
};
});
/**
* 列排序存储
*/
const { setColumnSortConfig, computedColumnSort } = useSmartTableColumnConfig(getTableInstance);
// ------------- toolbar配置 ----------------------
const { getToolbarConfigInfo } = useTableToolbarConfig(getTableProps, t, {
deleteByCheckbox,
showAddModal,
editByCheckbox,
query,
getSearchFormVisible: searchFormAction.getSearchFormVisible,
useYnByCheckbox,
setColumnSortConfig,
});
/**
* 表格事件
*/
const { getToolbarEvents } = useToolbarEvent(emit, {
setSearchFormVisible: searchFormAction.setSearchFormVisible,
});
const getTableEvents = computed(() => {
return {
...unref(getToolbarEvents),
...getProxyEvents,
};
});
/**
* 表格拖拽支持
*/
const { getTableDragColumn, getTableDragSlot } = useTableRowDrag(getTableProps, tableElRef, {
getData: () => getTableInstance().getData(),
loadData: (data) => getTableInstance().loadData(data),
});
/**
* checkbox 功能加强
*/
const { computeCheckboxTableProps } = useSmartTableCheckbox(
getTableProps,
emit,
getTableInstance,
);
const { getTableColumns, computedEditRules } = useSmartTableColumn(getTableProps, t);
/**
* 列动态class style支持
*/
const { computedTableClassStyle } = useSmartTableDynamicClassStyle(getTableProps);
/**
* 获取table v-bing
*/
const getTableBindValues = computed<SmartTableProps>(() => {
const tableProps = unref(getTableProps);
return {
...attrs,
editRules: unref(computedEditRules),
...tableProps,
// loading: unref(getLoading),
...unref(computeCheckboxTableProps),
// data: dataSource,
toolbarConfig: unref(getToolbarConfigInfo),
proxyConfig: unref(getProxyConfigRef),
customConfig: unref(getCustomConfig),
...unref(getTableEvents),
...unref(computedTableClassStyle),
};
});
const getTableSlots = computed(() => {
return {
...slots,
...unref(getTableDragSlot),
};
});
/**
*
*/
const tableAction: TableActionType = {
reload,
query,
setProps,
setLoading,
// 分页
getPagination,
setPagination,
commitVxeProxy,
getShowPagination,
setShowPagination,
deleteByCheckbox,
getCheckboxRecords,
getRadioRecord,
// openAddEditModal,
showAddModal,
editByCheckbox,
editByRowModal,
getAddEditFieldsValue,
resetAddEditFields,
setAddEditFieldsValue,
deleteByRow,
setRadioRow,
setCheckboxRow,
validateAddEdit,
validateAddEditFields,
getTableInstance,
getData: (rowIndex?: number) => {
if (rowIndex) {
return getTableInstance().getData(rowIndex);
}
return getTableInstance().getData();
},
useYnByCheckbox,
useYnByRow,
};
const computedTableColumns = computed(() => {
const columns = [...unref(getTableDragColumn), ...unref(getTableColumns)];
const columnSort = unref(computedColumnSort);
if (!columnSort) {
return columns;
}
return columns.sort((a, b) => {
if (!a.field) {
return 0;
}
if (columnSort.indexOf(a.field) === -1) {
return 0;
}
return columnSort.indexOf(a.field) - columnSort.indexOf(b.field);
});
});
createTableContext({ ...tableAction, wrapRef, getBindValues: getTableBindValues });
emit('register', tableAction, searchFormAction, getAddEditForm);
return {
registerSearchForm,
getSearchFormProps,
getTableProps,
tableAction,
getTableBindValues,
getPaginationInfo,
tableElRef,
handleSearchInfoChange,
getSearchFormSlot,
getSearchFormColumnSlot,
getHasAddEdit,
registerAddEditModal,
getAddEditFormProps,
getAddEditModalProps,
addEditModalRef,
wrapRef,
getAddEditFormSlots,
getSearchFormVisible,
getTableEvents,
getTableSlots,
tableColumns: computedTableColumns,
pagerConfig: getPaginationInfo,
setColumnSortConfig,
};
},
render() {
const {
getTableProps: { useSearchForm },
getSearchFormVisible,
} = this;
// const $this = this
const slots: any = {
default: renderTable(this),
};
/**
* 渲染搜索表格
*/
if (useSearchForm) {
slots.search = renderSearchForm(this);
}
// @ts-ignore
return (
<TableSearchLayout showSearch={getSearchFormVisible} class="smart-table" ref="wrapRef">
{slots}
</TableSearchLayout>
);
},
});
/**
* 渲染搜索表单
* @param smartTableInstance
*/
const renderSearchForm = (smartTableInstance) => {
return () => {
const {
registerSearchForm,
getSearchFormProps,
handleSearchInfoChange,
getSearchFormSlot,
getSearchFormColumnSlot,
id,
} = smartTableInstance;
const formAttrs = {
...getSearchFormProps,
onSubmit: handleSearchInfoChange,
submitOnReset: true,
onRegister: registerSearchForm,
};
// 获取插槽
const searchFormSlots = {
...getSearchFormSlot,
...getSearchFormColumnSlot,
};
if (
Object.keys(getSearchFormSlot).length + Object.keys(getSearchFormColumnSlot).length >
Object.keys(searchFormSlots).length
) {
error('搜索表单插槽命名重复');
}
return (
<BasicForm name={`${id}_search_form`} {...formAttrs}>
{searchFormSlots}
</BasicForm>
);
};
};
/**
* 渲染表格
*/
const renderTable = (instance) => {
return () => {
const {
getTableBindValues,
getTableSlots,
getHasAddEdit,
registerAddEditModal,
getAddEditFormProps,
getAddEditModalProps,
getAddEditFormSlots,
id,
tableColumns,
pagerConfig,
} = instance;
const tableProps = {
...getTableBindValues,
columns: tableColumns,
pagerConfig,
};
const result = [
<vxe-grid ref="tableElRef" {...tableProps}>
{{ ...getTableSlots }}
</vxe-grid>,
];
if (getHasAddEdit) {
result.push(
<SmartTableAddEditModal
ref="addEditModalRef"
{...getAddEditModalProps}
tableId={id}
onRegister={registerAddEditModal}
formConfig={getAddEditFormProps}
>
{getAddEditFormSlots}
</SmartTableAddEditModal>,
);
}
return result;
};
};

154
src/components/SmartTable/src/components/SmartTableAddEditModal.tsx

@ -0,0 +1,154 @@
import type { SmartAddEditModalCallbackData } from '@/components/SmartTable';
import { defineComponent, ref, unref } from 'vue';
import { message } from 'ant-design-vue';
import { BasicModal, useModalInner } from '@/components/Modal';
import { BasicForm, FormProps, useForm } from '@/components/Form';
import { isBoolean, isPromise, isFunction } from '@/utils/is';
import { useI18n } from '@/hooks/web/useI18n';
import { propTypes } from '@/utils/propTypes';
export default defineComponent({
name: 'SmartTableAddEditModal',
props: {
formConfig: {
type: Object as PropType<FormProps>,
required: true,
},
beforeSave: {
type: Function as PropType<(data) => any | Promise<any>>,
},
saveFunction: Function as PropType<(data) => Promise<any>>,
afterSave: Function as PropType<(data?) => boolean | Promise<boolean> | undefined>,
tableId: propTypes.string,
},
setup(props) {
const { t } = useI18n();
const isAddRef = ref(true);
const [registerForm, formAction] = useForm();
const { resetFields, getFieldsValue, setFieldsValue, validate, validateFields } = formAction;
const [register, { changeLoading, changeOkLoading, closeModal }] = useModalInner(
(data: SmartAddEditModalCallbackData) => {
const { isAdd, formData } = data;
resetFields();
isAddRef.value = isAdd;
if (isAdd) {
if (formData) {
setFieldsValue({
...formData,
isAdd,
});
}
return false;
}
loadEditData(data);
},
);
const loadEditData = async (data: SmartAddEditModalCallbackData) => {
const { getFunction, validateFunction, selectData, isAdd, formData } = data;
try {
if (!isFunction(getFunction)) {
throw new Error('proxyConfig.ajax.getById未定义');
}
changeLoading(true);
const editData = await getFunction(selectData);
// todo: 触发事件
if (isFunction(validateFunction)) {
const result = validateFunction(data, selectData);
if (isBoolean(result) && !result) {
return false;
}
if (isPromise(result)) {
const promiseResult = await result;
if (isBoolean(promiseResult) && !promiseResult) {
return false;
}
}
}
setFieldsValue({
...editData,
formData,
isAdd,
});
} finally {
changeLoading(false);
}
};
const handleSubmit = async () => {
try {
let data = await validate();
changeOkLoading(true);
// 保存前对参数进行处理
const beforeSave = props.beforeSave;
if (beforeSave) {
if (!isFunction(beforeSave)) {
throw new Error('参数错误,beforeSave必须是Function');
}
const result = beforeSave(data);
if (isPromise(result)) {
data = await result;
} else {
data = result;
}
}
const saveFunction = props.saveFunction;
if (!saveFunction) {
throw new Error('proxyConfig.ajax.save未定义,无法执行保存操作');
}
const saveResult = await saveFunction({
body: {
insertRecords: unref(isAddRef) ? [data] : [],
updateRecords: unref(isAddRef) ? [] : [data],
},
});
if (props.afterSave) {
let afterSaveResult = props.afterSave(saveResult);
if (isPromise(afterSaveResult)) {
afterSaveResult = await afterSaveResult;
}
if (isBoolean(afterSaveResult) && !afterSaveResult) {
// 返回结果验证错误,停止执行
return false;
}
}
message.success(
unref(isAddRef) ? t('common.message.saveSuccess') : t('common.message.editSuccess'),
);
closeModal();
} finally {
changeOkLoading(false);
}
};
return {
register,
registerForm,
resetFields,
getFieldsValue,
setFieldsValue,
handleSubmit,
validate,
validateFields,
getFormAction: () => formAction,
};
},
render() {
const { $attrs, register, registerForm, handleSubmit, $slots, tableId, formConfig } = this;
const attrs = {
...$attrs,
onRegister: register,
};
return (
<BasicModal {...attrs} onOk={handleSubmit}>
<BasicForm {...formConfig} name={`${tableId}_addEdit_form`} onRegister={registerForm}>
{$slots}
</BasicForm>
</BasicModal>
);
},
});

375
src/components/SmartTable/src/components/SmartTableColumnConfig.tsx

@ -0,0 +1,375 @@
import type {
VxeGridProps,
VxeGridPropTypes,
VxeColumnPropTypes,
VxeGridInstance,
VxeButtonProps,
VxePulldownInstance,
VxeTableDefines,
} from 'vxe-table';
import { t as vxeI18n } from 'vxe-table';
import { computed, defineComponent, inject, nextTick, Ref, ref, unref } from 'vue';
import type { SmartTableToolbarColumnConfig } from '@/components/SmartTable';
import Sortable from 'sortablejs';
const DRAG_CLASS = 'smart-table-column-config--drag';
/**
* 表格列设置组件
*/
export default defineComponent({
name: 'SmartTableColumnConfig',
props: {
config: {
type: Object as PropType<SmartTableToolbarColumnConfig>,
default: (): SmartTableToolbarColumnConfig => {
return {
trigger: 'click',
buttonProps: {
icon: 'vxe-icon-chart-radar',
},
columnOrder: false,
};
},
},
setColumnSortConfig: {
type: Function,
required: true,
},
},
setup(props) {
const getTableInstance = inject('getTableInstance') as () => VxeGridInstance;
const pullDownRef = ref<VxePulldownInstance>();
const configGridRef = ref<VxeGridInstance>();
/**
* 添加行顺序调整
*/
const { openDrag } = useDrag(configGridRef, getTableInstance, props.setColumnSortConfig);
/**
* 显示弹窗面板
*/
const showPanel = () => {
unref(pullDownRef)?.showPanel();
nextTick(() => {
if (props.config.columnOrder) {
openDrag();
}
});
};
const computedTableColumns = computed(() => {
return getTableInstance()?.getTableColumn().fullColumn;
});
/**
* FIX变更触发
* @param event
* @param fixed
* @param row
*/
const handleFixedChange = (event: MouseEvent, fixed: VxeColumnPropTypes.Fixed, row: any) => {
event.preventDefault();
if (fixed === row.fixed) {
//@ts-ignore
getTableInstance().extendTableMethods(['clearColumnFixed']).clearColumnFixed(row.field);
} else {
//@ts-ignore
getTableInstance().extendTableMethods(['setColumnFixed']).setColumnFixed(row.field, fixed);
}
};
const { handleShowHideOk, handleRestShowHide, handleCheckboxChange } =
useColumnShowHide(getTableInstance);
/**
* 表格props
*/
const computedGridProps = computed<VxeGridProps>(() => {
return {
columns: getGridColumns(props.config.columnOrder || false, handleFixedChange),
size: 'mini',
height: '396px',
headerAlign: 'center',
checkboxConfig: {
// trigger: 'row',
checkField: 'checked',
},
rowConfig: {
keyField: 'field',
},
onCheckboxChange: handleCheckboxChange,
data: unref(computedTableColumns)?.map((item) => {
const { visible, fixed, field } = item;
return {
title: item.getTitle(),
checked: visible,
fixed,
field,
id: item.id,
column: item,
};
}),
};
});
/**
* 按钮时间,触发下拉显示
*/
const buttonEvent = {
onClick: () => {
const trigger = props.config.trigger;
if (trigger === 'click') {
showPanel();
}
},
onMouseover: () => {
const trigger = props.config.trigger;
if (trigger === 'hover') {
showPanel();
}
},
};
return {
buttonEvent,
pullDownRef,
computedTableColumns,
computedGridProps,
configGridRef,
handleShowHideOk,
handleRestShowHide,
};
},
render() {
const { config, buttonEvent, computedGridProps, handleShowHideOk, handleRestShowHide } = this;
const slots = {
default: renderButton(config, buttonEvent),
dropdown: renderDropdown(computedGridProps, handleShowHideOk, handleRestShowHide),
};
return (
<vxe-pulldown popupClassName="smart-table-column-config" transfer={true} ref="pullDownRef">
{slots}
</vxe-pulldown>
);
},
});
/**
* 渲染按钮
* @param config
* @param event
*/
const renderButton = (config: SmartTableToolbarColumnConfig, event: any) => {
const props: VxeButtonProps = {
circle: true,
icon: 'vxe-icon-chart-radar',
...event,
...config.buttonProps,
};
return () => {
return <vxe-button title={vxeI18n('vxe.toolbar.custom')} {...props}></vxe-button>;
};
};
/**
* 渲染下拉表格
* @param props
* @param handleShowHideOk
* @param handleRestShowHide
*/
const renderDropdown = (props: VxeGridProps, handleShowHideOk, handleRestShowHide) => {
return () => {
return (
<div>
<vxe-grid ref="configGridRef" {...props} />
<div class="bottom-button">
<vxe-button type="text" onClick={handleRestShowHide}>
{vxeI18n('vxe.toolbar.customRestore')}
</vxe-button>
<vxe-button type="text" onClick={handleShowHideOk}>
{vxeI18n('vxe.toolbar.customConfirm')}
</vxe-button>
</div>
</div>
);
};
};
const getGridColumns = (columnOrder: boolean, changeFixed: Function): VxeGridPropTypes.Columns => {
const columns: VxeGridPropTypes.Columns = [];
if (columnOrder) {
columns.push({
title: '#',
field: 'drag',
align: 'left',
width: '40',
slots: {
default: ({ rowIndex, row }) => {
const fixed = !(row.fixed === undefined || row.fixed === null);
return (
<div
data-id={rowIndex}
title={fixed ? '锁定列不可调整顺序' : '拖动调整顺序'}
class={fixed ? 'smart-table-column-config--drag-fixed' : DRAG_CLASS}
>
<i data-id={rowIndex} class="vxe-icon-num-list" />
</div>
);
},
},
});
}
columns.push(
...([
{
type: 'checkbox',
width: '60',
headerAlign: 'left',
},
{
field: 'title',
width: 100,
title: '列名',
},
{
field: 'fixed',
width: 80,
title: '列冻结',
slots: {
default: ({ row }) => {
return (
<div class="smart-table-column-config-fixed-option">
<span
onClick={(event) => changeFixed(event, 'left', row)}
class={[
row.fixed === 'left' ? 'vxe-icon-fixed-left-fill' : 'vxe-icon-fixed-left',
'fixed-span',
{
'is--checked': row.fixed === 'left',
},
]}
title={vxeI18n(
row.fixed === 'left' ? 'vxe.toolbar.cancelfixed' : 'vxe.toolbar.fixedLeft',
)}
/>
<span
onClick={(event) => changeFixed(event, 'right', row)}
class={[
row.fixed === 'right' ? 'vxe-icon-fixed-right-fill' : 'vxe-icon-fixed-right',
'fixed-span',
{
'is--checked': row.fixed === 'right',
},
]}
title={vxeI18n(
row.fixed === 'right' ? 'vxe.toolbar.cancelfixed' : 'vxe.toolbar.fixedRight',
)}
/>
</div>
);
},
},
},
] as VxeGridPropTypes.Columns),
);
return columns;
};
interface ChangeColumn {
column: VxeTableDefines.ColumnInfo;
checked: boolean;
}
const useColumnShowHide = (getTable: () => VxeGridInstance) => {
/**
* 变更的列
*/
const changeColumnsRef = ref<Recordable<ChangeColumn | null>>({});
/**
* 显示隐藏变更触发
* @param row
* @param checked
*/
const handleCheckboxChange = ({ row, checked }) => {
const { column } = row;
const changeColumn = unref(changeColumnsRef)[column.id];
if (changeColumn === null || changeColumn === undefined) {
changeColumnsRef.value[column.id] = { column, checked };
} else {
changeColumnsRef.value[column.id] = null;
}
};
/**
* 重置列显示隐藏
*/
const handleRestShowHide = () => {
getTable().resetColumn({});
changeColumnsRef.value = {};
};
/**
* 点击确定显示隐藏列
*/
const handleShowHideOk = () => {
const table = getTable();
Object.entries(unref(changeColumnsRef)).forEach(([_, column]) => {
if (column !== null) {
if (column.checked) {
table.showColumn(column.column);
} else {
table.hideColumn(column.column);
}
}
});
changeColumnsRef.value = {};
};
return {
changeColumnsRef,
handleCheckboxChange,
handleRestShowHide,
handleShowHideOk,
};
};
const useDrag = (
configTableRef: Ref<VxeGridInstance | undefined>,
getTable: () => VxeGridInstance,
setColumnSortConfig: Function,
) => {
let sortable: Sortable | null = null;
const selector = '.body--wrapper>.vxe-table--body tbody';
const openDrag = async () => {
if (sortable != null) {
return false;
}
const sortEl = unref(configTableRef)?.$el.querySelector(selector);
sortable = Sortable.create(sortEl, {
handle: `.${DRAG_CLASS}`,
dataIdAttr: 'rowid',
onEnd: ({ newIndex, oldIndex }) => {
const table = getTable();
const fullColumn = table.getTableColumn().fullColumn;
const keySorts = fullColumn.map((item) => item.field);
const currentRow = fullColumn.splice(oldIndex!, 1)[0];
fullColumn.splice(newIndex!, 0, currentRow);
sortable?.sort(keySorts);
table.reloadColumn(fullColumn);
nextTick(() => {
if (table.id && table.customConfig?.storage === true) {
setColumnSortConfig();
}
});
},
});
};
return {
openDrag,
};
};

225
src/components/SmartTable/src/components/SmartVxeTableAction.vue

@ -0,0 +1,225 @@
<template>
<div :class="[prefixCls, getAlign]" @click.stop="onCellClick">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<template v-if="action.slot">
<slot name="customButton"></slot>
</template>
<template v-else>
<Tooltip v-if="!action.hasAuth" :title="$t('common.message.noPermission')" color="red">
<!-- 无权限 -->
<PopConfirmVxeButton disabled v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmVxeButton>
</Tooltip>
<Tooltip v-else-if="action.tooltip" v-bind="getTooltip(action.tooltip)">
<PopConfirmVxeButton v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmVxeButton>
</Tooltip>
<PopConfirmVxeButton v-else v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmVxeButton>
</template>
<Divider
type="vertical"
class="action-divider"
v-if="divider && index < getActions.length - 1"
/>
</template>
<Dropdown
:trigger="['hover']"
:dropMenuList="getDropdownList"
popconfirm
v-if="dropDownActions && getDropdownList.length > 0"
>
<slot name="more"></slot>
<vxe-button type="text" status="primary" size="small" v-if="!$slots.more">
更多
<Icon icon="mdi-light:chevron-down" />
</vxe-button>
</Dropdown>
</div>
</template>
<script lang="ts">
import type { ActionItem } from '../types/SmartTableActionType';
import type { TableActionType } from '../types/SmartTableType';
import { defineComponent, PropType, computed, toRaw, unref } from 'vue';
import { Divider, Tooltip, TooltipProps } from 'ant-design-vue';
import { Icon } from '@/components/Icon';
import { PopConfirmVxeButton } from '@/components/Button';
import { Dropdown } from '@/components/Dropdown';
import { useDesign } from '@/hooks/web/useDesign';
import { useTableContext } from '../hooks/userSmartTableContext';
import { hasPermission } from '@/utils/auth';
import { isBoolean, isFunction, isString } from '@/utils/is';
import { propTypes } from '@/utils/propTypes';
import { ACTION_COLUMN_FLAG } from '../const';
export default defineComponent({
name: 'TableAction',
components: { Icon, PopConfirmVxeButton, Divider, Dropdown, Tooltip },
props: {
actions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
divider: propTypes.bool.def(true),
outside: propTypes.bool,
stopButtonPropagation: propTypes.bool.def(false),
},
setup(props) {
const { prefixCls } = useDesign('basic-table-action');
let table: Partial<TableActionType> = {};
if (!props.outside) {
table = useTableContext();
}
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
return {
getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
type: 'text',
size: 'small',
status: action.danger ? 'danger' : 'primary',
...action,
...(popConfirm || {}),
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
hasAuth: hasPermission(action.auth),
};
});
});
const getDropdownList = computed((): any[] => {
//过滤掉隐藏的dropdown,避免出现多余的分割线
const list = (toRaw(props.dropDownActions) || []).filter((action) => {
return isIfShow(action);
});
return list.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider: index < list.length - 1 ? props.divider : false,
hasAuth: hasPermission(action.auth),
disabled: !hasPermission(action.auth) || action.disabled,
};
});
});
const getAlign = computed(() => {
const columns = (table as TableActionType)?.getColumns?.() || [];
const actionColumn = columns.find((item) => item.flag === ACTION_COLUMN_FLAG);
return actionColumn?.align ?? 'left';
});
function getTooltip(data: string | TooltipProps): TooltipProps {
return {
getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
placement: 'bottom',
...(isString(data) ? { title: data } : data),
};
}
function onCellClick(e: MouseEvent) {
if (!props.stopButtonPropagation) return;
const path = e.composedPath() as HTMLElement[];
const isInButton = path.find((ele) => {
return ele.tagName?.toUpperCase() === 'BUTTON';
});
isInButton && e.stopPropagation();
}
return { prefixCls, getActions, getDropdownList, getAlign, onCellClick, getTooltip };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-action';
.@{prefix-cls} {
display: flex;
align-items: center;
/* update-begin-author:taoyan date:2022-11-18 for: 表格默认行高比官方示例多出2px */
height: 22px;
/* update-end-author:taoyan date:2022-11-18 for: 表格默认行高比官方示例多出2px */
.action-divider {
display: table;
}
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
button {
display: flex;
align-items: center;
span {
margin-left: 0 !important;
}
}
button.ant-btn-circle {
span {
margin: auto !important;
}
}
.ant-divider,
.ant-divider-vertical {
margin: 0 2px;
}
.icon-more {
transform: rotate(90deg);
svg {
font-size: 1.1em;
font-weight: 700;
}
}
}
</style>

215
src/components/SmartTable/src/components/TableAction.vue

@ -0,0 +1,215 @@
<template>
<div :class="[prefixCls, getAlign]" @click="onCellClick">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<template v-if="action.slot">
<slot name="customButton"></slot>
</template>
<template v-else>
<Tooltip v-if="action.tooltip" v-bind="getTooltip(action.tooltip)">
<PopConfirmButton v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmButton>
</Tooltip>
<PopConfirmButton v-else v-bind="action">
<Icon :icon="action.icon" :class="{ 'mr-1': !!action.label }" v-if="action.icon" />
<template v-if="action.label">{{ action.label }}</template>
</PopConfirmButton>
</template>
<Divider
type="vertical"
class="action-divider"
v-if="divider && index < getActions.length - 1"
/>
</template>
<Dropdown
:trigger="['hover']"
:dropMenuList="getDropdownList"
popconfirm
v-if="dropDownActions && getDropdownList.length > 0"
>
<slot name="more"></slot>
<a-button type="link" size="small" v-if="!$slots.more">
更多
<Icon icon="mdi-light:chevron-down" />
</a-button>
</Dropdown>
</div>
</template>
<script lang="ts">
import type { ActionItem } from '../types/SmartTableActionType';
import type { TableActionType } from '../types/SmartTableType';
import { defineComponent, PropType, computed, toRaw, unref } from 'vue';
import { Divider, Tooltip, TooltipProps } from 'ant-design-vue';
import { Icon } from '@/components/Icon';
import { PopConfirmButton } from '@/components/Button';
import { Dropdown } from '@/components/Dropdown';
import { useDesign } from '@/hooks/web/useDesign';
import { useTableContext } from '../hooks/userSmartTableContext';
import { isBoolean, isFunction, isString } from '@/utils/is';
import { propTypes } from '@/utils/propTypes';
import { ACTION_COLUMN_FLAG } from '../const';
import { hasPermission } from '@/utils/auth';
export default defineComponent({
name: 'TableAction',
components: { Icon, PopConfirmButton, Divider, Dropdown, Tooltip },
props: {
actions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
divider: propTypes.bool.def(true),
outside: propTypes.bool,
stopButtonPropagation: propTypes.bool.def(false),
},
setup(props) {
const { prefixCls } = useDesign('basic-table-action');
let table: Partial<TableActionType> = {};
if (!props.outside) {
table = useTableContext();
}
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
return {
getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
type: 'link',
size: 'small',
...action,
...(popConfirm || {}),
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
const getDropdownList = computed((): any[] => {
//过滤掉隐藏的dropdown,避免出现多余的分割线
const list = (toRaw(props.dropDownActions) || []).filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
});
return list.map((action, index) => {
const { label, popConfirm } = action;
return {
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
text: label,
divider: index < list.length - 1 ? props.divider : false,
};
});
});
const getAlign = computed(() => {
const columns = (table as TableActionType)?.getColumns?.() || [];
const actionColumn = columns.find((item) => item.flag === ACTION_COLUMN_FLAG);
return actionColumn?.align ?? 'left';
});
function getTooltip(data: string | TooltipProps): TooltipProps {
return {
getPopupContainer: () => unref((table as any)?.wrapRef.value) ?? document.body,
placement: 'bottom',
...(isString(data) ? { title: data } : data),
};
}
function onCellClick(e: MouseEvent) {
if (!props.stopButtonPropagation) return;
const path = e.composedPath() as HTMLElement[];
const isInButton = path.find((ele) => {
return ele.tagName?.toUpperCase() === 'BUTTON';
});
isInButton && e.stopPropagation();
}
return { prefixCls, getActions, getDropdownList, getAlign, onCellClick, getTooltip };
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-basic-table-action';
.@{prefix-cls} {
display: flex;
align-items: center;
/* update-begin-author:taoyan date:2022-11-18 for: 表格默认行高比官方示例多出2px */
height: 22px;
/* update-end-author:taoyan date:2022-11-18 for: 表格默认行高比官方示例多出2px */
.action-divider {
display: table;
}
&.left {
justify-content: flex-start;
}
&.center {
justify-content: center;
}
&.right {
justify-content: flex-end;
}
button {
display: flex;
align-items: center;
span {
margin-left: 0 !important;
}
}
button.ant-btn-circle {
span {
margin: auto !important;
}
}
.ant-divider,
.ant-divider-vertical {
margin: 0 2px;
}
.icon-more {
transform: rotate(90deg);
svg {
font-size: 1.1em;
font-weight: 700;
}
}
}
</style>

32
src/components/SmartTable/src/const.ts

@ -0,0 +1,32 @@
import componentSetting from '@/settings/componentSetting';
const { table } = componentSetting;
const { pageSizeOptions, defaultPageSize, fetchSetting, pageLayouts, defaultSize } = table;
// Optional display number per page;
export const PAGE_SIZE_OPTIONS = pageSizeOptions;
// Number of items displayed per page
export const PAGE_SIZE = defaultPageSize;
// Common interface field settings
export const FETCH_SETTING = fetchSetting;
// Default Size
export const DEFAULT_SIZE = defaultSize;
export const DEFAULT_PAGE_LAYOUTS = pageLayouts;
export const ACTION_COLUMN_FLAG = 'ACTION';
export enum SmartTableCode {
/**
* 是否显示搜索
*/
showSearch = 'show_search',
/**
* 列配置
*/
column = 'column',
}

10
src/components/SmartTable/src/defaultConfig.ts

@ -0,0 +1,10 @@
import type { SmartCheckboxConfig } from '@/components/SmartTable';
/**
* 默认的checkbox配置
*/
export const defaultCheckboxConfig: SmartCheckboxConfig = {
rowTrigger: 'single',
rowCtrl: true,
rowShift: true,
};

124
src/components/SmartTable/src/hooks/useLoadData.ts

@ -0,0 +1,124 @@
import type { SmartTableProps, FetchParams, SmartTableProxyConfig } from '@/components/SmartTable';
import type { ComputedRef, Ref } from 'vue';
import type { VxeGridInstance, VxeGridPropTypes } from 'vxe-table';
import { computed, nextTick, ref, unref, watch } from 'vue';
import { isArray } from '@/utils/is';
import { error } from '@/utils/log';
interface ActionType {
setLoading: (loading: boolean) => void;
setPagination: (info: Partial<VxeGridPropTypes.PagerConfig>) => void;
}
export const useLoadData = (
tableProps: ComputedRef<SmartTableProps>,
tableRef: Ref<VxeGridInstance>,
pagerConfigRef: ComputedRef<VxeGridPropTypes.PagerConfig | undefined>,
proxyConfigRef: ComputedRef<SmartTableProxyConfig | undefined>,
emit: Function,
{ setLoading, setPagination }: ActionType,
) => {
const tableDataRef = ref<any[]>([]);
const sortListRef = ref<VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams[]>();
watch(
() => unref(tableProps).sortConfig?.defaultSort,
(defaultSort) => {
if (defaultSort) {
if (isArray(defaultSort)) {
sortListRef.value = defaultSort.map((item) => {
return {
...item,
sortBy: '',
column: {},
property: '',
} as VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams;
});
} else {
sortListRef.value = [
{
...defaultSort,
sortBy: '',
column: {},
property: '',
} as VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams,
];
}
}
},
);
const handleSortChange = (data) => {
sortListRef.value = data.sortList;
nextTick(() => query());
emit('sort-change', data);
};
/**
* 获取表格事件信息
*/
const getTableLoadDataEvent = computed(() => {
return {
onSortChange: handleSortChange,
};
});
/**
* 查询
* @param opt
*/
const query = (opt?: FetchParams) => doQuery(opt);
const doQuery = async (opt?: FetchParams) => {
const proxyConfig = unref(proxyConfigRef);
if (!proxyConfig || !proxyConfig.ajax?.query) {
if (process.env.NODE_ENV === 'development') {
error('proxy-config.ajax.query未定义');
}
}
const vxeTable = unref(tableRef);
// 过滤信息
const filterList = vxeTable.getCheckedFilters();
// 分页信息
let pageParams: any = {};
const pagerConfig = unref(pagerConfigRef);
if (pagerConfig) {
pageParams = {
currentPage: pagerConfig.currentPage,
pageSize: pagerConfig.pageSize,
};
}
const sortList = unref(sortListRef)!;
const params: VxeGridPropTypes.ProxyAjaxQueryParams = {
$grid: unref(tableRef),
filters: filterList,
page: pageParams,
sorts: sortList,
form: {},
// @ts-ignore
sort: sortList.length > 0 ? sortList[0] : {},
};
try {
setLoading(true);
const proxyData = await proxyConfig?.ajax?.query!(params, opt);
if (pagerConfig && !pagerConfig.enabled) {
const { result, total } = proxyConfig?.props || {};
tableDataRef.value = proxyData[result || 'rows'];
setPagination({
total: proxyData[total || 'total'],
});
} else {
tableDataRef.value = proxyData;
}
nextTick(() => {
emit('after-load');
});
} finally {
setLoading(false);
}
};
return {
tableDataRef,
getTableLoadDataEvent,
query,
};
};

24
src/components/SmartTable/src/hooks/useLoading.ts

@ -0,0 +1,24 @@
import { computed, ref, unref, watch } from 'vue';
import type { ComputedRef } from 'vue';
import type { SmartTableProps } from '@/components/SmartTable';
export const useLoading = (props: ComputedRef<SmartTableProps>) => {
const loadingRef = ref(unref(props).loading);
watch(
() => unref(props).loading,
(loading) => {
loadingRef.value = loading;
},
);
const getLoading = computed(() => unref(loadingRef));
const setLoading = (loading: boolean) => {
// getTableInstance().loading = loading
loadingRef.value = loading;
};
return { getLoading, setLoading };
};

73
src/components/SmartTable/src/hooks/usePagination.ts

@ -0,0 +1,73 @@
import { computed, ref, unref, watch } from 'vue';
import type { ComputedRef } from 'vue';
import type { SmartTableProps } from '@/components/SmartTable';
import { isBoolean } from '@/utils/is';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, DEFAULT_PAGE_LAYOUTS } from '../const';
import { VxeGridPropTypes } from 'vxe-table';
/**
* 分页支持
* @author zhongming4762
* @param propsRef
*/
export const usePagination = (propsRef: ComputedRef<SmartTableProps>) => {
const configRef = ref<VxeGridPropTypes.PagerConfig>({});
const show = ref(true);
watch(
() => unref(propsRef).pagerConfig,
(pagerConfig) => {
if (!isBoolean(pagerConfig) && pagerConfig) {
configRef.value = {
...unref(configRef),
...(pagerConfig ?? {}),
};
}
},
);
const getPaginationInfo = computed((): VxeGridPropTypes.PagerConfig | undefined => {
const { pagerConfig } = unref(propsRef);
if (!pagerConfig) {
return undefined;
}
if (!unref(show) || (isBoolean(pagerConfig) && !pagerConfig)) {
return undefined;
}
return {
currentPage: 1,
pageSize: PAGE_SIZE,
pageSizes: PAGE_SIZE_OPTIONS,
layouts: DEFAULT_PAGE_LAYOUTS,
...(isBoolean(pagerConfig) ? {} : pagerConfig),
...unref(configRef),
};
});
const setPagination = (info: Partial<VxeGridPropTypes.PagerConfig>) => {
const paginationInfo = unref(getPaginationInfo);
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
};
};
const getPagination = () => unref(getPaginationInfo);
function getShowPagination() {
return unref(show);
}
async function setShowPagination(flag: boolean) {
show.value = flag;
}
return {
getPaginationInfo,
setPagination,
getPagination,
getShowPagination,
setShowPagination,
};
};

174
src/components/SmartTable/src/hooks/useSmartTable.ts

@ -0,0 +1,174 @@
import { onUnmounted, ref, unref, watch } from 'vue';
import type { WatchStopHandle } from 'vue';
import type { NamePath } from 'ant-design-vue/es/form/interface';
import type { FetchParams, SmartTableProps, TableActionType } from '../types/SmartTableType';
import type { FormActionType } from '@/components/Form';
import type { SmartSearchFormActionType } from '../types/SmartSearchFormType';
import { isProdMode } from '@/utils/env';
import type { DynamicProps } from '#/utils';
import { getDynamicProps } from '@/utils';
import { error } from '@/utils/log';
import { VxeGridPropTypes } from 'vxe-table';
type Props = Partial<DynamicProps<SmartTableProps>>;
type UseTableMethod = TableActionType & {
getSearchForm: () => SmartSearchFormActionType;
getAddEditForm: () => FormActionType | undefined;
};
export const useSmartTable = (
tableProps?: Props,
): [
(
instance: TableActionType,
formInstance: UseTableMethod,
getAddEditForm: () => FormActionType,
) => void,
UseTableMethod,
] => {
// 表格是否加载
const loadedRef = ref<Nullable<boolean>>(false);
const tableRef = ref<Nullable<TableActionType>>(null);
const searchFormRef = ref<Nullable<UseTableMethod>>(null);
const addEditFormAction = ref<Nullable<() => FormActionType>>(null);
let stopWatch: WatchStopHandle;
const register = (
instance: TableActionType,
searchFormInstance: UseTableMethod,
getAddEditForm: () => FormActionType,
) => {
isProdMode() &&
onUnmounted(() => {
tableRef.value = null;
searchFormRef.value = null;
addEditFormAction.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(tableRef)) return;
tableRef.value = instance;
searchFormRef.value = searchFormInstance;
addEditFormAction.value = getAddEditForm;
loadedRef.value = true;
// 设置函数传递的props
tableProps && instance.setProps(getDynamicProps(tableProps));
/**
* 监控table props变化
*/
stopWatch?.();
stopWatch = watch(
() => tableProps,
() => {
tableProps && instance.setProps(getDynamicProps(tableProps));
},
{
// todo:是否重复执行
immediate: true,
deep: true,
},
);
};
const getTableAction = (): TableActionType => {
const table = unref(tableRef);
if (!table) {
error(
'The table instance has not been obtained yet, please make sure the table is presented when performing the table operation!',
);
}
return table as TableActionType;
};
const methods: UseTableMethod = {
setProps: (props: Partial<SmartTableProps>) => {
getTableAction().setProps(props);
},
getSearchForm: () => {
return unref(searchFormRef) as unknown as SmartSearchFormActionType;
},
getAddEditForm: () => {
return unref(addEditFormAction)?.call(null);
},
reload: async (opt?: FetchParams) => {
return await getTableAction().reload(opt);
},
query: async (opt?: FetchParams) => {
return await getTableAction().query(opt);
},
setLoading: (loading: boolean) => {
getTableAction().setLoading(loading);
},
setPagination: (info: Partial<VxeGridPropTypes.PagerConfig>) => {
getTableAction().setPagination(info);
},
setShowPagination: async (show: boolean) => {
await getTableAction().setShowPagination(show);
},
getPagination: () => getTableAction().getPagination(),
getShowPagination: () => {
return getTableAction().getShowPagination();
},
commitVxeProxy: (code, ...args) => {
return getTableAction().commitVxeProxy(code, args);
},
deleteByCheckbox: () => {
getTableAction().deleteByCheckbox();
},
getCheckboxRecords: (isFull: boolean) => {
return getTableAction().getCheckboxRecords(isFull);
},
getRadioRecord: (isFull: boolean) => {
return getTableAction().getRadioRecord(isFull);
},
showAddModal: (formData?: Recordable, selectData?: Recordable) => {
getTableAction().showAddModal(formData, selectData);
},
editByCheckbox: () => {
return getTableAction().editByCheckbox();
},
getAddEditFieldsValue: () => {
return getTableAction().getAddEditFieldsValue();
},
resetAddEditFields: () => {
return getTableAction().resetAddEditFields();
},
setAddEditFieldsValue: (data: any) => {
return getTableAction().setAddEditFieldsValue(data);
},
editByRowModal: (data, formData) => {
return getTableAction().editByRowModal(data, formData);
},
deleteByRow: (data) => {
return getTableAction().deleteByRow(data);
},
setRadioRow: (row) => {
return getTableAction().setRadioRow(row);
},
setCheckboxRow(rows: any, checked: boolean): Promise<any> {
return getTableAction().setCheckboxRow(rows, checked);
},
validateAddEdit: (nameList?: NamePath[]) => {
return getTableAction().validateAddEdit(nameList);
},
validateAddEditFields: (nameList?: NamePath[]) => {
return getTableAction().validateAddEdit(nameList);
},
getTableInstance: () => {
return getTableAction().getTableInstance();
},
getData: (rowIndex?: number) => {
return getTableAction().getData(rowIndex);
},
useYnByCheckbox: (useYn: boolean) => {
return getTableAction().useYnByCheckbox(useYn);
},
useYnByRow: (row: any | any[], useYn: boolean) => {
return getTableAction().useYnByRow(row, useYn);
},
};
return [register, methods];
};

88
src/components/SmartTable/src/hooks/useSmartTableCheckbox.ts

@ -0,0 +1,88 @@
import type { ComputedRef } from 'vue';
import type { SmartTableProps } from '@/components/SmartTable';
import type { VxeGridDefines, VxeGridInstance } from 'vxe-table';
import { computed, unref } from 'vue';
import { defaultCheckboxConfig } from '../defaultConfig';
export const useSmartTableCheckbox = (
tableProps: ComputedRef<SmartTableProps>,
emit: Function,
getTableInstance: () => VxeGridInstance,
) => {
let lastRowIndex: number | null = null;
const computeCheckboxTableProps = computed<SmartTableProps>(() => {
const checkboxConfig = {
...defaultCheckboxConfig,
...unref(tableProps).checkboxConfig,
};
const { rowTrigger, rowShift, rowCtrl } = checkboxConfig;
if (!rowTrigger) {
return {
onCellClick: (params: VxeGridDefines.CellClickEventParams) => emit('cell-click', params),
};
}
if (rowTrigger === 'multiple') {
return {
onCellClick: (params: VxeGridDefines.CellClickEventParams) => emit('cell-click', params),
checkboxConfig: {
...checkboxConfig,
trigger: 'row',
},
};
}
return {
onCellClick: (params: VxeGridDefines.CellClickEventParams) =>
handleCellClick(params, rowShift, rowCtrl),
checkboxConfig: {
...checkboxConfig,
},
};
});
/**
* 单元格被点击事件
*/
const handleCellClick = async (
params: VxeGridDefines.CellClickEventParams,
rowShift: boolean | undefined,
rowCtrl: boolean | undefined,
) => {
emit('cell-click', params);
const gridInstance = getTableInstance();
const { row, $event, rowIndex } = params;
const { ctrlKey, shiftKey } = $event as PointerEvent;
if (rowCtrl && ctrlKey) {
// 点击了ctrl
gridInstance.toggleCheckboxRow(row);
} else {
gridInstance.clearCheckboxRow();
gridInstance.setCheckboxRow(row, true);
}
if (rowShift && shiftKey) {
if (lastRowIndex == null) {
lastRowIndex = rowIndex;
} else {
// 第二次shift点击
const tableData = gridInstance.getData();
const indexList = [lastRowIndex, rowIndex];
indexList.sort((a, b) => a - b);
const checkboxList: any[] = [];
for (let i = 0; i < tableData.length; i++) {
if (i >= indexList[0] && i <= indexList[1]) {
checkboxList.push(tableData[i]);
}
}
gridInstance.clearCheckboxRow();
gridInstance.setCheckboxRow(checkboxList, true);
}
} else {
lastRowIndex = null;
}
};
return {
computeCheckboxTableProps,
};
};

190
src/components/SmartTable/src/hooks/useSmartTableColumn.tsx

@ -0,0 +1,190 @@
import type { ComputedRef, VNode } from 'vue';
import { computed, unref } from 'vue';
import type { SmartColumn, SmartTableProps } from '@/components/SmartTable';
import type { VxeColumnSlotTypes } from 'vxe-table';
import { isBoolean, isFunction } from '@/utils/is';
import { getFormSize } from '../utils';
import { VxeTablePropTypes } from 'vxe-table/types/table';
const getComponentProps = (
params: VxeColumnSlotTypes.DefaultSlotParams,
column: SmartColumn,
): Recordable => {
const componentProps = column.componentProps;
let props;
if (isFunction(componentProps)) {
props = componentProps(params);
} else {
props = componentProps;
}
return props;
};
const componentMap: {
[index: string]: (
column: SmartColumn,
t: Function,
) => (params: VxeColumnSlotTypes.DefaultSlotParams) => VNode | string;
} = {
switch: (column) => {
return (params: VxeColumnSlotTypes.DefaultSlotParams) => {
const props = {
checked: params.row[params.column.field],
...getComponentProps(params, column),
};
return <a-switch {...props} />;
};
},
tag: (column) => {
return (params: VxeColumnSlotTypes.DefaultSlotParams) => {
const props = getComponentProps(params, column);
const defaultValue = props?.default || params.row[params.column.field];
return <a-tag {...props}>{defaultValue}</a-tag>;
};
},
button: (column) => {
return (params: VxeColumnSlotTypes.DefaultSlotParams) => {
const props: Recordable = {
size: params.$table.props.size,
...getComponentProps(params, column),
};
const defaultValue = props?.default || params.row[params.column.field];
return <a-button {...props}>{defaultValue}</a-button>;
};
},
booleanTag: (column, t) => {
return (params) => {
const componentProps = getComponentProps(params, column);
const defaultValue = componentProps?.default || params.row[params.column.field];
if (!isBoolean(defaultValue)) {
return '';
}
const props: Recordable = {
...componentProps,
color: defaultValue ? '#108ee9' : '#f50',
};
return <a-tag {...props}>{defaultValue ? t('common.form.yes') : t('common.form.no')}</a-tag>;
};
},
useYnTag: (column, t) => {
return (params) => {
const componentProps = getComponentProps(params, column);
const defaultValue = componentProps?.default || params.row[params.column.field];
if (!isBoolean(defaultValue)) {
return '';
}
const props: Recordable = {
...componentProps,
color: defaultValue ? '#108ee9' : '#f50',
};
return (
<a-tag {...props}>{defaultValue ? t('common.form.use') : t('common.form.noUse')}</a-tag>
);
};
},
};
export const useSmartTableColumn = (tableProps: ComputedRef<SmartTableProps>, t: Function) => {
const getTableColumns = computed<Array<SmartColumn>>((): SmartColumn[] => {
const propsColumns = unref(tableProps).columns || [];
const tableSize = unref(tableProps).size;
const result = propsColumns.map((column) => {
const { component, slots } = column;
if (!component || slots) {
return column;
}
const defaultSlot = componentMap[component];
if (!defaultSlot) {
return column;
}
return {
...column,
slots: {
default: defaultSlot(column, t),
},
};
});
convertEditRender(result, tableSize);
return result;
});
/**
* 可编辑表格计算属性
*/
const computedEditRules = computed<VxeTablePropTypes.EditRules | null>(() => {
const rules: VxeTablePropTypes.EditRules = {};
const propsColumns = unref(tableProps).columns || [];
for (const { editRender, field } of propsColumns) {
if (!editRender) {
continue;
}
if (editRender.rules) {
rules[field!] = editRender.rules;
continue;
}
if (!editRender.required) {
continue;
}
rules[field!] = [
{
required: true,
content: '请输入',
},
];
}
if (Object.keys(rules).length === 0) {
return null;
}
return rules;
});
return {
getTableColumns,
computedEditRules,
};
};
/**
* 转换可编辑列
* @param columns
* @param tableSize
*/
const convertEditRender = (columns: SmartColumn[], tableSize) => {
for (const column of columns) {
const { editRender } = column;
if (!editRender) {
continue;
}
if (!editRender.props) {
editRender.props = {};
}
// 处理尺寸
if (!editRender.props?.size) {
const size = getFormSize(tableSize);
if (editRender.props) {
editRender.props.size = size;
} else {
editRender.props = {
size,
};
}
}
// 处理自动聚焦
const { autofocus, name } = editRender;
if (isBoolean(autofocus) && autofocus) {
if (name === 'ASelect') {
editRender.autofocus = '.ant-select-selection-search-input';
}
if (name === 'ADatePicker') {
editRender.autofocus = '.ant-picker-input input';
}
}
// 处理事件冒泡
if (editRender.stopEnterBubbling !== false) {
editRender.props.onKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.stopPropagation();
}
};
}
}
};

48
src/components/SmartTable/src/hooks/useSmartTableColumnConfig.ts

@ -0,0 +1,48 @@
import type { VxeGridInstance } from 'vxe-table';
import { computed } from 'vue';
const SMART_TABLE_CUSTOM_COLUMN_SORT = 'SMART_TABLE_CUSTOM_COLUMN_SORT';
export const useSmartTableColumnConfig = (getTableInstance: () => VxeGridInstance) => {
const setColumnSortConfig = () => {
const fullColumnList = getTableInstance()?.getTableColumn()?.fullColumn || [];
const tableId = getTableInstance()?.id;
if (!tableId) {
return;
}
const columns = fullColumnList
.map((item) => {
const field = item.field;
if (!field) {
console.warn(item);
console.warn('SMART-TABLE:列未设置field,列顺序设置无法正常使用');
}
return field;
})
.filter((item) => item !== undefined && item !== null);
const allConfig =
JSON.parse(localStorage.getItem(SMART_TABLE_CUSTOM_COLUMN_SORT) || '{}') || {};
allConfig[tableId] = columns;
localStorage.setItem(SMART_TABLE_CUSTOM_COLUMN_SORT, JSON.stringify(allConfig));
};
const getColumnSortConfig = (tableId: string) => {
return JSON.parse(localStorage.getItem(SMART_TABLE_CUSTOM_COLUMN_SORT) || '{}')[tableId];
};
const computedColumnSort = computed(() => {
const tableId = getTableInstance()?.id;
if (!tableId) {
return null;
}
if (getTableInstance()?.customConfig?.storage !== true) {
return null;
}
return getColumnSortConfig(tableId);
});
return {
setColumnSortConfig,
computedColumnSort,
};
};

124
src/components/SmartTable/src/hooks/useSmartTableDynamicClassStyle.ts

@ -0,0 +1,124 @@
import type { ComputedRef } from 'vue';
import type {
SmartTableProps,
SmartColumnDynamicClass,
SmartColumnDynamicStyle,
SmartColumnAutoClass,
} from '@/components/SmartTable';
import type { VxeTablePropTypes } from 'vxe-table';
import { computed, unref } from 'vue';
import { warn } from '@/utils/log';
import { isObject, isString } from '@/utils/is';
const autoClass: { [index in SmartColumnAutoClass]: SmartColumnDynamicClass } = {
Boolean: ({ row, column }) => {
const value = row[column.field];
if (value === null || value === undefined) {
return '';
}
if (value) {
return 'text-color--success-bold';
}
return 'text-color--danger-bold';
},
};
export const useSmartTableDynamicClassStyle = (tablePropsRef: ComputedRef<SmartTableProps>) => {
/**
* 动态class计算属性
*/
const computedClassName = computed<VxeTablePropTypes.CellClassName | undefined>(() => {
const tableProps = unref(tablePropsRef);
const tableDynamicClass: Recordable<SmartColumnDynamicClass> = {};
tableProps.columns?.forEach((column) => {
if (column.dynamicClass) {
if (!column.field) {
warn('列未设置field,dynamicClass失效');
console.warn(column);
} else {
tableDynamicClass[column.field] = column.dynamicClass;
}
} else if (column.autoClass) {
if (!column.field) {
warn('列未设置field,dynamicClass失效');
console.warn(column);
} else {
tableDynamicClass[column.field] = autoClass[column.autoClass];
}
}
});
if (tableProps.cellClassName || tableProps.rowClassName) {
if (Object.keys(tableDynamicClass).length > 0) {
warn('表格设置了cellClassName或rowClassName,列dynamicClass失效');
}
return undefined;
}
if (Object.keys(tableDynamicClass).length === 0) {
return undefined;
}
return (params) => {
const { column } = params;
const field = column.field;
const dynamicClass = tableDynamicClass[field];
if (!dynamicClass) {
return undefined;
}
if (isString(dynamicClass)) {
return dynamicClass;
}
return dynamicClass(params);
};
});
const computedStyle = computed(() => {
const tableProps = unref(tablePropsRef);
const tableDynamicStyle: Recordable<SmartColumnDynamicStyle> = {};
tableProps.columns?.forEach((column) => {
if (column.dynamicStyle) {
if (!column.field) {
warn('列未设置field,dynamicStyle失效');
console.warn(column);
} else {
tableDynamicStyle[column.field] = column.dynamicStyle;
}
}
});
if (tableProps.cellStyle || tableProps.rowStyle) {
if (Object.keys(tableDynamicStyle).length > 0) {
warn('表格设置了cellStyle或rowStyle,列dynamicStyle失效');
}
return undefined;
}
if (Object.keys(tableDynamicStyle).length === 0) {
return undefined;
}
return (params) => {
const { column } = params;
const field = column.field;
const dynamicStyle = tableDynamicStyle[field];
if (!dynamicStyle) {
return undefined;
}
if (isObject(dynamicStyle)) {
return dynamicStyle;
}
return dynamicStyle(params);
};
});
const computedTableClassStyle = computed(() => {
const props: SmartTableProps = {};
if (unref(computedClassName)) {
props.cellClassName = unref(computedClassName);
}
if (unref(computedStyle)) {
props.cellStyle = unref(computedStyle);
}
return props;
});
return {
computedTableClassStyle,
};
};

260
src/components/SmartTable/src/hooks/useTableAjax.ts

@ -0,0 +1,260 @@
import type {
SmartTableAjaxQueryParams,
FetchParams,
SmartTableProps,
SmartTableProxyConfig,
SmartSearchFormParameter,
} from '@/components/SmartTable';
import type { VxeGridInstance, VxeGridPropTypes } from 'vxe-table';
import type { ComputedRef, Ref } from 'vue';
import { computed, createVNode, unref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { isArray } from '@/utils/is';
import { omit, merge } from 'lodash-es';
import { error } from '@/utils/log';
import { useI18n } from '@/hooks/web/useI18n';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { useMessage } from '@/hooks/web/useMessage';
interface ActionType {
commitVxeProxy: (code: string, ...args) => void;
getSearchFormParameter: () => SmartSearchFormParameter;
setLoading: (loading: boolean) => void;
getCheckboxRecords: (isFull: boolean) => Array<any>;
}
const { createConfirm, warnMessage, successMessage } = useMessage();
export const useTableAjax = (
propsRef: ComputedRef<SmartTableProps>,
vxeGridRef: Ref<VxeGridInstance>,
emit,
{ commitVxeProxy, getSearchFormParameter, setLoading, getCheckboxRecords }: ActionType,
) => {
const { t } = useI18n();
const getProxyConfigRef = computed<SmartTableProxyConfig | undefined>(() => {
const { proxyConfig, useSearchForm, sortConfig } = unref(propsRef);
if (!proxyConfig) {
return;
}
const ajax = proxyConfig.ajax || {};
let queryAjax: any = {};
if (ajax.query) {
queryAjax = {
query: async (params, args) => {
try {
const { form, filters, page, sorts, sort, $grid } =
params as VxeGridPropTypes.ProxyAjaxQueryParams;
let fetchParams: FetchParams = {};
if (args && args.length > 0) {
fetchParams = args[0];
}
const searchParameter: SmartTableAjaxQueryParams = merge(
{
$grid,
form,
filters,
page,
sorts,
sort,
},
fetchParams,
);
let ajaxParameter: Recordable = {
...form,
...page,
};
if (sorts.length > 0) {
const sortNameList: string[] = [];
const sortOrderList: string[] = [];
for (const item of sorts) {
sortNameList.push(item.field);
sortOrderList.push(item.order);
}
ajaxParameter.sortName = sortNameList.join(',');
ajaxParameter.sortOrder = sortOrderList.join(',');
}
const { searchForm, searchSymbolForm, noSymbolForm, searchWithSymbol } =
getSearchFormParameter();
if (useSearchForm) {
if (searchWithSymbol) {
// 处理搜索符号
searchParameter.searchFormSymbol = searchSymbolForm;
Object.assign(
ajaxParameter,
{
parameter: searchSymbolForm,
},
noSymbolForm,
);
} else {
Object.assign(ajaxParameter, searchForm);
}
searchParameter.searchForm = searchForm;
}
ajaxParameter = omit(ajaxParameter, ['total']);
ajaxParameter = merge(ajaxParameter, searchParameter.searchInfo);
// 添加额外的查询条件
searchParameter.ajaxParameter = ajaxParameter;
let result = await ajax.query!(searchParameter);
if (proxyConfig.afterLoad) {
result = proxyConfig.afterLoad(result);
}
return result;
} catch (e) {
console.error(e);
throw e;
}
},
};
}
const sort = sortConfig?.remote === true;
return {
// autoLoad: false,
sort,
props: {
result: 'rows',
total: 'total',
},
...proxyConfig,
ajax: {
...ajax,
...queryAjax,
},
};
});
/**
* 重载数据函数
* @param opt
*/
const reload = async (opt?: FetchParams) => {
try {
setLoading(true);
await commitVxeProxy('reload', opt);
emit('proxy-query', { status: true, isReload: true, isInited: false });
} finally {
setLoading(false);
}
};
const query = async (opt?: FetchParams) => {
try {
setLoading(true);
await commitVxeProxy('query', opt);
emit('proxy-query', { status: true, isReload: false, isInited: false });
} finally {
setLoading(false);
}
};
/**
* 通过checkbox删除
*/
const deleteByCheckbox = async () => {
const rows = getCheckboxRecords(false);
if (!rows.length) {
message.warn(t('common.notice.deleteChoose'));
return false;
}
return await doDelete(rows);
};
const deleteByRow = (row: any | any[]) => {
if (isArray(row)) {
return doDelete(row);
}
return doDelete([row]);
};
const getProxyEvents = {
onProxyQuery: (...args) => emit('proxy-query', args),
onProxyDelete: (...args) => emit('proxy-delete', args),
};
const doDelete = async (rows: any[]): Promise<boolean | undefined> => {
const proxyConfig = unref(propsRef)?.proxyConfig;
const deleteMethod = proxyConfig?.ajax?.delete;
if (!deleteMethod) {
error('proxyConfig.ajax.delete未配置,无法删除');
return false;
}
if (rows.length === 0) {
return false;
}
Modal.confirm({
title: t('common.button.confirm'),
icon: createVNode(ExclamationCircleOutlined),
content: t('common.notice.deleteConfirm'),
onOk: async () => {
const result = await deleteMethod({
$grid: unref(vxeGridRef),
body: {
removeRecords: rows,
},
});
message.success(t('common.message.deleteSuccess'));
emit('proxy-delete', { status: true });
const afterDelete = proxyConfig?.afterDelete || query;
afterDelete && afterDelete(result);
return Promise.resolve(true);
},
});
};
const useYnByCheckbox = async (useYn: boolean) => {
const rows = getCheckboxRecords(false);
if (!rows.length) {
warnMessage(t('common.notice.select'));
return false;
}
return doUseYn(rows, useYn);
};
const useYnByRow = (row: any | any[], useYn: boolean) => {
if (isArray(row)) {
return doUseYn(row, useYn);
}
return doUseYn([row], useYn);
};
const doUseYn = async (rows: any[], useYn: boolean): Promise<boolean | undefined> => {
const proxyConfig = unref(propsRef)?.proxyConfig;
const useYnMethod = proxyConfig?.ajax?.useYn;
if (!useYnMethod) {
error('proxyConfig.ajax.useYn未配置,无法执行启用停用操作');
return false;
}
if (rows.length === 0) {
return false;
}
createConfirm({
iconType: 'warning',
content: useYn ? t('common.notice.useYnTrueConfirm') : t('common.notice.useYnFalseConfirm'),
onOk: async () => {
const result = await useYnMethod(rows, useYn);
successMessage({
message: t('common.message.OperationSucceeded'),
});
const afterHandler = proxyConfig?.afterUserYn || query;
afterHandler && afterHandler(result);
return Promise.resolve(true);
},
});
};
return {
reload,
query,
getProxyConfigRef,
deleteByRow,
deleteByCheckbox,
getProxyEvents,
useYnByCheckbox,
useYnByRow,
};
};

195
src/components/SmartTable/src/hooks/useTableDrag.ts

@ -0,0 +1,195 @@
import type { ComputedRef, Ref } from 'vue';
import type {
SmartColumn,
SmartTableProps,
SmartTableRowDragConfig,
} from '@/components/SmartTable';
import type { VxeGridInstance } from 'vxe-table';
import { h, onMounted, nextTick, computed, onUnmounted, unref, watch } from 'vue';
import Sortable from 'sortablejs';
import { isBoolean } from '@/utils/is';
import { Icon } from '@/components/Icon';
export const TABLE_DRAG_SLOT_NAME = 'smart-table-drop-slot';
interface TableAction {
getData: (rowIndex?: number) => any[];
loadData: (data: any[]) => Promise<any>;
}
const defaultRowDragConfig: SmartTableRowDragConfig = {
handle: 'smart-table-drop-handle',
animation: 150,
icon: 'ant-design:menu-outlined',
ghostClass: 'blue-background-class',
dataIdAttr: 'rowid',
};
/**
* 支持行拖动功能
*/
export const useTableRowDrag = (
tablePropsRef: ComputedRef<SmartTableProps>,
tableRef: Ref<VxeGridInstance>,
{ getData }: TableAction,
) => {
let sortable: Sortable | null = null;
const getKeyField = computed(() => unref(tablePropsRef).rowConfig?.keyField);
// 是否有左侧锁定列
const getHasFixedLeft = computed(() => {
const tableColumns = unref(tablePropsRef).columns;
if (!tableColumns) {
return false;
}
return tableColumns.some((item) => item.fixed === 'left');
});
/**
* 获取拖拽配置
*/
const getRowDragConfig = computed<SmartTableRowDragConfig | false>(() => {
const { dragConfig } = unref(tablePropsRef).rowConfig || {};
if (!dragConfig) {
return false;
}
let config: SmartTableRowDragConfig;
if (isBoolean(dragConfig)) {
config = defaultRowDragConfig;
} else {
config = {
...defaultRowDragConfig,
...dragConfig,
};
}
return {
...config,
handle: `.${config.handle}`,
onEnd: async ({ newIndex, oldIndex }) => {
if (newIndex !== oldIndex) {
const tableData = getData();
// 记录原顺序
const keySorts = tableData.map((item) => {
return item[unref(getKeyField)!] + '';
});
const currentRow = tableData.splice(oldIndex!, 1)[0];
tableData.splice(newIndex!, 0, currentRow);
// 还原顺序
sortable?.sort(keySorts);
// 更新数据排序
unref(tableRef).reactData.tableData = tableData;
}
},
};
});
/**
* 获取拖拽插槽
*/
const getTableDragSlot = computed(() => {
const dragConfig = unref(getRowDragConfig);
if (!dragConfig) {
return {};
}
if (dragConfig.custom) {
// 自定义列,返回null
return {};
}
const slots: Recordable = {};
slots[TABLE_DRAG_SLOT_NAME] = ({ rowIndex }) => {
return h(
'div',
{ 'data-id': rowIndex, class: [dragConfig.handle?.slice(1)] },
h(Icon, { icon: dragConfig.icon, 'data-id': rowIndex }),
);
};
return slots;
});
/**
* 获取拖拽列信息
*/
const getTableDragColumn = computed<SmartColumn[]>(() => {
const dragConfig = unref(getRowDragConfig);
if (!dragConfig) {
return [];
}
if (dragConfig.custom) {
// 自定义列,返回null
return [];
}
const column: SmartColumn = {
title: '#',
field: 'drag',
width: 60,
// fixed: 'left',
slots: {
default: TABLE_DRAG_SLOT_NAME,
},
};
if (unref(getHasFixedLeft)) {
column.fixed = 'left';
}
return [column];
});
/**
* 开启拖拽功能
*/
const openRowDrop = async () => {
if (!unref(getKeyField)) {
throw new Error('row-config.key-field无法开启行拖拽,请设置key-field');
}
if (sortable !== null) {
sortable.destroy();
}
const el = unref(tableRef).$el;
let sortEl;
if (!unref(getHasFixedLeft)) {
sortEl = el.querySelector('.body--wrapper>.vxe-table--body tbody');
} else {
const selector = '.fixed-left--wrapper>.vxe-table--body tbody';
sortEl = el.querySelector(selector);
if (sortEl == null) {
while (sortEl === null) {
await nextTick(() => (sortEl = el.querySelector(selector)));
}
}
}
sortable = Sortable.create(sortEl, unref(getRowDragConfig) as SmartTableRowDragConfig);
};
/**
* 关闭行拖拽
*/
const closeRowDrop = () => {
if (sortable !== null) {
sortable.destroy();
sortable = null;
}
};
onMounted(() => {
if (unref(getRowDragConfig)) {
openRowDrop();
}
});
onUnmounted(() => closeRowDrop());
watch(getRowDragConfig, (value) => {
if (value) {
if (sortable === null && unref(tableRef)) {
openRowDrop();
}
} else {
closeRowDrop();
}
});
return {
openRowDrop,
closeRowDrop,
getTableDragColumn,
getTableDragSlot,
};
};

213
src/components/SmartTable/src/hooks/useTableModalAddEdit.ts

@ -0,0 +1,213 @@
import type { ComputedRef, Ref, Slots } from 'vue';
import type { SmartAddEditModalCallbackData, SmartTableProps } from '@/components/SmartTable';
import type { FormProps } from '@/components/Form';
import type { ModalProps } from '@/components/Modal';
import { computed, nextTick, ref, Slot, unref } from 'vue';
import { useI18n } from '@/hooks/web/useI18n';
import { message } from 'ant-design-vue';
import { error } from '@/utils/log';
interface TableAction {
getCheckboxRecords: (isFull: boolean) => Array<any>;
openAddEditModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void;
query: (parameter?) => Promise<void>;
}
const SizeMap: { [index: string]: 'default' | 'small' | 'large' } = {
midum: 'default',
small: 'small',
mini: 'small',
tiny: 'small',
};
export const useTableModalAddEditConfig = (
emit: Function,
tableProps: ComputedRef<SmartTableProps>,
slots: Slots,
{ getCheckboxRecords, openAddEditModal, query }: TableAction,
) => {
const { t } = useI18n();
const isAddRef = ref(true);
/**
* 是否有添加修改弹窗
*/
const getHasAddEdit = computed<boolean>(() => {
return unref(tableProps).addEditConfig !== undefined;
});
/**
* 添加修改弹窗props
*/
const getAddEditFormProps = computed<FormProps>(() => {
const { size, addEditConfig } = unref(tableProps);
const formConfig = addEditConfig?.formConfig;
if (!formConfig) {
return {};
}
const props: FormProps = {
...getDefaultFormConfig(),
...formConfig,
};
if (size) {
props.size = formConfig.size || SizeMap[size];
}
return props;
});
function replaceFormSlotKey(key: string) {
if (!key) return '';
return key?.replace?.(/addEditForm-/, '') ?? '';
}
/**
* 添加修改表单form插槽
*/
const getAddEditFormSlots: ComputedRef<Slots> = computed(() => {
// 获取form插槽
const formSlots: { [name: string]: Slot | undefined } = {};
Object.keys(slots)
.map((item) => (item.startsWith('addEditForm-') ? item : null))
.filter((item) => !!item)
.forEach((item) => {
const formKey = replaceFormSlotKey(item as string);
formSlots[formKey] = slots[item as string];
});
// 获取column插槽
const columnSlots: { [name: string]: Slot | undefined } = {};
const schemas = unref(tableProps).addEditConfig?.formConfig?.schemas || [];
schemas
.map((item: any) => item.slot)
.filter((item) => !!item)
.forEach((item) => {
columnSlots[item as string] = slots[item as string];
});
const result = {
...formSlots,
...columnSlots,
};
if (
Object.keys(formSlots).length + Object.keys(columnSlots).length >
Object.keys(result).length
) {
error('添加修改表单插槽命名重复');
}
return result;
});
/**
* 添加修改弹窗props
*/
const getAddEditModalProps = computed<Partial<ModalProps>>(() => {
const { addEditConfig, proxyConfig } = unref(tableProps);
if (!addEditConfig) {
return {};
}
const { modalConfig, beforeSave, afterSave } = addEditConfig!;
const saveFunction = proxyConfig?.ajax?.save;
return {
...getDefaultModalConfig(isAddRef, t),
...(modalConfig || {}),
beforeSave,
saveFunction,
afterSave:
afterSave ||
(() => {
query();
return true;
}),
};
});
const showAddModal = (formData?: Recordable, selectData?: Recordable) => {
if (!unref(getHasAddEdit)) {
throw new Error('addEditConfig未定义');
}
isAddRef.value = true;
nextTick(() => {
emit('add-edit-modal-show', { isAdd: true });
});
openAddEditModal(true, getCallbackData(true, selectData, formData));
};
const editByRowModal = (row, formData?: Recordable) => {
return doEdit(row, formData);
};
const editByCheckbox = () => {
const selectRows = getCheckboxRecords(false);
if (selectRows.length !== 1) {
message.warn(t('common.notice.choseOne'));
return false;
}
const editRow = selectRows[0];
return doEdit(editRow);
};
const doEdit = async (row, formData?: Recordable) => {
if (!unref(getHasAddEdit)) {
throw new Error('addEditConfig未定义');
}
isAddRef.value = false;
nextTick(() => {
emit('add-edit-modal-show', { isAdd: false, row, formData });
});
if (unref(tableProps).addEditConfig?.openModalHandler) {
unref(tableProps).addEditConfig?.openModalHandler?.(row, formData);
} else {
openAddEditModal(true, getCallbackData(false, row, formData));
}
return true;
};
const getCallbackData = (
isAdd: boolean,
selectData?: Recordable,
formData?: Recordable,
): SmartAddEditModalCallbackData => {
const getByIdFunction = unref(tableProps)?.proxyConfig?.ajax?.getById;
if (!getByIdFunction) {
throw new Error('proxyConfig.ajax.getById未设置');
}
return {
getFunction: getByIdFunction,
isAdd,
selectData,
formData,
validateFunction: unref(tableProps)?.addEditConfig?.afterLoadData,
};
};
return {
showAddModal,
editByCheckbox,
getHasAddEdit,
getAddEditFormProps,
getAddEditModalProps,
editByRowModal,
getAddEditFormSlots,
};
};
const getDefaultFormConfig = (): Partial<FormProps> => {
return {
showActionButtonGroup: false,
labelCol: {
span: 5,
},
wrapperCol: {
span: 19,
},
baseColProps: {
span: 24,
xxl: 12,
},
};
};
const getDefaultModalConfig = (isAddRef: Ref<boolean>, t: Function): Partial<ModalProps> => {
const title = unref(isAddRef) ? t('common.title.add') : t('common.title.edit');
return {
title,
};
};

83
src/components/SmartTable/src/hooks/useTablePager.tsx

@ -0,0 +1,83 @@
import type { SmartTableProps } from '@/components/SmartTable';
import { computed, ComputedRef, ref, unref } from 'vue';
import type { VxeGridPropTypes } from 'vxe-table';
import { isBoolean } from '@/utils/is';
import {
DEFAULT_PAGE_LAYOUTS,
PAGE_SIZE,
PAGE_SIZE_OPTIONS,
} from '@/components/SmartTable/src/const';
export const useTablePager = (tableProps: ComputedRef<SmartTableProps>, emit: Function) => {
const configRef = ref<VxeGridPropTypes.PagerConfig>({});
const show = ref(true);
/**
* 获取分页配置
*/
const getPagerConfig = computed((): VxeGridPropTypes.PagerConfig | undefined => {
const { pagerConfig } = unref(tableProps);
if (!pagerConfig) {
return undefined;
}
if (!unref(show) || (isBoolean(pagerConfig) && !pagerConfig)) {
return undefined;
}
return {
currentPage: 1,
pageSize: PAGE_SIZE,
pageSizes: PAGE_SIZE_OPTIONS,
layouts: DEFAULT_PAGE_LAYOUTS,
...(isBoolean(pagerConfig) ? {} : pagerConfig),
...unref(configRef),
};
});
/**
* 分页触发
*/
const handlePageChange = (eventData) => {
configRef.value.pageSize = eventData.pageSize;
configRef.value.currentPage = eventData.currentPage;
emit('page-change', eventData);
};
/**
* 插槽信息
*/
const getPagerSlots = computed(() => {
if (!unref(getPagerConfig)) {
return {};
}
return {
pager: () => <vxe-pager {...unref(getPagerConfig)} onPageChange={handlePageChange} />,
};
});
async function setShowPagination(flag: boolean) {
show.value = flag;
}
function getShowPagination() {
return unref(show);
}
const getPagination = () => unref(getPagerConfig);
const setPagination = (info: Partial<VxeGridPropTypes.PagerConfig>) => {
const paginationInfo = unref(getPagerConfig);
configRef.value = {
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
...info,
};
};
return {
getPagerSlots,
setShowPagination,
getShowPagination,
getPagination,
setPagination,
getPagerConfig,
};
};

205
src/components/SmartTable/src/hooks/useTableSearchForm.ts

@ -0,0 +1,205 @@
import type { ComputedRef, Slot, Slots } from 'vue';
import type { SmartSearchFormProps, SmartSearchFormSchema } from '@/components/SmartTable';
import type { FormProps } from '@/components/Form';
import type { FetchParams, SmartTableProps } from '../types/SmartTableType';
import type { SmartSearchFormParameter } from '../types/SmartSearchFormType';
import type { VxeGridPropTypes } from 'vxe-table';
import { computed, ref, unref } from 'vue';
import { useForm } from '@/components/Form';
import { isArray, isBoolean } from '@/utils/is';
import { getFormSize } from '../utils';
export const useTableSearchForm = (
propsRef: ComputedRef<SmartTableProps>,
slots: Slots,
fetch: (opt?: FetchParams | undefined) => Promise<void>,
getLoading: ComputedRef<boolean | undefined>,
setPagination: (info: Partial<VxeGridPropTypes.PagerConfig>) => void,
) => {
/**
* 搜索form显示状态
*/
const searchFormVisibleRef = ref(unref(propsRef)?.searchFormConfig?.defaultVisible !== false);
const setSearchFormVisible = (visible?: boolean) => {
if (isBoolean(visible)) {
searchFormVisibleRef.value = visible;
} else {
searchFormVisibleRef.value = !unref(searchFormVisibleRef);
}
};
const getSearchFormVisible = computed(() => unref(searchFormVisibleRef));
const [registerSearchForm, searchFormAction] = useForm();
/**
* searchForm props计算属性
*/
const getSearchFormProps = computed((): Partial<FormProps> => {
const { searchFormConfig, size } = unref(propsRef);
const { submitButtonOptions, resetButtonOptions } = searchFormConfig || {};
const props: FormProps = {
...searchFormConfig,
submitButtonOptions: {
loading: unref(getLoading),
...{ preIcon: 'ant-design:search-outlined' },
...submitButtonOptions,
},
resetButtonOptions: {
...{ preIcon: 'ic:baseline-restart-alt' },
...resetButtonOptions,
},
};
if (size) {
props.size = searchFormConfig?.size || getFormSize(size);
}
return props;
});
/**
* 搜索条件触发
*/
const handleSearchInfoChange = () => {
setPagination({
currentPage: 1,
});
fetch({
page: {
currentPage: 1,
},
});
};
function replaceFormSlotKey(key: string) {
if (!key) return '';
return key?.replace?.(/searchForm-/, '') ?? '';
}
/**
* form slot key计算属性
*/
const getSearchFormSlot: ComputedRef<Slots> = computed(() => {
const result: { [name: string]: Slot | undefined } = {};
Object.keys(slots)
.map((item) => (item.startsWith('searchForm-') ? item : null))
.filter((item) => !!item)
.forEach((item) => {
const formKey = replaceFormSlotKey(item as string);
result[formKey] = slots[item as string];
});
return result;
});
const getSearchFormColumnSlot = computed<Slots>(() => {
const { searchFormConfig } = unref(propsRef);
const slotNames =
(searchFormConfig?.schemas
?.map((item: any) => {
const { slot } = item;
return slot;
})
.filter((item) => !!item) as string[]) || [];
const result: { [name: string]: Slot | undefined } = {};
slotNames.forEach((item) => {
result[item] = slots[item];
});
return result;
});
/**
* 获取搜索表单参数
*/
const getSearchFormParameter = (): SmartSearchFormParameter => {
if (!unref(propsRef).useSearchForm) {
return {};
}
const searchForm = searchFormAction.getFieldsValue();
const searchWithSymbol = unref(propsRef).searchFormConfig?.searchWithSymbol;
const result: SmartSearchFormParameter = {
searchWithSymbol: isBoolean(searchWithSymbol) && searchWithSymbol,
};
if (result.searchWithSymbol) {
// 处理搜索符号
const { symbolForm, noSymbolForm } = dealSearchSymbol(searchForm);
result.searchSymbolForm = symbolForm;
result.noSymbolForm = noSymbolForm;
}
result.searchForm = searchForm;
return result;
};
/**
* 获取搜索符号
*/
const getSearchFormSymbolRef = computed<{ [index: string]: SmartSearchFormSchema } | boolean>(
() => {
const { searchFormConfig, useSearchForm } = unref(propsRef);
const searchWithSymbol = searchFormConfig?.searchWithSymbol;
if (!useSearchForm || !searchWithSymbol) {
return false;
}
const { schemas } = searchFormConfig as Partial<SmartSearchFormProps>;
const result: { [index: string]: SmartSearchFormSchema } = {};
schemas?.forEach((item) => {
result[item.field] = item;
});
return result;
},
);
const dealSearchSymbol = (info: Recordable) => {
const symbolForm: Recordable = {};
const noSymbolForm: Recordable = {};
const getSearchFormSymbol = unref(getSearchFormSymbolRef);
if (isBoolean(getSearchFormSymbol)) {
return info;
}
Object.keys(info).forEach((key) => {
const value = info[key];
const schema = getSearchFormSymbol[key];
const { searchSymbol: symbol, customSymbol } = schema;
if (customSymbol) {
// 自定义符号
const customSymbolResult = customSymbol({
schema,
value,
model: info,
});
if (customSymbolResult) {
Object.assign(symbolForm, customSymbolResult);
}
} else if (schema.searchSymbol) {
if (symbol === 'between') {
// between特殊处理
if (value && isArray(value) && value.length === 2) {
symbolForm[`${key}@>=`] = value[0];
symbolForm[`${key}@<=`] = value[1];
}
} else {
symbolForm[`${key}@${symbol}`] = value;
}
} else {
noSymbolForm[key] = value;
}
});
return {
symbolForm,
noSymbolForm,
};
};
return {
getSearchFormProps,
handleSearchInfoChange,
getSearchFormSlot,
getSearchFormColumnSlot,
registerSearchForm,
getSearchFormVisible,
searchFormAction: {
...searchFormAction,
getSearchFormParameter,
getSearchFormVisible,
setSearchFormVisible,
},
};
};

358
src/components/SmartTable/src/hooks/useTableToolbarConfig.ts

@ -0,0 +1,358 @@
import type { ComputedRef } from 'vue';
import type { ButtonProps } from '@/components/Button';
import type {
SmartTableProps,
SmartTableToolbarConfig,
FetchParams,
} from '../types/SmartTableType';
import type { SmartTableButton, SmartTableToolbarTool } from '../types/SmartTableButton';
import type { SizeType } from 'ant-design-vue/es/config-provider';
import type { VxeToolbarPropTypes } from 'vxe-table';
import { computed, ref, unref } from 'vue';
import { error, warn } from '@/utils/log';
import { merge } from 'lodash-es';
import { isArray, isBoolean, isPromise } from '@/utils/is';
import SmartTableColumnConfig from '../components/SmartTableColumnConfig';
import {
VxeTableToolButtonCustomRenderer,
VxeTableToolAntRenderer,
VxeTableToolButtonSlotRenderer,
VxeTableToolComponentRenderer,
} from '../renderer/VxeTableButtonRenderer';
import { SmartTableCode } from '../const';
const tableButtonSizeMap: { [key: string]: SizeType } = {
medium: 'middle',
small: 'small',
mini: 'small',
tiny: 'small',
};
interface TableAction {
deleteByCheckbox: () => Promise<boolean | undefined>;
showAddModal: () => void;
editByCheckbox: () => Promise<boolean> | boolean;
query: (opt?: FetchParams) => Promise<any>;
getSearchFormVisible: ComputedRef<boolean>;
useYnByCheckbox: (useYn: boolean) => Promise<boolean | undefined>;
setColumnSortConfig: () => void;
}
export const useTableToolbarConfig = (
tableProps: ComputedRef<SmartTableProps>,
t: Function,
{
deleteByCheckbox,
showAddModal,
editByCheckbox,
query,
getSearchFormVisible,
useYnByCheckbox,
setColumnSortConfig,
}: TableAction,
) => {
// const configRef = ref<SmartTableToolbarConfig>({})
const getToolbarConfigInfo = computed<SmartTableToolbarConfig | undefined>(() => {
const { toolbarConfig, size: tableSize } = unref(tableProps);
if (!toolbarConfig) {
return undefined;
}
let buttons = dealButtons(toolbarConfig.buttons, tableSize);
buttons = dealButtonAuth(buttons);
let refresh = toolbarConfig.refresh;
if (refresh) {
refresh = getDefaultRefresh();
}
return {
...toolbarConfig,
buttons,
refresh,
tools: getTools(toolbarConfig),
};
});
const dealButtonAuth = (
buttons: SmartTableButton[] | undefined,
): SmartTableButton[] | undefined => {
if (!buttons) {
return undefined;
}
const authConfig = unref(tableProps).authConfig;
if (!authConfig) {
// 未配置权限
return buttons;
}
const { toolbar, authHandler, displayMode } = authConfig;
if (!authHandler) {
error('未设置authConfig.authHandler');
}
return buttons
.map((button) => {
const { auth, code } = button;
const configAuth = code && toolbar ? toolbar[code] : undefined;
if (auth && configAuth) {
warn('toolbarConfig与AuthConfig权限配置冲突,toolbarConfig配置');
}
const buttonAuth = auth || configAuth;
if (!buttonAuth) {
return button;
}
const hasAuth = authHandler!(buttonAuth);
if (hasAuth) {
return button;
}
if (displayMode === 'hide') {
return null;
} else {
return {
...button,
props: {
...unref(button.props),
disabled: true,
},
};
}
})
.filter((item) => item !== null) as SmartTableButton[];
};
const getTools = (toolbarConfig: SmartTableToolbarConfig) => {
const { tools, showSearch, column } = toolbarConfig;
if (!tools && !showSearch && !column) {
return undefined;
}
const result: SmartTableToolbarTool[] = [...(tools || [])];
if (showSearch && unref(tableProps).useSearchForm) {
if (isBoolean(showSearch)) {
const props = computed(() => {
return {
circle: true,
icon: 'vxe-icon-search',
title: unref(getSearchFormVisible)
? t('component.table.hideSearch')
: t('component.table.showSearch'),
};
});
result.push({
code: SmartTableCode.showSearch,
toolRender: {
name: VxeTableToolAntRenderer,
},
props,
});
} else {
result.push(showSearch);
}
}
// 处理列配置
if (column) {
const columnConfig = isBoolean(column) ? undefined : column;
result.push({
code: SmartTableCode.column,
toolRender: {
name: VxeTableToolComponentRenderer,
props: {
config: columnConfig,
setColumnSortConfig,
},
},
component: SmartTableColumnConfig,
});
}
return result;
};
const getDefaultRefresh = (): VxeToolbarPropTypes.RefreshConfig => {
return {
queryMethod: (params) => {
return query(params);
},
};
};
const dealButtons = (
buttons: SmartTableButton[] | undefined,
tableSize,
): SmartTableButton[] | undefined => {
if (!buttons) {
return undefined;
}
return buttons.map((item) => {
if (item.code === 'ModalAdd') {
// 添加按钮处理
const defaultConfig = getDefaultAddButtonConfig(t);
return merge(
{ size: tableButtonSizeMap[tableSize] },
defaultConfig,
{
props: {
onClick: () => {
showAddModal();
},
},
},
item,
) as SmartTableButton;
} else if (item.code == 'ModalEdit') {
return merge(
{ size: tableButtonSizeMap[tableSize] },
getDefaultEditButtonConfig(t),
{
props: {
onClick: () => {
editByCheckbox();
},
},
},
item,
) as SmartTableButton;
} else if (item.code === 'delete') {
return merge(
{ size: tableButtonSizeMap[tableSize] },
getDefaultDeleteButtonConfig(t),
{
props: {
onClick: () => {
deleteByCheckbox && deleteByCheckbox();
},
},
},
item,
) as SmartTableButton;
} else if (item.code === 'useYnFalse' || item.code === 'useYnTrue') {
const useYn = item.code === 'useYnTrue';
return merge(
{ size: tableButtonSizeMap[tableSize] },
getDefaultUseYnButtonConfig(t, useYn),
{
props: {
onClick: () => {
useYnByCheckbox && useYnByCheckbox(useYn);
},
},
},
item,
) as SmartTableButton;
}
// props添加响应性
const loading = ref(false);
const props = computed<ButtonProps>(() => {
const buttonProps = unref(item.props) as ButtonProps | undefined;
const result: ButtonProps = {
...buttonProps,
};
// 点击事件加载状态添加操作
if (item.clickLoading && buttonProps?.loading === undefined) {
result.loading = unref(loading);
const defaultClickHandler = buttonProps?.onClick;
if (defaultClickHandler) {
const handler = isArray(defaultClickHandler)
? defaultClickHandler[0]
: defaultClickHandler;
result.onClick = async () => {
try {
loading.value = true;
// @ts-ignore
const handlerResult = handler();
if (isPromise(handlerResult)) {
await handlerResult;
}
} finally {
loading.value = false;
}
};
}
}
return result;
});
if (item.slot) {
return {
size: tableButtonSizeMap[tableSize],
buttonRender: {
name: VxeTableToolButtonSlotRenderer,
},
...item,
props,
};
}
// 如果是ant 按钮使用VxeTableToolButtonRenderer进行渲染
if (item.customRender) {
return {
size: tableButtonSizeMap[tableSize],
buttonRender: {
name: VxeTableToolButtonCustomRenderer,
},
...item,
props,
};
}
return item;
});
};
return {
getToolbarConfigInfo,
};
};
const getDefaultAddButtonConfig = (t: Function): SmartTableButton => {
return {
name: t('common.button.add'),
code: 'ModalAdd',
props: {
preIcon: 'ant-design:plus-outlined',
type: 'primary',
},
buttonRender: {
name: VxeTableToolButtonCustomRenderer,
},
};
};
const getDefaultEditButtonConfig = (t: Function): SmartTableButton => {
return {
name: t('common.button.edit'),
code: 'ModalEdit',
props: {
color: 'warning',
preIcon: 'ant-design:edit-outlined',
},
buttonRender: {
name: VxeTableToolButtonCustomRenderer,
},
};
};
const getDefaultDeleteButtonConfig = (t: Function): SmartTableButton => {
return {
name: t('common.button.delete'),
code: 'delete',
props: {
color: 'error',
preIcon: 'ant-design:edit-outlined',
},
buttonRender: {
name: VxeTableToolButtonCustomRenderer,
},
};
};
const getDefaultUseYnButtonConfig = (t: Function, useYn: boolean): SmartTableButton => {
return {
name: useYn ? t('common.button.use') : t('common.button.noUse'),
code: useYn ? 'useYnTrue' : 'useYnFalse',
props: {
color: useYn ? '' : 'error',
preIcon: useYn ? 'ant-design:check-outlined' : 'ant-design:close-outlined',
},
buttonRender: {
name: VxeTableToolButtonCustomRenderer,
},
};
};

35
src/components/SmartTable/src/hooks/useToolbarEvent.ts

@ -0,0 +1,35 @@
import type { VxeGridListeners } from 'vxe-table';
import { computed } from 'vue';
import { SmartTableCode } from '../const';
interface Action {
setSearchFormVisible: (visible?: boolean) => void;
}
export const useToolbarEvent = (emit: Function, { setSearchFormVisible }: Action) => {
const events: VxeGridListeners = {
toolbarToolClick: (params) => {
emit('toolbar-tool-click', params);
const { code } = params;
switch (code) {
case SmartTableCode.showSearch: {
setSearchFormVisible();
break;
}
}
},
};
const getToolbarEvents = computed(() => {
const result: any = {};
Object.keys(events).forEach((item) => {
result[`on${item.slice(0, 1).toUpperCase() + item.slice(1)}`] = events[item];
});
return result;
});
return {
getToolbarEvents,
};
};

22
src/components/SmartTable/src/hooks/userSmartTableContext.ts

@ -0,0 +1,22 @@
import type { SmartTableProps, TableActionType } from '../types/SmartTableType';
import type { ComputedRef, Ref } from 'vue';
import { inject, provide } from 'vue';
const key = Symbol('smart-table');
type Instance = TableActionType & {
wrapRef: Ref<Nullable<HTMLElement>>;
getBindValues: ComputedRef<Recordable>;
};
type RetInstance = Omit<Instance, 'getBindValues'> & {
getBindValues: ComputedRef<SmartTableProps>;
};
export function createTableContext(instance: Instance) {
provide(key, instance);
}
export function useTableContext(): RetInstance {
return inject(key) as RetInstance;
}

44
src/components/SmartTable/src/props.ts

@ -0,0 +1,44 @@
import type {
SmartTableProps,
SmartTableProxyConfig,
TableHeightType,
SmartTableToolbarConfig,
SmartCheckboxConfig,
} from './types/SmartTableType';
import type { SmartColumn } from './types/SmartTableColumnType';
import type { VxeGridPropTypes } from 'vxe-table';
import { ComponentPropsOptions } from 'vue';
import { propTypes } from '@/utils/propTypes';
import { SmartTableAddEditConfig } from '@/components/SmartTable';
import { buildUUID } from '@/utils/uuid';
import { defaultCheckboxConfig } from './defaultConfig';
export const smartTableProps: ComponentPropsOptions<SmartTableProps> = {
columns: {
type: [Array] as PropType<SmartColumn[]>,
default: () => [],
},
height: {
type: [String, Number] as PropType<TableHeightType>,
},
loading: propTypes.bool,
// 使用搜索表单
useSearchForm: propTypes.bool,
toolbarConfig: Object as PropType<SmartTableToolbarConfig>,
pagerConfig: [Object, Boolean] as PropType<boolean | VxeGridPropTypes.PagerConfig>,
proxyConfig: Object as PropType<SmartTableProxyConfig>,
addEditConfig: Object as PropType<SmartTableAddEditConfig>,
size: {
type: String as PropType<VxeGridPropTypes.Size>,
default: 'small',
},
id: propTypes.string.def(buildUUID()),
checkboxConfig: {
type: Object as PropType<SmartCheckboxConfig>,
default: () => {
return defaultCheckboxConfig;
},
},
};

75
src/components/SmartTable/src/renderer/VxeTableAntRenderer.css

@ -0,0 +1,75 @@
.vxe-table.cell--highlight .vxe-cell > .ant-time-picker .ant-time-picker-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-time-picker .ant-time-picker-input, .vxe-table.cell--highlight .vxe-cell > .ant-calendar-picker .ant-calendar-picker-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-calendar-picker .ant-calendar-picker-input, .vxe-table.cell--highlight .vxe-cell > .ant-cascader-picker .ant-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-cascader-picker .ant-input, .vxe-table.cell--highlight .vxe-cell > .ant-select .ant-select-selection,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-select .ant-select-selection, .vxe-table.cell--highlight .vxe-cell > .ant-input-number .ant-input-number-handler-wrap,
.vxe-table.cell--highlight .vxe-cell > .ant-input-number .ant-input-number-handler-down,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input-number .ant-input-number-handler-wrap,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input-number .ant-input-number-handler-down, .vxe-table.cell--highlight .vxe-cell > .ant-select .ant-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-select .ant-input, .vxe-table.cell--highlight .vxe-cell > .ant-input, .vxe-table.cell--highlight .vxe-cell > .ant-input-number,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input-number {
border: 0;
box-shadow: none;
}
.vxe-cell > .ant-input,
.vxe-tree-cell > .ant-input, .vxe-form .vxe-form--item-content > .ant-input, .vxe-cell > .ant-input-number,
.vxe-tree-cell > .ant-input-number, .vxe-form .vxe-form--item-content > .ant-input-number, .vxe-cell > .ant-select,
.vxe-tree-cell > .ant-select, .vxe-form .vxe-form--item-content > .ant-select, .vxe-cell > .ant-cascader-picker,
.vxe-tree-cell > .ant-cascader-picker, .vxe-form .vxe-form--item-content > .ant-cascader-picker, .vxe-cell > .ant-calendar-picker,
.vxe-tree-cell > .ant-calendar-picker, .vxe-form .vxe-form--item-content > .ant-calendar-picker, .vxe-cell > .ant-time-picker,
.vxe-tree-cell > .ant-time-picker, .vxe-form .vxe-form--item-content > .ant-time-picker {
width: 100%;
}
.vxe-table--filter-antd-wrapper {
padding: 0.8em 1em;
}
.vxe-table--filter-antd-wrapper > .ant-input, .vxe-table--filter-antd-wrapper > .ant-input-number, .vxe-table--filter-antd-wrapper > .ant-select, .vxe-table--filter-antd-wrapper > .ant-rate {
width: 180px;
}
.vxe-cell > .ant-rate,
.vxe-tree-cell > .ant-rate {
vertical-align: bottom;
}
.vxe-cell > .ant-rate .anticon-star,
.vxe-tree-cell > .ant-rate .anticon-star {
display: block;
}
.col--valid-error > .vxe-cell > .ant-input, .col--valid-error > .vxe-cell > .ant-select .ant-input, .col--valid-error > .vxe-cell > .ant-select .ant-select-selection, .col--valid-error > .vxe-cell > .ant-input-number, .col--valid-error > .vxe-cell > .ant-cascader-picker .ant-cascader-input, .col--valid-error > .vxe-cell > .ant-calendar-picker .ant-calendar-picker-input, .col--valid-error > .vxe-tree-cell > .ant-input, .col--valid-error > .vxe-tree-cell > .ant-select .ant-input, .col--valid-error > .vxe-tree-cell > .ant-select .ant-select-selection, .col--valid-error > .vxe-tree-cell > .ant-input-number, .col--valid-error > .vxe-tree-cell > .ant-cascader-picker .ant-cascader-input, .col--valid-error > .vxe-tree-cell > .ant-calendar-picker .ant-calendar-picker-input {
border-color: #f56c6c;
box-shadow: none;
}
.vxe-table.cell--highlight .vxe-cell > .ant-input, .vxe-table.cell--highlight .vxe-cell > .ant-input-number,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input-number {
padding: 0;
}
.vxe-table.cell--highlight .vxe-cell > .ant-select .ant-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-select .ant-input {
padding: 0;
}
.vxe-table.cell--highlight .vxe-cell > .ant-input-number .ant-input-number-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-input-number .ant-input-number-input {
padding: 0;
}
.vxe-table.cell--highlight .vxe-cell > .ant-select .ant-select-selection .ant-select-selection__rendered,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-select .ant-select-selection .ant-select-selection__rendered {
margin: 0;
}
.vxe-table.cell--highlight .vxe-cell > .ant-cascader-picker .ant-cascader-picker-label,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-cascader-picker .ant-cascader-picker-label {
padding: 0;
}
.vxe-table.cell--highlight .vxe-cell > .ant-calendar-picker .ant-calendar-picker-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-calendar-picker .ant-calendar-picker-input {
padding: 0;
}
.vxe-table.cell--highlight .vxe-cell > .ant-time-picker .ant-time-picker-input,
.vxe-table.cell--highlight .vxe-tree-cell > .ant-time-picker .ant-time-picker-input {
padding: 0;
}

998
src/components/SmartTable/src/renderer/VxeTableAntdRenderer.ts

@ -0,0 +1,998 @@
import { h, resolveComponent, ComponentOptions } from 'vue';
import XEUtils, { isArray, isString } from 'xe-utils';
import './VxeTableAntRenderer.css';
import {
VXETableCore,
VxeTableDefines,
VxeColumnPropTypes,
VxeGlobalRendererHandles,
VxeGlobalInterceptorHandles,
FormItemRenderOptions,
FormItemContentRenderParams,
} from 'vxe-table';
function isEmptyValue(cellValue: any) {
return cellValue === null || cellValue === undefined || cellValue === '';
}
function getOnName(type: string) {
return 'on' + type.substring(0, 1).toLocaleUpperCase() + type.substring(1);
}
function getModelProp(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
let prop = 'value';
switch (renderOpts.name) {
case 'ASwitch':
prop = 'checked';
break;
}
return prop;
}
function getModelEvent(renderOpts: VxeGlobalRendererHandles.RenderOptions) {
let type = 'update:value';
switch (renderOpts.name) {
case 'ASwitch':
type = 'update:checked';
break;
}
return type;
}
function getChangeEvent(_: VxeGlobalRendererHandles.RenderOptions) {
return 'change';
}
function getCellEditFilterProps(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
_: VxeGlobalRendererHandles.RenderEditParams | VxeGlobalRendererHandles.RenderFilterParams,
value: any,
defaultProps?: { [prop: string]: any },
) {
return XEUtils.assign({}, defaultProps, renderOpts.props, { [getModelProp(renderOpts)]: value });
}
function getItemProps(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
_: FormItemContentRenderParams,
value: any,
defaultProps?: { [prop: string]: any },
) {
return XEUtils.assign({}, defaultProps, renderOpts.props, { [getModelProp(renderOpts)]: value });
}
function formatText(cellValue: any) {
return '' + (isEmptyValue(cellValue) ? '' : cellValue);
}
function getCellLabelVNs(
renderOpts: VxeColumnPropTypes.EditRender,
_: VxeGlobalRendererHandles.RenderCellParams,
cellLabel: any,
) {
const { placeholder } = renderOpts;
return [
h(
'span',
{
class: 'vxe-cell--label',
},
placeholder && isEmptyValue(cellLabel)
? [
h(
'span',
{
class: 'vxe-cell--placeholder',
},
formatText(placeholder),
),
]
: formatText(cellLabel),
),
];
}
function getOns(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderParams,
inputFunc?: Function,
changeFunc?: Function,
) {
const { events } = renderOpts;
const modelEvent = getModelEvent(renderOpts);
const changeEvent = getChangeEvent(renderOpts);
const isSameEvent = changeEvent === modelEvent;
const ons: { [type: string]: Function } = {};
XEUtils.objectEach(events, (func: Function, key: string) => {
ons[getOnName(key)] = function (...args: any[]) {
func(params, ...args);
};
});
if (inputFunc) {
ons[getOnName(modelEvent)] = function (targetEvnt: any) {
inputFunc(targetEvnt);
if (events && events[modelEvent]) {
events[modelEvent](params, targetEvnt);
}
if (isSameEvent && changeFunc) {
changeFunc(targetEvnt);
}
};
}
if (!isSameEvent && changeFunc) {
ons[getOnName(changeEvent)] = function (...args: any[]) {
changeFunc(...args);
if (events && events[changeEvent]) {
events[changeEvent](params, ...args);
}
};
}
return ons;
}
function getEditOns(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const { $table, row, column } = params;
return getOns(
renderOpts,
params,
(value: any) => {
// 处理 model 值双向绑定
XEUtils.set(row, column.field, value);
},
() => {
// 处理 change 事件相关逻辑
$table.updateStatus(params);
},
);
}
function getFilterOns(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderFilterParams,
option: VxeTableDefines.FilterOption,
changeFunc: Function,
) {
return getOns(
renderOpts,
params,
(value: any) => {
// 处理 model 值双向绑定
option.data = value;
},
changeFunc,
);
}
function getItemOns(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: FormItemContentRenderParams,
) {
const { $form, data, field } = params;
return getOns(
renderOpts,
params,
(value: any) => {
// 处理 model 值双向绑定
XEUtils.set(data, field, value);
},
() => {
// 处理 change 事件相关逻辑
$form.updateStatus(params);
},
);
}
function matchCascaderData(index: number, list: any[], values: any[], labels: any[]) {
const val = values[index];
if (list && values.length > index) {
XEUtils.each(list, (item) => {
if (item.value === val) {
labels.push(item.label);
matchCascaderData(++index, item.children, values, labels);
}
});
}
}
function formatDatePicker(defaultFormat: string) {
return function (
renderOpts: VxeColumnPropTypes.EditRender,
params: VxeGlobalRendererHandles.RenderCellParams,
) {
return getCellLabelVNs(
renderOpts,
params,
getDatePickerCellValue(renderOpts, params, defaultFormat),
);
};
}
function getSelectCellValue(
renderOpts: VxeColumnPropTypes.EditRender,
params: VxeGlobalRendererHandles.RenderCellParams,
) {
const {
options = [],
optionGroups,
props = {},
optionProps = {},
optionGroupProps = {},
} = renderOpts;
const { row, column } = params;
const labelProp = optionProps.label || 'label';
const valueProp = optionProps.value || 'value';
const groupOptions = optionGroupProps.options || 'options';
const cellValue = XEUtils.get(row, column.field);
if (!isEmptyValue(cellValue)) {
return XEUtils.map(
props.mode === 'multiple' ? cellValue : [cellValue],
optionGroups
? (value) => {
let selectItem;
for (let index = 0; index < optionGroups.length; index++) {
selectItem = XEUtils.find(
optionGroups[index][groupOptions],
(item) => item[valueProp] === value,
);
if (selectItem) {
break;
}
}
return selectItem ? selectItem[labelProp] : value;
}
: (value) => {
const selectItem = XEUtils.find(options, (item) => item[valueProp] === value);
return selectItem ? selectItem[labelProp] : value;
},
).join(', ');
}
return '';
}
function getCascaderCellValue(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams,
) {
const { props = {} } = renderOpts;
const { row, column } = params;
const cellValue = XEUtils.get(row, column.field);
const values = cellValue || [];
const labels: Array<any> = [];
matchCascaderData(0, props.options, values, labels);
return (
props.showAllLevels === false ? labels.slice(labels.length - 1, labels.length) : labels
).join(` ${props.separator || '/'} `);
}
function getRangePickerCellValue(
renderOpts: VxeColumnPropTypes.EditRender,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.RenderEditParams,
) {
const { props = {} } = renderOpts;
const { row, column } = params;
let cellValue = XEUtils.get(row, column.field);
if (cellValue) {
cellValue = XEUtils.map(cellValue, (date: any) =>
date.format(props.format || 'YYYY-MM-DD'),
).join(' ~ ');
}
return cellValue;
}
function getTreeSelectCellValue(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.RenderEditParams,
) {
const { props = {} } = renderOpts;
const { treeData, treeCheckable } = props;
const { row, column } = params;
const cellValue = XEUtils.get(row, column.field);
if (!isEmptyValue(cellValue)) {
return XEUtils.map(treeCheckable ? cellValue : [cellValue], (value) => {
const matchObj = XEUtils.findTree(treeData, (item: any) => item.value === value, {
children: 'children',
});
return matchObj ? matchObj.item.title : value;
}).join(', ');
}
return cellValue;
}
function getDatePickerCellValue(
renderOpts: VxeGlobalRendererHandles.RenderOptions,
params: VxeGlobalRendererHandles.RenderCellParams | VxeGlobalRendererHandles.ExportMethodParams,
defaultFormat: string,
) {
const { props = {} } = renderOpts;
const { row, column } = params;
let cellValue = XEUtils.get(row, column.field);
if (isString(cellValue)) {
return cellValue;
}
if (cellValue) {
let format = defaultFormat;
if (props.tableFormat) {
format = props.tableFormat;
} else if (props.format) {
if (isArray(props.format) && props.format.length > 0) {
format = props.format[0];
} else {
format = props.format as string;
}
}
cellValue = cellValue.format(format);
}
return cellValue;
}
function createEditRender(defaultProps?: { [key: string]: any }) {
return function (
renderOpts: VxeColumnPropTypes.EditRender & { name: string },
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const { row, column } = params;
const { name, attrs } = renderOpts;
const cellValue = XEUtils.get(row, column.field);
return [
h(resolveComponent(name), {
...attrs,
...getCellEditFilterProps(renderOpts, params, cellValue, defaultProps),
...getEditOns(renderOpts, params),
}),
];
};
}
function defaultButtonEditRender(
renderOpts: VxeColumnPropTypes.EditRender,
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const { attrs } = renderOpts;
return [
h(
resolveComponent('a-button'),
{
...attrs,
...getCellEditFilterProps(renderOpts, params, null),
...getOns(renderOpts, params),
},
cellText(renderOpts.content),
),
];
}
function defaultButtonsEditRender(
renderOpts: VxeColumnPropTypes.EditRender,
params: VxeGlobalRendererHandles.RenderEditParams,
) {
const { children } = renderOpts;
if (children) {
return children.map(
(childRenderOpts: VxeColumnPropTypes.EditRender) =>
defaultButtonEditRender(childRenderOpts, params)[0],
);
}
return [];
}
function createFilterRender(defaultProps?: { [key: string]: any }) {
return function (
renderOpts: VxeColumnPropTypes.FilterRender & { name: string },
params: VxeGlobalRendererHandles.RenderFilterParams,
) {
const { column } = params;
const { name, attrs } = renderOpts;
return [
h(
'div',
{
class: 'vxe-table--filter-antd-wrapper',
},
column.filters.map((option, oIndex) => {
const optionValue = option.data;
return h(resolveComponent(name), {
key: oIndex,
...attrs,
...getCellEditFilterProps(renderOpts, params, optionValue, defaultProps),
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(params, !!option.data, option);
}),
});
}),
),
];
};
}
function handleConfirmFilter(
params: VxeGlobalRendererHandles.RenderFilterParams,
checked: boolean,
option: VxeTableDefines.FilterOption,
) {
const { $panel } = params;
$panel.changeOption(null, checked, option);
}
/**
* 模糊匹配
* @param params
*/
function defaultFuzzyFilterMethod(params: VxeGlobalRendererHandles.FilterMethodParams) {
const { option, row, column } = params;
const { data } = option;
const cellValue = XEUtils.get(row, column.field);
return XEUtils.toValueString(cellValue).indexOf(data) > -1;
}
/**
* 精确匹配
* @param params
*/
function defaultExactFilterMethod(params: VxeGlobalRendererHandles.FilterMethodParams) {
const { option, row, column } = params;
const { data } = option;
const cellValue = XEUtils.get(row, column.field);
/* eslint-disable eqeqeq */
return cellValue === data;
}
function cellText(cellValue: any): string[] {
return [formatText(cellValue)];
}
function renderOptions(options: any[], optionProps: VxeGlobalRendererHandles.RenderOptionProps) {
const labelProp = optionProps.label || 'label';
const valueProp = optionProps.value || 'value';
return XEUtils.map(options, (item, oIndex) => {
return h(
resolveComponent('a-select-option') as ComponentOptions,
{
key: oIndex,
value: item[valueProp],
disabled: item.disabled,
},
{
default: () => cellText(item[labelProp]),
},
);
});
}
function createFormItemRender(defaultProps?: { [key: string]: any }) {
return function (
renderOpts: FormItemRenderOptions & { name: string },
params: FormItemContentRenderParams,
) {
const { data, field } = params;
const { name } = renderOpts;
const { attrs } = renderOpts;
const itemValue = XEUtils.get(data, field);
return [
h(resolveComponent(name), {
...attrs,
...getItemProps(renderOpts, params, itemValue, defaultProps),
...getItemOns(renderOpts, params),
}),
];
};
}
function defaultButtonItemRender(
renderOpts: FormItemRenderOptions,
params: FormItemContentRenderParams,
) {
const { attrs } = renderOpts;
const props = getItemProps(renderOpts, params, null);
return [
h(
resolveComponent('a-button') as ComponentOptions,
{
...attrs,
...props,
...getItemOns(renderOpts, params),
},
{
default: () => cellText(renderOpts.content || props.content),
},
),
];
}
function defaultButtonsItemRender(
renderOpts: FormItemRenderOptions,
params: FormItemContentRenderParams,
) {
const { children } = renderOpts;
if (children) {
return children.map(
(childRenderOpts: FormItemRenderOptions) =>
defaultButtonItemRender(childRenderOpts, params)[0],
);
}
return [];
}
function createDatePickerExportMethod(defaultFormat: string) {
return function (params: VxeGlobalRendererHandles.ExportMethodParams) {
const { row, column, options } = params;
return options && options.original
? XEUtils.get(row, column.field)
: getDatePickerCellValue(column.editRender || column.cellRender, params, defaultFormat);
};
}
function createExportMethod(getExportCellValue: Function) {
return function (params: VxeGlobalRendererHandles.ExportMethodParams) {
const { row, column, options } = params;
return options && options.original
? XEUtils.get(row, column.field)
: getExportCellValue(column.editRender || column.cellRender, params);
};
}
function createFormItemRadioAndCheckboxRender() {
return function (
renderOpts: FormItemRenderOptions & { name: string },
params: FormItemContentRenderParams,
) {
const { name, options = [], optionProps = {} } = renderOpts;
const { data, field } = params;
const { attrs } = renderOpts;
const labelProp = optionProps.label || 'label';
const valueProp = optionProps.value || 'value';
const itemValue = XEUtils.get(data, field);
return [
h(
resolveComponent(`${name}Group`) as ComponentOptions,
{
...attrs,
...getItemProps(renderOpts, params, itemValue),
...getItemOns(renderOpts, params),
},
{
default: () => {
return options.map((option, oIndex) => {
return h(
resolveComponent(name) as ComponentOptions,
{
key: oIndex,
value: option[valueProp],
disabled: option.disabled,
},
{
default: () => cellText(option[labelProp]),
},
);
});
},
},
),
];
};
}
/**
* 检查触发源是否属于目标节点
*/
function getEventTargetNode(evnt: any, container: HTMLElement, className: string) {
let targetElem;
let target = evnt.target;
while (target && target.nodeType && target !== document) {
if (
className &&
target.className &&
target.className.split &&
target.className.split(' ').indexOf(className) > -1
) {
targetElem = target;
} else if (target === container) {
return { flag: className ? !!targetElem : true, container, targetElem: targetElem };
}
target = target.parentNode;
}
return { flag: false };
}
/**
* 事件兼容性处理
*/
function handleClearEvent(
params:
| VxeGlobalInterceptorHandles.InterceptorClearFilterParams
| VxeGlobalInterceptorHandles.InterceptorClearActivedParams
| VxeGlobalInterceptorHandles.InterceptorClearAreasParams,
) {
const { $event } = params;
const bodyElem = document.body;
if (
// 下拉框
getEventTargetNode($event, bodyElem, 'ant-select-dropdown').flag ||
// 级联
getEventTargetNode($event, bodyElem, 'ant-cascader-menus').flag ||
// 日期
getEventTargetNode($event, bodyElem, 'ant-picker-dropdown').flag ||
getEventTargetNode($event, bodyElem, 'ant-calendar-picker-container').flag ||
// 时间选择
getEventTargetNode($event, bodyElem, 'ant-time-picker-panel').flag
) {
return false;
}
}
/**
* 基于 vxe-table 的表格适配插件,用于兼容 ant-design-vue 组件库
*/
export const VXETablePluginAntd = {
install(vxetable: VXETableCore) {
// 检查版本
if (!/^(4)\./.test(vxetable.version)) {
console.error('[vxe-table-plugin-antd] Version vxe-table 4.x is required');
}
vxetable.renderer.mixin({
AAutoComplete: {
autofocus: 'input.ant-input',
renderDefault: createEditRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: defaultExactFilterMethod,
renderItemContent: createFormItemRender(),
},
AInput: {
autofocus: 'input.ant-input',
renderDefault: createEditRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: defaultFuzzyFilterMethod,
renderItemContent: createFormItemRender(),
},
AInputNumber: {
autofocus: 'input.ant-input-number-input',
renderDefault: createEditRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: defaultFuzzyFilterMethod,
renderItemContent: createFormItemRender(),
},
ASelect: {
renderEdit(renderOpts, params) {
const {
options = [],
optionGroups,
optionProps = {},
optionGroupProps = {},
} = renderOpts;
const { row, column } = params;
const { attrs } = renderOpts;
const cellValue = XEUtils.get(row, column.field);
const props = getCellEditFilterProps(renderOpts, params, cellValue);
const ons = getEditOns(renderOpts, params);
if (optionGroups) {
const groupOptions = optionGroupProps.options || 'options';
const groupLabel = optionGroupProps.label || 'label';
return [
h(
resolveComponent('a-select') as ComponentOptions,
{
...props,
...attrs,
...ons,
},
{
default: () => {
return XEUtils.map(optionGroups, (group, gIndex) => {
return h(
resolveComponent('a-select-opt-group') as ComponentOptions,
{
key: gIndex,
},
{
label: () => {
return h('span', {}, group[groupLabel]);
},
default: () => renderOptions(group[groupOptions], optionProps),
},
);
});
},
},
),
];
}
return [
h(
resolveComponent('a-select') as ComponentOptions,
{
...props,
...attrs,
...ons,
},
{
default: () => renderOptions(options, optionProps),
},
),
];
},
renderCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getSelectCellValue(renderOpts, params));
},
renderFilter(renderOpts, params) {
const {
options = [],
optionGroups,
optionProps = {},
optionGroupProps = {},
} = renderOpts;
const groupOptions = optionGroupProps.options || 'options';
const groupLabel = optionGroupProps.label || 'label';
const { column } = params;
const { attrs } = renderOpts;
return [
h(
'div',
{
class: 'vxe-table--filter-antd-wrapper',
},
optionGroups
? column.filters.map((option, oIndex) => {
const optionValue = option.data;
const props = getCellEditFilterProps(renderOpts, params, optionValue);
return h(
resolveComponent('a-select') as ComponentOptions,
{
key: oIndex,
...attrs,
...props,
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(
params,
props.mode === 'multiple'
? option.data && option.data.length > 0
: !XEUtils.eqNull(option.data),
option,
);
}),
},
{
default: () => {
return XEUtils.map(optionGroups, (group, gIndex) => {
return h(
resolveComponent('a-select-opt-group') as ComponentOptions,
{
key: gIndex,
},
{
label: () => {
return h('span', {}, group[groupLabel]);
},
default: () => renderOptions(group[groupOptions], optionProps),
},
);
});
},
},
);
})
: column.filters.map((option, oIndex) => {
const optionValue = option.data;
const props = getCellEditFilterProps(renderOpts, params, optionValue);
return h(
resolveComponent('a-select') as ComponentOptions,
{
key: oIndex,
...attrs,
...props,
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(
params,
props.mode === 'multiple'
? option.data && option.data.length > 0
: !XEUtils.eqNull(option.data),
option,
);
}),
},
{
default: () => renderOptions(options, optionProps),
},
);
}),
),
];
},
defaultFilterMethod(params) {
const { option, row, column } = params;
const { data } = option;
const { field, filterRender: renderOpts } = column;
const { props = {} } = renderOpts;
const cellValue = XEUtils.get(row, field);
if (props.mode === 'multiple') {
if (XEUtils.isArray(cellValue)) {
return XEUtils.includeArrays(cellValue, data);
}
return data.indexOf(cellValue) > -1;
}
/* eslint-disable eqeqeq */
return cellValue == data;
},
renderItemContent(renderOpts, params) {
const {
options = [],
optionGroups,
optionProps = {},
optionGroupProps = {},
} = renderOpts;
const { data, field } = params;
const { attrs } = renderOpts;
const itemValue = XEUtils.get(data, field);
const props = getItemProps(renderOpts, params, itemValue);
const ons = getItemOns(renderOpts, params);
if (optionGroups) {
const groupOptions = optionGroupProps.options || 'options';
const groupLabel = optionGroupProps.label || 'label';
return [
h(
resolveComponent('a-select') as ComponentOptions,
{
...attrs,
...props,
...ons,
},
{
default: () => {
return XEUtils.map(optionGroups, (group, gIndex) => {
return h(
resolveComponent('a-select-opt-group') as ComponentOptions,
{
key: gIndex,
},
{
label: () => {
return h('span', {}, group[groupLabel]);
},
default: () => renderOptions(group[groupOptions], optionProps),
},
);
});
},
},
),
];
}
return [
h(
resolveComponent('a-select') as ComponentOptions,
{
...attrs,
...props,
...ons,
},
{
default: () => renderOptions(options, optionProps),
},
),
];
},
exportMethod: createExportMethod(getSelectCellValue),
},
ACascader: {
renderEdit: createEditRender(),
renderCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getCascaderCellValue(renderOpts, params));
},
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getCascaderCellValue),
},
ADatePicker: {
renderEdit: createEditRender(),
renderCell: formatDatePicker('YYYY-MM-DD'),
renderItemContent: createFormItemRender(),
exportMethod: createDatePickerExportMethod('YYYY-MM-DD'),
},
AMonthPicker: {
renderEdit: createEditRender(),
renderCell: formatDatePicker('YYYY-MM'),
renderItemContent: createFormItemRender(),
exportMethod: createDatePickerExportMethod('YYYY-MM'),
},
ARangePicker: {
renderEdit: createEditRender(),
renderCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getRangePickerCellValue(renderOpts, params));
},
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getRangePickerCellValue),
},
AWeekPicker: {
renderEdit: createEditRender(),
renderCell: formatDatePicker('YYYY-WW周'),
renderItemContent: createFormItemRender(),
exportMethod: createDatePickerExportMethod('YYYY-WW周'),
},
ATimePicker: {
renderEdit: createEditRender(),
renderCell: formatDatePicker('HH:mm:ss'),
renderItemContent: createFormItemRender(),
exportMethod: createDatePickerExportMethod('HH:mm:ss'),
},
ATreeSelect: {
renderEdit: createEditRender(),
renderCell(renderOpts, params) {
return getCellLabelVNs(renderOpts, params, getTreeSelectCellValue(renderOpts, params));
},
renderItemContent: createFormItemRender(),
exportMethod: createExportMethod(getTreeSelectCellValue),
},
ARate: {
renderDefault: createEditRender(),
renderEdit: createEditRender(),
renderFilter: createFilterRender(),
defaultFilterMethod: defaultExactFilterMethod,
renderItemContent: createFormItemRender(),
},
ASwitch: {
autofocus: 'button.ant-switch',
renderDefault: createEditRender(),
renderEdit: createEditRender(),
renderFilter(renderOpts, params) {
const { column } = params;
const { name, attrs } = renderOpts;
return [
h(
'div',
{
class: 'vxe-table--filter-antd-wrapper',
},
column.filters.map((option, oIndex) => {
const optionValue = option.data;
return h(name as string, {
key: oIndex,
...attrs,
...getCellEditFilterProps(renderOpts, params, optionValue),
...getFilterOns(renderOpts, params, option, () => {
// 处理 change 事件相关逻辑
handleConfirmFilter(params, XEUtils.isBoolean(option.data), option);
}),
});
}),
),
];
},
defaultFilterMethod: defaultExactFilterMethod,
renderItemContent: createFormItemRender(),
},
ARadio: {
renderItemContent: createFormItemRadioAndCheckboxRender(),
},
ACheckbox: {
renderItemContent: createFormItemRadioAndCheckboxRender(),
},
AButton: {
renderEdit: defaultButtonEditRender,
renderDefault: defaultButtonEditRender,
renderItemContent: defaultButtonItemRender,
},
AButtons: {
renderEdit: defaultButtonsEditRender,
renderDefault: defaultButtonsEditRender,
renderItemContent: defaultButtonsItemRender,
},
});
vxetable.interceptor.add('event.clearFilter', handleClearEvent);
vxetable.interceptor.add('event.clearActived', handleClearEvent);
vxetable.interceptor.add('event.clearAreas', handleClearEvent);
},
};
if (typeof window !== 'undefined' && window.VXETable && window.VXETable.use) {
window.VXETable.use(VXETablePluginAntd);
}
export default VXETablePluginAntd;

99
src/components/SmartTable/src/renderer/VxeTableButtonRenderer.tsx

@ -0,0 +1,99 @@
import VXETable, { VxeGlobalRendererHandles } from 'vxe-table';
import { SmartTableButton } from '@/components/SmartTable';
import { omit } from 'lodash-es';
import { unref, h } from 'vue';
import { isString } from '@/utils/is';
import type { ButtonProps } from '@/components/Button';
import { hasPermission } from '@/utils/auth';
export type VxeTableRenderer =
| 'VxeTableToolButtonCustomRenderer'
| 'VxeTableToolButtonSlotRenderer'
| 'VxeTableToolAntRenderer'
| 'VxeTableToolComponentRenderer';
export const VxeTableToolButtonCustomRenderer: VxeTableRenderer =
'VxeTableToolButtonCustomRenderer';
export const VxeTableToolAntRenderer: VxeTableRenderer = 'VxeTableToolAntRenderer';
export const VxeTableToolButtonSlotRenderer: VxeTableRenderer = 'VxeTableToolButtonSlotRenderer';
export const VxeTableToolComponentRenderer: VxeTableRenderer = 'VxeTableToolComponentRenderer';
VXETable.renderer.add(VxeTableToolButtonCustomRenderer, {
renderToolbarButton(
_: VxeGlobalRendererHandles.RenderButtonOptions,
params: VxeGlobalRendererHandles.RenderButtonParams,
): VxeGlobalRendererHandles.RenderResult {
const button = params.button as SmartTableButton;
let buttonPros = {
...button,
...(unref(button.props) || {}),
};
buttonPros = omit(buttonPros, ['props', 'buttonRender']);
// 权限处理
const hasAuth = hasPermission(button?.auth);
if (!hasAuth) {
const auth = button.auth;
if (!isString(auth)) {
if (auth?.displayMode === 'hide') {
return '';
}
}
buttonPros.disabled = true;
}
if (button.customRender === 'element') {
return <el-button {...buttonPros}>{buttonPros.name}</el-button>;
} else {
return <a-button {...buttonPros}>{buttonPros.name}</a-button>;
}
},
});
VXETable.renderer.add(VxeTableToolAntRenderer, {
renderToolbarTool(
_: VxeGlobalRendererHandles.RenderToolOptions,
params: VxeGlobalRendererHandles.RenderToolParams,
): VxeGlobalRendererHandles.RenderResult {
const { tool, $grid } = params;
const props = unref((tool as any).props);
const handleClick = (event) => {
($grid as any)?.triggerToolbarTolEvent(tool, event);
};
return <vxe-button onClick={(event) => handleClick(event)} {...props} />;
// return (
// <a-tooltip title="显示搜索">
// <a-button {...props} onClick={(event) => handleClick(event)} />
// </a-tooltip>
// )
},
});
/**
* vxe-table 插槽渲染器
*/
VXETable.renderer.add(VxeTableToolButtonSlotRenderer, {
renderToolbarButton(
_: VxeGlobalRendererHandles.RenderButtonOptions,
params: VxeGlobalRendererHandles.RenderButtonParams,
): VxeGlobalRendererHandles.RenderResult {
const button = params.button as SmartTableButton & ButtonProps;
if (!button.slot || isString(button.slot)) {
return '';
}
return button.slot(button);
},
});
/**
* 自定义组件渲染
*/
VXETable.renderer.add(VxeTableToolComponentRenderer, {
renderToolbarTool(
{ props }: VxeGlobalRendererHandles.RenderToolOptions,
{ tool }: VxeGlobalRendererHandles.RenderToolParams<any>,
): VxeGlobalRendererHandles.RenderResult {
return h((tool as any).component, props);
},
});

39
src/components/SmartTable/src/style/SmartTableColumnConfig.less

@ -0,0 +1,39 @@
.smart-table-column-config-fixed-option {
.fixed-span {
padding: 5px;
cursor: pointer;
&:hover {
color: var(--vxe-primary-color);
}
}
.is--checked {
color: var(--vxe-primary-color);
}
}
.smart-table-column-config {
.vxe-pulldown--wrapper {
border: 1px solid var(--vxe-table-border-color);
border-radius: var(--vxe-border-radius);
box-shadow: 0 1px 6px rgb(0 0 0 / 20%);
}
.bottom-button {
height: 30px;
line-height: 30px;
.vxe-button {
width: 50%;
}
}
.smart-table-column-config--drag {
cursor: pointer;
}
.smart-table-column-config--drag-fixed {
cursor: no-drop;
}
}

53
src/components/SmartTable/src/types/SmartSearchFormType.ts

@ -0,0 +1,53 @@
import type { FormActionType, FormProps, FormSchema } from '@/components/Form';
export type SearchSymbol =
| '='
| 'like'
| '>'
| '>='
| '<>'
| '<'
| '<='
| 'in'
| 'notLike'
| 'likeLeft'
| 'likeRight'
| 'notIn'
| 'groupBy'
| 'between';
export type SmartSearchFormSchema = {
searchSymbol?: SearchSymbol;
customSymbol?: ({
schema,
value,
model,
}: {
schema: SmartSearchFormSchema;
value: any;
model: Recordable;
}) => Recordable;
} & FormSchema;
export interface SmartSearchFormProps extends FormProps {
schemas?: SmartSearchFormSchema[];
searchWithSymbol?: boolean;
defaultVisible?: boolean;
}
export interface SmartSearchFormParameter {
searchSymbolForm?: Recordable;
noSymbolForm?: Recordable;
searchForm?: Recordable;
searchWithSymbol?: boolean;
}
export interface SmartSearchFormActionType extends FormActionType {
getSearchFormParameter: () => SmartSearchFormParameter;
/**
* 设置搜索表单的显示状态
* @param visible
*/
setSearchFormVisible: (visible?: boolean) => void;
}

31
src/components/SmartTable/src/types/SmartTableActionType.ts

@ -0,0 +1,31 @@
import type { ButtonProps } from '@/components/Button';
import type { TooltipProps } from 'ant-design-vue';
import type { SmartAuth } from '#/utils';
export interface ActionItem extends ButtonProps {
onClick?: Fn;
label?: string;
danger?: boolean;
// color?: 'success' | 'error' | 'warning';
icon?: string;
popConfirm?: PopConfirm;
disabled?: boolean;
divider?: boolean;
// 权限编码控制是否显示
auth?: SmartAuth | string;
// 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean);
tooltip?: string | TooltipProps;
// 自定义类名
class?: string | Record<string, boolean> | any[];
}
export interface PopConfirm {
title: string;
okText?: string;
cancelText?: string;
confirm: Fn;
cancel?: Fn;
icon?: string;
placement?: string;
}

35
src/components/SmartTable/src/types/SmartTableAddEditType.ts

@ -0,0 +1,35 @@
import type { ModalProps } from '@/components/Modal';
import type { FormProps } from '@/components/Form';
/**
* 添加编辑表单配置
*/
export interface SmartTableAddEditConfig<T = any> {
/**
* modal配置
*/
modalConfig?: Partial<ModalProps>;
/**
* 表单配置项
*/
formConfig?: Partial<FormProps>;
// 编辑加载完数据执行,返回false或Promise<false>停止后续执行
afterLoadData?: (data: T) => Promise<boolean | undefined> | boolean | undefined;
// 保存之前对数据进行处理
beforeSave?: (data: T) => T | Promise<T>;
// 保存之后操作,默认reload
afterSave?: (saveResult?) => boolean | Promise<boolean> | void;
// 自定义弹窗事件
openModalHandler?: (row: any, formData?: Recordable) => void;
}
export interface SmartAddEditModalCallbackData<T = any> {
getFunction?: (T) => Promise<T>;
validateFunction?:
| ((data: T, customData: any) => Promise<boolean | undefined> | boolean | undefined)
| undefined;
isAdd: boolean;
selectData?: Recordable<T>;
formData?: Recordable;
afterSave?: () => void;
}

30
src/components/SmartTable/src/types/SmartTableAuthType.ts

@ -0,0 +1,30 @@
import { SmartAuth, SmartAuthDisplayMode } from '#/utils';
type SmartTableAuth = string | SmartAuth;
/**
* smart-table auth配置
*/
export interface SmartTableAuthConfig {
toolbar?: SmartTableAuthToolbarConfig;
/**
* 判断权限函数
* @param auth
*/
authHandler: (auth?: SmartAuth | string) => boolean;
/**
* 默认的显示模式
*/
displayMode?: SmartAuthDisplayMode;
}
export interface SmartTableAuthToolbarConfig {
query?: SmartTableAuth;
insert?: SmartTableAuth;
delete?: SmartTableAuth;
remove?: SmartTableAuth;
import?: SmartTableAuth;
export?: SmartTableAuth;
ModalAdd?: SmartTableAuth;
ModalEdit?: SmartTableAuth;
}

75
src/components/SmartTable/src/types/SmartTableButton.ts

@ -0,0 +1,75 @@
import type { ButtonProps } from '@/components/Button';
import type { Component, ComputedRef, Ref, VNode } from 'vue';
import type { SmartAuth } from '#/utils';
import type { VxeToolbarPropTypes } from 'vxe-table';
export type SmartTableButtonType = 'text' | 'submit' | 'reset' | 'button';
export type SmartTableButtonStatus = 'primary' | 'success' | 'info' | 'warning' | 'danger';
export type SmartTableButtonCode =
| 'reload'
| 'query'
| 'insert'
| 'insert_actived'
| 'mark_cancel'
| 'delete'
| 'remove'
| 'save'
| 'import'
| 'open_import'
| 'export'
| 'open_export'
| 'reset_custom'
| 'ModalAdd'
| 'ModalEdit'
| 'useYnTrue'
| 'useYnFalse';
export type SmartTableToolCode = 'show_search';
export type SmartTableButtonCustomRender = 'ant' | 'element';
export interface SmartTableButtonRender {
name?: string;
props?: any;
events?: any;
}
export interface SmartTableBasicButtonDropdowns {
name?: string | 'smart-auto';
// 按钮类型
type?: SmartTableButtonType;
// 按钮状态
status?: SmartTableButtonStatus;
code?: SmartTableButtonCode;
visible?: boolean;
disabled?: boolean | ComputedRef<boolean>;
icon?: string;
round?: boolean;
circle?: boolean;
auth?: string | SmartAuth;
}
export interface SmartTableButton extends SmartTableBasicButtonDropdowns {
placement?: string;
'destroy-on-close'?: boolean;
transfer?: boolean;
dropdowns?: SmartTableBasicButtonDropdowns[];
buttonRender?: SmartTableButtonRender;
props?: ButtonProps | Ref<ButtonProps> | ComputedRef<ButtonProps>;
// 是否是ant-design按钮,false:使用vxe-table原有的按钮,true使用VxeTableToolButtonRenderer进行渲染
customRender?: SmartTableButtonCustomRender;
// 点击事件是否触发加载状态
clickLoading?: boolean;
// 是否使用插槽
slot?: (button: SmartTableButton & ButtonProps) => VNode | string;
}
export interface SmartTableToolbarTool extends VxeToolbarPropTypes.ToolConfig {
code?: SmartTableToolCode | string;
props?: Recordable;
component?: Component;
}

83
src/components/SmartTable/src/types/SmartTableColumnType.ts

@ -0,0 +1,83 @@
import type { VxeTableDefines, VxeColumnSlotTypes, VxeColumnPropTypes } from 'vxe-table';
import { VxeTableConstructor, VxeTablePrivateMethods } from 'vxe-table/types/table';
export type SmartTableColumnComponent = 'switch' | 'tag' | 'button' | 'booleanTag' | 'useYnTag';
export type SmartEditRenderName =
| 'AInput'
| 'AAutocomplete'
| 'AInputNumber'
| 'ASelect'
| 'ACascader'
| 'ADatePicker'
| 'AMonthPicker'
| 'ARangePicker'
| 'AWeekPicker'
| 'ATimePicker'
| 'ATreeSelect'
| 'ASwitch'
| 'ARate'
| 'AButton'
| 'AButtons'
| 'input'
| 'textarea'
| 'select'
| '$input'
| '$select'
| '$switch';
// @ts-ignore
export interface SmartEditRender extends VxeColumnPropTypes.EditRender {
autofocus?: boolean | string;
name?: SmartEditRenderName;
// 是否阻止回车键事件冒泡,组织后回车不会调到下一行,默认true
stopEnterBubbling?: boolean;
// 是否必填
required?: boolean;
// 校验规则
rules?: VxeTableDefines.ValidatorRule[];
}
export type SmartColumnDynamicClass =
| string
| ((params: {
$table: VxeTableConstructor & VxeTablePrivateMethods;
row: any;
rowIndex: number;
$rowIndex: number;
_rowIndex: number;
column: VxeTableDefines.ColumnInfo;
columnIndex: number;
$columnIndex: number;
_columnIndex: number;
}) => void | null | string | { [key: string]: boolean });
export type SmartColumnDynamicStyle =
| any
| ((params: {
$table: VxeTableConstructor & VxeTablePrivateMethods;
row: any;
rowIndex: number;
$rowIndex: number;
_rowIndex: number;
column: VxeTableDefines.ColumnInfo;
columnIndex: number;
$columnIndex: number;
_columnIndex: number;
}) => void | null | string | { [key: string]: boolean });
export type SmartColumnAutoClass = 'Boolean';
// @ts-ignore
export interface SmartColumn extends VxeTableDefines.ColumnOptions {
flag?: 'INDEX' | 'DEFAULT' | 'CHECKBOX' | 'RADIO' | 'ACTION';
component?: SmartTableColumnComponent;
componentProps?: Recordable | ((params: VxeColumnSlotTypes.DefaultSlotParams) => Recordable);
editRender?: SmartEditRender;
// 动态CLASS
dynamicClass?: SmartColumnDynamicClass;
// 动态style
dynamicStyle?: SmartColumnDynamicStyle;
// 自动class
autoClass?: SmartColumnAutoClass;
}

11
src/components/SmartTable/src/types/SmartTableToolbarConfigType.ts

@ -0,0 +1,11 @@
import type { VxeButtonProps } from 'vxe-table';
/**
* toolbar column config
*/
export interface SmartTableToolbarColumnConfig {
trigger?: 'manual' | 'click' | 'hover';
buttonProps?: VxeButtonProps;
// 是否支持列排序
columnOrder?: boolean;
}

159
src/components/SmartTable/src/types/SmartTableType.ts

@ -0,0 +1,159 @@
import type { VxeTableDefines, VxeGridProps, VxeGridPropTypes, VxeGridInstance } from 'vxe-table';
import type { SmartSearchFormProps } from './SmartSearchFormType';
import type { SmartTableButton, SmartTableToolbarTool } from './SmartTableButton';
import type { SmartTableAddEditConfig } from './SmartTableAddEditType';
import type { NamePath } from 'ant-design-vue/es/form/interface';
import type { VxeTablePropTypes } from 'vxe-table/types/table';
import type { Options as SortableOptions } from 'sortablejs';
import type { SmartColumn } from './SmartTableColumnType';
import type { SmartTableAuthConfig } from './SmartTableAuthType';
import type { SmartTableToolbarColumnConfig } from '@/components/SmartTable';
/**
* 表格高度
*/
export type TableHeightType = 'auto' | number | string;
export interface FetchParams {
searchInfo?: Recordable;
page?: Partial<VxeGridPropTypes.ProxyAjaxQueryPageParams>;
sorts?: Partial<VxeGridPropTypes.ProxyAjaxQuerySortCheckedParams>[];
filters?: Partial<VxeTableDefines.FilterCheckedParams>[];
}
// @ts-ignore
export interface SmartTableToolbarConfig extends VxeGridPropTypes.ToolbarConfig {
buttons?: SmartTableButton[];
tools?: SmartTableToolbarTool[];
// 是否显示搜索
showSearch?: boolean | SmartTableToolbarTool;
column?: SmartTableToolbarColumnConfig | boolean;
}
export interface SmartTableAjaxQueryParams extends VxeGridPropTypes.ProxyAjaxQueryParams {
searchInfo?: Recordable;
searchFormSymbol?: Recordable;
searchForm?: Recordable;
ajaxParameter?: Recordable;
}
export interface SmartTableProxyConfig<T = any> extends VxeGridPropTypes.ProxyConfig {
ajax?: {
query?(params: SmartTableAjaxQueryParams, ...args: any[]): Promise<any>;
queryAll?(params: VxeGridPropTypes.ProxyAjaxQueryAllParams, ...args: any[]): Promise<any>;
delete?(params: VxeGridPropTypes.ProxyAjaxDeleteParams, ...args: any[]): Promise<any>;
save?(params: VxeGridPropTypes.ProxyAjaxSaveParams, ...args: any[]): Promise<any>;
getById?(params: T): Promise<T>;
useYn?(rows: T[], useYn: boolean): Promise<any>;
};
// 删除回调
afterDelete?: (result?: any) => void;
afterLoad?: (result: any) => any;
afterUserYn?: (result: any) => void;
}
/**
* 行拖拽配置
*/
export interface SmartTableRowDragConfig extends SortableOptions {
icon?: string;
// 是否自定义
custom?: boolean;
}
export interface SmartTableRowConfig extends VxeTablePropTypes.RowConfig {
dragConfig?: boolean | SmartTableRowDragConfig;
}
/**
* 行触发模式,单选/多选
*/
export type RowTriggerMode = 'single' | 'multiple';
/**
* 表格 checkbox配置
*/
export interface SmartCheckboxConfig extends VxeTablePropTypes.CheckboxConfig {
// 是否支持行触发选中
rowTrigger?: RowTriggerMode;
// 是否支持ctrl选中,rowTrigger必须为single
rowCtrl?: boolean;
// 是否支持shift多选,rowTrigger必须为single
rowShift?: boolean;
}
// @ts-ignore
export interface SmartTableProps<T = any> extends VxeGridProps<T> {
columns?: SmartColumn[];
// 默认的排序参数
// 请求接口配置
height?: TableHeightType;
// 表格加载状态
loading?: boolean;
// checkbox配置
checkboxConfig?: SmartCheckboxConfig;
// 搜索表单配置
searchFormConfig?: Partial<SmartSearchFormProps>;
// 是否使用搜索表单
useSearchForm?: boolean;
// 搜索是否带有符号
toolbarConfig?: SmartTableToolbarConfig;
// 分页配置
pagerConfig?: boolean | VxeGridPropTypes.PagerConfig;
proxyConfig?: SmartTableProxyConfig;
// 添加修改配置
addEditConfig?: SmartTableAddEditConfig;
rowConfig?: SmartTableRowConfig;
authConfig?: SmartTableAuthConfig;
}
/**
* 表格操作函数
*/
export interface TableActionType {
reload: (opt?: FetchParams) => Promise<void>;
query: (opt?: FetchParams) => Promise<void>;
/**
* 设置props
* @param props
*/
setProps: (props: Partial<SmartTableProps>) => void;
// 设置加载状态
setLoading: (loading: boolean) => void;
setPagination: (info: Partial<VxeGridPropTypes.PagerConfig>) => void;
getPagination: () => VxeGridPropTypes.PagerConfig | boolean | undefined;
commitVxeProxy: (code: string, ...args) => void;
setShowPagination: (show: boolean) => Promise<void>;
getShowPagination: () => boolean;
deleteByCheckbox: () => void;
getCheckboxRecords: (isFull: boolean) => Array<any>;
getRadioRecord: (isFull: boolean) => any;
// openAddEditModal: <T = any>(props?: boolean, data?: T, openOnSet?: boolean) => void
showAddModal: (formData?: Recordable, selectData?: Recordable) => void;
editByCheckbox: () => Promise<boolean> | boolean;
editByRowModal: <T = any>(data: T, formData?: Recordable) => Promise<boolean> | boolean;
getAddEditFieldsValue: () => Recordable;
resetAddEditFields: () => Promise<void>;
setAddEditFieldsValue: (data: any) => Promise<void>;
deleteByRow: (rows: any | any[]) => Promise<any>;
/**
* 用于 type=radio,设置某一行为选中状态
* @param row 指定行
*/
setRadioRow(row: any): Promise<any>;
/**
* 用于 type=checkbox,设置行为选中状态,第二个参数为选中与否
* @param rows 指定行
* @param checked 是否选中
*/
setCheckboxRow(rows: any | any[], checked: boolean): Promise<any>;
validateAddEditFields: (nameList?: NamePath[]) => Promise<any>;
validateAddEdit: (nameList?: NamePath[]) => Promise<any>;
getTableInstance: () => VxeGridInstance;
getData: (rowIndex?: number) => any[];
useYnByCheckbox: (useYn: boolean) => Promise<boolean | undefined>;
useYnByRow: (row: any | any[], useYn: boolean) => Promise<boolean | undefined>;
}

21
src/components/SmartTable/src/utils.ts

@ -0,0 +1,21 @@
import type { VxeTablePropTypes } from 'vxe-table';
const FormSizeMap: Record<string, 'default' | 'small' | 'large'> = {
midum: 'default',
small: 'small',
mini: 'small',
tiny: 'small',
};
/**
* 获取form尺寸
* @param size
*/
export const getFormSize = (
size: VxeTablePropTypes.Size | undefined,
): 'default' | 'small' | 'large' | undefined => {
if (size) {
return FormSizeMap[size];
}
return undefined;
};

21
src/components/registerGlobComp.ts

@ -1,8 +1,25 @@
import type { App } from 'vue';
import { Button } from './Button';
import { Input, Layout } from 'ant-design-vue';
import { Input, Layout, Radio, Tag } from 'ant-design-vue';
import VXETable from 'vxe-table';
import { i18n } from '@/locales/setupI18n';
export function registerGlobComp(app: App) {
app.use(Input).use(Button).use(Layout).use(VXETable);
VXETable.setup({
// @ts-ignore
i18n: (key, args) => {
// @ts-ignore
return i18n.global.t(key, args);
},
translate(key: string, args?: any): string {
if (key.startsWith('{') && key.endsWith('}')) {
const i18nKey = key.replace('{', '').replace('}', '');
// @ts-ignore
return i18n.global.t(i18nKey, args);
}
return key;
},
});
app.use(Input).use(Button).use(Layout).use(Radio).use(Tag).use(VXETable);
}

20
src/design/public.less

@ -53,3 +53,23 @@
background-color: @primary-color;
}
}
.full-height {
height: 100%;
}
.form-divider {
.ant-divider-inner-text {
font-weight: 600;
}
}
.smart-table-search-form {
&.ant-form {
padding: 12px 10px 6px;
}
}
.page-container {
padding: 5px;
}

31
src/hooks/setting/UseSizeSetting.ts

@ -0,0 +1,31 @@
import { useAppStore } from '@/store/modules/app';
import { computed } from 'vue';
const tableButtonSizeMap: { [index: string]: string } = {
medium: 'middle',
small: 'small',
mini: 'small',
};
export const useSizeSetting = () => {
const appStore = useAppStore();
const getButtonSize = computed(() => appStore.getSizeSetting.button);
const getTableSize = computed(() => appStore.getSizeSetting.table);
const getFormSize = computed(() => appStore.getSizeSetting.form);
const tableButtonSizeConfig = computed(() => {
// @ts-ignore
return tableButtonSizeMap[getTableSize.value] || 'small';
});
return {
getButtonSize,
getTableSize,
getFormSize,
tableSizeConfig: getTableSize,
buttonSizeConfig: getButtonSize,
formSizeConfig: getFormSize,
tableButtonSizeConfig,
};
};

24
src/hooks/web/useMessage.tsx

@ -109,5 +109,29 @@ export function useMessage() {
createErrorModal,
createInfoModal,
createWarningModal,
successMessage,
warnMessage,
};
}
export interface MessageOptions {
message: string;
}
/**
* 成功
* @param options
*/
const successMessage = (options: MessageOptions) => {
if (isString(options)) {
return Message.warning(options);
}
return Message.warning(options.message);
};
const warnMessage = (options: MessageOptions | string) => {
if (isString(options)) {
return Message.warning(options);
}
return Message.warning(options.message);
};

2
src/locales/lang/en.ts

@ -1,6 +1,7 @@
import { generateModuleMessage, genMessage } from '../helper';
import antdLocale from 'ant-design-vue/es/locale/en_US';
import { deepMerge } from '@/utils';
import vxeEnUS from 'vxe-table/lib/locale/lang/en-US';
const modules = import.meta.glob('./en/**/*.{json,ts,js}', { eager: true });
const modulesLocales = import.meta.glob('../../modules/**/lang/en_US.ts', { eager: true });
@ -11,6 +12,7 @@ export default {
generateModuleMessage(modulesLocales as Recordable<Recordable>),
),
antdLocale,
...vxeEnUS,
},
dateLocale: null,
dateLocaleName: 'en',

2
src/locales/lang/zh_CN.ts

@ -1,6 +1,7 @@
import { generateModuleMessage, genMessage } from '../helper';
import antdLocale from 'ant-design-vue/es/locale/zh_CN';
import { deepMerge } from '@/utils';
import vxeZhCN from 'vxe-table/lib/locale/lang/zh-CN';
const modules = import.meta.glob('./zh-CN/**/*.{json,ts,js}', { eager: true });
const modulesLocales = import.meta.glob('../../modules/**/lang/zh_CN.ts', { eager: true });
@ -17,5 +18,6 @@ export default {
genMessage(modules as Recordable<Recordable>, 'zh-CN').antdLocale.DatePicker,
),
},
...vxeZhCN,
},
};

106
src/modules/app/i18n/lang/en_US.ts

@ -0,0 +1,106 @@
export default {
common: {
button: {
add: 'Add',
edit: 'Edit',
delete: 'Delete',
search: 'Search',
reset: 'Reset',
confirm: 'Confirm',
use: 'Use',
noUse: 'No use',
save: 'Save',
look: 'Look',
download: 'Download',
cancel: 'Cancel',
submit: 'Submit',
reload: 'Reload',
choose: 'Choose',
publish: 'Publish',
revoke: 'Revoke',
},
title: {
details: 'Details',
close: 'Close',
add: 'Add',
edit: 'Edit',
useYn: 'Use Yn',
},
form: {
use: 'Use',
noUse: 'No use',
delete: 'Y',
noDelete: 'N',
yes: 'Y',
no: 'N',
},
formValidate: {
seq: 'Please enter serial number',
remark: 'Please enter remark',
enter: 'Please enter message',
},
table: {
createTime: 'Creation time',
createUser: 'Creation user',
updateTime: 'Update time',
updateUser: 'Update user',
useYn: 'Enabled',
deleteYn: 'Delete',
operation: 'Operation',
remark: 'Remark',
seq: 'Seq',
},
notice: {
confirm: 'Confirm',
confirmSave: 'Are you sure you want to save?',
deleteConfirm: 'Are you sure you want to delete?',
deleteChoose: 'Please select the data to delete',
select: 'Please select',
choseOne: 'Please select a row of data',
useYnTrueConfirm: 'Are you sure you want to perform the enable operation?',
useYnFalseConfirm: 'Are you sure you want to perform the deactivation operation?',
},
message: {
saveSuccess: 'Saved successfully',
editSuccess: 'Edit success',
deleteSuccess: 'Delete successfully',
deleteFail: 'Deletion failed',
submitSuccess: 'Submit successfully',
use: 'enable',
noUse: 'deactivate',
OperationSucceeded: 'Operation succeeded',
noPermission: 'No permission',
captchaValidateFail: 'Captcha validate fail',
},
},
app: {
multiTab: {
dropdownMenu: {
location: 'Locate the current tab',
closeAll: 'Close all tabs',
closeOther: 'Close other tabs',
refreshCurrent: 'Refresh current',
},
closeOperation: 'Action',
},
title: {
'404': 'The page you visited does not exist',
'403': 'You do not have permission to access this page',
backHome: 'Back Home',
},
common: {
notice: {
logout: 'Are you sure you want to logout?',
},
},
message: {
changeTheme: 'Switching topics',
},
changePassword: {
oldPassword: 'Old password',
newPassword: 'New password',
newPasswordConfirm: 'Confirm',
successMessage: 'Password modified successfully, please login again!',
},
},
};

106
src/modules/app/i18n/lang/zh_CN.ts

@ -0,0 +1,106 @@
export default {
common: {
button: {
add: '添加',
edit: '修改',
delete: '删除',
search: '搜索',
reset: '重置',
confirm: '确认',
use: '启用',
noUse: '停用',
save: '保存',
look: '查看',
download: '下载',
cancel: '取消',
submit: '提交',
reload: '重新加载',
choose: '选择',
publish: '发布',
revoke: '撤销',
},
title: {
details: '详情',
close: '关闭',
add: '添加',
edit: '修改',
useYn: '启用状态',
},
form: {
use: '启用',
noUse: '停用',
delete: 'Y',
noDelete: 'N',
yes: '是',
no: '否',
},
formValidate: {
seq: '请输入序号',
remark: '请输入备注',
enter: '请输入内容',
},
table: {
createTime: '创建时间',
createUser: '创建人',
updateTime: '更新时间',
updateUser: '更新人',
useYn: '启用状态',
deleteYn: '是否删除',
operation: '操作',
remark: '备注',
seq: '序号',
},
notice: {
confirm: '确认',
saveConfirm: '确定要保存吗?',
deleteConfirm: '确定要删除吗?',
deleteChoose: '请选择要删除的数据',
select: '请选择',
choseOne: '请选择一行数据',
useYnTrueConfirm: '确认要执行启用操作吗?',
useYnFalseConfirm: '确认要执行停用操作吗?',
},
message: {
saveSuccess: '保存成功',
editSuccess: '修改成功',
deleteSuccess: '删除成功',
deleteFail: '删除失败',
submitSuccess: '提交成功',
use: '启用',
noUse: '停用',
OperationSucceeded: '操作成功',
noPermission: '无权限',
captchaValidateFail: '验证码验证失败',
},
},
app: {
multiTab: {
dropdownMenu: {
location: '定位当前选项卡',
closeAll: '关闭全部选项卡',
closeOther: '关闭其他选项卡',
refreshCurrent: '刷新页面',
},
closeOperation: '关闭操作',
},
title: {
'404': '您访问的页面不存在',
'403': '您无权限访问此页面',
backHome: '返回主页',
},
common: {
notice: {
logout: '确定要登出吗?',
},
},
message: {
changeTheme: '正在切换主题',
},
changePassword: {
oldPassword: '原密码',
newPassword: '新密码',
newPasswordConfirm: '确认新密码',
successMessage: '密码修改成功,请重新登录!',
},
},
};

57
src/modules/system/constants/SystemConstants.ts

@ -0,0 +1,57 @@
/**
* 用户的权限信息
*/
export const SystemPermissions = {
user: {
add: 'sys:user:save',
delete: 'sys:user:delete',
update: 'sys:user:update',
setRole: 'sys:user:setRole',
useYn: 'sys:user:setUseYn',
createAccount: 'sys:account:add',
unlockUserAccount: 'sys:user:unlockUserAccount',
unlockPassword: 'sys:user:resetPassword',
},
role: {
add: 'sys:role:save',
delete: 'sys:role:delete',
update: 'sys:role:update',
setRoleUser: 'sys:role:setRoleUser',
setFunction: 'sys:role:setFunction',
},
userGroup: {
add: 'sys:userGroup:save',
delete: 'sys:userGroup:delete',
update: 'sys:userGroup:update',
setUser: 'sys:userGroup:setUser',
},
function: {
add: 'sys:function:save',
delete: 'sys:function:delete',
update: 'sys:function:update',
},
i18n: {
add: 'sys:i18n:save',
delete: 'sys:i18n:delete',
update: 'sys:i18n:update',
reload: 'sys:i18n:reload',
},
auth: {
offline: 'sys:auth.offline',
},
};
/**
* 系统用户标识
*/
export const SYS_USER_TYPE = 'SYSTEM_USER';
/**
* 数据权限
*/
export const DATA_SCOPE = [
{ key: 'DATA_ALL', value: 'system.views.user.title.dataAll' },
{ key: 'DATA_DEPT', value: 'system.views.user.title.dataDept' },
{ key: 'DATA_DEPT_AND_CHILD', value: 'system.views.user.title.dataDeptAll' },
{ key: 'DATA_PERSONAL', value: 'system.views.user.title.dataPersonal' },
];

81
src/modules/system/views/function/FunctionListView.api.ts

@ -0,0 +1,81 @@
import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
import TreeUtils from '@/utils/TreeUtils';
import { deepMerge } from '@/utils';
enum Api {
list = 'sys/function/list',
getById = 'sys/function/getById',
save = 'sys/function/saveUpdate',
delete = 'sys/function/batchDeleteById',
}
export const listTree = async (params) => {
const parameter = {
sortName: 'seq',
...deepMerge(params, {
parameter: {
QUERY_CREATE_UPDATE_USER: true,
},
}),
};
const result = await defHttp.post({
service: ApiServiceEnum.SMART_SYSTEM,
url: Api.list,
data: parameter,
});
return (
TreeUtils.convertList2Tree(
result,
(item) => item.functionId,
(item) => item.parentId,
0,
) || []
);
};
export const listApi = (params) => {
const parameter = {
sortName: 'seq',
...params,
};
return defHttp.post({
service: ApiServiceEnum.SMART_SYSTEM,
url: Api.list,
data: parameter,
});
};
export const getByIdApi = async (data) => {
const {
function: functionData,
createUser,
parent,
updateUser,
} = await defHttp.post({
service: ApiServiceEnum.SMART_SYSTEM,
url: Api.getById,
data: data.functionId,
});
return {
...functionData,
createUser: createUser && createUser.fullName,
updateUser: updateUser && updateUser.fullName,
parentName: (parent && parent.functionName) || '根目录',
};
};
export const saveApi = ({ body: { insertRecords, updateRecords } }) => {
return defHttp.post({
service: ApiServiceEnum.SMART_SYSTEM,
url: Api.save,
data: [...insertRecords, ...updateRecords][0],
});
};
export const deleteApi = ({ body: { removeRecords } }) => {
return defHttp.post({
service: ApiServiceEnum.SMART_SYSTEM,
url: Api.delete,
data: removeRecords.map((item) => item.functionId),
});
};

303
src/modules/system/views/function/FunctionListView.config.ts

@ -0,0 +1,303 @@
import type { SmartColumn, SmartSearchFormSchema } from '@/components/SmartTable';
import type { FormSchema } from '@/components/Form';
export const tableColumns: SmartColumn[] = [
{
type: 'checkbox',
width: 60,
align: 'center',
fixed: 'left',
},
{
title: '{system.views.function.table.functionName}',
field: 'functionName',
width: 220,
fixed: 'left',
treeNode: true,
},
{
title: '{system.views.function.table.functionType}',
field: 'functionType',
width: 110,
align: 'center',
headerAlign: 'left',
slots: {
default: 'table-functionType',
},
},
{
title: '{system.views.function.table.icon}',
field: 'icon',
width: 80,
align: 'center',
headerAlign: 'left',
slots: {
default: 'table-icon',
},
},
{
title: 'URL',
field: 'url',
minWidth: 200,
},
{
title: '{system.views.function.table.permission}',
field: 'permission',
width: 160,
},
{
title: '{system.views.function.table.httpMethod}',
field: 'httpMethod',
width: 120,
},
{
title: '{common.table.seq}',
field: 'seq',
width: 100,
sortable: true,
},
{
title: '{common.table.createTime}',
field: 'createTime',
width: 165,
sortable: true,
},
{
title: '{common.table.createUser}',
field: 'createUserId',
width: 120,
formatter: ({ row }: any) => {
if (row.createUser) {
return row.createUser.fullName;
}
return '';
},
},
{
title: '{common.table.updateTime}',
field: 'updateTime',
width: 165,
sortable: true,
},
{
title: '{common.table.updateUser}',
field: 'updateUserId',
width: 120,
formatter: ({ row }: any) => {
if (row.updateUser) {
return row.updateUser.fullName;
}
return '';
},
},
{
title: '{common.table.operation}',
field: 'operation',
width: 200,
fixed: 'right',
slots: {
default: 'table-operation',
},
},
];
export const getAddEditForm = (t: Function): FormSchema[] => {
return [
{
field: 'functionId',
label: '',
component: 'Input',
show: false,
},
{
field: 'parentName',
label: '上级',
component: 'Input',
componentProps: {
disabled: true,
},
},
{
field: 'parentId',
label: '',
component: 'Input',
show: false,
},
{
field: 'functionName',
label: t('system.views.function.table.functionName'),
component: 'Input',
required: true,
},
{
field: 'functionType',
label: t('system.views.function.table.functionType'),
slot: 'addEditForm-functionType',
required: true,
},
{
field: 'i18nCode',
label: t('system.views.function.table.i18nCode'),
component: 'Input',
},
{
field: 'icon',
label: t('system.views.function.table.icon'),
component: 'IconPicker',
},
{
field: 'seq',
required: true,
label: t('common.table.seq'),
component: 'InputNumber',
defaultValue: 1,
},
{
field: 'componentName',
label: t('system.views.function.table.componentName'),
component: 'Input',
dynamicRules: ({ model }) => {
return [
{
required: model.functionType === 'MENU',
trigger: 'blur',
message: t('system.views.function.validate.componentName'),
},
];
},
show: ({ model }) => {
return model.functionType !== 'FUNCTION';
},
},
{
field: 'component',
label: t('system.views.function.table.component'),
component: 'Input',
dynamicRules: ({ model }) => {
return [
{
required: model.functionType === 'MENU',
trigger: 'blur',
message: t('system.views.function.validate.component'),
},
];
},
show: ({ model }) => {
return model.functionType !== 'FUNCTION';
},
},
{
field: 'url',
label: 'URL',
component: 'Input',
dynamicRules: ({ model }) => {
return [
{
required: model.functionType === 'MENU',
trigger: 'blur',
message: t('system.views.function.validate.url'),
},
];
},
},
{
field: 'redirect',
label: 'Redirect',
component: 'Input',
show: ({ model }) => {
return model.functionType !== 'FUNCTION';
},
},
{
field: 'httpMethod',
label: t('system.views.function.table.httpMethod'),
component: 'Select',
componentProps: {
options: ['GET', 'POST', 'PUT', 'DELETE'].map((item) => ({ label: item, value: item })),
},
show: ({ model }) => {
return model.functionType === 'FUNCTION';
},
},
{
field: 'permission',
label: t('system.views.function.table.permission'),
component: 'Input',
dynamicRules: ({ model }) => {
return [
{
required: model.functionType === 'FUNCTION',
message: t('system.views.function.validate.permission'),
trigger: 'blur',
},
];
},
show: ({ model }) => {
return model.functionType === 'FUNCTION';
},
},
{
field: 'isMenu',
label: t('system.views.function.table.menuIs'),
component: 'Switch',
defaultValue: true,
show: ({ model }) => {
return model.functionType !== 'FUNCTION';
},
},
{
field: 'internalOrExternal',
label: t('system.views.function.table.internalOrExternal'),
component: 'Switch',
defaultValue: false,
show: ({ model }) => {
return model.functionType === 'MENU';
},
},
{
field: 'dataRule',
label: t('system.views.function.table.dataRule'),
component: 'Switch',
defaultValue: false,
show: ({ model }) => {
return model.functionType === 'FUNCTION';
},
},
{
field: 'cached',
label: t('system.views.function.title.cached'),
component: 'Switch',
defaultValue: true,
show: ({ model }) => {
return model.functionType === 'MENU';
},
},
];
};
export const getSearchSchemas = (t: Function): SmartSearchFormSchema[] => {
return [
{
label: t('common.title.useYn'),
field: 'useYn',
component: 'Select',
defaultValue: 1,
searchSymbol: '=',
componentProps: {
style: {
width: '100px',
},
options: [
{
label: t('common.form.use'),
value: 1,
},
{
label: t('common.form.noUse'),
value: 0,
},
],
},
},
];
};

284
src/modules/system/views/function/FunctionListView.vue

@ -0,0 +1,284 @@
<template>
<div class="full-height page-container">
<SmartTable @register="registerTable" :size="getTableSize">
<template #table-icon="{ row }">
<Icon v-if="row.icon" :icon="getIcon(row.icon)" />
<div v-else></div>
</template>
<template #table-functionType="{ row }">
<a-tag :color="getTagData(row.functionType).color">
{{ getTagData(row.functionType).text }}
</a-tag>
</template>
<template #table-operation="{ row }">
<SmartVxeTableAction :actions="getTableActions(row)" />
</template>
<template #addEditForm-functionType="{ model }">
<a-radio-group v-model:value="model.functionType">
<a-radio
v-for="(type, key) in functionTypes"
:key="key"
:disabled="type.disabled"
:value="type.value"
>
{{ type.label }}
</a-radio>
</a-radio-group>
</template>
</SmartTable>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, unref } from 'vue';
import {
ActionItem,
SmartTable,
useSmartTable,
SmartVxeTableAction,
} from '@/components/SmartTable';
import { Icon } from '@/components/Icon';
import { tableColumns, getAddEditForm, getSearchSchemas } from './FunctionListView.config';
import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
import { listApi, deleteApi, getByIdApi, saveApi } from './FunctionListView.api';
import { useI18n } from '@/hooks/web/useI18n';
import StringUtils from '@/utils/StringUtils';
import { SystemPermissions } from '@/modules/system/constants/SystemConstants';
import { hasPermission } from '@/utils/auth';
const permissions = SystemPermissions.function;
const { t } = useI18n();
const { getTableSize } = useSizeSetting();
const currentRowRef = ref<any>({});
const getTableActions = (row: Recordable): ActionItem[] => {
return [
{
label: t('common.button.add'),
icon: 'ant-design:plus-outlined',
disabled: row.functionType === 'FUNCTION' || !hasPermission(permissions.add),
onClick: () => {
currentRowRef.value = row;
const data: Recordable = {
parentId: row.functionId,
parentName: row.functionName,
};
const functionType = row.functionType;
switch (functionType) {
case 'CATALOG': {
setTypeDisabled(['function']);
break;
}
case 'MENU': {
setTypeDisabled(['catalogue', 'menu']);
data.functionType = 'FUNCTION';
break;
}
case 'FUNCTION': {
setTypeDisabled(['catalogue', 'menu', 'function']);
break;
}
}
showAddModal(data, row);
},
},
{
label: t('common.button.edit'),
icon: 'ant-design:edit-outlined',
color: 'warning',
auth: permissions.update,
onClick: () => {
currentRowRef.value = row;
setTypeDisabled(['catalogue', 'menu', 'function']);
editByRowModal(row);
},
},
{
label: t('common.button.delete'),
icon: 'ant-design:delete-outlined',
danger: true,
auth: permissions.delete,
onClick: () => {
currentRowRef.value = row;
deleteByRow(row);
},
},
];
};
const [
registerTable,
{ showAddModal, editByRowModal, getSearchForm, getTableInstance, deleteByRow },
] = useSmartTable({
id: 'FunctionListView',
columns: tableColumns,
resizableConfig: {},
border: true,
align: 'left',
height: 'auto',
useSearchForm: true,
searchFormConfig: {
compact: true,
layout: 'inline',
colon: true,
schemas: getSearchSchemas(t),
searchWithSymbol: true,
actionColOptions: {
span: undefined,
},
},
columnConfig: {
resizable: true,
isHover: true,
},
authConfig: {
authHandler: hasPermission,
toolbar: {
ModalAdd: permissions.add,
},
},
treeConfig: {
lazy: true,
loadMethod: ({ row }) => {
const { searchSymbolForm } = getSearchForm().getSearchFormParameter();
const parameter = {
parameter: {
...searchSymbolForm,
'parentId@=': row.functionId,
},
};
return listApi(parameter);
},
},
rowConfig: {
keyField: 'functionId',
},
sortConfig: {
remote: true,
defaultSort: {
field: 'seq',
order: 'asc',
},
},
addEditConfig: {
modalConfig: {
width: 860,
},
formConfig: {
colon: true,
baseColProps: {
span: 12,
},
labelCol: { span: 7 },
wrapperCol: { span: 16 },
schemas: getAddEditForm(t),
},
afterSave: () => {
// 保存完成之后重新加载节点
return getTableInstance().reloadTreeExpand(unref(currentRowRef));
},
},
proxyConfig: {
ajax: {
query: ({ ajaxParameter }) => {
const parameter = {
...ajaxParameter,
parameter: {
...ajaxParameter?.parameter,
'parentId@=': 0,
},
};
return listApi(parameter);
},
delete: (params) => deleteApi(params),
save: saveApi,
getById: getByIdApi,
},
},
toolbarConfig: {
refresh: true,
buttons: [
{
code: 'ModalAdd',
props: {
onClick: () => {
setTypeDisabled(['function']);
showAddModal({
parentId: '0',
parentName: '根目录',
});
},
},
},
],
},
});
const getTagData = (functionType: string) => {
switch (functionType) {
case 'CATALOG': {
return {
color: '#f50',
text: t('system.views.function.common.catalogue'),
};
}
case 'MENU': {
return {
color: '#2db7f5',
text: t('system.views.function.common.menu'),
};
}
case 'FUNCTION': {
return {
color: '#108ee9',
text: t('system.views.function.common.function'),
};
}
default: {
return {
text: '',
};
}
}
};
const getIcon = (compatibleIcon: string) => {
if (compatibleIcon && compatibleIcon.indexOf(':') === -1) {
compatibleIcon = StringUtils.humpToLine(compatibleIcon);
compatibleIcon = 'ant-design:' + compatibleIcon;
}
return compatibleIcon;
};
const defaultFunctionTypes = (t: Function) => {
return {
catalogue: {
value: 'CATALOG',
label: t('system.views.function.common.catalogue'),
disabled: false,
},
menu: {
value: 'MENU',
label: t('system.views.function.common.menu'),
disabled: false,
},
function: {
value: 'FUNCTION',
label: t('system.views.function.common.function'),
disabled: false,
},
};
};
/**
* 设置不可用类型
* @param keys
*/
const setTypeDisabled = (keys: Array<string>) => {
Object.keys(functionTypes).forEach((key) => {
functionTypes[key].disabled = keys.includes(key);
});
};
const functionTypes: any = reactive(defaultFunctionTypes(t));
</script>
<style scoped></style>

75
src/modules/system/views/function/lang/en_US.ts

@ -0,0 +1,75 @@
export default {
trans: true,
key: 'system.views.function',
data: {
common: {
catalogue: 'Catalogue',
menu: 'Menu',
function: 'Function',
rootCatalogue: 'Root catalogue',
},
table: {
functionName: 'Function name',
functionType: 'Function type',
i18nCode: 'I18n code',
icon: 'Icon',
permission: 'Permission',
httpMethod: 'Http method',
menuIs: 'Is menu',
internalOrExternal: 'Open externally',
dataRule: 'Data permissions',
component: 'Component path',
componentName: 'Component name',
},
validate: {
httpMethod: 'Please select the request method',
functionName: 'Please enter the function name',
functionType: 'Please enter the function type',
i18nCode: 'Please enter the i18n code',
url: 'Please enter the url',
permission: 'Please enter the permission',
icon: 'Please enter the icon',
component: 'Please enter component path',
componentName: 'Please enter component name',
redirect: 'Please enter redirect',
},
},
};
/**
* Function I18N
*/
// export default {
// system: {
// views: {
// function: {
// common: {
// catalogue: 'Catalogue',
// menu: 'Menu',
// function: 'Function',
// rootCatalogue: 'Root catalogue'
// },
// table: {
// functionName: 'Function name',
// functionType: 'Function type',
// i18nCode: 'I18n code',
// icon: 'Icon',
// permission: 'Permission',
// httpMethod: 'Http method',
// menuIs: 'Is menu',
// internalOrExternal: 'Open externally',
// dataRule: 'Data permissions'
// },
// validate: {
// httpMethod: 'Please select the request method',
// functionName: 'Please enter the function name',
// functionType: 'Please enter the function type',
// i18nCode: 'Please enter the i18n code',
// url: 'Please enter the url',
// permission: 'Please enter the permission',
// icon: 'Please enter the icon'
// }
// }
// }
// }
// }

45
src/modules/system/views/function/lang/zh_CN.ts

@ -0,0 +1,45 @@
/**
* Function I18N
*/
export default {
system: {
views: {
function: {
common: {
catalogue: '目录',
menu: '菜单',
function: '功能',
rootCatalogue: '跟目录',
},
table: {
functionName: '功能名称',
functionType: '类型',
i18nCode: '国际化编码',
icon: '图标',
permission: '权限标识',
httpMethod: '请求方式',
menuIs: '是否菜单',
internalOrExternal: '是否外部打开',
dataRule: '数据权限',
component: '组件路径',
componentName: '组件名称',
},
validate: {
httpMethod: '请选择请求方式',
functionName: '请输入功能名称',
functionType: '请选择功能类型',
i18nCode: '请输入国际化编码',
url: '请输入URL',
permission: '请输入权限标识',
icon: '请输入图标',
component: '请输入组件路径',
componentName: '请输入组件名称',
redirect: '请输入redirect',
},
title: {
cached: '缓存路由',
},
},
},
},
};

2
src/router/guard/permissionGuard.ts

@ -36,7 +36,7 @@ export function createPermissionGuard(router: Router) {
if (to.path === LOGIN_PATH && token) {
const isSessionTimeout = userStore.getSessionTimeout;
try {
await userStore.afterLoginAction();
// await userStore.afterLoginAction();
if (!isSessionTimeout) {
next((to.query?.redirect as string) || '/');
return;

21
src/router/helper/routeHelper.ts

@ -18,7 +18,7 @@ let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
// Dynamic introduction
function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}');
dynamicViewsModules = dynamicViewsModules || getComponents();
if (!routes) return;
routes.forEach((item) => {
if (!item.component && item.meta?.frameSrc) {
@ -46,6 +46,10 @@ function dynamicImport(
) {
const keys = Object.keys(dynamicViewsModules);
const matchKeys = keys.filter((key) => {
if (component.startsWith('@')) {
const keyPath = '@' + key.replace('../..', '');
return keyPath === component;
}
const k = key.replace('../../views', '');
const startFlag = component.startsWith('/');
const endFlag = component.endsWith('.vue') || component.endsWith('.tsx');
@ -181,3 +185,18 @@ function isMultipleRoute(routeModule: AppRouteModule) {
}
return flag;
}
const getComponents = (): Record<string, () => Promise<Recordable>> => {
const result: Record<string, () => Promise<Recordable>> = {};
const viewsRecord = import.meta.glob('../../views/**/*.{vue,tsx}');
Object.keys(viewsRecord).forEach((item) => {
// @ts-ignore
result[item] = viewsRecord[item];
});
const moduleViewsRecord = import.meta.glob('../../modules/**/*.{vue,tsx}');
Object.keys(moduleViewsRecord).forEach((item) => {
// @ts-ignore
result[item] = moduleViewsRecord[item];
});
return result;
};

14
src/settings/componentSetting.ts

@ -1,6 +1,7 @@
// Used to configure the general configuration of some components without modifying the components
import type { SorterResult } from '../components/Table';
import { VxePagerPropTypes } from 'vxe-table';
export default {
// basic-table setting
@ -18,9 +19,18 @@ export default {
totalField: 'total',
},
// Number of pages that can be selected
pageSizeOptions: ['10', '50', '80', '100'],
pageSizeOptions: [50, 100, 500, 1000, 3000],
// Default display quantity on one page
defaultPageSize: 10,
defaultPageSize: 50,
pageLayouts: [
'Sizes',
'PrevJump',
'PrevPage',
'Number',
'NextJump',
'FullJump',
'Total',
] as VxePagerPropTypes.Layouts,
// Default Size
defaultSize: 'middle',
// Custom general sort function

6
src/settings/projectSetting.ts

@ -183,6 +183,12 @@ const setting: ProjectConfig = {
// Whether to cancel the http request that has been sent but not responded when switching the interface.
// If it is enabled, I want to overwrite a single interface. Can be set in a separate interface
removeAllHttpPending: false,
sizeConfig: {
button: 'small',
table: 'small',
form: 'small',
},
};
export default setting;

4
src/store/modules/app.ts

@ -4,6 +4,7 @@ import type {
MenuSetting,
TransitionSetting,
MultiTabsSetting,
SizeConfig,
} from '#/config';
import type { BeforeMiniState, ApiAddress } from '#/store';
@ -66,6 +67,9 @@ export const useAppStore = defineStore({
getApiAddress() {
return JSON.parse(localStorage.getItem(API_ADDRESS) || '{}');
},
getSizeSetting(): SizeConfig {
return this.getProjectConfig.sizeConfig;
},
},
actions: {
setPageLoading(loading: boolean): void {

70
src/utils/auth/index.ts

@ -2,6 +2,12 @@ import { Persistent, BasicKeys } from '@/utils/cache/persistent';
import { CacheTypeEnum, TOKEN_KEY } from '@/enums/cacheEnum';
import projectSetting from '@/settings/projectSetting';
import sha256 from 'crypto-js/sha256';
import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
import { UserInfo } from '#/store';
import { useUserStore } from '@/store/modules/user';
import { usePermission } from '@/hooks/web/usePermission';
import { isString } from '@/utils/is';
import { SmartAuth } from '#/utils';
const { permissionCacheType } = projectSetting;
const isLocal = permissionCacheType === CacheTypeEnum.LOCAL;
@ -33,3 +39,67 @@ export function clearAuthCache(immediate = true) {
export const createPassword = (username: string, password: string) => {
return sha256(sha256(`${username}${password}888888$#@`)).toString();
};
/**
* 是否是超级管理员
*/
export const isSuperAdmin = (): boolean => {
const userStore = useUserStore();
return userStore.getRoleList.includes('SUPERADMIN');
};
/**
* 是否拥有权限
* @param auth
*/
export const hasPermission = (auth?: SmartAuth | string): boolean => {
if (!auth) {
return true;
}
const { hasPermission } = usePermission();
if (isString(auth)) {
return hasPermission(auth);
}
const { permission, multipleMode } = auth;
if (isString(permission)) {
return hasPermission(permission);
}
if (multipleMode === 'or') {
return permission.some((item) => hasPermission(item));
} else {
return permission.every((item) => hasPermission(item));
}
};
/**
* 申请临时 token
* @param resource 申请资源
* @param once 是否只使用一次
*/
export const applyTempToken = async (resource: string, once = true): Promise<string> => {
return await defHttp.post({
service: ApiServiceEnum.SMART_AUTH,
url: 'auth/tempToken/apply',
data: { resource, once },
});
};
/**
* 获取用户信息
*/
export const getUserInfo = (): UserInfo => {
const userStore = useUserStore();
return userStore.getUserInfo;
};
/**
* 获取当前用户ID
*/
export const getCurrentUserId = (): string | number => {
return getUserInfo().userId;
};
export const getUserRole = (): string[] => {
const userStore = useUserStore();
return userStore.getRoleList;
};

7
src/utils/http/axios/index.ts

@ -10,7 +10,7 @@ import { checkStatus } from './checkStatus';
import { useGlobSetting } from '@/hooks/setting';
import { useMessage } from '@/hooks/web/useMessage';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { isString, isUndefined, isNull, isEmpty } from '@/utils/is';
import { isString, isUndefined, isNull, isEmpty, isNumber } from '@/utils/is';
import { getToken } from '@/utils/auth';
import { setObjToUrlParams, deepMerge } from '@/utils';
import { useErrorLogStoreWithOut } from '@/store/modules/errorLog';
@ -125,7 +125,10 @@ const transform: AxiosTransform = {
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 || config.data instanceof FormData)
(Object.keys(config.data).length > 0 ||
config.data instanceof FormData ||
isString(data) ||
isNumber(data))
) {
config.data = data;
config.params = params;

9
src/utils/is.ts

@ -1,3 +1,5 @@
import { isFunction } from 'lodash-es';
export {
isArguments,
isArrayBuffer,
@ -11,7 +13,6 @@ export {
isEqual,
isEqualWith,
isError,
isFunction,
isFinite,
isLength,
isMap,
@ -70,3 +71,9 @@ export function isPascalCase(str: string): boolean {
const regex = /^[A-Z][A-Za-z]*$/;
return regex.test(str);
}
export function isPromise<T = any>(val: unknown): val is Promise<T> {
return is(val, 'Promise') && isFunction((val as any).then) && isFunction((val as any).catch);
}
export { isFunction };

8
types/config.d.ts

@ -136,6 +136,8 @@ export interface ProjectConfig {
closeMessageOnSwitch: boolean;
// Whether to cancel the http request that has been sent but not responded when switching the interface.
removeAllHttpPending: boolean;
// 尺寸配置
sizeConfig: SizeConfig;
}
export interface GlobConfig {
@ -171,3 +173,9 @@ export interface GlobEnvConfig {
* 后台api模式,standalone:单体架构,cloud:微服务架构
*/
type ApiMode = 'standalone' | 'cloud';
export interface SizeConfig {
table: VxeTablePropTypes.Size;
button: string;
form: FormSize;
}

11
types/utils.d.ts

@ -3,3 +3,14 @@ import type { ComputedRef, Ref } from 'vue';
export type DynamicProps<T> = {
[P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
};
export type SmartAuthDisplayMode = 'hide' | 'disabled';
/**
* 权限
*/
export interface SmartAuth {
permission: string | string[];
displayMode?: SmartAuthDisplayMode;
multipleMode?: 'and' | 'or';
}

2
vite.config.ts

@ -12,6 +12,8 @@ export default defineApplicationConfig({
'@iconify/iconify',
'ant-design-vue/es/locale/zh_CN',
'ant-design-vue/es/locale/en_US',
'vxe-table/lib/locale/lang/zh-CN',
'vxe-table/es/locale/lang/en-US',
],
},
server: {

Loading…
Cancel
Save