UNPKG

@ithinkdt/core

Version:

iThinkDT Core

503 lines (452 loc) 18.9 kB
import { computed, onScopeDispose, reactive, readonly, ref, shallowRef, watch, effectScope, shallowReactive, isReactive, } from 'vue' // eslint-disable-next-line vue/prefer-import-from-vue import { isObject } from '@vue/shared' import { promiseTimeout, toReactive, useUrlSearchParams } from '@vueuse/core' import { useFetch } from '../http' import { pageInit } from './plugin' const dsCache = {} export function useDs({ name = Symbol(), class: clazz = Object, pageable, shallow = true, autoFetch = false, autoTrim = true, refreshAfterSave = false, pageSize, defaultSorts, syncUrlParams, syncCategory = 'query', syncInclude, syncExclude, ...api }) { let ds if (dsCache[name]) { ds = dsCache[name] if (pageable !== ds.pageable || shallow !== ds.shallow || clazz !== ds.clazz) { console.warn( `[page]: data source '${name}' option 'pageable' or 'shallow' or 'class' is different, please set other name`, ) } } else { const scope = effectScope(true) scope.run(() => { pageSize ??= pageInit.defaultPageSize ?? 10 const data = shallowRef([]) const mode = ref('all') const paginData = computed(() => { if (mode.value === 'selection') { const name = sortParams.value.name const _selection = [...selection.value] if (!name) return _selection let _data = [..._selection].sort((a, b) => a[name] - b[name]) if (sortParams.value.order === 'desc') { _data = _data.reverse() } return _data } if (pageable === true) { const start = (pagination.currentPage - 1) * pagination.pageSize return data.value.slice(start, start + pagination.pageSize) } return data.value }) watch( paginData, (data) => { for (let i = 0; i < data.length; i++) { let it = data[i] it.__index = mode.value === 'selection' ? i + 1 : index(i) if (!isReactive(it)) { if (clazz) it = new clazz(it) data[i] = shallow ? shallowReactive(it) : reactive(it) } } }, { flush: 'sync' }, ) const pagination = reactive({ show: computed(() => mode.value === 'all' && pageable), total: 0, currentPage: 1, pageSize, }) const queryParams = ref(new clazz()) const sortParams = ref(defaultSorts ?? { order: undefined, name: undefined }) const { loading, exec: listAPI } = api.list ? useFetch(api.list, { method: 'post' }) : () => { throw new Error('未设置 list API') } const detailAPI = api.get ? useFetch(api.get, { method: 'get' }) : ({ id }) => Promise.resolve(data.value.find((it) => it.id === id) ?? undefined) const putAPI = api.create || api.save ? useFetch(api.create ?? api.save) : () => { throw new Error('未设置 create 或 save API') } const updateAPI = api.update || api.save ? useFetch(api.update ?? api.save) : () => { throw new Error('未设置 update 或 save API') } const deleteAPI = api.delete ? useFetch(api.delete) : () => { throw new Error('未设置 delete API') } const deletesAPI = api.deletes ? useFetch(api.deletes) : () => { throw new Error('未设置 deletes API') } const { loading: importing, exec: importsAPI } = api.imports ? useFetch(api.imports, { reqType: 'form-data', }) : () => { throw new Error('未设置 imports API') } const { loading: exporting, exec: exportsAPI } = api.exports ? useFetch(api.exports, { download: true, }) : () => { throw new Error('未设置 exports API') } let lastReq let $delay const fetch = (query = queryParams.value, sort = sortParams.value, pagin = pagination, reset = false) => { if (autoTrim) { for (const k of Object.keys(query)) { const v = query[k] if (typeof v === 'string') query[k] = v?.trim() } } if (!loading.value) { $delay = promiseTimeout(300) loading.value = true } queryParams.value = new clazz(query) sortParams.value = sort pagination.currentPage = reset ? 1 : pagin.currentPage ?? pagination.currentPage pagination.pageSize = pagin.pageSize ?? pagination.pageSize lastReq?.__abort?.abort() const abort = new AbortController() const _fetch = (lastReq = listAPI( pageInit.transformListParams( queryParams.value, sortParams.value, pageable === 'remote' ? pagination : undefined, ), { signal: abort.signal, }, ).then(async (resp) => { if (lastReq !== _fetch) return lastReq let _data if (pageable) { const respData = resp _data = respData.records pagination.total = respData.total || 0 if (_data.length === 0 && pagination.currentPage > 1) { return paginate(pagination.currentPage - 1) } } else { const respData = resp _data = respData pagination.total = respData.length || 0 } data.value = _data return resp })) lastReq.__abort = abort _fetch .catch((error) => { if (lastReq !== _fetch || error?.name === 'AbortError') return data.value = [] pagination.total = 0 }) .finally(async () => { if (lastReq !== _fetch) return await $delay loading.value = false }) return _fetch } const paginate = pageable ? (currentPage, pageSize) => { if (typeof currentPage !== 'number') { pageSize = currentPage.pageSize currentPage = currentPage.currentPage } pagination.pageSize = pageSize ?? pagination.pageSize pagination.currentPage = currentPage ?? pagination.currentPage if (pagination.total < (currentPage - 1) * pageSize + 1) { pagination.currentPage = Number.parseInt((pagination.total / pagination.pageSize).toString()) + 1 } return pageable === 'remote' ? fetch(undefined, undefined, pagination) : Promise.resolve({ total: pagination.total, records: paginData.value }) } : undefined if (pageable) { Object.assign(pagination, { onSizeChange: (pageSize) => paginate({ pageSize }), onCurrentChange: (currentPage) => paginate({ currentPage }), }) } const index = (i) => { return (pagination.currentPage - 1) * pagination.pageSize + i + 1 } const selection = shallowRef([]) const selectedKeys = ref([]) const selectedKeySet = reactive(new Set()) const select = (keysOrOps = Symbol.split, selected = true) => { let keys = keysOrOps if (keysOrOps !== Symbol.split && isObject(keysOrOps)) { keys = keysOrOps.keys ?? keysOrOps.key selected = keysOrOps.selected ?? true } if (Array.isArray(keys)) { for (const key of keys) select(key, selected) return } const id = keys if (id && typeof id !== 'boolean') { const index = selectedKeys.value.indexOf(id) if (index === -1 && selected) { selectedKeySet.add(id) selectedKeys.value.push(id) let row = paginData.value.find((it) => it.id === id) if (!row) row = data.value.find((it) => it.id === id) selection.value.push(row) } else if (index !== -1 && !selected) { selectedKeySet.delete(id) selectedKeys.value.splice(index, 1) selection.value.splice(index, 1) } } else { selected = id ?? true if (selected) { for (const row of paginData.value) { const include = selectedKeySet.has(row.id) if (!include) select(row.id, true) } } else { selectedKeySet.clear() selectedKeys.value.length = 0 selection.value.length = 0 } } } const _clone = (p) => (clazz ? new clazz(p) : Object.assign(Object.create(Object.getPrototypeOf(p)), p)) const curd = reactive({ get: async (idOrModel) => { let detail = await (detailAPI(typeof idOrModel === 'object' ? idOrModel : { id: idOrModel }) || Promise.resolve(typeof idOrModel === 'object' ? idOrModel : undefined)) if (clazz) { detail = new clazz(detail) } return detail }, create: (params) => { return putAPI(_clone(params)).then((ret) => { fetch() return ret }) }, update: (params) => { params = _clone(params) return updateAPI(params).then(async (ret) => { if (refreshAfterSave) { fetch() return ret } if (api.get) { params = await detailAPI({ id: params.id }) } else { params.updateDate = Date.now() params.updateUser = pageInit.getUsername() } for (let i = 0; i < data.value.length; i++) { if (data.value[i].id === params?.id) { data.value[i] = params break } } data.value = [...data.value] return ret }) }, save: (params) => { params = _clone(params) return params.id ? curd.update(params) : curd.create(params) }, delete: (idsOrModels) => { const ids = Array.isArray(idsOrModels) ? idsOrModels.map((idOrModel) => idOrModel?.id ?? idOrModel) : [idsOrModels?.id ?? idsOrModels] return (ids.length === 1 && api.delete ? deleteAPI({ id: ids[0] }) : deletesAPI(ids)).then( (ret) => { for (const id of ids) select(id, false) fetch() return ret }, ) }, imports: (fileOrParams, options) => { return importsAPI( fileOrParams instanceof Blob ? { file: fileOrParams } : fileOrParams, options, ).then((ret) => { fetch() return ret }) }, exports: (...p) => exportsAPI(...p), importing, exporting, }) let first = true ds = { // 数据 mode, pageable, shallow, pageSize, clazz, data: paginData, loading: readonly(loading), total: computed(() => pagination.total), load: fetch, refresh: (reset = false) => fetch(undefined, undefined, undefined, reset), query: (params) => fetch(params, undefined, undefined, first ? (first = false) : true), queries: toReactive(queryParams), sort: (name, order) => fetch(undefined, typeof name === 'string' ? { name, order } : name), sorts: toReactive(sortParams), clear() { data.value = [] pagination.total = 0 selection.value = [] selectedKeys.value = [] selectedKeySet.clear() if (pagination) { pagination.currentPage = 1 } }, curd, ...curd, // 分页 paginate, pagination: pageable ? pagination : undefined, index, selection, selectedKeys, selectedKeySet, select, } if (autoFetch && !ds.loading.value && ds.data.value.length === 0) { setTimeout(() => ds.load(), 1) } }) ds.__scope = scope ds.__rel_count = 0 dsCache[name] = ds } if (syncUrlParams) { const urlParams = useUrlSearchParams() syncCategory ||= 'query' syncCategory = typeof syncCategory === 'string' ? syncCategory === 'all' ? ['query', 'sort', 'pagin'] : [syncCategory] : syncCategory const excludeFields = new Set([ ...(syncExclude ?? []), '__ds_sort_order', '__ds_sort_name', '__ds_page_size', '__ds_page_number', ]) watch( urlParams, (urlParams) => { if (syncCategory.includes('query')) { for (const k of Object.keys(urlParams)) { if (syncInclude?.includes(k) === false || excludeFields.has(k)) continue if (`set${k}Str` in ds.queries) { ds.queries[`set${k}Str`](urlParams[k]) } else { ds.queries[k] = urlParams[k]?.trim() || undefined } } } if (syncCategory.includes('sort')) { Object.assign(ds.sorts, { order: urlParams.__ds_sort_order || undefined, name: urlParams.__ds_sort_name || undefined, }) } if (syncCategory.includes('pagin')) { Object.assign(ds.pagination, { pageSize: Number.parseInt(urlParams.__ds_page_size) || ds.pageSize, currentPage: Number.parseInt(urlParams.__ds_page_number) || 1, }) } }, { immediate: true, deep: true }, ) watch( [ syncCategory.includes('query') ? ds.queries : ref(1), syncCategory.includes('sort') ? ds.sorts : ref(1), syncCategory.includes('pagin') ? ds.pagination : ref(1), ], ([queries, sorts, pagination]) => { if (syncCategory.includes('query')) { for (const k of Object.keys(queries)) { if (syncInclude?.includes(k) === false || excludeFields.has(k)) continue urlParams[k] = (`get${k}Str` in queries ? queries[`get${k}Str`]() : queries[k]) || undefined } } if (syncCategory.includes('sort')) { urlParams.__ds_sort_order = sorts.order || undefined urlParams.__ds_sort_name = sorts.name || undefined } if (syncCategory.includes('pagin')) { urlParams.__ds_page_size = pagination.pageSize === ds.pageSize ? undefined : pagination.pageSize urlParams.__ds_page_number = pagination.currentPage <= 1 ? undefined : pagination.currentPage } }, { immediate: true, deep: true }, ) } ds.__rel_count++ onScopeDispose(() => { setTimeout(() => { if (--ds.__rel_count < 1) { delete dsCache[name] ds.__scope.stop() } }, 500) }) return ds }