@ithinkdt/page 4.0.19 → 4.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auto-imports.js CHANGED
@@ -1,7 +1,11 @@
1
1
  export const Page = [
2
2
  {
3
3
  from: '@ithinkdt/page',
4
- imports: ['useDs', 'useDataPagination', 'useFilterHelper', 'useFormHelper', 'useFormModal', 'useDeleteHelper', 'useSimpleCrud', 'useModal', 'useTableHelper', 'calcActionWidth', 'useDescriptionsHelper'],
4
+ imports: [
5
+ 'useDs', 'useDataPagination', 'useFilterHelper', 'useFormHelper', 'useFormModal', 'useDeleteHelper',
6
+ 'useSimpleCrud', 'useModal', 'useTableHelper', 'calcActionWidth', 'useDescriptionsHelper',
7
+ 'useListPageCustomization',
8
+ ],
5
9
  },
6
10
  ]
7
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ithinkdt/page",
3
- "version": "4.0.19",
3
+ "version": "4.0.20",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "iThinkDT Page",
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "@vueuse/core": "^14.3.0",
43
43
  "nanoid": "^5.1.11",
44
- "@ithinkdt/common": "^4.0.8"
44
+ "@ithinkdt/common": "^4.0.9"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "vue": ">=3.5",
@@ -49,9 +49,9 @@
49
49
  },
50
50
  "devDependencies": {
51
51
  "typescript": "~6.0.3",
52
- "vite": "^8.0.10",
52
+ "vite": "^8.0.14",
53
53
  "vue": "^3.5.34",
54
- "vue-router": "^5.0.6"
54
+ "vue-router": "^5.0.7"
55
55
  },
