committed by
GitHub
455 changed files with 26994 additions and 8712 deletions
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben/layouts': patch |
||||
|
--- |
||||
|
|
||||
|
fix route spinner timing during fast and overlapping navigation |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben/common-ui': patch |
||||
|
--- |
||||
|
|
||||
|
fix: forward ApiComponent updates for custom model value props |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben-core/preferences': patch |
||||
|
--- |
||||
|
|
||||
|
fix: apply preference reset theme updates before cache persistence |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben-core/form-ui': patch |
||||
|
--- |
||||
|
|
||||
|
fix(@vben-core/form-ui): 字段名与 <form> 固有属性冲突时剥离原生 name(#8214) |
||||
@ -1,7 +0,0 @@ |
|||||
--- |
|
||||
'@vben/styles': patch |
|
||||
'@vben-core/form-ui': patch |
|
||||
'@vben/web-naive': patch |
|
||||
--- |
|
||||
|
|
||||
feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray |
|
||||
@ -0,0 +1,6 @@ |
|||||
|
--- |
||||
|
'@vben/web-antdv-next': patch |
||||
|
'@vben/playground': patch |
||||
|
--- |
||||
|
|
||||
|
fix: bind VbenTiptap through the standard Vue model protocol |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben/layouts': patch |
||||
|
--- |
||||
|
|
||||
|
fix(@vben/layouts): keep the notification status indicator circular across theme radii |
||||
@ -0,0 +1,7 @@ |
|||||
|
--- |
||||
|
'@vben-core/layout-ui': patch |
||||
|
--- |
||||
|
|
||||
|
fix(@vben-core/layout-ui): guard sidebar hover handlers in mobile drawer mode |
||||
|
|
||||
|
移动端抽屉模式不存在 hover 语义。resize 跨断点时浏览器会对正在卸载/重排的侧栏派发合成 mouseenter/mouseleave,`handleMouseleave` 缺少 `isMobile` 守卫会把折叠态写入 `collapse` 并经 v-model 链持久化,导致窗口放大后侧栏保持折叠(#8274)。本次为 `handleMouseenter`/`handleMouseleave` 增加 `isMobile` 守卫,并附 4 项回归测试(移动端 mouseenter/mouseleave 不写状态、桌面端行为不变)。 |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben-core/preferences': patch |
||||
|
--- |
||||
|
|
||||
|
fix(@vben-core/preferences): avoid unscoped localStorage before initialization |
||||
@ -0,0 +1,5 @@ |
|||||
|
--- |
||||
|
'@vben/plugins': patch |
||||
|
--- |
||||
|
|
||||
|
fix(@vben/plugins): keep vxe toolbar options stable when slot content updates |
||||
@ -0,0 +1,3 @@ |
|||||
|
# @vben/backend-mock |
||||
|
|
||||
|
## 5.8.0 |
||||
@ -0,0 +1,23 @@ |
|||||
|
# @vben/web-antd |
||||
|
|
||||
|
## 5.8.0 |
||||
|
|
||||
|
### Patch Changes |
||||
|
|
||||
|
- [#8308](https://github.com/vbenjs/vue-vben-admin/pull/8308) [`81f179e`](https://github.com/vbenjs/vue-vben-admin/commit/81f179ed4c39cffe05aa0243843679ffd5c13dcd) Thanks [@kilisamemarisaaa](https://github.com/kilisamemarisaaa)! - fix(apps): pass withCredentials as request config for refresh/logout |
||||
|
|
||||
|
- Updated dependencies [[`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998), [`1d95f29`](https://github.com/vbenjs/vue-vben-admin/commit/1d95f298bdce48a1f3d4cec1ed1f182bfe265268), [`390efe9`](https://github.com/vbenjs/vue-vben-admin/commit/390efe9f604d7994f82156b89b0a76f61788a5d9), [`728ba5f`](https://github.com/vbenjs/vue-vben-admin/commit/728ba5f5744b5c2de3850a99bff5afe0b9872198)]: |
||||
|
- @vben/styles@5.8.0 |
||||
|
- @vben/plugins@5.8.0 |
||||
|
- @vben/preferences@5.8.0 |
||||
|
- @vben/layouts@5.8.0 |
||||
|
- @vben/common-ui@5.8.0 |
||||
|
- @vben/access@5.8.0 |
||||
|
- @vben/hooks@5.8.0 |
||||
|
- @vben/constants@5.8.0 |
||||
|
- @vben/request@5.8.0 |
||||
|
- @vben/icons@5.8.0 |
||||
|
- @vben/locales@5.8.0 |
||||
|
- @vben/stores@5.8.0 |
||||
|
- @vben/types@5.8.0 |
||||
|
- @vben/utils@5.8.0 |
||||
@ -0,0 +1,23 @@ |
|||||
|
# @vben/web-antdv-next |
||||
|
|
||||
|
## 5.8.0 |
||||
|
|
||||
|
### Patch Changes |
||||
|
|
||||
|
- [#8308](https://github.com/vbenjs/vue-vben-admin/pull/8308) [`81f179e`](https://github.com/vbenjs/vue-vben-admin/commit/81f179ed4c39cffe05aa0243843679ffd5c13dcd) Thanks [@kilisamemarisaaa](https://github.com/kilisamemarisaaa)! - fix(apps): pass withCredentials as request config for refresh/logout |
||||
|
|
||||
|
- Updated dependencies [[`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998), [`1d95f29`](https://github.com/vbenjs/vue-vben-admin/commit/1d95f298bdce48a1f3d4cec1ed1f182bfe265268), [`390efe9`](https://github.com/vbenjs/vue-vben-admin/commit/390efe9f604d7994f82156b89b0a76f61788a5d9), [`728ba5f`](https://github.com/vbenjs/vue-vben-admin/commit/728ba5f5744b5c2de3850a99bff5afe0b9872198)]: |
||||
|
- @vben/styles@5.8.0 |
||||
|
- @vben/plugins@5.8.0 |
||||
|
- @vben/preferences@5.8.0 |
||||
|
- @vben/layouts@5.8.0 |
||||
|
- @vben/common-ui@5.8.0 |
||||
|
- @vben/access@5.8.0 |
||||
|
- @vben/hooks@5.8.0 |
||||
|
- @vben/constants@5.8.0 |
||||
|
- @vben/request@5.8.0 |
||||
|
- @vben/icons@5.8.0 |
||||
|
- @vben/locales@5.8.0 |
||||
|
- @vben/stores@5.8.0 |
||||
|
- @vben/types@5.8.0 |
||||
|
- @vben/utils@5.8.0 |
||||
@ -0,0 +1,130 @@ |
|||||
|
/* eslint-disable vue/one-component-per-file, vue/require-default-prop */ |
||||
|
|
||||
|
import type { App, Component } from 'vue'; |
||||
|
|
||||
|
import { createApp, defineComponent, h } from 'vue'; |
||||
|
|
||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
|
import { createThemeAwareButton } from '../theme-aware-button'; |
||||
|
|
||||
|
const mocks = vi.hoisted(() => ({ |
||||
|
compact: false, |
||||
|
contextTheme: undefined as object | undefined, |
||||
|
isDark: false, |
||||
|
providerThemes: [] as any[], |
||||
|
tokens: { colorPrimary: '#e11d48' }, |
||||
|
})); |
||||
|
|
||||
|
vi.mock('@vben/hooks', () => ({ |
||||
|
useAntdDesignTokens: () => ({ tokens: mocks.tokens }), |
||||
|
})); |
||||
|
|
||||
|
vi.mock('@vben/preferences', () => ({ |
||||
|
preferences: { |
||||
|
app: { |
||||
|
get compact() { |
||||
|
return mocks.compact; |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
usePreferences: () => ({ |
||||
|
isDark: { |
||||
|
get value() { |
||||
|
return mocks.isDark; |
||||
|
}, |
||||
|
}, |
||||
|
}), |
||||
|
})); |
||||
|
|
||||
|
vi.mock('antdv-next/config-provider/context', () => ({ |
||||
|
useConfig: () => ({ value: { theme: mocks.contextTheme } }), |
||||
|
})); |
||||
|
|
||||
|
vi.mock('antdv-next', async () => { |
||||
|
const { defineComponent, h } = |
||||
|
await vi.importActual<typeof import('vue')>('vue'); |
||||
|
return { |
||||
|
ConfigProvider: defineComponent({ |
||||
|
props: { theme: Object }, |
||||
|
setup(props, { slots }) { |
||||
|
mocks.providerThemes.push(props.theme); |
||||
|
return () => |
||||
|
h('section', { 'data-theme-provider': '' }, slots.default?.()); |
||||
|
}, |
||||
|
}), |
||||
|
theme: { |
||||
|
compactAlgorithm: 'compact', |
||||
|
darkAlgorithm: 'dark', |
||||
|
defaultAlgorithm: 'default', |
||||
|
}, |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const Button = defineComponent({ |
||||
|
inheritAttrs: false, |
||||
|
props: { type: String }, |
||||
|
setup(props, { attrs, slots }) { |
||||
|
return () => |
||||
|
h( |
||||
|
'button', |
||||
|
{ ...attrs, 'data-button-type': props.type }, |
||||
|
slots.default?.(), |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
let activeApp: App | undefined; |
||||
|
|
||||
|
function mountButton(component: Component) { |
||||
|
const container = document.createElement('div'); |
||||
|
document.body.append(container); |
||||
|
activeApp = createApp(() => |
||||
|
h(component, { 'data-probe': 'button' }, () => 'Submit'), |
||||
|
); |
||||
|
activeApp.mount(container); |
||||
|
return container; |
||||
|
} |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
mocks.compact = false; |
||||
|
mocks.contextTheme = undefined; |
||||
|
mocks.isDark = false; |
||||
|
mocks.providerThemes.length = 0; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
activeApp?.unmount(); |
||||
|
activeApp = undefined; |
||||
|
document.body.innerHTML = ''; |
||||
|
}); |
||||
|
|
||||
|
describe('createThemeAwareButton', () => { |
||||
|
it('uses the existing ConfigProvider context without adding a wrapper', () => { |
||||
|
mocks.contextTheme = { token: mocks.tokens }; |
||||
|
const container = mountButton(createThemeAwareButton(Button, 'primary')); |
||||
|
|
||||
|
expect(container.querySelector('[data-theme-provider]')).toBeNull(); |
||||
|
expect(container.querySelector('button')?.dataset.buttonType).toBe( |
||||
|
'primary', |
||||
|
); |
||||
|
expect(container.querySelector('button')?.dataset.probe).toBe('button'); |
||||
|
}); |
||||
|
|
||||
|
it('provides the current Vben theme when no ConfigProvider is present', () => { |
||||
|
mocks.compact = true; |
||||
|
mocks.isDark = true; |
||||
|
const container = mountButton(createThemeAwareButton(Button, 'default')); |
||||
|
|
||||
|
expect(container.querySelector('[data-theme-provider]')).not.toBeNull(); |
||||
|
expect(mocks.providerThemes).toEqual([ |
||||
|
{ |
||||
|
algorithm: ['dark', 'compact'], |
||||
|
token: mocks.tokens, |
||||
|
}, |
||||
|
]); |
||||
|
expect(container.querySelector('button')?.dataset.buttonType).toBe( |
||||
|
'default', |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,47 @@ |
|||||
|
import type { Component } from 'vue'; |
||||
|
|
||||
|
import { computed, defineComponent, h } from 'vue'; |
||||
|
|
||||
|
import { useAntdDesignTokens } from '@vben/hooks'; |
||||
|
import { preferences, usePreferences } from '@vben/preferences'; |
||||
|
|
||||
|
import { ConfigProvider, theme } from 'antdv-next'; |
||||
|
import { useConfig } from 'antdv-next/config-provider/context'; |
||||
|
|
||||
|
function createThemeAwareButton( |
||||
|
Button: Component, |
||||
|
type: 'default' | 'primary', |
||||
|
) { |
||||
|
return defineComponent({ |
||||
|
inheritAttrs: false, |
||||
|
setup(props, { attrs, slots }) { |
||||
|
const config = useConfig(); |
||||
|
if (config.value?.theme) { |
||||
|
return () => h(Button, { ...attrs, ...props, type }, slots); |
||||
|
} |
||||
|
|
||||
|
const { isDark } = usePreferences(); |
||||
|
const { tokens } = useAntdDesignTokens(); |
||||
|
const buttonTheme = computed(() => { |
||||
|
const algorithm = [ |
||||
|
isDark.value ? theme.darkAlgorithm : theme.defaultAlgorithm, |
||||
|
]; |
||||
|
if (preferences.app.compact) { |
||||
|
algorithm.push(theme.compactAlgorithm); |
||||
|
} |
||||
|
return { algorithm, token: tokens }; |
||||
|
}); |
||||
|
|
||||
|
return () => |
||||
|
h( |
||||
|
ConfigProvider, |
||||
|
{ theme: buttonTheme.value }, |
||||
|
{ |
||||
|
default: () => h(Button, { ...attrs, ...props, type }, slots), |
||||
|
}, |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export { createThemeAwareButton }; |
||||
@ -0,0 +1,33 @@ |
|||||
|
import { describe, expect, it, vi } from 'vitest'; |
||||
|
|
||||
|
import { initSetupVbenForm } from './form'; |
||||
|
|
||||
|
const mocks = vi.hoisted(() => ({ |
||||
|
setupVbenForm: vi.fn(), |
||||
|
})); |
||||
|
|
||||
|
vi.mock('@vben/common-ui', () => ({ |
||||
|
setupVbenForm: mocks.setupVbenForm, |
||||
|
useVbenForm: vi.fn(), |
||||
|
z: {}, |
||||
|
})); |
||||
|
|
||||
|
vi.mock('@vben/locales', () => ({ |
||||
|
$t: (key: string) => key, |
||||
|
})); |
||||
|
|
||||
|
describe('antdv-next form adapter', () => { |
||||
|
it('keeps VbenTiptap on the standard Vue model protocol', async () => { |
||||
|
await initSetupVbenForm(); |
||||
|
|
||||
|
expect(mocks.setupVbenForm).toHaveBeenCalledWith( |
||||
|
expect.objectContaining({ |
||||
|
config: expect.objectContaining({ |
||||
|
modelPropNameMap: expect.objectContaining({ |
||||
|
RichEditor: 'modelValue', |
||||
|
}), |
||||
|
}), |
||||
|
}), |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,23 @@ |
|||||
|
# @vben/web-ele |
||||
|
|
||||
|
## 5.8.0 |
||||
|
|
||||
|
### Patch Changes |
||||
|
|
||||
|
- [#8308](https://github.com/vbenjs/vue-vben-admin/pull/8308) [`81f179e`](https://github.com/vbenjs/vue-vben-admin/commit/81f179ed4c39cffe05aa0243843679ffd5c13dcd) Thanks [@kilisamemarisaaa](https://github.com/kilisamemarisaaa)! - fix(apps): pass withCredentials as request config for refresh/logout |
||||
|
|
||||
|
- Updated dependencies [[`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998), [`1d95f29`](https://github.com/vbenjs/vue-vben-admin/commit/1d95f298bdce48a1f3d4cec1ed1f182bfe265268), [`390efe9`](https://github.com/vbenjs/vue-vben-admin/commit/390efe9f604d7994f82156b89b0a76f61788a5d9), [`728ba5f`](https://github.com/vbenjs/vue-vben-admin/commit/728ba5f5744b5c2de3850a99bff5afe0b9872198)]: |
||||
|
- @vben/styles@5.8.0 |
||||
|
- @vben/plugins@5.8.0 |
||||
|
- @vben/preferences@5.8.0 |
||||
|
- @vben/layouts@5.8.0 |
||||
|
- @vben/common-ui@5.8.0 |
||||
|
- @vben/access@5.8.0 |
||||
|
- @vben/hooks@5.8.0 |
||||
|
- @vben/constants@5.8.0 |
||||
|
- @vben/request@5.8.0 |
||||
|
- @vben/icons@5.8.0 |
||||
|
- @vben/locales@5.8.0 |
||||
|
- @vben/stores@5.8.0 |
||||
|
- @vben/types@5.8.0 |
||||
|
- @vben/utils@5.8.0 |
||||
@ -0,0 +1,25 @@ |
|||||
|
# @vben/web-naive |
||||
|
|
||||
|
## 5.8.0 |
||||
|
|
||||
|
### Patch Changes |
||||
|
|
||||
|
- [#8308](https://github.com/vbenjs/vue-vben-admin/pull/8308) [`81f179e`](https://github.com/vbenjs/vue-vben-admin/commit/81f179ed4c39cffe05aa0243843679ffd5c13dcd) Thanks [@kilisamemarisaaa](https://github.com/kilisamemarisaaa)! - fix(apps): pass withCredentials as request config for refresh/logout |
||||
|
|
||||
|
- [#7978](https://github.com/vbenjs/vue-vben-admin/pull/7978) [`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998) Thanks [@SaleriHQ](https://github.com/SaleriHQ)! - feat(@core/form-ui): 新增 useVbenForm 数组编辑器 VbenFormFieldArray |
||||
|
|
||||
|
- Updated dependencies [[`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998), [`1d95f29`](https://github.com/vbenjs/vue-vben-admin/commit/1d95f298bdce48a1f3d4cec1ed1f182bfe265268), [`390efe9`](https://github.com/vbenjs/vue-vben-admin/commit/390efe9f604d7994f82156b89b0a76f61788a5d9), [`728ba5f`](https://github.com/vbenjs/vue-vben-admin/commit/728ba5f5744b5c2de3850a99bff5afe0b9872198)]: |
||||
|
- @vben/styles@5.8.0 |
||||
|
- @vben/plugins@5.8.0 |
||||
|
- @vben/preferences@5.8.0 |
||||
|
- @vben/layouts@5.8.0 |
||||
|
- @vben/common-ui@5.8.0 |
||||
|
- @vben/access@5.8.0 |
||||
|
- @vben/hooks@5.8.0 |
||||
|
- @vben/constants@5.8.0 |
||||
|
- @vben/request@5.8.0 |
||||
|
- @vben/icons@5.8.0 |
||||
|
- @vben/locales@5.8.0 |
||||
|
- @vben/stores@5.8.0 |
||||
|
- @vben/types@5.8.0 |
||||
|
- @vben/utils@5.8.0 |
||||
@ -0,0 +1,23 @@ |
|||||
|
# @vben/web-tdesign |
||||
|
|
||||
|
## 5.8.0 |
||||
|
|
||||
|
### Patch Changes |
||||
|
|
||||
|
- [#8308](https://github.com/vbenjs/vue-vben-admin/pull/8308) [`81f179e`](https://github.com/vbenjs/vue-vben-admin/commit/81f179ed4c39cffe05aa0243843679ffd5c13dcd) Thanks [@kilisamemarisaaa](https://github.com/kilisamemarisaaa)! - fix(apps): pass withCredentials as request config for refresh/logout |
||||
|
|
||||
|
- Updated dependencies [[`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998), [`1d95f29`](https://github.com/vbenjs/vue-vben-admin/commit/1d95f298bdce48a1f3d4cec1ed1f182bfe265268), [`390efe9`](https://github.com/vbenjs/vue-vben-admin/commit/390efe9f604d7994f82156b89b0a76f61788a5d9), [`728ba5f`](https://github.com/vbenjs/vue-vben-admin/commit/728ba5f5744b5c2de3850a99bff5afe0b9872198)]: |
||||
|
- @vben/styles@5.8.0 |
||||
|
- @vben/plugins@5.8.0 |
||||
|
- @vben/preferences@5.8.0 |
||||
|
- @vben/layouts@5.8.0 |
||||
|
- @vben/common-ui@5.8.0 |
||||
|
- @vben/access@5.8.0 |
||||
|
- @vben/hooks@5.8.0 |
||||
|
- @vben/constants@5.8.0 |
||||
|
- @vben/request@5.8.0 |
||||
|
- @vben/icons@5.8.0 |
||||
|
- @vben/locales@5.8.0 |
||||
|
- @vben/stores@5.8.0 |
||||
|
- @vben/types@5.8.0 |
||||
|
- @vben/utils@5.8.0 |
||||
@ -0,0 +1,12 @@ |
|||||
|
# @vben/docs |
||||
|
|
||||
|
## 5.8.0 |
||||
|
|
||||
|
### Patch Changes |
||||
|
|
||||
|
- Updated dependencies [[`9ffd42f`](https://github.com/vbenjs/vue-vben-admin/commit/9ffd42f013825f94278165027bc210a5314d3998), [`142b544`](https://github.com/vbenjs/vue-vben-admin/commit/142b5442c2270090720a92671a0573cfe6974fa3), [`1d95f29`](https://github.com/vbenjs/vue-vben-admin/commit/1d95f298bdce48a1f3d4cec1ed1f182bfe265268)]: |
||||
|
- @vben/styles@5.8.0 |
||||
|
- @vben-core/shadcn-ui@5.8.0 |
||||
|
- @vben/plugins@5.8.0 |
||||
|
- @vben/common-ui@5.8.0 |
||||
|
- @vben/locales@5.8.0 |
||||
@ -0,0 +1,370 @@ |
|||||
|
--- |
||||
|
outline: deep |
||||
|
--- |
||||
|
|
||||
|
# Cache |
||||
|
|
||||
|
::: tip Preface |
||||
|
|
||||
|
A strategy-pattern-based async storage solution that supports multiple backends (localStorage, IndexedDB, Memory) behind a unified API. All methods are async so callers need no changes when switching drivers. |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
::: tip |
||||
|
|
||||
|
`@vben/utils` re-exports the full cache module — business code can import everything uniformly from `@vben/utils`. |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
## Architecture |
||||
|
|
||||
|
```shell |
||||
|
┌───────────────────────────────────────────────┐ |
||||
|
│ StorageManager │ |
||||
|
│ ┌─────────────┐ ┌───────────────────────┐ │ |
||||
|
│ │ Prefix isolation │ │ TTL expiry │ │ |
||||
|
│ └─────────────┘ └───────────────────────┘ │ |
||||
|
├───────────────────────────────────────────────┤ |
||||
|
│ IStorageDriver │ |
||||
|
├──────────┬─────────────────┬──────────────────┤ |
||||
|
│ Local │ IndexedDB │ Memory │ |
||||
|
│ Storage │ Driver │ Driver │ |
||||
|
│ Driver │ │ │ |
||||
|
└──────────┴─────────────────┴──────────────────┘ |
||||
|
``` |
||||
|
|
||||
|
**Layer responsibilities:** |
||||
|
|
||||
|
| Layer | Responsibility | |
||||
|
| --- | --- | |
||||
|
| `StorageManager` | Namespace prefix isolation, TTL expiry checks, unified public API | |
||||
|
| `IStorageDriver` | Pure KV storage abstraction interface | |
||||
|
| Driver implementations | Talk to concrete storage engines, unaware of prefix or TTL | |
||||
|
|
||||
|
## Quick Start |
||||
|
|
||||
|
### Basic usage |
||||
|
|
||||
|
When `driver` is omitted, the browser uses `LocalStorageDriver` if `localStorage` is available, otherwise falls back to `MemoryStorageDriver` (e.g. Safari private mode); SSR/Node uses `MemoryStorageDriver`: |
||||
|
|
||||
|
```ts |
||||
|
import { StorageManager } from '@vben/utils'; |
||||
|
|
||||
|
const cache = new StorageManager({ prefix: 'myapp' }); |
||||
|
|
||||
|
// Write a value |
||||
|
await cache.setItem('user', { name: 'John', age: 28 }); |
||||
|
|
||||
|
// Read a value |
||||
|
const user = await cache.getItem('user'); |
||||
|
// => { name: 'John', age: 28 } |
||||
|
|
||||
|
// Read with a default value |
||||
|
const settings = await cache.getItem('settings', { theme: 'light' }); |
||||
|
// Returns { theme: 'light' } if absent |
||||
|
|
||||
|
// Delete a value |
||||
|
await cache.removeItem('user'); |
||||
|
|
||||
|
// Clear all entries under the current prefix |
||||
|
await cache.clear(); |
||||
|
``` |
||||
|
|
||||
|
### With TTL expiry |
||||
|
|
||||
|
The third argument of `setItem` is the TTL in milliseconds. Once expired, reads return the default value (lazy deletion): |
||||
|
|
||||
|
```ts |
||||
|
import { StorageManager } from '@vben/utils'; |
||||
|
|
||||
|
const cache = new StorageManager({ prefix: 'session' }); |
||||
|
|
||||
|
// Expires in 5 minutes |
||||
|
await cache.setItem('token', 'abc123', 5 * 60 * 1000); |
||||
|
|
||||
|
// Reads normally within 5 minutes |
||||
|
const token = await cache.getItem('token'); |
||||
|
// => 'abc123' |
||||
|
|
||||
|
// Returns null after 5 minutes |
||||
|
const expiredToken = await cache.getItem('token'); |
||||
|
// => null |
||||
|
|
||||
|
// Actively clean up all expired entries |
||||
|
await cache.clearExpiredItems(); |
||||
|
``` |
||||
|
|
||||
|
## Storage Drivers |
||||
|
|
||||
|
### Local storage driver (default) |
||||
|
|
||||
|
`LocalStorageDriver`: based on the browser's `localStorage` / `sessionStorage`, data is persisted. |
||||
|
|
||||
|
```ts |
||||
|
import { LocalStorageDriver, StorageManager } from '@vben/utils'; |
||||
|
|
||||
|
// Use localStorage (default) |
||||
|
const cache = new StorageManager({ |
||||
|
driver: new LocalStorageDriver(), |
||||
|
prefix: 'app', |
||||
|
}); |
||||
|
|
||||
|
// Use sessionStorage |
||||
|
const sessionCache = new StorageManager({ |
||||
|
driver: new LocalStorageDriver({ storageType: 'sessionStorage' }), |
||||
|
prefix: 'app', |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Characteristics:** |
||||
|
|
||||
|
- Synchronous API wrapped in async to keep the interface unified |
||||
|
- Automatic JSON serialization / deserialization |
||||
|
- Corrupt data is auto-cleared and returns `null` |
||||
|
- Storage limit ~5–10MB (browser-dependent) |
||||
|
|
||||
|
**Use cases:** user preferences, small config data, token storage |
||||
|
|
||||
|
### IndexedDB driver |
||||
|
|
||||
|
`IndexedDBDriver`: based on the browser's IndexedDB, supports large structured data storage. |
||||
|
|
||||
|
```ts |
||||
|
import { IndexedDBDriver, StorageManager } from '@vben/utils'; |
||||
|
|
||||
|
const cache = new StorageManager({ |
||||
|
driver: new IndexedDBDriver({ |
||||
|
dbName: 'my-app-db', // Database name, default 'vben-storage' |
||||
|
dbVersion: 1, // Database version, default 1 |
||||
|
storeName: 'cache-store', // Object store name, default 'kv-store' |
||||
|
}), |
||||
|
prefix: 'data', |
||||
|
}); |
||||
|
|
||||
|
// Store large or complex data (IndexedDB natively supports structured cloning) |
||||
|
await cache.setItem('table-data', largeDataArray); |
||||
|
await cache.setItem('config', { |
||||
|
columns: [...], |
||||
|
filters: [...], |
||||
|
pagination: { page: 1, size: 20 }, |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Characteristics:** |
||||
|
|
||||
|
- Lazy initialization: opens the database on first operation, no manual `init()` |
||||
|
- Large capacity (typically hundreds of MB to GB) |
||||
|
- Supports structured cloning (Date, RegExp, Blob, etc.) |
||||
|
- Natively async, does not block the main thread |
||||
|
|
||||
|
**Use cases:** offline data caching, large table data, file/image caching, complex business data |
||||
|
|
||||
|
### Memory storage driver |
||||
|
|
||||
|
`MemoryStorageDriver`: based on an in-memory `Map`, data is not persisted and is lost on page refresh. |
||||
|
|
||||
|
```ts |
||||
|
import { MemoryStorageDriver, StorageManager } from '@vben/utils'; |
||||
|
|
||||
|
const cache = new StorageManager({ |
||||
|
driver: new MemoryStorageDriver(), |
||||
|
prefix: 'test', |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
**Characteristics:** |
||||
|
|
||||
|
- Fastest read/write |
||||
|
- No browser API dependency |
||||
|
- Data is destroyed with the page lifecycle |
||||
|
|
||||
|
**Use cases:** unit tests, SSR rendering, temporary runtime caching |
||||
|
|
||||
|
### Driver comparison |
||||
|
|
||||
|
| Feature | LocalStorageDriver | IndexedDBDriver | MemoryStorageDriver | |
||||
|
| --- | --- | --- | --- | |
||||
|
| Persistence | ✅ | ✅ | ❌ | |
||||
|
| Capacity | 5–10 MB | Hundreds of MB+ | Memory-bound | |
||||
|
| Speed | Fast (sync) | Medium (async I/O) | Fastest | |
||||
|
| Data type | JSON-serializable only | Structured clone | Any JS object | |
||||
|
| Browser support | All modern browsers | All modern browsers | Any environment | |
||||
|
| Blocks main thread | Yes | No | No | |
||||
|
| Use case | Config, tokens, small data | Offline cache, big data | Tests, SSR | |
||||
|
|
||||
|
## API Reference |
||||
|
|
||||
|
### StorageManager |
||||
|
|
||||
|
#### Constructor |
||||
|
|
||||
|
```ts |
||||
|
new StorageManager(options?: StorageManagerOptions) |
||||
|
``` |
||||
|
|
||||
|
| Param | Type | Default | Description | |
||||
|
| --- | --- | --- | --- | |
||||
|
| `driver` | `IStorageDriver` | `new LocalStorageDriver()` in browser when `localStorage` is available, `new MemoryStorageDriver()` otherwise (Safari private mode, SSR/Node) | Storage driver instance | |
||||
|
| `prefix` | `string` | `''` | Key prefix for namespace isolation | |
||||
|
|
||||
|
#### Methods |
||||
|
|
||||
|
| Method | Signature | Description | |
||||
|
| --- | --- | --- | |
||||
|
| `getItem` | `getItem<T>(key: string, defaultValue?: T \| null): Promise<T \| null>` | Get an entry; returns the default if expired or absent | |
||||
|
| `setItem` | `setItem(key: string, value: unknown, ttl?: number): Promise<void>` | Set an entry, with optional TTL (ms) | |
||||
|
| `removeItem` | `removeItem(key: string): Promise<void>` | Delete the given entry | |
||||
|
| `clear` | `clear(): Promise<void>` | Clear all entries under the current prefix | |
||||
|
| `clearExpiredItems` | `clearExpiredItems(): Promise<void>` | Actively clean up all expired entries | |
||||
|
| `keys` | `keys(): Promise<string[]>` | Return all keys under the current prefix (prefix stripped) | |
||||
|
|
||||
|
### IStorageDriver interface |
||||
|
|
||||
|
Custom drivers implement this interface: |
||||
|
|
||||
|
```ts |
||||
|
interface IStorageDriver { |
||||
|
clear(): Promise<void>; |
||||
|
getItem<T>(key: string): Promise<null | T>; |
||||
|
keys(): Promise<string[]>; |
||||
|
removeItem(key: string): Promise<void>; |
||||
|
setItem(key: string, value: unknown): Promise<void>; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Advanced Usage |
||||
|
|
||||
|
### Custom Driver |
||||
|
|
||||
|
Implement `IStorageDriver` to plug in any storage engine. Example with cookies: |
||||
|
|
||||
|
```ts |
||||
|
import type { IStorageDriver } from '@vben/utils'; |
||||
|
|
||||
|
class CookieStorageDriver implements IStorageDriver { |
||||
|
async getItem<T>(key: string): Promise<null | T> { |
||||
|
const value = getCookie(key); |
||||
|
return value ? JSON.parse(value) : null; |
||||
|
} |
||||
|
|
||||
|
async setItem(key: string, value: unknown): Promise<void> { |
||||
|
setCookie(key, JSON.stringify(value)); |
||||
|
} |
||||
|
|
||||
|
async removeItem(key: string): Promise<void> { |
||||
|
deleteCookie(key); |
||||
|
} |
||||
|
|
||||
|
async clear(): Promise<void> { |
||||
|
clearAllCookies(); |
||||
|
} |
||||
|
|
||||
|
async keys(): Promise<string[]> { |
||||
|
return getAllCookieNames(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const cache = new StorageManager({ |
||||
|
driver: new CookieStorageDriver(), |
||||
|
prefix: 'ck', |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
### Dynamic driver selection by environment |
||||
|
|
||||
|
```ts |
||||
|
import { |
||||
|
IndexedDBDriver, |
||||
|
LocalStorageDriver, |
||||
|
MemoryStorageDriver, |
||||
|
StorageManager, |
||||
|
} from '@vben/utils'; |
||||
|
|
||||
|
function createStorageManager(prefix: string) { |
||||
|
// SSR environment uses the memory driver |
||||
|
if (typeof window === 'undefined') { |
||||
|
return new StorageManager({ |
||||
|
driver: new MemoryStorageDriver(), |
||||
|
prefix, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Large-data scenarios use IndexedDB |
||||
|
if (needsLargeStorage()) { |
||||
|
return new StorageManager({ |
||||
|
driver: new IndexedDBDriver({ dbName: `${prefix}-db` }), |
||||
|
prefix, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Default to localStorage |
||||
|
return new StorageManager({ prefix }); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Namespace isolation |
||||
|
|
||||
|
Different modules use different prefixes so they do not interfere: |
||||
|
|
||||
|
```ts |
||||
|
const userCache = new StorageManager({ prefix: 'user' }); |
||||
|
const configCache = new StorageManager({ prefix: 'config' }); |
||||
|
|
||||
|
await userCache.setItem('profile', { name: 'John' }); |
||||
|
await configCache.setItem('profile', { theme: 'dark' }); |
||||
|
|
||||
|
await userCache.getItem('profile'); // => { name: 'John' } |
||||
|
await configCache.getItem('profile'); // => { theme: 'dark' } |
||||
|
|
||||
|
// Clears only the user-prefixed data, config is unaffected |
||||
|
await userCache.clear(); |
||||
|
await configCache.getItem('profile'); // => { theme: 'dark' } |
||||
|
``` |
||||
|
|
||||
|
### Scheduled cleanup of expired data |
||||
|
|
||||
|
```ts |
||||
|
const cache = new StorageManager({ prefix: 'app' }); |
||||
|
|
||||
|
// Clean up once on app startup |
||||
|
await cache.clearExpiredItems(); |
||||
|
|
||||
|
// Or schedule it (every 10 minutes) |
||||
|
setInterval( |
||||
|
async () => { |
||||
|
await cache.clearExpiredItems(); |
||||
|
}, |
||||
|
10 * 60 * 1000, |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
## Storage Format |
||||
|
|
||||
|
The structure `StorageManager` stores at the Driver layer: |
||||
|
|
||||
|
```ts |
||||
|
interface StorageItem<T> { |
||||
|
expiry?: number; // Expiry timestamp (ms); undefined means never expires |
||||
|
value: T; // Actual business data |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The actual stored key is formatted as `{prefix}-{key}`. For example, `prefix = 'app'`, `key = 'user'` produces the stored key `app-user`. |
||||
|
|
||||
|
## Expiry Strategy |
||||
|
|
||||
|
A dual strategy of **lazy deletion + active cleanup**: |
||||
|
|
||||
|
| Strategy | When | Description | |
||||
|
| --- | --- | --- | |
||||
|
| Lazy deletion | On `getItem` | Checks expiry on read; deletes and returns the default if expired | |
||||
|
| Active cleanup | On `clearExpiredItems` | Iterates all prefixed keys and deletes expired ones | |
||||
|
|
||||
|
## Notes |
||||
|
|
||||
|
1. **All methods are async** — even the synchronous localStorage is wrapped in Promises so callers need no changes when switching drivers. |
||||
|
2. **TTL is in milliseconds** — `setItem('key', value, 60000)` expires in 60 seconds. |
||||
|
3. **IndexedDB lazy initialization** — no manual `init()` or `open()`; the DB connection is opened on first operation and reused. |
||||
|
4. **Prefix isolation is logical** — `clear()` only clears data under the current prefix; with an empty prefix, `clear()` / `keys()` operate on all keys in the selected driver. |
||||
|
5. **LocalStorageDriver error handling** — auto-clears corrupt data on JSON parse failure and returns `null`. |
||||
|
6. **IndexedDB version upgrade** — increment `dbVersion` to modify the objectStore structure; the current implementation creates the objectStore in the `upgradeneeded` handler. |
||||
@ -0,0 +1,151 @@ |
|||||
|
--- |
||||
|
outline: deep |
||||
|
--- |
||||
|
|
||||
|
# Stores |
||||
|
|
||||
|
::: tip |
||||
|
|
||||
|
`@vben/stores` is already imported uniformly under each `app`; no separate installation is needed. The package also re-exports `pinia`'s `defineStore` and `storeToRefs`, so business code can import them uniformly from `@vben/stores`. |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
## User Store |
||||
|
|
||||
|
`useUserStore`, store id `core-user`. Wraps user info and roles. |
||||
|
|
||||
|
::: details UserState definition |
||||
|
|
||||
|
| Field | Default | Description | |
||||
|
| ----------- | ------- | ----------- | |
||||
|
| `userInfo` | `null` | User info | |
||||
|
| `userRoles` | `[]` | User roles | |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
### Set user info |
||||
|
|
||||
|
`setUserInfo(userInfo)`: Sets user info and syncs roles from `userInfo.roles` into `userRoles`. |
||||
|
|
||||
|
```ts |
||||
|
import { useUserStore } from '@vben/stores'; |
||||
|
|
||||
|
const userStore = useUserStore(); |
||||
|
userStore.setUserInfo({ id: 1, name: 'vben', roles: ['admin'] }); |
||||
|
userStore.userRoles; // ['admin'] |
||||
|
``` |
||||
|
|
||||
|
`useUserStore` has no `persist` configured — user info is runtime state, usually returned by an API after login and invalidated on logout. |
||||
|
|
||||
|
### Set user roles |
||||
|
|
||||
|
`setUserRoles(roles)`: Directly sets the user role list. |
||||
|
|
||||
|
```ts |
||||
|
import { useUserStore } from '@vben/stores'; |
||||
|
|
||||
|
const userStore = useUserStore(); |
||||
|
userStore.setUserRoles(['admin', 'editor']); |
||||
|
userStore.userRoles; // ['admin', 'editor'] |
||||
|
``` |
||||
|
|
||||
|
### Get user info |
||||
|
|
||||
|
`useUserStore` has no dedicated getter — read the state directly. Use `storeToRefs` to destructure while keeping reactivity. |
||||
|
|
||||
|
```ts |
||||
|
import { storeToRefs, useUserStore } from '@vben/stores'; |
||||
|
|
||||
|
const userStore = useUserStore(); |
||||
|
|
||||
|
// Direct access |
||||
|
userStore.userInfo; |
||||
|
userStore.userRoles; |
||||
|
|
||||
|
// Keep reactivity |
||||
|
const { userInfo, userRoles } = storeToRefs(userStore); |
||||
|
``` |
||||
|
|
||||
|
## Timezone Store |
||||
|
|
||||
|
`useTimezoneStore`, store id `core-timezone`. A setup-style store wrapping timezone state. |
||||
|
|
||||
|
::: details Exposed state and methods |
||||
|
|
||||
|
| Name | Description | |
||||
|
| --- | --- | |
||||
|
| `timezone` | Current timezone; initial value comes from `getCurrentTimezone()` | |
||||
|
| `setTimezone(timezone)` | Set the timezone and sync it to the dayjs default timezone | |
||||
|
| `getTimezoneOptions()` | Get the timezone option list; defaults to `DEFAULT_TIME_ZONE_OPTIONS` | |
||||
|
| `$reset()` | Reset the timezone to `getCurrentTimezone()` | |
||||
|
|
||||
|
::: |
||||
|
|
||||
|
### Set timezone |
||||
|
|
||||
|
`setTimezone(timezone)`: Sets the current timezone and syncs it to the dayjs default timezone (`dayjs.tz.setDefault`). |
||||
|
|
||||
|
```ts |
||||
|
import { useTimezoneStore } from '@vben/stores'; |
||||
|
|
||||
|
const store = useTimezoneStore(); |
||||
|
await store.setTimezone('America/New_York'); |
||||
|
store.timezone; // 'America/New_York' |
||||
|
``` |
||||
|
|
||||
|
### Get timezone options |
||||
|
|
||||
|
`getTimezoneOptions()`: Returns the timezone option list, defaults to `DEFAULT_TIME_ZONE_OPTIONS`; can be overridden via `setTimezoneHandler`. |
||||
|
|
||||
|
```ts |
||||
|
import { useTimezoneStore } from '@vben/stores'; |
||||
|
|
||||
|
const store = useTimezoneStore(); |
||||
|
const options = await store.getTimezoneOptions(); |
||||
|
// [{ label: 'UTC+8', value: 'Asia/Shanghai' }, ...] |
||||
|
``` |
||||
|
|
||||
|
### Reset timezone |
||||
|
|
||||
|
`$reset()`: Resets `timezone` to the current timezone returned by `getCurrentTimezone()`. It only resets the store's internal ref and does **not** sync dayjs's default timezone (only `setTimezone` does). |
||||
|
|
||||
|
```ts |
||||
|
import { useTimezoneStore } from '@vben/stores'; |
||||
|
|
||||
|
const store = useTimezoneStore(); |
||||
|
store.$reset(); |
||||
|
store.timezone; // back to the value of getCurrentTimezone() |
||||
|
``` |
||||
|
|
||||
|
### Inject custom timezone handler |
||||
|
|
||||
|
`setTimezoneHandler`: Injects a custom timezone handler module that can override `getTimezone` / `getTimezoneOptions` / `setTimezone`, useful for persisting user timezone preferences via a backend API. |
||||
|
|
||||
|
```ts |
||||
|
import { setTimezoneHandler, useTimezoneStore } from '@vben/stores'; |
||||
|
|
||||
|
setTimezoneHandler({ |
||||
|
async getTimezone() { |
||||
|
return (await fetchUserSettings()).timezone; |
||||
|
}, |
||||
|
async setTimezone(timezone) { |
||||
|
await saveUserSettings({ timezone }); |
||||
|
}, |
||||
|
async getTimezoneOptions() { |
||||
|
return [{ label: 'UTC+8', value: 'Asia/Shanghai' }]; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const store = useTimezoneStore(); |
||||
|
await store.setTimezone('Asia/Shanghai'); |
||||
|
``` |
||||
|
|
||||
|
### Persistence strategy |
||||
|
|
||||
|
```ts |
||||
|
persist: { |
||||
|
pick: ['timezone']; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
The `timezone` field is persisted and preserved on page refresh; the handler logic injected by `setTimezoneHandler` is runtime config and is not persisted. |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue