mirror of
https://github.com/1Panel-dev/CordysCRM.git
synced 2026-05-23 19:34:47 +08:00
feat: add crmTable
This commit is contained in:
11
frontend/packages/lib-shared/enums/tableEnum.ts
Normal file
11
frontend/packages/lib-shared/enums/tableEnum.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export enum TableKeyEnum {
|
||||
SYSTEM_USER = 'systemUser', // TODO lmy 没用 可删
|
||||
}
|
||||
|
||||
// 具有特殊功能的列
|
||||
export enum SpecialColumnEnum {
|
||||
// 选择框
|
||||
SELECTION = 'selection',
|
||||
// 操作列
|
||||
OPERATION = 'operation',
|
||||
}
|
||||
28
frontend/packages/lib-shared/method/equal.ts
Normal file
28
frontend/packages/lib-shared/method/equal.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { sortBy } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 比较两个一维数组对象是否相等,不考虑顺序,
|
||||
* @param arr1 数组1
|
||||
* @param arr2 数组2
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isArraysEqualWithOrder<T>(arr1: T[], arr2: T[]): boolean {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
const sortArr1 = sortBy(arr1, 'dataIndex');
|
||||
const sortArr2 = sortBy(arr2, 'dataIndex');
|
||||
for (let i = 0; i < sortArr1.length; i++) {
|
||||
const obj1 = sortArr1[i];
|
||||
const obj2 = sortArr2[i];
|
||||
|
||||
// 逐一比较对象
|
||||
if (JSON.stringify(obj1) !== JSON.stringify(obj2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {};
|
||||
@@ -5,3 +5,26 @@ export default interface CommonResponse<T> {
|
||||
messageDetail: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 表格查询
|
||||
export interface TableQueryParams {
|
||||
// 当前页
|
||||
current?: number;
|
||||
// 每页条数
|
||||
pageSize?: number;
|
||||
// 排序仅针对单个字段
|
||||
sort?: object;
|
||||
// 表头筛选
|
||||
filter?: object;
|
||||
// 查询条件
|
||||
keyword?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface CommonList<T> {
|
||||
[x: string]: any;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
current: number;
|
||||
list: T[];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"naive-ui": "^2.40.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"vfonts": "^0.0.3"
|
||||
"vfonts": "^0.0.3",
|
||||
"vue-draggable-plus": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-legacy": "^6.0.0",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themeOverridesConfig">
|
||||
<RouterView />
|
||||
<Suspense>
|
||||
<RouterView />
|
||||
</Suspense>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<n-popover trigger="click" placement="bottom-end" @update:show="handleUpdateShow">
|
||||
<template #trigger>
|
||||
<CrmIcon type="icon-icon-setting" class="cursor-pointer" />
|
||||
</template>
|
||||
<div class="flex w-[175px] items-center justify-between text-[12px]">
|
||||
<div class="font-medium text-[var(--text-n1)]">{{ t('crmTable.columnSetting.tableHeaderDisplaySettings') }}</div>
|
||||
<n-button text size="tiny" :disabled="!hasChange" @click="handleReset">
|
||||
{{ t('crmTable.columnSetting.resetDefault') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<VueDraggable v-model="cachedColumns" handle=".sort-handle" @change="handleChange">
|
||||
<div
|
||||
v-for="element in cachedColumns"
|
||||
:key="element.key"
|
||||
class="flex w-[175px] items-center justify-between py-[6px]"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<CrmIcon type="icon-icon_drag" class="sort-handle cursor-move text-[var(--text-n4)]" :size="12" />
|
||||
<span class="one-line-text ml-[8px] text-[12px]">
|
||||
{{ t(element.title as string) }}
|
||||
</span>
|
||||
</div>
|
||||
<n-switch v-model:value="element.showInTable" size="small" @update:value="handleChange" />
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</n-popover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NButton, NPopover, NSwitch } from 'naive-ui';
|
||||
|
||||
import type { CrmDataTableColumn } from '@/components/pure/crm-table/type';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useTableStore } from '@/store';
|
||||
|
||||
import type { TableKeyEnum } from '@lib/shared/enums/tableEnum';
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
tableKey: TableKeyEnum;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'changeColumnsSetting'): void; // 数据发生变化
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const tableStore = useTableStore();
|
||||
|
||||
const hasChange = ref(false); // 是否有改动
|
||||
const cachedColumns = ref<CrmDataTableColumn[]>([]);
|
||||
|
||||
async function getCachedColumns() {
|
||||
const columns = await tableStore.getCanSetColumns(props.tableKey);
|
||||
cachedColumns.value = columns;
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (props.tableKey) {
|
||||
getCachedColumns();
|
||||
}
|
||||
});
|
||||
|
||||
function handleReset() {
|
||||
getCachedColumns();
|
||||
hasChange.value = false;
|
||||
}
|
||||
|
||||
function handleChange() {
|
||||
hasChange.value = true;
|
||||
}
|
||||
|
||||
async function handleUpdateShow(show: boolean) {
|
||||
if (!show) {
|
||||
if (hasChange.value) {
|
||||
await tableStore.setColumns(props.tableKey, [...cachedColumns.value]);
|
||||
emit('changeColumnsSetting');
|
||||
hasChange.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
115
frontend/packages/web/src/components/pure/crm-table/index.vue
Normal file
115
frontend/packages/web/src/components/pure/crm-table/index.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<n-data-table
|
||||
v-bind="{ ...$attrs }"
|
||||
v-model:checked-row-keys="checkedRowKeys"
|
||||
:columns="currentColumns"
|
||||
:row-key="getRowKey"
|
||||
@update:sorter="handleSorterChange"
|
||||
@update:filters="handleFiltersChange"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NDataTable } from 'naive-ui';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { CrmDataTableColumn } from '@/components/pure/crm-table/type';
|
||||
import ColumnSetting from './components/columnSetting.vue';
|
||||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { useTableStore } from '@/store';
|
||||
|
||||
import { SpecialColumnEnum, TableKeyEnum } from '@lib/shared/enums/tableEnum';
|
||||
import type { DataTableFilterState, DataTableRowKey, DataTableSortState } from 'naive-ui';
|
||||
|
||||
const props = defineProps<{
|
||||
columns: CrmDataTableColumn[];
|
||||
tableRowKey?: string;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'pageChange', value: number): void;
|
||||
(e: 'pageSizeChange', value: number): void;
|
||||
(e: 'sorterChange', value: { [key: string]: string }): void;
|
||||
(e: 'filterChange', value: DataTableFilterState): void;
|
||||
}>();
|
||||
const attrs = useAttrs();
|
||||
const { t } = useI18n();
|
||||
const tableStore = useTableStore();
|
||||
|
||||
const checkedRowKeys = defineModel<DataTableRowKey[]>('checkedRowKeys', { default: [] });
|
||||
|
||||
const currentColumns = ref<CrmDataTableColumn[]>([]);
|
||||
|
||||
// TODO lmy 设置列
|
||||
async function initColumn() {
|
||||
let columns = cloneDeep(props.columns);
|
||||
if (attrs.showSetting) {
|
||||
columns = await tableStore.getShowInTableColumns(attrs.tableKey as TableKeyEnum);
|
||||
currentColumns.value = columns.map((column) => {
|
||||
// 操作列
|
||||
if (column.key === SpecialColumnEnum.OPERATION) {
|
||||
return {
|
||||
...column,
|
||||
title() {
|
||||
const children = [h('div', t('common.operation'))];
|
||||
if (attrs.showSetting) {
|
||||
children.push(
|
||||
h(ColumnSetting, {
|
||||
tableKey: attrs.tableKey as TableKeyEnum,
|
||||
onChangeColumnsSetting: () => {
|
||||
initColumn();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return h('div', { class: 'flex items-center gap-[8px]' }, children);
|
||||
},
|
||||
};
|
||||
}
|
||||
return column;
|
||||
});
|
||||
} else {
|
||||
currentColumns.value = columns;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.columns,
|
||||
() => {
|
||||
initColumn();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function getRowKey(rowData: Record<string, any>) {
|
||||
return props.tableRowKey ? rowData[props.tableRowKey] : rowData.id;
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
emit('pageChange', page);
|
||||
}
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
emit('pageSizeChange', pageSize);
|
||||
}
|
||||
|
||||
function handleSorterChange(sorter: DataTableSortState) {
|
||||
let sortOrder = '';
|
||||
if (sorter.order === 'ascend') {
|
||||
sortOrder = 'asc';
|
||||
} else if (sorter.order === 'descend') {
|
||||
sortOrder = 'desc';
|
||||
}
|
||||
emit('sorterChange', !sorter.order ? {} : { [sorter.columnKey]: sortOrder });
|
||||
}
|
||||
|
||||
function handleFiltersChange(filters: DataTableFilterState) {
|
||||
emit('filterChange', filters);
|
||||
}
|
||||
|
||||
function handleCheck(rowKeys: DataTableRowKey[]) {
|
||||
console.log('🤔️ => handleCheck', rowKeys);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
'crmTable.columnSetting.tableHeaderDisplaySettings': 'Table header display settings',
|
||||
'crmTable.columnSetting.resetDefault': 'Restore default',
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
'crmTable.columnSetting.tableHeaderDisplaySettings': '表头显示设置',
|
||||
'crmTable.columnSetting.resetDefault': '恢复默认',
|
||||
};
|
||||
33
frontend/packages/web/src/components/pure/crm-table/type.ts
Normal file
33
frontend/packages/web/src/components/pure/crm-table/type.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { VNodeChild } from 'vue';
|
||||
|
||||
import type { TableKeyEnum } from '@lib/shared/enums/tableEnum';
|
||||
import type { DataTableColumn, DataTableColumnKey, DataTableProps, DataTableRowData, DataTableRowKey } from 'naive-ui';
|
||||
|
||||
export type CrmTableDataItem<T> = T & {
|
||||
updateTime?: string | number | null;
|
||||
createTime?: string | number | null;
|
||||
children?: CrmTableDataItem<T>[];
|
||||
} & DataTableRowData;
|
||||
|
||||
export type CrmDataTableColumn = DataTableColumn & {
|
||||
showInTable?: boolean; // 是否展示在表格上
|
||||
key?: DataTableColumnKey; // 这一列的 key,不可重复
|
||||
title?: string | (() => VNodeChild);
|
||||
};
|
||||
|
||||
export interface CrmTableProps<T> extends DataTableProps {
|
||||
'columns': CrmDataTableColumn[];
|
||||
'tableKey'?: TableKeyEnum; // 表格key, 用于存储表格列配置,pageSize等
|
||||
'tableRowKey'?: string; // 表格行的key
|
||||
'data': CrmTableDataItem<T>[];
|
||||
'showSetting'?: boolean; // 是否显示表格配置
|
||||
'showPagination'?: boolean; // 是否显示分页
|
||||
'onUpdate:checkedRowKeys'?: (key: DataTableRowKey[]) => void; // 覆写类型防止报错
|
||||
}
|
||||
|
||||
// 表格存储
|
||||
export interface TableStorageConfigItem {
|
||||
column: CrmDataTableColumn[]; // 列配置
|
||||
pageSize?: number;
|
||||
columnBackup: CrmDataTableColumn[]; // 列配置的备份,用于比较当前定义的列配置是否和备份的列配置相同
|
||||
}
|
||||
169
frontend/packages/web/src/components/pure/crm-table/useTable.ts
Normal file
169
frontend/packages/web/src/components/pure/crm-table/useTable.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { UnwrapRef } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { useAppStore, useTableStore } from '@/store';
|
||||
|
||||
import type { CrmTableDataItem, CrmTableProps } from './type';
|
||||
import type { CommonList, TableQueryParams } from '@lib/shared/models/common';
|
||||
import type { DataTableFilterState, PaginationProps } from 'naive-ui';
|
||||
|
||||
const tableStore = useTableStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
export default function useTable<T>(
|
||||
loadListFunc?: (v?: TableQueryParams | any) => Promise<CommonList<CrmTableDataItem<T>> | CrmTableDataItem<T>>,
|
||||
props?: Partial<CrmTableProps<T>>
|
||||
) {
|
||||
const defaultProps: CrmTableProps<T> = {
|
||||
bordered: false,
|
||||
loading: false, // 加载效果
|
||||
data: [], // 表格数据
|
||||
columns: [],
|
||||
tableRowKey: 'id', // 表格行的key
|
||||
pagination: {
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: appStore.pageSize,
|
||||
pageSizes: appStore.pageSizes,
|
||||
showSizePicker: appStore.showSizePicker,
|
||||
showQuickJumper: appStore.showQuickJumper,
|
||||
}, // false | PaginationProps; false表示不分页
|
||||
...props,
|
||||
};
|
||||
|
||||
const propsRes = ref<CrmTableProps<T>>(cloneDeep(defaultProps));
|
||||
|
||||
// 如果表格设置了tableKey,设置缓存的分页大小
|
||||
if (propsRes.value.pagination && typeof propsRes.value.pagination === 'object' && propsRes.value.tableKey) {
|
||||
tableStore.getPageSize(propsRes.value.tableKey).then((res) => {
|
||||
if (propsRes.value.pagination && res) {
|
||||
propsRes.value.pagination.pageSize = res;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载效果
|
||||
function setLoading(status: boolean) {
|
||||
propsRes.value.loading = status;
|
||||
}
|
||||
|
||||
// 设置请求参数
|
||||
const loadListParams = ref<TableQueryParams>({});
|
||||
function setLoadListParams(params?: TableQueryParams) {
|
||||
loadListParams.value = params || {};
|
||||
}
|
||||
|
||||
// 获取分页参数
|
||||
async function getPaginationParams() {
|
||||
const { page, pageSize } = propsRes.value.pagination as PaginationProps;
|
||||
if (propsRes.value.tableKey) {
|
||||
const cachedPageSize = await tableStore.getPageSize(propsRes.value.tableKey);
|
||||
return { current: page, pageSize: cachedPageSize };
|
||||
}
|
||||
return { current: page, pageSize };
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页设置
|
||||
* @param page 当前页
|
||||
* @param total 总页数
|
||||
*/
|
||||
function setPagination(page: number, total?: number) {
|
||||
if (propsRes.value.pagination && typeof propsRes.value.pagination === 'object') {
|
||||
propsRes.value.pagination.page = page;
|
||||
if (total !== undefined) {
|
||||
propsRes.value.pagination.itemCount = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tableQueryParams = ref<TableQueryParams>({}); // 表格请求参数集合
|
||||
const keyword = ref('');
|
||||
const sortItem = ref<Record<string, any>>({}); // 排序
|
||||
|
||||
const filterItem = ref<Record<string, any>>({}); // 筛选
|
||||
|
||||
function processRecordItem(item: CrmTableDataItem<T>): CrmTableDataItem<T> {
|
||||
if (item.updateTime) {
|
||||
item.updateTime = dayjs(item.updateTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
if (item.createTime) {
|
||||
item.createTime = dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
if (!loadListFunc) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
tableQueryParams.value = {
|
||||
...(!propsRes.value.pagination ? {} : await getPaginationParams()),
|
||||
keyword: keyword.value,
|
||||
sort: sortItem.value,
|
||||
...loadListParams.value,
|
||||
filter: filterItem.value,
|
||||
};
|
||||
const data = await loadListFunc(tableQueryParams.value);
|
||||
if (!propsRes.value.pagination && Array.isArray(data)) {
|
||||
propsRes.value.data = data.map((item: CrmTableDataItem<T>) => {
|
||||
return processRecordItem(item);
|
||||
}) as unknown as UnwrapRef<CrmTableDataItem<T>[]>;
|
||||
} else {
|
||||
const tmpArr = data as CommonList<CrmTableDataItem<T>>;
|
||||
propsRes.value.data = tmpArr.list.map((item: CrmTableDataItem<T>) => {
|
||||
return processRecordItem(item);
|
||||
}) as unknown as UnwrapRef<CrmTableDataItem<T>[]>;
|
||||
// 设置分页
|
||||
setPagination(tmpArr.current, tmpArr.total);
|
||||
}
|
||||
} catch (error) {
|
||||
propsRes.value.data = [];
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件触发组
|
||||
const propsEvent = ref({
|
||||
// 分页触发
|
||||
pageChange: async (page: number) => {
|
||||
setPagination(page);
|
||||
await loadList();
|
||||
},
|
||||
// 修改每页显示条数触发
|
||||
pageSizeChange: async (pageSize: number) => {
|
||||
if (propsRes.value.pagination && typeof propsRes.value.pagination === 'object') {
|
||||
propsRes.value.pagination.pageSize = pageSize;
|
||||
// 如果表格设置了tableKey,缓存分页大小
|
||||
if (propsRes.value.tableKey) {
|
||||
await tableStore.setPageSize(propsRes.value.tableKey, pageSize);
|
||||
}
|
||||
}
|
||||
loadList();
|
||||
},
|
||||
// 排序触发
|
||||
sorterChange: (sortObj: { [key: string]: string }) => {
|
||||
sortItem.value = sortObj;
|
||||
loadList();
|
||||
},
|
||||
// 筛选触发
|
||||
filterChange: (filters: DataTableFilterState) => {
|
||||
filterItem.value = { ...filters };
|
||||
loadList();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
propsRes,
|
||||
propsEvent,
|
||||
setLoading,
|
||||
setLoadListParams,
|
||||
loadList,
|
||||
setPagination,
|
||||
};
|
||||
}
|
||||
109
frontend/packages/web/src/hooks/useLocalForage.ts
Normal file
109
frontend/packages/web/src/hooks/useLocalForage.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import localforage from 'localforage';
|
||||
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
export default function useLocalForage() {
|
||||
const appStore = useAppStore();
|
||||
|
||||
/**
|
||||
* 检测并序列化函数
|
||||
* @param val 要存储的值
|
||||
*/
|
||||
const serializeValue = (val: any): any => {
|
||||
if (typeof val === 'function') {
|
||||
return `function:${val.toString()}`;
|
||||
}
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
const newVal = { ...val };
|
||||
Object.keys(newVal).forEach((key) => {
|
||||
newVal[key] = serializeValue(newVal[key]);
|
||||
});
|
||||
return newVal;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((item) => serializeValue(item));
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
const deserializeFunction = (funcStr: string) => {
|
||||
try {
|
||||
if (!funcStr.trim().startsWith('function') && !funcStr.trim().startsWith('(')) {
|
||||
funcStr = `function ${funcStr}`;
|
||||
}
|
||||
// eslint-disable-next-line no-eval
|
||||
const func = eval(`(${funcStr})`);
|
||||
return func;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 反序列化值,将特殊格式字符串转换回函数
|
||||
* @param val 从存储中读取的值
|
||||
*/
|
||||
const deserializeValue = <T>(val: any): T | null => {
|
||||
if (typeof val === 'string' && val.startsWith('function:')) {
|
||||
return deserializeFunction(val.slice(9)) as T;
|
||||
}
|
||||
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
||||
const newVal = { ...val };
|
||||
Object.keys(newVal).forEach((key) => {
|
||||
newVal[key] = deserializeValue(newVal[key]);
|
||||
});
|
||||
return newVal as T;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((item) => deserializeValue(item) as T) as T;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
/**
|
||||
* 读取本地存储的数据
|
||||
* @param key 唯一 key
|
||||
* @param notIsolatedByProject 存储数据时是否不按项目隔离数据
|
||||
*/
|
||||
const getItem = async <T>(key: string, notIsolatedByProject = false): Promise<T | null> => {
|
||||
const itemKey = notIsolatedByProject ? key : `${appStore.currentProjectId}-${key}`;
|
||||
try {
|
||||
const res = await localforage.getItem<T>(itemKey);
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
return deserializeValue<T>(res);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 永久存储数据
|
||||
* @param key 唯一 key
|
||||
* @param val 存储的值
|
||||
* @param notIsolatedByProject 是否不按项目隔离数据
|
||||
*/
|
||||
const setItem = async (
|
||||
key: string,
|
||||
val: string | number | boolean | Record<string, any>,
|
||||
notIsolatedByProject = false
|
||||
) => {
|
||||
try {
|
||||
const itemKey = notIsolatedByProject ? key : `${appStore.currentProjectId}-${key}`;
|
||||
await localforage.setItem(itemKey, serializeValue(val));
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getItem,
|
||||
setItem,
|
||||
};
|
||||
}
|
||||
135
frontend/packages/web/src/hooks/useTableStore.ts
Normal file
135
frontend/packages/web/src/hooks/useTableStore.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { CrmDataTableColumn, TableStorageConfigItem } from '@/components/pure/crm-table/type';
|
||||
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
import useLocalForage from './useLocalForage';
|
||||
import { SpecialColumnEnum, TableKeyEnum } from '@lib/shared/enums/tableEnum';
|
||||
import { isArraysEqualWithOrder } from '@lib/shared/method/equal';
|
||||
|
||||
export default function useTableStore() {
|
||||
const { getItem, setItem } = useLocalForage();
|
||||
const appStore = useAppStore();
|
||||
|
||||
async function getTableColumnsMap(tableKey: TableKeyEnum): Promise<TableStorageConfigItem | null> {
|
||||
const isSystemOrOrgKey = tableKey.startsWith('SYSTEM') || tableKey.startsWith('ORGANIZATION');
|
||||
const tableColumnsMap = await getItem<TableStorageConfigItem>(tableKey, isSystemOrOrgKey);
|
||||
return tableColumnsMap;
|
||||
}
|
||||
|
||||
async function setTableColumnsMap(tableKey: TableKeyEnum, tableColumnsMap: TableStorageConfigItem) {
|
||||
const isSystemOrOrgKey = tableKey.startsWith('SYSTEM') || tableKey.startsWith('ORGANIZATION');
|
||||
await setItem(tableKey, tableColumnsMap, isSystemOrOrgKey);
|
||||
}
|
||||
|
||||
function columnsTransform(columns: CrmDataTableColumn[]) {
|
||||
columns.forEach((item) => {
|
||||
if (item.showInTable === undefined) {
|
||||
// 默认在表格中展示
|
||||
item.showInTable = true;
|
||||
}
|
||||
});
|
||||
return columns;
|
||||
}
|
||||
|
||||
async function initColumn(tableKey: TableKeyEnum, column: CrmDataTableColumn[]) {
|
||||
try {
|
||||
const tableColumnsMap = await getTableColumnsMap(tableKey);
|
||||
if (!tableColumnsMap) {
|
||||
// 如果没有在indexDB里初始化
|
||||
column = columnsTransform(column);
|
||||
setTableColumnsMap(tableKey, {
|
||||
column,
|
||||
columnBackup: cloneDeep(column),
|
||||
});
|
||||
} else {
|
||||
// 初始化过了,但是可能有新变动,如列的顺序,列的显示隐藏,列的拖拽
|
||||
column = columnsTransform(column);
|
||||
const { columnBackup: oldColumn } = tableColumnsMap;
|
||||
// 比较页面上定义的 column 和 浏览器备份的column 是否相同
|
||||
const isEqual = isArraysEqualWithOrder(oldColumn, column);
|
||||
if (!isEqual) {
|
||||
column.forEach((col) => {
|
||||
const storedCol = tableColumnsMap.column.find((sc) => sc.key === col.key);
|
||||
if (storedCol) {
|
||||
col.width = storedCol.width; // 使用上一次拖拽存储的宽度,避免组件里边使用时候初始化到最初的列宽
|
||||
}
|
||||
});
|
||||
// 如果不相等,说明有变动将新的column存入indexDB
|
||||
setTableColumnsMap(tableKey, {
|
||||
column,
|
||||
columnBackup: cloneDeep(column),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
// 表头显示设置的列
|
||||
async function getCanSetColumns(tableKey: TableKeyEnum) {
|
||||
const tableColumnsMap = await getTableColumnsMap(tableKey);
|
||||
if (tableColumnsMap) {
|
||||
return tableColumnsMap.column.filter(
|
||||
(item) => item.key !== SpecialColumnEnum.OPERATION && item.type !== SpecialColumnEnum.SELECTION
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// 在表格上展示的列
|
||||
async function getShowInTableColumns(tableKey: TableKeyEnum) {
|
||||
const tableColumnsMap = await getTableColumnsMap(tableKey);
|
||||
if (tableColumnsMap) {
|
||||
return tableColumnsMap.column.filter((i) => i.showInTable);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async function setColumns(tableKey: TableKeyEnum, columns: CrmDataTableColumn[]) {
|
||||
try {
|
||||
const tableColumnsMap = await getTableColumnsMap(tableKey);
|
||||
if (tableColumnsMap) {
|
||||
const operationColumn = tableColumnsMap.column.find((i) => i.key === SpecialColumnEnum.OPERATION);
|
||||
const selectColumn = tableColumnsMap.column.find((i) => i.type === SpecialColumnEnum.SELECTION);
|
||||
if (selectColumn) columns.unshift(selectColumn); // 加上选择框列
|
||||
if (operationColumn) columns.push(operationColumn); // 加上操作列
|
||||
tableColumnsMap.column = cloneDeep(columns);
|
||||
await setTableColumnsMap(tableKey, tableColumnsMap);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('tableStore.setColumns', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getPageSize(tableKey: TableKeyEnum) {
|
||||
const tableColumnsMap = await getTableColumnsMap(tableKey);
|
||||
return tableColumnsMap ? tableColumnsMap.pageSize : appStore.pageSize;
|
||||
}
|
||||
|
||||
async function setPageSize(tableKey: TableKeyEnum, pageSize: number): Promise<void> {
|
||||
try {
|
||||
const tableColumnsMap = await getTableColumnsMap(tableKey);
|
||||
if (tableColumnsMap) {
|
||||
tableColumnsMap.pageSize = pageSize;
|
||||
await setTableColumnsMap(tableKey, tableColumnsMap);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
initColumn,
|
||||
getCanSetColumns,
|
||||
setColumns,
|
||||
getShowInTableColumns,
|
||||
setPageSize,
|
||||
getPageSize,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
// TODO 国际化接口对接
|
||||
// import localforage from 'localforage';
|
||||
@@ -11,11 +10,12 @@ import App from './App.vue';
|
||||
import { setupI18n } from './locale';
|
||||
import useLocale from './locale/useLocale';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
|
||||
async function setupApp() {
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(createPinia());
|
||||
app.use(store);
|
||||
// 注册国际化,需要异步阻塞,确保语言包加载完毕
|
||||
await setupI18n(app);
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import useTableStore from '@/hooks/useTableStore';
|
||||
|
||||
import useAppStore from './modules/app';
|
||||
import { debouncePlugin } from './plugins';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
|
||||
const pinia = createPinia().use(debouncePlugin).use(piniaPluginPersistedstate);
|
||||
|
||||
export { useAppStore, useTableStore };
|
||||
|
||||
export default pinia;
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import type { PaginationSizeOption } from 'naive-ui';
|
||||
|
||||
export interface AppState {
|
||||
currentProjectId: string;
|
||||
menuCollapsed: boolean;
|
||||
pageSize: number;
|
||||
showSizePicker: boolean;
|
||||
showQuickJumper: boolean;
|
||||
pageSizes: Array<number | PaginationSizeOption>;
|
||||
}
|
||||
|
||||
const useAppStore = defineStore('app', {
|
||||
state: (): AppState => ({
|
||||
currentProjectId: '',
|
||||
menuCollapsed: false,
|
||||
// 分页
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
}),
|
||||
getters: {
|
||||
getMenuCollapsed(state: AppState) {
|
||||
@@ -18,6 +31,9 @@ const useAppStore = defineStore('app', {
|
||||
this.menuCollapsed = collapsed;
|
||||
},
|
||||
},
|
||||
persist: {
|
||||
paths: ['currentProjectId'],
|
||||
},
|
||||
});
|
||||
|
||||
export default useAppStore;
|
||||
|
||||
@@ -36,15 +36,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</CrmCard>
|
||||
|
||||
<TableDemo class="my-[16px]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NAlert, NButton } from 'naive-ui';
|
||||
|
||||
import CrmCard from '@/components/pure/crm-card/index.vue';
|
||||
import TableDemo from './TableDemo.vue';
|
||||
|
||||
import useDiscreteApi from '@/hooks/useDiscreteApi';
|
||||
|
||||
import { NAlert, NButton } from 'naive-ui';
|
||||
// 暂时提供参考 you can delete it ^_^
|
||||
const { message, notification, dialog } = useDiscreteApi();
|
||||
|
||||
|
||||
208
frontend/packages/web/src/views/TableDemo.vue
Normal file
208
frontend/packages/web/src/views/TableDemo.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<CrmTable
|
||||
v-bind="propsRes"
|
||||
@page-change="propsEvent.pageChange"
|
||||
@page-size-change="propsEvent.pageSizeChange"
|
||||
@sorter-change="propsEvent.sorterChange"
|
||||
@filter-change="propsEvent.filterChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CrmTable from '@/components/pure/crm-table/index.vue';
|
||||
import { CrmDataTableColumn, CrmTableDataItem } from '@/components/pure/crm-table/type';
|
||||
import useTable from '@/components/pure/crm-table/useTable';
|
||||
|
||||
import { useTableStore } from '@/store';
|
||||
|
||||
import { TableKeyEnum } from '@lib/shared/enums/tableEnum';
|
||||
import type { CommonList } from '@lib/shared/models/common';
|
||||
|
||||
const tableStore = useTableStore();
|
||||
|
||||
interface RoleItem {
|
||||
id: string;
|
||||
num: string;
|
||||
status: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const columns: CrmDataTableColumn[] = [
|
||||
{
|
||||
type: 'selection',
|
||||
// multiple: false, // 设置单选
|
||||
},
|
||||
{
|
||||
title: 'common.creator',
|
||||
key: 'num',
|
||||
width: 60,
|
||||
sortOrder: false,
|
||||
sorter: 'default',
|
||||
},
|
||||
{
|
||||
title: 'common.execute',
|
||||
key: 'title',
|
||||
width: 100,
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
sortOrder: false,
|
||||
sorter: 'default',
|
||||
filterOptions: [
|
||||
{
|
||||
label: '222',
|
||||
value: '222',
|
||||
},
|
||||
{
|
||||
label: 'string',
|
||||
value: 'string',
|
||||
},
|
||||
],
|
||||
filter(value, row) {
|
||||
return row.title === value;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'common.text',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
ellipsis: {
|
||||
tooltip: true,
|
||||
},
|
||||
filterOptions: [
|
||||
{
|
||||
label: 'London',
|
||||
value: 'London',
|
||||
},
|
||||
{
|
||||
label: 'New York',
|
||||
value: 'New York',
|
||||
},
|
||||
],
|
||||
filter(value, row) {
|
||||
return row.status === value;
|
||||
},
|
||||
},
|
||||
{ key: 'operation', width: 80 },
|
||||
];
|
||||
|
||||
function getRoleList() {
|
||||
const data: CommonList<CrmTableDataItem<RoleItem>> = {
|
||||
list: [
|
||||
{
|
||||
id: '11',
|
||||
num: 'string',
|
||||
title: 'string',
|
||||
status: 'string',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
num: '232324323',
|
||||
title: '222',
|
||||
status: 'aaaa',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '33',
|
||||
num: 'string',
|
||||
title: 'string',
|
||||
status: 'string',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '44',
|
||||
num: '232324323',
|
||||
title: '222',
|
||||
status: 'aaaa',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '55',
|
||||
num: 'string',
|
||||
title: 'string',
|
||||
status: 'string',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '66',
|
||||
num: '232324323',
|
||||
title: '222',
|
||||
status: 'aaaa',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '77',
|
||||
num: 'string',
|
||||
title: 'string',
|
||||
status: 'string',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '88',
|
||||
num: '232324323',
|
||||
title: '222',
|
||||
status: 'aaaa',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '99',
|
||||
num: 'string',
|
||||
title: 'string',
|
||||
status: 'string',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
num: '232324323',
|
||||
title: '222',
|
||||
status: 'aaaa',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
num: '232324323',
|
||||
title: '222',
|
||||
status: 'aaaa',
|
||||
updateTime: null,
|
||||
createTime: null,
|
||||
},
|
||||
],
|
||||
total: 11,
|
||||
pageSize: 10,
|
||||
current: 1,
|
||||
};
|
||||
return new Promise<CommonList<CrmTableDataItem<RoleItem>>>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(data);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getRoleList, {
|
||||
tableKey: TableKeyEnum.SYSTEM_USER,
|
||||
showSetting: true,
|
||||
columns,
|
||||
});
|
||||
|
||||
function searchData() {
|
||||
setLoadListParams({ keyword: '' });
|
||||
loadList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
searchData();
|
||||
});
|
||||
|
||||
await tableStore.initColumn(TableKeyEnum.SYSTEM_USER, columns);
|
||||
</script>
|
||||
Reference in New Issue
Block a user