@ithinkdt/core
Version:
iThinkDT Core
503 lines (452 loc) • 18.9 kB
JavaScript
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
}