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