56
56
  "scripts": {
57
57
  "release": "pnpm publish --no-git-checks"
package/src/crud.js CHANGED
@@ -4,7 +4,7 @@ import { useFormModal } from './form.js'
4
4
  import { IgnoreRejectionError, PAGE_INJECTION, pageInjection } from './plugin.js'
5
5
 
6
6
  export function useDeleteHelper(options) {
7
- const { i18n: useI18n, keyField, getConfirmRenderer } = (hasInjectionContext() ? inject(PAGE_INJECTION, pageInjection) : pageInjection) ?? {}
7
+ const { i18n: useI18n, keyField, getConfirmRenderer } = hasInjectionContext() ? inject(PAGE_INJECTION, pageInjection) : pageInjection
8
8
  const { t } = useI18n()
9
9
 
10
10
  let onDel
@@ -136,7 +136,7 @@ function _useSimpleCrudModal(crudType, getItems, request, options) {
136
136
  }
137
137
 
138
138
  export function useSimpleCrud(options) {
139
- const { i18n: useI18n } = (hasInjectionContext() ? inject(PAGE_INJECTION, pageInjection) : pageInjection) ?? {}
139
+ const { i18n: useI18n } = hasInjectionContext() ? inject(PAGE_INJECTION, pageInjection) : pageInjection
140
140
  const { t } = useI18n()
141
141
 
142
142
  const createModal = _useSimpleCrudModal('create', options.createItems ?? options.formItems ?? options.items, options.post ?? options.save, options)
@@ -1,5 +1,5 @@
1
- import { toReactive } from '@vueuse/core'
2
- import { computed, hasInjectionContext, inject, reactive, ref, shallowReactive, unref } from 'vue'
1
+ import { toReactive, tryOnScopeDispose } from '@vueuse/core'
2
+ import { computed, effectScope, hasInjectionContext, inject, reactive, ref, shallowReactive, unref } from 'vue'
3
3
 
4
4
  import { isNone } from '@ithinkdt/common/object'
5
5
  import { uncapitalize } from '@ithinkdt/common/string'
@@ -103,10 +103,20 @@ export function useDescriptionsHelper(
103
103
  }
104
104
 
105
105
  const items0 = shallowReactive([])
106
+
107
+ let scope
108
+ tryOnScopeDispose(() => {
109
+ scope?.stop()
110
+ })
111
+
106
112
  function reinit() {
113
+ scope?.stop()
114
+ scope = effectScope()
107
115
  items0.length = 0
108
- // eslint-disable-next-line unicorn/no-array-callback-reference
109
- items0.push(...items({ it, group, model }).filter(Boolean).map(reactive))
116
+ scope.run(() => {
117
+ // eslint-disable-next-line unicorn/no-array-callback-reference
118
+ items0.push(...items({ it, group, model }).filter(Boolean).map(reactive))
119
+ })
110
120
  }
111
121
  reinit()
112
122
 
package/src/form.d.ts CHANGED
@@ -166,7 +166,18 @@ export interface FilterHelperOptions<Model extends {}> extends FormHelperOptions
166
166
  /**
167
167
  * 自定义
168
168
  */
169
- customizable?: boolean | string | undefined
169
+ customizable?: boolean | string | undefined | {
170
+ get: () => Awaitable<string | null>
171
+ set: (value: string) => Awaitable<void>
172
+ remove: () => Awaitable<void>
173
+ /**
174
+ * 事件总线,用于监听配置变化事件,若存在则不会监听 `storage` 事件
175
+ */
176
+ $change?: {
177
+ on: (callback: (payload: { newValue: unknown }) => void) => void
178
+ off?: (callback: (payload: { newValue: unknown }) => void) => void
179
+ }
180
+ }
170
181
  /**
171
182
  * 缓存表单
172
183
  */
package/src/form.js CHANGED
@@ -1,6 +1,6 @@
1
- import { StorageSerializers, toReactive, useStorage, useStorageAsync } from '@vueuse/core'
1
+ import { StorageSerializers, toReactive, tryOnScopeDispose, useEventListener, useStorage } from '@vueuse/core'
2
2
  import { nanoid } from 'nanoid'
3
- import { computed, h, hasInjectionContext, inject, reactive, readonly, ref, shallowReactive, shallowRef, toRaw, unref } from 'vue'
3
+ import { computed, effectScope, h, hasInjectionContext, inject, reactive, readonly, ref, shallowReactive, shallowRef, toRaw, unref, watch } from 'vue'
4
4
  import { useRoute } from 'vue-router'
5
5
 
6
6
  import { copy, isNone } from '@ithinkdt/common/object'
@@ -222,8 +222,17 @@ export function useFormHelper(items, { initial: initial0, rules, onChange, inFil
222
222
 
223
223
  const items1 = shallowReactive([])
224
224
 
225
+ let scope
226
+ tryOnScopeDispose(() => {
227
+ scope?.stop()
228
+ })
229
+
225
230
  function reinit() {
226
- items0.value = items({ model, reset, invalid, validation, validate, restoreValidation, it, group }).filter(Boolean)
231
+ scope?.stop()
232
+ scope = effectScope()
233
+ scope.run(() => {
234
+ items0.value = items({ model, reset, invalid, validation, validate, restoreValidation, it, group }).filter(Boolean)
235
+ })
227
236
 
228
237
  items1.length = 0
229
238
  for (const item of items0.value) {
@@ -338,11 +347,21 @@ export function useFilterHelper(items, { customizable = false, cached, cacheVers
338
347
 
339
348
  const custom = (params) => {
340
349
  if (typeof params === 'boolean') {
341
- if (params) customs.value = getDefaultCustom()
350
+ if (params) setCustoms(getDefaultCustom())
342
351
  } else if (Array.isArray(params)) {
343
- customs.value.sort = params
352
+ customs.value.sort = JSON.stringify(params) === JSON.stringify(returns.items.map(col => col.name))
353
+ ? []
354
+ : params
355
+ setCustoms()
344
356
  } else {
345
357
  customs.value.hidden[params.key] = 'hidden' in params ? params.hidden : customs.value.hidden[params.key]
358
+ if (!customs.value.hidden[params.key]) {
359
+ delete customs.value.hidden[params.key]
360
+ }
361
+ if (params.hidden) {
362
+ returns.model[params.key] = undefined
363
+ }
364
+ setCustoms()
346
365
  }
347
366
  const _items = returns.items
348
367
  .map((item, i, arr) => {
@@ -364,14 +383,65 @@ export function useFilterHelper(items, { customizable = false, cached, cacheVers
364
383
  return ret
365
384
  }
366
385
  if (customizable === true) {
367
- customizable = 'filter-form-customization#' + useRoute().name
386
+ customizable = `customization#${useRoute().name}#filter-form`
387
+ }
388
+
389
+ const customs = ref(getDefaultCustom())
390
+ let setCustoms = (value = customs.value) => {
391
+ customs.value = value
392
+ }
393
+ if (customizable) {
394
+ let key, storage
395
+ if (typeof customizable === 'object') {
396
+ key = `customization#${useRoute().name}#filter-form`
397
+ storage = {
398
+ get $change() { return customizable.$change },
399
+ getItem: () => customizable.get(),
400
+ setItem: (key, value) => customizable.set(value),
401
+ removeItem: () => customizable.remove(),
402
+ }
403
+ } else {
404
+ key = customizable
405
+ storage = customizationStorage
406
+ }
407
+
408
+ const read = async (raw) => {
409
+ raw ??= await storage.getItem(key)
410
+ if (raw === null) {
411
+ customs.value = getDefaultCustom()
412
+ return
413
+ }
414
+ const [sort, hidden] = StorageSerializers.object.read(raw)
415
+ customs.value = {
416
+ sort,
417
+ hidden: Object.fromEntries(hidden.map(k => [k, true])),
418
+ }
419
+ }
420
+ read().then(reinit)
421
+ if (storage.$change) {
422
+ const cb = (payload) => {
423
+ read(payload.newValue).then(reinit)
424
+ }
425
+ storage.$change.on(cb)
426
+ storage.$change.off && tryOnScopeDispose(() => storage.$change.off(cb))
427
+ } else {
428
+ useEventListener(globalThis, 'storage', (event) => {
429
+ if (event.key !== key) return
430
+ Promise.resolve().then(() => read(event.newValue)).then(reinit)
431
+ }, { passive: true })
432
+ }
433
+
434
+ setCustoms = async (value = customs.value) => {
435
+ customs.value = value
436
+ if (value === null) return storage.removeItem(key)
437
+ const sort = value.sort ?? []
438
+ const hidden = Object.keys(value.hidden ?? {}).filter(k => value.hidden[k])
439
+
440
+ if (sort.length === 0 && hidden.length === 0) return storage.removeItem(key)
441
+ const raw = StorageSerializers.object.write([sort, hidden])
442
+ return storage.setItem(key, raw)
443
+ }
368
444
  }
369
- const customs = customizable
370
- ? useStorageAsync(customizable, getDefaultCustom, customizationStorage, {
371
- onReady: reinit,
372
- serializer: StorageSerializers.object,
373
- })
374
- : ref(getDefaultCustom())
375
445
 
376
446
  if (!customizable) {
377
447
  reinit()
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './plugin.js'
2
2
  export * from './data-source.js'
3
3
  export * from './description.js'
4
4
  export * from './form.js'
5
+ export * from './list.js'
5
6
  export * from './crud.js'
6
7
  export * from './table.js'
7
8
  export * from './modal.js'
package/src/list.d.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { Falsely } from '@ithinkdt/common/typed'
1
+ import { EventHook } from '@vueuse/core'
2
+ import { Ref } from 'vue'
3
+
4
+ import { Awaitable, Falsely } from '@ithinkdt/common/typed'
2
5
 
3
6
  import { FormItemHelper, FormItemOptions } from './form.js'
4
7
  import { TableColumnHelper, TableColumnOptions } from './table.js'
@@ -8,3 +11,108 @@ export interface ListPageConfig<Model extends {}> {
8
11
  crud?: (FormItemOptions<Partial<Model>> | Falsely)[] | ((helper: FormItemHelper<Model> & { type: 'create' | 'edit' | 'view' }) => (FormItemOptions<Partial<Model>> | Falsely)[])
9
12
  table?: (TableColumnOptions<Model> | Falsely)[] | ((helper: TableColumnHelper<Model>) => (TableColumnOptions<Model> | Falsely)[])
10
13
  }
14
+
15
+ export interface CustomAsync {
16
+ get: () => Awaitable<string | null>
17
+ set: (value: string) => Awaitable<void>
18
+ remove: () => Awaitable<void>
19
+ $change: EventHook<{ newValue: unknown }>
20
+ }
21
+
22
+ export interface CustomProfile {
23
+ key: string
24
+ name: string | undefined
25
+ locked?: boolean
26
+ }
27
+
28
+ export interface Customizer {
29
+ profiles: Readonly<CustomProfile[]>
30
+ get profile(): string
31
+ set profile(value: string | null | undefined)
32
+ get preference(): string | undefined
33
+ set preference(value: string | null | undefined)
34
+
35
+ get changing(): boolean
36
+
37
+ tableColumn: CustomAsync
38
+ filterForm: CustomAsync
39
+
40
+ /**
41
+ * 复制配置
42
+ *
43
+ * @param key - 配置键
44
+ * @param name - 新的配置名
45
+ * @returns 新的配置键
46
+ */
47
+ copy(this: void, key: string, name?: string): Promise<string>
48
+
49
+ /**
50
+ * 保存配置
51
+ *
52
+ * @param name - 新的配置名
53
+ * @param key - 配置键,无则更新当前配置
54
+ */
55
+ save(this: void, name: string, key?: string): void
56
+ /**
57
+ * 保存配置
58
+ */
59
+ save(this: void, params: {
60
+ /**
61
+ * 新的配置名
62
+ */
63
+ name: string
64
+ /**
65
+ * 配置键,无则更新当前配置
66
+ */
67
+ key?: string
68
+ }): void
69
+ /**
70
+ * 移除配置
71
+ *
72
+ * @param key - 配置键
73
+ */
74
+ remove(this: void, key: string): void
75
+
76
+ /**
77
+ * 锁定配置
78
+ * @param key - 配置键
79
+ * @param locked - 是否锁定
80
+ */
81
+ lock(this: void, key: string, locked?: boolean): void
82
+ /**
83
+ * 锁定配置
84
+ */
85
+ lock(this: void, params: {
86
+ /**
87
+ * 配置键
88
+ */
89
+ key: string
90
+ /**
91
+ * 是否锁定
92
+ */
93
+ locked?: boolean
94
+ }): void
95
+ }
96
+
97
+ export declare function useListPageCustomization(
98
+ options?: {
99
+ /**
100
+ * 是否持久化配置
101
+ */
102
+ persist?: boolean | 'manual'
103
+ /**
104
+ * 配置键,默认取路由 name
105
+ */
106
+ key?: string
107
+ /**
108
+ * 从路由 params 取当前配置的参数名
109
+ */
110
+ routeParamName?: string
111
+ },
112
+ ): Omit<Customizer, 'profiles' | 'profile' | 'default' | 'changing'> & {
113
+ customizer: Customizer
114
+ profiles: Ref<CustomProfile[]>
115
+ profile: Ref<string, string | null | undefined>
116
+ preference: Ref<string | undefined, string | null | undefined>
117
+ changing: Ref<boolean>
118
+ }
package/src/list.js CHANGED
@@ -1,2 +1,249 @@
1
- /* eslint-disable unicorn/require-module-specifiers */
2
- export {}
1
+ import { StorageSerializers, createEventHook, tryOnScopeDispose, useStorageAsync } from '@vueuse/core'
2
+ import { nanoid } from 'nanoid'
3
+ import { computed, hasInjectionContext, inject, markRaw, nextTick, reactive, ref } from 'vue'
4
+ import { useRoute } from 'vue-router'
5
+
6
+ import { debounce } from '@ithinkdt/common/fn'
7
+
8
+ import { PAGE_INJECTION, pageInjection } from './plugin.js'
9
+
10
+ class CustomStorage {
11
+ constructor(type, storage, key, profile, { onSet } = {}) {
12
+ this.type = type
13
+ this.storage = storage
14
+ this.key = key
15
+ this.profile = profile
16
+ this.onSet = onSet
17
+
18
+ this.$change = createEventHook()
19
+ }
20
+
21
+ get storeKey() {
22
+ return `customization#${this.key}~${this.profile}#${this.type}`
23
+ }
24
+
25
+ get() {
26
+ return this.storage.getItem(this.storeKey)
27
+ }
28
+
29
+ async set(value) {
30
+ await this.onSet?.(value, this)
31
+ const oldValue = await this.get()
32
+ if (value === oldValue) return
33
+ await this.storage.setItem(this.storeKey, value)
34
+ }
35
+
36
+ remove() {
37
+ return this.storage.removeItem(this.storeKey)
38
+ }
39
+ }
40
+
41
+ export function useListPageCustomization({
42
+ persist = true,
43
+ key,
44
+ routeParamName = 'profile',
45
+ } = {}) {
46
+ const route = useRoute()
47
+ key ??= route.name
48
+
49
+ const { customizationStorage = localStorage, i18n: useI18n } = hasInjectionContext() ? inject(PAGE_INJECTION, pageInjection) : pageInjection
50
+ const { t } = useI18n()
51
+
52
+ let storage
53
+ if (persist) {
54
+ storage = customizationStorage
55
+ } else {
56
+ const store = {}
57
+ storage = {
58
+ getItem: key => store[key] ?? null,
59
+ setItem: (key, value) => { store[key] = value },
60
+ removeItem: (key) => { delete store[key] },
61
+ }
62
+ }
63
+
64
+ const { promise: ready, resolve } = Promise.withResolvers()
65
+
66
+ const changing = ref(false)
67
+
68
+ const debounceResetChanged = debounce(() => {
69
+ changing.value = false
70
+ }, 2000)
71
+
72
+ const getDefaultName = (i, namePrefix = t('common.page.custom.profile')) => {
73
+ const name = `${namePrefix}${i}`
74
+ if (config.value.profiles.some(profile => profile.name === name)) return getDefaultName(i + 1, namePrefix)
75
+ return name
76
+ }
77
+
78
+ const onSet = async (newValue, storage) => {
79
+ const profile = config.value.profiles.find(profile => profile.key === storage.profile)
80
+ if (profile?.locked) {
81
+ const profileName = getDefaultName(1, profile.key === 'default' ? undefined : profile?.name)
82
+ const profileKey = nanoid()
83
+ const index = config.value.profiles.findIndex(profile => profile.key === storage.profile)
84
+ config.value.profiles.splice(index + 1, 0, { key: profileKey, name: profileName })
85
+ await switchProfile(profileKey, false)
86
+ }
87
+ changing.value = true
88
+ debounceResetChanged()
89
+ }
90
+
91
+ const filterStorage = new CustomStorage('filter-form', storage, key, 'default', { onSet })
92
+ const tableStorage = new CustomStorage('table-column', storage, key, 'default', { onSet })
93
+ tryOnScopeDispose(() => {
94
+ filterStorage.$event.clear()
95
+ tableStorage.$event.clear()
96
+ })
97
+
98
+ let laskKey = 'default'
99
+ async function switchProfile(key, emitChange) {
100
+ if (!key) {
101
+ await ready
102
+ key = config.value.preference ?? config.value.latest ?? 'default'
103
+ }
104
+
105
+ emitChange ??= key !== laskKey
106
+ laskKey = key
107
+ config.value.latest = key
108
+ filterStorage.profile = key
109
+ tableStorage.profile = key
110
+
111
+ if (!emitChange) return
112
+ const [filterForm, tableColumn] = await Promise.all([
113
+ filterStorage.get(),
114
+ tableStorage.get(),
115
+ ])
116
+ filterStorage.$change.trigger({
117
+ newValue: filterForm,
118
+ })
119
+ tableStorage.$change.trigger({
120
+ newValue: tableColumn,
121
+ })
122
+ }
123
+
124
+ const config = useStorageAsync(
125
+ `customization#${key}#profile-info`,
126
+ {
127
+ latest: 'default',
128
+ preference: undefined,
129
+ profiles: [
130
+ { key: 'default', locked: true },
131
+ ],
132
+ },
133
+ storage,
134
+ {
135
+ writeDefaults: false,
136
+ serializer: StorageSerializers.object,
137
+ onReady() {
138
+ resolve()
139
+ nextTick(() => {
140
+ config.value.profiles[0].name = t('common.page.custom.profileDefault')
141
+ const key = route.params[routeParamName]
142
+ switchProfile(key)
143
+ })
144
+ },
145
+ },
146
+ )
147
+
148
+ const profile = computed({
149
+ get: () => config.value.latest ?? 'default',
150
+ set: (key) => {
151
+ if (key !== config.value.latest) switchProfile(key)
152
+ },
153
+ })
154
+ const preference = computed({
155
+ get: () => config.value.preference,
156
+ set: (key) => {
157
+ config.value.preference = key ?? undefined
158
+ },
159
+ })
160
+
161
+ function removeProfile(key) {
162
+ const removeProfile = config.value.profiles.find(profile => profile.key === key)
163
+
164
+ let newProfiles = config.value.profiles
165
+ if (removeProfile) {
166
+ newProfiles = config.value.profiles.filter(profile => profile.key !== key)
167
+ }
168
+ config.value.profiles = newProfiles
169
+ if (preference.value === key) {
170
+ preference.value = undefined
171
+ }
172
+
173
+ if (profile.value === key) {
174
+ config.value.latest = undefined
175
+ switchProfile()
176
+ }
177
+
178
+ storage.removeItem(
179
+ `customization#${filterStorage.key}~${key}#${filterStorage.type}`,
180
+ )
181
+ storage.removeItem(
182
+ `customization#${tableStorage.key}~${key}#${tableStorage.type}`,
183
+ )
184
+ }
185
+
186
+ function saveProfile(name, key) {
187
+ if (typeof name === 'object') {
188
+ name = name.name
189
+ key = name.key
190
+ }
191
+ key ??= config.value.latest
192
+ const profile = config.value.profiles.find(profile => profile.key === key)
193
+ if (!profile) {
194
+ console.debug(`[customizer] profile not found: ${key}`)
195
+ return
196
+ }
197
+ profile.name = name
198
+ }
199
+
200
+ async function copyProfile(key, name) {
201
+ const index = config.value.profiles.findIndex(profile => profile.key === key)
202
+ if (index === -1) {
203
+ console.debug(`[customizer] profile not found: ${key}`)
204
+ return
205
+ }
206
+ const newProfile = { key: nanoid(), name: name ?? getDefaultName(1, config.value.profiles[index].name) }
207
+ config.value.profiles.splice(index + 1, 0, newProfile)
208
+ await Promise.all([
209
+ storage.setItem(
210
+ `customization#${filterStorage.key}~${newProfile.key}#${filterStorage.type}`,
211
+ await storage.getItem(`customization#${filterStorage.key}~${key}#${filterStorage.type}`),
212
+ ),
213
+ storage.setItem(
214
+ `customization#${tableStorage.key}~${newProfile.key}#${tableStorage.type}`,
215
+ await storage.getItem(`customization#${tableStorage.key}~${key}#${tableStorage.type}`),
216
+ ),
217
+ ])
218
+ profile.value = newProfile.key
219
+ return newProfile.key
220
+ }
221
+
222
+ function lockProfile(key, locked) {
223
+ if (typeof key === 'object') {
224
+ key = key.key
225
+ locked = key.locked
226
+ }
227
+ const profile = config.value.profiles.find(profile => profile.key === key)
228
+ if (!profile) {
229
+ console.debug(`[customizer] profile not found: ${key}`)
230
+ return
231
+ }
232
+ profile.locked = locked
233
+ }
234
+
235
+ const returns = {
236
+ profiles: computed(() => config.value.profiles),
237
+ profile,
238
+ preference,
239
+ changing,
240
+ remove: removeProfile,
241
+ save: saveProfile,
242
+ copy: copyProfile,
243
+ lock: lockProfile,
244
+ filterForm: markRaw(filterStorage),
245
+ tableColumn: markRaw(tableStorage),
246
+ }
247
+ returns.customizer = reactive(returns)
248
+ return returns
249
+ }
package/src/modal.js CHANGED
@@ -1,4 +1,4 @@
1
- import { tryOnUnmounted } from '@vueuse/core'
1
+ import { tryOnScopeDispose } from '@vueuse/core'
2
2
  import { nanoid } from 'nanoid'
3
3
  import { computed, defineComponent, h, hasInjectionContext, inject, isVNode, provide, reactive, ref, toValue, unref } from 'vue'
4
4
 
@@ -120,7 +120,7 @@ export function useModal({ content, confirmText, confirmLoading, cancelText, can
120
120
 
121
121
  const removeModal = addModal(h(Wrapper, { key: nanoid() }))
122
122
 
123
- tryOnUnmounted(removeModal)
123
+ tryOnScopeDispose(removeModal)
124
124
  const returns = [open, close]
125
125
  returns.open = open
126
126
  returns.close = close
package/src/plugin.d.ts CHANGED
@@ -31,6 +31,8 @@ export interface PageOptions extends PresetDataSourceOptions {
31
31
  'common.page.table.presetAction.editTitle': string
32
32
  'common.page.table.presetAction.viewTitle': string
33
33
  'common.page.table.presetAction.deleteTitle': string
34
+ 'common.page.custom.profile': string
35
+ 'common.page.custom.profileDefault': string
34
36
  }>
35
37
 
36
38
  getConfirmRenderer: () => ((params: CrudConfirmParams) => void)
@@ -49,14 +51,21 @@ export interface PageOptions extends PresetDataSourceOptions {
49
51
  index: number,
50
52
  ) => VNodeChild)
51
53
  /**
52
- * 是否默认缓存 filter 表单数据
54
+ * 自定义配置存储实现,默认使用 localStorage
53
55
  */
54
- defaultFilterCached?: boolean
56
+ customizationStorage?: StorageLikeAsync & {
57
+ /**
58
+ * 事件总线,用于监听配置变化事件,若存在则不会监听 `storage` 事件
59
+ */
60
+ $change?: {
61
+ on: (callback: (payload: { newValue: unknown }) => void) => void
62
+ off?: (callback: (payload: { newValue: unknown }) => void) => void
63
+ }
64
+ }
55
65
  /**
56
- * 自定义数据存储实现,默认使用 localStorage
66
+ * 是否默认缓存 filter 表单数据
57
67
  */
58
- customizationStorage?: StorageLikeAsync
59
-
68
+ defaultFilterCached?: boolean
60
69
  /**
61
70
  * 自定义缓存实现,默认使用 sessionStorage
62
71
  */
package/src/table.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MaybeRef, MaybeRefOrGetter, Ref, VNodeChild } from 'vue'
2
2
 
3
- import { Falsely } from '@ithinkdt/common/typed'
3
+ import { Awaitable, Falsely } from '@ithinkdt/common/typed'
4
4
 
5
5
  import { DescriptionTypePresets } from './description'
6
6
 
@@ -161,7 +161,18 @@ export interface TableOptions<Model extends {}> {
161
161
  actionTitle?: MaybeRefOrGetter<VNodeChild>
162
162
  actionWidth?: MaybeRef<number | string | undefined> | ((actionTexts: string[]) => number | string)
163
163
  actionHidden?: MaybeRef<boolean | undefined>
164
- customizable?: boolean | string | undefined
164
+ customizable?: boolean | string | undefined | {
165
+ get: () => Awaitable<string | null>
166
+ set: (value: string) => Awaitable<void>
167
+ remove: () => Awaitable<void>
168
+ /**
169
+ * 事件总线,用于监听配置变化事件,若存在则不会监听 `storage` 事件
170
+ */
171
+ $change?: {
172
+ on: (callback: (payload: { newValue: unknown }) => void) => void
173
+ off?: (callback: (payload: { newValue: unknown }) => void) => void
174
+ }
175
+ }
165
176
  }
166
177
 
167
178
  export interface TableHelper<Model extends {}> {
package/src/table.js CHANGED
@@ -1,6 +1,6 @@
1
- import { StorageSerializers, useStorageAsync } from '@vueuse/core'
1
+ import { StorageSerializers, tryOnScopeDispose, useEventListener } from '@vueuse/core'
2
2
  import { nanoid } from 'nanoid'
3
- import { computed, hasInjectionContext, inject, reactive, ref, shallowReactive, shallowRef, toValue, unref } from 'vue'
3
+ import { computed, effectScope, hasInjectionContext, inject, reactive, ref, shallowReactive, shallowRef, toValue, unref } from 'vue'
4
4
  import { useRoute } from 'vue-router'
5
5
 
6
6
  import { isNone } from '@ithinkdt/common/object'
@@ -126,13 +126,26 @@ export function useTableHelper(columns, { customizable, actions, actionWidth, ac
126
126
 
127
127
  const custom = (params) => {
128
128
  if (typeof params === 'boolean') {
129
- if (params) customs.value = getDefaultCustom()
129
+ if (params) setCustoms(getDefaultCustom())
130
130
  } else if (Array.isArray(params)) {
131
- customs.value.sort = params
131
+ customs.value.sort = JSON.stringify(params) === JSON.stringify(columns0.value.map(col => col.key))
132
+ ? []
133
+ : params
134
+ setCustoms()
132
135
  } else {
133
136
  customs.value.hidden[params.key] = 'hidden' in params ? params.hidden : customs.value.hidden[params.key]
134
137
  customs.value.fixed[params.key] = 'fixed' in params ? params.fixed : customs.value.fixed[params.key]
135
138
  customs.value.width[params.key] = 'width' in params ? params.width : customs.value.width[params.key]
139
+ if (!customs.value.hidden[params.key]) {
140
+ delete customs.value.hidden[params.key]
141
+ }
142
+ if (!customs.value.fixed[params.key]) {
143
+ delete customs.value.fixed[params.key]
144
+ }
145
+ if (!customs.value.width[params.key]) {
146
+ delete customs.value.width[params.key]
147
+ }
148
+ setCustoms()
136
149
  }
137
150
  const columns2 = columns0.value.map((col, i, arr) => {
138
151
  const index = customs.value.sort.indexOf(col.key)
@@ -149,140 +162,205 @@ export function useTableHelper(columns, { customizable, actions, actionWidth, ac
149
162
  columns1.push(...columns2.toSorted((a, b) => a.__order - b.__order))
150
163
  }
151
164
 
165
+ let scope
166
+ tryOnScopeDispose(() => {
167
+ scope?.stop()
168
+ })
169
+
152
170
  function reinit() {
153
- columns0.value = columns({ col, cols, group }).filter(Boolean)
171
+ scope?.stop()
172
+ scope = effectScope()
173
+ scope.run(() => {
174
+ columns0.value = columns({ col, cols, group }).filter(Boolean)
154
175
 
155
- if (actions) {
156
- if (Array.isArray(actions)) {
157
- for (const act of actions) {
158
- if (!act.preset) continue
159
- act.title ??= act.preset === 'edit'
160
- ? () => t('common.page.table.presetAction.editTitle')
161
- : act.preset === 'view'
162
- ? () => t('common.page.table.presetAction.viewTitle')
163
- : act.preset === 'delete'
164
- ? () => t('common.page.table.presetAction.deleteTitle')
165
- : ''
166
- act.text ??= act.preset === 'edit'
167
- ? () => t('common.page.crud.editTitle')
168
- : act.preset === 'view'
169
- ? () => t('common.page.crud.viewTitle')
170
- : act.preset === 'delete'
171
- ? () => t('common.page.crud.deleteTitle')
172
- : ''
176
+ if (actions) {
177
+ if (Array.isArray(actions)) {
178
+ for (const act of actions) {
179
+ if (!act.preset) continue
180
+ act.title ??= act.preset === 'edit'
181
+ ? () => t('common.page.table.presetAction.editTitle')
182
+ : act.preset === 'view'
183
+ ? () => t('common.page.table.presetAction.viewTitle')
184
+ : act.preset === 'delete'
185
+ ? () => t('common.page.table.presetAction.deleteTitle')
186
+ : ''
187
+ act.text ??= act.preset === 'edit'
188
+ ? () => t('common.page.crud.editTitle')
189
+ : act.preset === 'view'
190
+ ? () => t('common.page.crud.viewTitle')
191
+ : act.preset === 'delete'
192
+ ? () => t('common.page.crud.deleteTitle')
193
+ : ''
173
194
 
174
- act.color ??= act.preset === 'delete' ? 'danger' : 'primary'
195
+ act.color ??= act.preset === 'delete' ? 'danger' : 'primary'
196
+ }
175
197
  }
176
- }
177
198
 
178
- let width
179
- if (typeof actionWidth === 'function') {
180
- if (typeof actions === 'function') {
181
- console.warn(`[table] actions 为函数时,actionWidth 不能为函数!`)
199
+ let width
200
+ if (typeof actionWidth === 'function') {
201
+ if (typeof actions === 'function') {
202
+ console.warn(`[table] actions 为函数时,actionWidth 不能为函数!`)
203
+ } else {
204
+ width = computed(() => actionWidth(
205
+ actions.filter(act => unref(act.auth) !== false).map(act => toValue(act.text))),
206
+ )
207
+ }
182
208
  } else {
183
- width = computed(() => actionWidth(
184
- actions.filter(act => unref(act.auth) !== false).map(act => toValue(act.text))),
185
- )
209
+ width = actionWidth ?? 140
186
210
  }
187
- } else {
188
- width = actionWidth ?? 140
211
+
212
+ columns0.value.push(
213
+ col(
214
+ '$actions',
215
+ actionTitle ?? (() => t('common.page.table.actionTitle')),
216
+ typeof actions === 'function'
217
+ ? (value, record, i) => actions(record, i)
218
+ : (value, record, i) => {
219
+ const action0 = actions.filter((act) => {
220
+ return unref(act.auth) !== false && act.hidden?.(record, i) !== true
221
+ })
222
+ .map(act => ({
223
+ ...act,
224
+ text: toValue(act.text),
225
+ title: toValue(act.title),
226
+ disabled: act.disabled?.(record, i),
227
+ color: typeof act.color === 'function' ? act.color(record, i) : act.color,
228
+ onClick: ev => act.onClick?.(record, i, ev),
229
+ }))
230
+ return getTableActionsRenderer()(action0, record, i)
231
+ },
232
+ {
233
+ fixed: 'right',
234
+ width,
235
+ ellipsis: false,
236
+ hidden: computed(() => unref(actionHidden) ?? (Array.isArray(actions) && actions.length === 0)),
237
+ },
238
+ ),
239
+ )
189
240
  }
190
241
 
191
- columns0.value.push(
192
- col(
193
- '$actions',
194
- actionTitle ?? (() => t('common.page.table.actionTitle')),
195
- typeof actions === 'function'
196
- ? (value, record, i) => actions(record, i)
197
- : (value, record, i) => {
198
- const action0 = actions.filter((act) => {
199
- return unref(act.auth) !== false && act.hidden?.(record, i) !== true
200
- })
201
- .map(act => ({
202
- ...act,
203
- text: toValue(act.text),
204
- title: toValue(act.title),
205
- disabled: act.disabled?.(record, i),
206
- color: typeof act.color === 'function' ? act.color(record, i) : act.color,
207
- onClick: ev => act.onClick?.(record, i, ev),
208
- }))
209
- return getTableActionsRenderer()(action0, record, i)
210
- },
242
+ if (index) {
243
+ columns0.value.unshift(
244
+ col(
245
+ '$index',
246
+ indexTitle ?? (() => t('common.page.table.indexTitle')),
247
+ typeof actions === 'function'
248
+ ? (value, record, i) => index(i, record)
249
+ : (value, record, i) => {
250
+ const base = unref(index) === true ? 0 : unref(index)
251
+ return (base + i + 1).toString()
252
+ },
253
+ {
254
+ hidden: computed(() => unref(index) === false),
255
+ width: 70,
256
+ maxWidth: 100,
257
+ align: 'center',
258
+ fixed: 'left',
259
+ },
260
+ ),
261
+ )
262
+ }
263
+ if (expandable) {
264
+ columns0.value.unshift(
211
265
  {
212
- fixed: 'right',
213
- width,
214
- ellipsis: false,
215
- hidden: computed(() => unref(actionHidden) ?? (Array.isArray(actions) && actions.length === 0)),
266
+ key: '$expand',
267
+ prop: '$expand',
268
+ type: 'expand',
269
+ expandable: typeof expandable === 'function' ? expandable : undefined,
270
+ renderExpand,
271
+ hidden: computed(() => unref(expandable) === false),
272
+ fixed: 'left',
273
+ visible: false,
216
274
  },
217
- ),
218
- )
219
- }
220
-
221
- if (index) {
222
- columns0.value.unshift(
223
- col(
224
- '$index',
225
- indexTitle ?? (() => t('common.page.table.indexTitle')),
226
- typeof actions === 'function'
227
- ? (value, record, i) => index(i, record)
228
- : (value, record, i) => {
229
- const base = unref(index) === true ? 0 : unref(index)
230
- return (base + i + 1).toString()
231
- },
275
+ )
276
+ }
277
+ if (selectable) {
278
+ columns0.value.unshift(
232
279
  {
233
- hidden: computed(() => unref(index) === false),
234
- width: 70,
235
- maxWidth: 100,
236
- align: 'center',
280
+ key: '$selection',
281
+ prop: '$selection',
282
+ type: 'selection',
283
+ selectType,
284
+ selectMenus,
285
+ selectable: typeof selectable === 'function' ? selectable : undefined,
286
+ hidden: computed(() => unref(selectable) === false),
237
287
  fixed: 'left',
288
+ width: 50,
289
+ maxWidth: 50,
290
+ visible: false,
238
291
  },
239
- ),
240
- )
241
- }
242
- if (expandable) {
243
- columns0.value.unshift(
244
- {
245
- key: '$expand',
246
- prop: '$expand',
247
- type: 'expand',
248
- expandable: typeof expandable === 'function' ? expandable : undefined,
249
- renderExpand,
250
- hidden: computed(() => unref(expandable) === false),
251
- fixed: 'left',
252
- visible: false,
253
- },
254
- )
255
- }
256
- if (selectable) {
257
- columns0.value.unshift(
258
- {
259
- key: '$selection',
260
- prop: '$selection',
261
- type: 'selection',
262
- selectType,
263
- selectMenus,
264
- selectable: typeof selectable === 'function' ? selectable : undefined,
265
- hidden: computed(() => unref(selectable) === false),
266
- fixed: 'left',
267
- width: 50,
268
- maxWidth: 50,
269
- visible: false,
270
- },
271
- )
272
- }
292
+ )
293
+ }
294
+ })
273
295
 
274
296
  custom(false)
275
297
  }
276
298
 
277
299
  if (customizable === true) {
278
- customizable = 'table-column-customization#' + useRoute().name
300
+ customizable = `customization#${useRoute().name}#table-column`
301
+ }
302
+ const customs = ref(getDefaultCustom())
303
+ let setCustoms = (value = customs.value) => {
304
+ customs.value = value
305
+ }
306
+ if (customizable) {
307
+ let key, storage
308
+ if (typeof customizable === 'object') {
309
+ key = `customization#${useRoute().name}#table-column`
310
+ storage = {
311
+ get $change() { return customizable.$change },
312
+ getItem: () => customizable.get(),
313
+ setItem: (key, value) => customizable.set(value),
314
+ removeItem: () => customizable.remove(),
315
+ }
316
+ } else {
317
+ key = customizable
318
+ storage = customizationStorage
319
+ }
320
+
321
+ const read = async (raw) => {
322
+ if (raw === undefined) {
323
+ raw = await storage.getItem(key)
324
+ }
325
+ if (raw === null) {
326
+ customs.value = getDefaultCustom()
327
+ return
328
+ }
329
+ const [sort, hidden, fixed, width] = StorageSerializers.object.read(raw)
330
+ customs.value = {
331
+ sort,
332
+ hidden: Object.fromEntries(hidden.map(k => [k, true])),
333
+ fixed: Object.fromEntries(fixed.map(k => [k, true])),
334
+ width,
335
+ }
336
+ }
337
+ read().then(reinit)
338
+ if (storage.$change) {
339
+ const cb = (payload) => {
340
+ read(payload.newValue).then(reinit)
341
+ }
342
+ storage.$change.on(cb)
343
+ storage.$change.off && tryOnScopeDispose(() => storage.$change.off(cb))
344
+ } else {
345
+ useEventListener(globalThis, 'storage', (event) => {
346
+ if (event.key !== key) return
347
+ Promise.resolve().then(() => read(event.newValue)).then(reinit)
348
+ }, { passive: true })
349
+ }
350
+
351
+ setCustoms = async (value = customs.value) => {
352
+ customs.value = value
353
+ if (value === null) return storage.removeItem(key)
354
+ const sort = value.sort ?? []
355
+ const hidden = Object.keys(value.hidden ?? {}).filter(k => value.hidden[k])
356
+ const fixed = Object.keys(value.fixed ?? {}).filter(k => value.fixed[k])
357
+ const width = value.width ?? {}
358
+
359
+ if (sort.length === 0 && hidden.length === 0 && fixed.length === 0 && Object.keys(width).length === 0) return storage.removeItem(key)
360
+ const raw = StorageSerializers.object.write([sort, hidden, fixed, width])
361
+ return storage.setItem(key, raw)
362
+ }
279
363
  }
280
- const customs = customizable
281
- ? useStorageAsync(customizable, getDefaultCustom, customizationStorage, {
282
- onReady: reinit,
283
- serializer: StorageSerializers.object,
284
- })
285
- : ref(getDefaultCustom())
286
364
 
287
365
  if (!customizable) {
288
366
  reinit()