UNPKG

fastlion-amis

Version:

一种MIS页面生成工具

931 lines (824 loc) 30.4 kB
import { saveAs } from 'file-saver'; import { types, getParent, flow, getEnv, getRoot, isAlive, Instance } from 'mobx-state-tree'; import { IRendererStore } from './index'; import { ServiceStore } from './service'; import { extendObject, createObject, isObjectShallowModified, sortArray, isEmpty, qsstringify, isMobile, uuidv4, jsMemoryHeap } from '../utils/helper'; import { Api, Payload, fetchOptions, Action, ApiObject } from '../types'; import pick from 'lodash/pick'; import union from 'lodash/union' import intersection from 'lodash/intersection' import { resolveVariableAndFilter } from '../utils/tpl-builtin'; import { buildApi, normalizeApiResponseData } from '../utils/api'; import { sortFn, filterFn } from '../utils/utils'; import { Condition } from '../components/table/SecondFilter/types'; import { handleFilter, handleSort } from './utils/commonTableFunction'; import { cloneDeep, filter } from 'lodash'; class ServerError extends Error { type = 'ServerError'; } // 作为前端item唯一性标识的字段-取得复杂点方便前端处理并且后端不会识别到-如果需要做表格项唯一性判断都要从这里取 export const DATAKEYID = 'front_data_instance_unique_mark_' + Math.floor(Math.random() * 1000) export const CRUDStore = ServiceStore.named('CRUDStore') .props({ pristineQuery: types.optional(types.frozen(), {}), query: types.optional(types.frozen(), {}), prevPage: 1, page: 1, perPage: 20, total: 0, mode: 'normal', hasNext: false, selectedAction: types.frozen(), columns: types.frozen(), items: types.optional(types.array(types.frozen()), []), selectedItems: types.optional(types.array(types.frozen()), []), unSelectedItems: types.optional(types.array(types.frozen()), []), filterTogggable: false, filterVisible: true, hasInnerModalOpen: false, searching: false, // Jay 是否正在通过search发起请求 filterLoading: false, loadmoreLoading: false }) .views(self => ({ get lastPage() { return Math.max( Math.ceil(self.total / (self.perPage < 1 ? 10 : self.perPage)), 1 ); }, get filterData() { return createObject(self.data, { ...self.query }); }, get mergedData() { return extendObject(self.data, { perPage: self.perPage, ...self.query, ...self.data, selectedItems: self.selectedItems, unSelectedItems: self.unSelectedItems }); }, get hasModalOpened() { return self.dialogOpen || self.drawerOpen || self.hasInnerModalOpen; }, get selectedItemsAsArray() { return self.selectedItems.concat(); }, fetchCtxOf( data: any, options: { pageField?: string; perPageField?: string; } ) { return createObject(data, { ...self.query, [options.pageField || 'page']: self.page, [options.perPageField || 'perPage']: self.perPage, ...data }); } })) .actions(self => { let fetchCancel: Function | null = null; let childStore: any = {} // 缓存map 保存data使用 // 单次可以缓存的最大条数 -- 内存大100条 内存小就40条 const halfSingleCacheNumber = 10 let cacheData: any = { cacheMap: {}, curApi: '', parentDataSetId: '', openCache: false, } function setPristineQuery() { self.pristineQuery = self.query; } function updateQuery( values: object, updater?: Function, pageField: string = 'page', perPageField: string = 'perPage', replace: boolean = false ) { const originQuery = self.query; self.query = replace ? { ...values } : { ...self.query, ...values }; if (self.query[pageField || 'page']) { self.page = parseInt(self.query[pageField || 'page'], 10); } if (self.query[perPageField || 'perPage']) { self.perPage = parseInt(self.query[perPageField || 'perPage'], 10); } updater && isObjectShallowModified(originQuery, self.query, false) && setTimeout(updater.bind(null, `?${qsstringify(self.query)}`), 4); } function recursiveDelete(obj: object, propertyPath: string,) { if (obj && typeof obj === 'object' && obj[propertyPath]) { delete obj[propertyPath]; } if (obj?.['__super']) { recursiveDelete(obj["__super"], propertyPath) } } function initStaticData(data?: { items: Array<any>, total: number }, options?: { loadDataMode?: string, perPage: number, page: number, source?: string }) { if (!options?.loadDataMode && data) { const newdata = { items: data.items.slice(0, options?.perPage), total: data.items.length, itemsRaw: data.items, count: data.items.length, sortItemsRaw: data.items }; self.data = newdata; self.total = data.total; } else { const newdata = self.data.itemsRaw.slice((self.page - 1) * self.perPage, self.page * self.perPage) self.reInitData({ ...self.data, items: newdata }); } } const fetchInitData: ( api: Api, data?: object, options?: fetchOptions & { forceReload?: boolean; loadDataOnce?: boolean; // 配置数据是否一次性加载,如果是这样,由前端来完成分页,排序等功能。 loadDataOnceFetchOnFilter?: boolean; // 在开启loadDataOnce时,filter时是否去重新请求api source?: string; // 支持自定义属于映射,默认不配置,读取 rows 或者 items loadDataMode?: string | boolean; syncResponse2Query?: boolean; openCache?: boolean // 是否需要缓存数据 } ) => Promise<any> = flow(function* getInitData( api: Api, data: object, options: fetchOptions & { forceReload?: boolean; loadDataOnce?: boolean; // 配置数据是否一次性加载,如果是这样,由前端来完成分页,排序等功能。 loadDataOnceFetchOnFilter?: boolean; // 在开启loadDataOnce时,filter时是否去重新请求api source?: string; // 支持自定义属于映射,默认不配置,读取 rows 或者 items loadDataMode?: string | boolean; showLoading?: boolean; syncResponse2Query?: boolean; openCache?: boolean // 是否需要缓存数据 } = {} ) { //移动端加载更多不需要loading const showLoading = isMobile() && options.loadDataMode === 'load-more'; try { showLoading ? (self.loadmoreLoading = true) : (self.filterLoading = true); //切换上一个下一个时,由于配置了loadDateOnce导致不会重新请求数据,所以这边要加条件判断是否强制刷新 // let foreUp = options.forceReload // if (options.loadDataMode != 'load-more' && options.loadDataOnce) { // foreUp = true // } const ctx: any = createObject(self.data, { ...self.query, ...data, [options.pageField || 'page']: self.page, [options.perPageField || 'perPage']: self.perPage }); // 一次性加载不要发送 perPage 属性 if (options.loadDataOnce) { recursiveDelete(ctx, options.perPageField || 'perPage') } // 如果需要远程请求的情况 const genrateUrl = buildApi(api, ctx).url // 切换当前选中的缓存数据 if (options.openCache) { cacheData.curApi = genrateUrl } // 缓存当前url ai-tool会用到该url cacheData.curApiForCache = genrateUrl // 如果他不需要强制刷新但是是一次性加载的情况 // 如果不是需要缓存的表格或者是需要缓存的表格但是有缓存数据源,就走这个分支 if (!options.forceReload && options.loadDataOnce && self.total && (!options.openCache || cacheData.cacheMap[genrateUrl])) { let items if (!options.openCache) { items = options.source ? resolveVariableAndFilter( options.source, createObject(self.mergedData, { items: self.data.itemsRaw, rows: self.data.itemsRaw }), '| raw' ) : self.items.concat(); } else { items = cacheData.cacheMap[genrateUrl].items } if (self.query.orderBy) { const dir = /desc/i.test(self.query.orderDir) ? -1 : 1; items = sortArray(items, self.query.orderBy, dir); } if (childStore && childStore?.modefiedMap) { items = handleSort(items.slice(), Array.from(childStore.orderColumns.keys?.()), childStore.orderColumns, childStore?.modefiedMap?.modifiedDataSet) } const originData = isMobile() ? items.slice(0, self.page * self.perPage) : items.slice( (self.page - 1) * self.perPage, self.page * self.perPage) // 如果需要缓存 修改整体data const data = { ...(options.openCache ? cacheData.cacheMap[genrateUrl] : self.data), total: items.length, items: originData }; // 没有原值给他补上去原值设置-或者第一个不一样 if (options.openCache && (!data.itemsRaw || (data.itemsRaw?.[0]?.[DATAKEYID] !== items?.[0]?.[DATAKEYID]))) { data.itemsRaw = data.sortItemsRaw = originData } self.total = parseInt(data.total ?? data.count, 10) || 0; self.reInitData(data); self.filterLoading = false; self.loadmoreLoading = false; return; } if (fetchCancel) { fetchCancel(); fetchCancel = null; self.fetching = false; self.searching = false; self.loadmoreLoading = false } options.silent || (!showLoading && self.markFetching(true)); !showLoading && (self.searching = true); if (showLoading) self.loadmoreLoading = true; // 预留缓存数据 let json: Payload // 如果有缓存数据 if (options.openCache && !options.forceReload && cacheData.cacheMap[genrateUrl]) { json = { ok: true, data: cacheData.cacheMap[genrateUrl], msg: '请求成功', status: 200 } } else { json = yield getEnv(self).fetcher(api, ctx, { ...options, cancelExecutor: (executor: Function) => (fetchCancel = executor) }); fetchCancel = null; } if (!json.ok) { self.filterLoading = false; self.updateMessage( json.msg ?? options.errorMessage ?? self.__('CRUD.fetchFailed'), true ); getEnv(self).notify( 'error', json.msg, json.msgTimeout !== undefined ? { closeButton: true, timeout: json.msgTimeout } : undefined ); } else { if (!json.data) { throw new Error(self.__('CRUD.invalidData')); } self.updatedAt = Date.now(); let result = normalizeApiResponseData(json.data); // 数据标记 方便唯一性查找 如果有datakeyid不重新赋值 result.items = result.items?.map((_: any) => ({ ..._, [DATAKEYID]: _[DATAKEYID] || uuidv4() })) result.rows = result.rows?.map((_: any) => ({ ..._, [DATAKEYID]: _[DATAKEYID] || uuidv4() })) const { total, count, page, hasNext, items: oItems, rows: oRows, columns, ...rest } = result; let items: Array<any>; if (options.source) { items = resolveVariableAndFilter( options.source, createObject(self.filterData, result), '| raw' ); } else { items = result.items || result.rows; } if (!Array.isArray(items)) { throw new Error(self.__('CRUD.invalidArray')); } else { // 确保成员是对象。 items.map((item: any) => typeof item === 'string' ? { text: item } : item ); } // 点击加载更多数据 let rowsData = []; if (options.loadDataMode && Array.isArray(self.data.items)) { rowsData = self.data.items.concat(items); } else { // 第一次的时候就是直接加载请求的数据 rowsData = items; } const data = { ...((api as ApiObject).replaceData ? {} : self.pristine), items: rowsData, count: count, total: total, dataSetId: uuidv4(), // 数据集唯一性id 方便后续需要对数据集进行操作的时候作为数据集是否有更新的标识 ...rest }; // 如果需要缓存才走这里 - 把数据标记带进去 if (options.openCache) { checkcacheData() cacheData.cacheMap[genrateUrl] = { ...cloneDeep(data), createTime: new Date().getTime() } } data.itemsRaw = data.sortItemsRaw = oItems || oRows; if (options.loadDataOnce) { // 记录原始集合,后续可能基于原始数据做排序查找。 // data.itemsRaw = oItems || oRows; if (self.query.orderBy) { const dir = /desc/i.test(self.query.orderDir) ? -1 : 1; rowsData = sortArray(rowsData, self.query.orderBy, dir); } data.items = rowsData.slice( (self.page - 1) * self.perPage, self.page * self.perPage ); data.count = data.total = rowsData.length; } if (Array.isArray(columns)) { self.columns = columns.concat(); } else { self.columns = undefined; } self.items.replace(rowsData); self.reInitData(data, !!(api as ApiObject).replaceData); options.syncResponse2Query !== false && updateQuery( pick(rest, Object.keys(self.query)), undefined, options.pageField || 'page', options.perPageField || 'perPage' ); self.total = parseInt(data.total ?? data.count, 10) || 0; typeof page !== 'undefined' && (self.page = parseInt(page, 10)); // 分页情况不清楚,只能知道有没有下一页。 if (typeof hasNext !== 'undefined') { self.mode = 'simple'; self.total = 0; self.hasNext = !!hasNext; } self.updateMessage(json.msg ?? options.successMessage); // 配置了获取成功提示后提示,默认是空不会提示。 options && options.successMessage && getEnv(self).notify('success', self.msg); } self.markFetching(false); self.searching = false; self.filterLoading = false; self.loadmoreLoading = false; return json; } catch (e) { const env = getEnv(self) as IRendererStore; if (!isAlive(self) || self.disposed) { return; } self.markFetching(false); self.searching = false; if (env.isCancel(e)) { return; } self.filterLoading = false; console.error(e.stack); env.notify('error', e.message); return; } }); function checkcacheData() { // 如果缓存数量超过100条 开始删除最末尾的几个条目 // if (Object.values(cacheData.cacheMap).length > (2 * halfSingleCacheNumber)) { const keyTimeMap = [] for (const key in cacheData.cacheMap) { keyTimeMap.push({ createTime: cacheData.cacheMap[key].createTime, key, }) } // 根据创建时间排序,越新的越前面 keyTimeMap.sort((pre: any, cur: any) => cur.createTime - pre.createTime) // 100条之后的筛选出来 const olderData = keyTimeMap.slice(2 * halfSingleCacheNumber) // 清楚每条 olderData.map(_ => { delete cacheData.cacheMap[_.key] }) // } } function changePage(page: number, perPage?: number | string) { self.page = page; perPage && (self.perPage = parseInt(perPage as string, 10)); } function changeTotal(total: number) { self.total = total } function selectAction(action: Action) { self.selectedAction = action; } const saveRemote: ( api: Api, data?: object, options?: fetchOptions ) => Promise<any> = flow(function* saveRemote( api: Api, data: object, options: fetchOptions = {} ) { try { options = { method: 'post', // 默认走 post ...options }; // 需要隐藏不展示loading if (!options.shouldHideLoaing) self.markSaving(true); const json: Payload = yield getEnv(self).fetcher(api, data, options); self.markSaving(false); if (!isEmpty(json.data) || json.ok) { self.updateData( normalizeApiResponseData(json.data), { __saved: Date.now() }, !!api && (api as ApiObject).replaceData ); self.updatedAt = Date.now(); } if (!json.ok) { self.updateMessage( json.msg ?? options.errorMessage ?? self.__('saveFailed'), true ); let infoType = 'info'; if (json.status == 10001) infoType = 'info'; if (json.status == 10003) infoType = 'warning'; if (json.status == 10004) infoType = 'error'; getEnv(self).notify( infoType, self.msg, json.msgTimeout !== undefined ? { closeButton: true, timeout: json.msgTimeout } : undefined ); throw new ServerError(self.msg); } else { self.updateMessage(json.msg ?? options.successMessage); self.msg && getEnv(self).notify( 'success', self.msg, json.msgTimeout !== undefined ? { closeButton: true, timeout: json.msgTimeout } : undefined ); } return json.data; } catch (e) { self.markSaving(false); if (!isAlive(self) || self.disposed) { return; } e.type !== 'ServerError' && getEnv(self).notify('error', e.message); throw e; } }); function preFetchDataSlience(api: Api, _ctx: any, options: any) { const apis: { api: Api, data: any }[] = [] // 获取父级的数据集id const parentDataSetId = (_ctx.__super.hasOwnProperty('dataSetId') && _ctx.__super.dataSetId) || (_ctx.__super.__super.hasOwnProperty('dataSetId') && _ctx.__super.__super.dataSetId) // cacheData.cacheMap = {} // 更新数据集id cacheData.parentDataSetId = parentDataSetId // 开始静默请求 // 获取super或者super中的super父级item数据 保证是父级传入的不会取到其他地方的 const parentItems: any[] = (_ctx.__super.hasOwnProperty('items') && _ctx.__super.items || (_ctx.__super.__super.hasOwnProperty('items') && _ctx.__super.__super.items)) const ctxIndex = parentItems.findIndex((_: any) => _[DATAKEYID] === _ctx[DATAKEYID]) // 缓存的前半部分 const preParentArr = parentItems.slice(Math.max(0, ctxIndex - halfSingleCacheNumber), ctxIndex).reverse() // 缓存的后半部分 const afterParentArr = parentItems.slice(ctxIndex, Math.min(ctxIndex + halfSingleCacheNumber, parentItems.length)) const unionArr = [] // 按顺序合并两个数组 while (preParentArr.length || afterParentArr.length) { const preItem = preParentArr.shift() preItem && unionArr.push(preItem) const afterItem = afterParentArr.shift() afterItem && unionArr.push(afterItem) } unionArr?.map((_: any) => { apis.push({ api, data: _ }) }) const newCacheMap = {} // 获取data const fetchData = async () => { // 从头部获取 const fetchApi = apis.shift() // fetchApi缓存次数 let fetchCancel if (!fetchApi) return try { const ctx: any = createObject(self.data, { ...self.query, ...fetchApi?.data, [options.pageField || 'page']: self.page, [options.perPageField || 'perPage']: self.perPage }); // 一次性加载不要发送 perPage 属性 if (options.loadDataOnce) { recursiveDelete(ctx, options.perPageField || 'perPage') } const genrateUrl = fetchApi?.api && buildApi(fetchApi?.api, ctx).url if (!cacheData.cacheMap[genrateUrl || '']) { const json = await getEnv(self).fetcher(fetchApi?.api, ctx, { ...options, cancelExecutor: (executor: Function) => (fetchCancel = executor) }); if (json.ok) { const result = normalizeApiResponseData(json.data); // 数据标记 方便唯一性查找 result.items = result.items?.map((_: any) => ({ ..._, [DATAKEYID]: _[DATAKEYID] || uuidv4() })) result.rows = result.rows?.map((_: any) => ({ ..._, [DATAKEYID]: _[DATAKEYID] || uuidv4() })) const { total, count, page, hasNext, items: oItems, rows: oRows, columns, ...rest } = result; let items: Array<any>; items = result.items || result.rows; if (!Array.isArray(items)) { throw new Error(self.__('CRUD.invalidArray')); } else { // 确保成员是对象。 items.map((item: any) => typeof item === 'string' ? { text: item } : item ); } const data = { items: items, count: count, total: total, createTime: new Date().getTime(), dataSetId: uuidv4(), // 数据集唯一性id 方便后续需要对数据集进行操作的时候作为数据集是否有更新的标识 ...rest }; // 如果需要缓存才走这里 - 把数据标记带进去 newCacheMap[genrateUrl || ''] = data } else { throw new Error(self.__('CRUD.invalidData')); } // 如果有apis的长度 并且 有单次缓存大小 就继续获取 } else { // 已经有的跟新一下创建时间 newCacheMap[genrateUrl || ''] = { ...cacheData.cacheMap[genrateUrl || ''], createTime: new Date().getTime(), } } // 执行完了更新缓存 if (apis.length) { fetchData() } else { cacheData.cacheMap = newCacheMap } } catch (error) { console.error(error) throw new Error(self.__('CRUD.invalidData')); } } // 打开一个长度为5的请求队列 预加载数据 new Array(5).fill(1).map(_ => fetchData()) } const setFilterTogglable = (toggable: boolean, filterVisible?: boolean) => { self.filterTogggable = toggable; filterVisible !== void 0 && (self.filterVisible = filterVisible); }; const setFilterVisible = (visible: boolean) => { self.filterVisible = visible; }; const setSelectedItems = (items: Array<any>) => { self.selectedItems.replace(items); }; const setUnSelectedItems = (items: Array<any>) => { self.unSelectedItems.replace(items); }; const setInnerModalOpened = (value: boolean) => { self.hasInnerModalOpen = value; }; const initFromScope = function (scope: any, source: string) { let rowsData: Array<any> = resolveVariableAndFilter( source, scope, '| raw' ); if (!Array.isArray(rowsData) && !self.items.length) { return; } rowsData = Array.isArray(rowsData) ? rowsData : []; const data = { ...self.pristine, items: rowsData, count: 0, total: 0 }; self.items.replace(rowsData); self.reInitData(data); }; // 跟新缓存数据源-- 使用这种方式调整 function updateCacheData(data: any) { cacheData.cacheMap[cacheData.curApi] = { ...cacheData.cacheMap[cacheData.curApi] } cacheData.cacheMap[cacheData.curApi].items = [...data.items] cacheData.cacheMap[cacheData.curApi].dataSetId = data.dataSetId } function clearCacheData() { cacheData = null } // 获取缓存数据源 function getCacheData() { const jsonStr = JSON.stringify(cacheData); const encoder = new TextEncoder(); // TextEncoder 是 Web API 的一部分,可能不在所有环境中都可用 const bytes = encoder.encode(jsonStr).length; console.log(`缓存大小: ${bytes / 1024}`); return cacheData } function getRawCacheData() { return cacheData } const exportAsCSV = async ( options: { loadDataOnce?: boolean; api?: Api; data?: any } = {} ) => { let items = options.loadDataOnce ? self.data.itemsRaw : self.data.items; if (options.api) { const env = getEnv(self); const res = await env.fetcher(options.api, options.data); if (!res.data) { return; } if (Array.isArray(res.data)) { items = res.data; } else { items = res.data.rows || res.data.items; } } import('papaparse').then((papaparse: any) => { const csvText = papaparse.unparse(items); if (csvText) { const blob = new Blob( // 加上 BOM 这样 Excel 打开的时候就不会乱码 [new Uint8Array([0xef, 0xbb, 0xbf]), csvText], { type: 'text/plain;charset=utf-8' } ); saveAs(blob, 'data.csv'); } }); }; function sortColumns(orderMap: Map<string, any>, filterMap?: Map<string, any>, loadDataOnce = false, isBiTable = false, modifiedData?: any) { // asc 升序 desc 降序 let sortData = []; let filterData = [] let hasFilter = false let isCancel = orderMap.size <= 0 && (filterMap?.size ?? 0) <= 0 if (isCancel) { const items = loadDataOnce ? self.data.itemsRaw.slice((self.page - 1) * self.perPage, self.page * self.perPage) : self.data.itemsRaw.slice() sortData = items if (items && filterMap && filterMap.size > 0) { sortData = handleFilter(items.slice(), Array.from(filterMap.keys()), filterMap) } } else { const items = loadDataOnce ? self.data.itemsRaw.slice() : self.data.items.slice() sortData = isBiTable ? items : handleSort(items.slice(), Array.from(orderMap.keys()), orderMap, modifiedData) if (loadDataOnce && filterMap && filterMap.size > 0) { hasFilter = true filterData = handleFilter(sortData, Array.from(filterMap.keys()), filterMap) self.total = filterData.length } } self.reInitData( { ...self.data, items: loadDataOnce ? (hasFilter ? filterData.slice((self.page - 1) * self.perPage, self.page * self.perPage) : (isCancel && self.page > 1) ? sortData : sortData.slice((self.page - 1) * self.perPage, self.page * self.perPage)) : sortData, sortItemsRaw: isCancel ? self.data.itemsRaw.slice() : sortData, } ) self.markFetching(false) } function filterColumns(filterMap: Map<string, any>, orderMap?: Map<string, any>, loadDataOnce: boolean = false, isBiTable = false, caseSensitive = false) { const items = loadDataOnce ? self.data.sortItemsRaw.slice() : self.data.itemsRaw.slice() let filterItems = isBiTable ? items : handleFilter(items, Array.from(filterMap.keys()), filterMap, caseSensitive) if (orderMap && orderMap.size > 0) { filterItems = handleSort(filterItems.slice(), Array.from(orderMap.keys()), orderMap) } const filterData = { ...self.data, items: loadDataOnce ? filterItems.slice((self.page - 1) * self.perPage, self.page * self.perPage) : filterItems, } if (loadDataOnce) { self.total = filterItems.length } self.reInitData(filterData) } //清空数据 function clearData() { const data = { items: [], total: 0, count: undefined }; self.data = data; self.total = 0; } // 设置子store function setChildStore(store: any) { childStore = store } // 设置子store function clearChildStore() { childStore = null } return { clearCacheData, preFetchDataSlience, getCacheData, updateCacheData, setChildStore, clearChildStore, setPristineQuery, updateQuery, fetchInitData, changePage, changeTotal, selectAction, saveRemote, setFilterTogglable, setFilterVisible, setSelectedItems, setUnSelectedItems, setInnerModalOpened, initFromScope, exportAsCSV, sortColumns, filterColumns, clearData, initStaticData, getRawCacheData }; }); export type ICRUDStore = Instance<typeof CRUDStore>;