style: apply vsh lint formatting (#7923)

master^2
leo 2026-05-19 13:40:13 +08:00 committed by GitHub
parent 84e77f64ea
commit d71c81e8ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 193 additions and 193 deletions

View File

@ -1,5 +1,5 @@
--- ---
"@vben/layouts": patch '@vben/layouts': patch
--- ---
fix: update primary color when toggling dark/light mode with custom theme fix: update primary color when toggling dark/light mode with custom theme

View File

@ -1,5 +1,5 @@
--- ---
"@vben/common-ui": patch '@vben/common-ui': patch
--- ---
fix: skip fixed footer height in auto-content-height calculation fix: skip fixed footer height in auto-content-height calculation

View File

@ -1,5 +1,5 @@
--- ---
"@vben/icons": patch '@vben/icons': patch
--- ---
fix: guard svg icon loading during docs SSR fix: guard svg icon loading during docs SSR

View File

@ -1,5 +1,5 @@
--- ---
"@vben-core/shadcn-ui": patch '@vben-core/shadcn-ui': patch
--- ---
fix: preserve tree default value when treeData starts empty fix: preserve tree default value when treeData starts empty

View File

@ -11,10 +11,9 @@ const content = ref('<p>开始编辑你的内容...</p>');
<VbenTiptap v-model="content" /> <VbenTiptap v-model="content" />
<div class="mt-4"> <div class="mt-4">
<p class="text-sm text-gray-500">当前内容:</p> <p class="text-sm text-gray-500">当前内容:</p>
<pre <pre class="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-40">
class="mt-2 p-2 bg-gray-100 rounded text-xs overflow-auto max-h-40" {{ content }}
>{{ content }}</pre </pre>
>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,7 +1,6 @@
# Cache 模块 # Cache 模块
基于**策略模式**的异步存储管理方案支持多种存储后端localStorage、IndexedDB、Memory提供统一的 API 基于**策略模式**的异步存储管理方案支持多种存储后端localStorage、IndexedDB、Memory提供统一的 API 接口。
接口。
## 架构设计 ## 架构设计
@ -22,11 +21,11 @@
**分层职责:** **分层职责:**
| 层级 | 职责 | | 层级 | 职责 |
|------------------|----------------------------| | ---------------- | -------------------------------------------- |
| `StorageManager` | 命名空间前缀隔离、TTL 过期检查、统一对外 API | | `StorageManager` | 命名空间前缀隔离、TTL 过期检查、统一对外 API |
| `IStorageDriver` | 纯粹的 KV 存取抽象接口 | | `IStorageDriver` | 纯粹的 KV 存取抽象接口 |
| 各 Driver 实现 | 对接具体存储引擎,不感知前缀和 TTL | | 各 Driver 实现 | 对接具体存储引擎,不感知前缀和 TTL |
--- ---
@ -35,9 +34,9 @@
### 基本使用(默认 localStorage ### 基本使用(默认 localStorage
```typescript ```typescript
import {StorageManager} from '@vben-core/shared/cache'; import { StorageManager } from '@vben-core/shared/cache';
const cache = new StorageManager({prefix: 'myapp'}); const cache = new StorageManager({ prefix: 'myapp' });
// 使用 IndexedDB // 使用 IndexedDB
//new StorageManager({ driver: new IndexedDBDriver(), prefix: 'app' }); //new StorageManager({ driver: new IndexedDBDriver(), prefix: 'app' });
@ -48,14 +47,14 @@ const cache = new StorageManager({prefix: 'myapp'});
//new StorageManager({ driver: new MemoryStorageDriver(), prefix: 'test' }); //new StorageManager({ driver: new MemoryStorageDriver(), prefix: 'test' });
// 存储数据 // 存储数据
await cache.setItem('user', {name: '张三', age: 28}); await cache.setItem('user', { name: '张三', age: 28 });
// 读取数据 // 读取数据
const user = await cache.getItem('user'); const user = await cache.getItem('user');
// => { name: '张三', age: 28 } // => { name: '张三', age: 28 }
// 带默认值读取 // 带默认值读取
const settings = await cache.getItem('settings', {theme: 'light'}); const settings = await cache.getItem('settings', { theme: 'light' });
// 如果不存在,返回 { theme: 'light' } // 如果不存在,返回 { theme: 'light' }
// 删除数据 // 删除数据
@ -68,7 +67,7 @@ await cache.clear();
### 带 TTL 过期 ### 带 TTL 过期
```typescript ```typescript
const cache = new StorageManager({prefix: 'session'}); const cache = new StorageManager({ prefix: 'session' });
// 设置 5 分钟后过期TTL 单位为毫秒) // 设置 5 分钟后过期TTL 单位为毫秒)
await cache.setItem('token', 'abc123', 5 * 60 * 1000); await cache.setItem('token', 'abc123', 5 * 60 * 1000);
@ -191,20 +190,20 @@ const cache = new StorageManager({
new StorageManager(options?: StorageManagerOptions) new StorageManager(options?: StorageManagerOptions)
``` ```
| 参数 | 类型 | 默认值 | 说明 | | 参数 | 类型 | 默认值 | 说明 |
|----------|------------------|----------------------------|--------------| | --- | --- | --- | --- |
| `driver` | `IStorageDriver` | `new LocalStorageDriver()` | 存储驱动实例 | | `driver` | `IStorageDriver` | `new LocalStorageDriver()` | 存储驱动实例 |
| `prefix` | `string` | `''` | 键前缀,用于命名空间隔离 | | `prefix` | `string` | `''` | 键前缀,用于命名空间隔离 |
#### 方法 #### 方法
| 方法 | 签名 | 说明 | | 方法 | 签名 | 说明 |
|---------------------|-------------------------------------------------------------------------|-------------------| | --- | --- | --- |
| `getItem` | `getItem<T>(key: string, defaultValue?: T \| null): Promise<T \| null>` | 获取存储项,过期或不存在返回默认值 | | `getItem` | `getItem<T>(key: string, defaultValue?: T \| null): Promise<T \| null>` | 获取存储项,过期或不存在返回默认值 |
| `setItem` | `setItem<T>(key: string, value: T, ttl?: number): Promise<void>` | 设置存储项,可选 TTL毫秒 | | `setItem` | `setItem<T>(key: string, value: T, ttl?: number): Promise<void>` | 设置存储项,可选 TTL毫秒 |
| `removeItem` | `removeItem(key: string): Promise<void>` | 删除指定存储项 | | `removeItem` | `removeItem(key: string): Promise<void>` | 删除指定存储项 |
| `clear` | `clear(): Promise<void>` | 清除当前前缀下所有存储项 | | `clear` | `clear(): Promise<void>` | 清除当前前缀下所有存储项 |
| `clearExpiredItems` | `clearExpiredItems(): Promise<void>` | 主动清理所有过期项 | | `clearExpiredItems` | `clearExpiredItems(): Promise<void>` | 主动清理所有过期项 |
--- ---
@ -233,7 +232,7 @@ interface IStorageDriver {
### 自定义 Driver ### 自定义 Driver
```typescript ```typescript
import type {IStorageDriver} from '@vben-core/shared/cache'; import type { IStorageDriver } from '@vben-core/shared/cache';
class CookieStorageDriver implements IStorageDriver { class CookieStorageDriver implements IStorageDriver {
async getItem<T>(key: string): Promise<null | T> { async getItem<T>(key: string): Promise<null | T> {
@ -301,11 +300,11 @@ function createStorageManager(prefix: string) {
```typescript ```typescript
// 不同模块使用不同前缀,互不干扰 // 不同模块使用不同前缀,互不干扰
const userCache = new StorageManager({prefix: 'user'}); const userCache = new StorageManager({ prefix: 'user' });
const configCache = new StorageManager({prefix: 'config'}); const configCache = new StorageManager({ prefix: 'config' });
await userCache.setItem('profile', {name: '张三'}); await userCache.setItem('profile', { name: '张三' });
await configCache.setItem('profile', {theme: 'dark'}); await configCache.setItem('profile', { theme: 'dark' });
// 各自独立 // 各自独立
await userCache.getItem('profile'); // => { name: '张三' } await userCache.getItem('profile'); // => { name: '张三' }
@ -356,24 +355,24 @@ interface StorageItem<T> {
采用**惰性删除 + 主动清理**双重策略: 采用**惰性删除 + 主动清理**双重策略:
| 策略 | 触发时机 | 说明 | | 策略 | 触发时机 | 说明 |
|------|--------------------------|---------------------| | --- | --- | --- |
| 惰性删除 | 调用 `getItem` | 读取时检查过期,过期则删除并返回默认值 | | 惰性删除 | 调用 `getItem` 时 | 读取时检查过期,过期则删除并返回默认值 |
| 主动清理 | 调用 `clearExpiredItems` 时 | 遍历所有带前缀的 key删除已过期项 | | 主动清理 | 调用 `clearExpiredItems` 时 | 遍历所有带前缀的 key删除已过期项 |
--- ---
## 各 Driver 对比 ## 各 Driver 对比
| 特性 | LocalStorageDriver | IndexedDBDriver | MemoryStorageDriver | | 特性 | LocalStorageDriver | IndexedDBDriver | MemoryStorageDriver |
|-------|--------------------|-----------------|---------------------| | ---------- | ------------------- | ---------------- | ------------------- |
| 持久化 | ✅ | ✅ | ❌ | | 持久化 | ✅ | ✅ | ❌ |
| 容量 | 5-10 MB | 数百 MB+ | 受内存限制 | | 容量 | 5-10 MB | 数百 MB+ | 受内存限制 |
| 速度 | 快(同步) | 中等(异步 I/O | 最快 | | 速度 | 快(同步) | 中等(异步 I/O | 最快 |
| 数据类型 | 仅 JSON 可序列化 | 结构化克隆 | 任意 JS 对象 | | 数据类型 | 仅 JSON 可序列化 | 结构化克隆 | 任意 JS 对象 |
| 浏览器支持 | 所有现代浏览器 | 所有现代浏览器 | 任意环境 | | 浏览器支持 | 所有现代浏览器 | 所有现代浏览器 | 任意环境 |
| 阻塞主线程 | 是 | 否 | 否 | | 阻塞主线程 | 是 | 否 | 否 |
| 适用场景 | 配置、Token、小数据 | 离线缓存、大数据 | 测试、SSR | | 适用场景 | 配置、Token、小数据 | 离线缓存、大数据 | 测试、SSR |
--- ---
@ -388,12 +387,12 @@ class PreferenceManager {
constructor() { constructor() {
this.cache = new StorageManager(); this.cache = new StorageManager();
this.state = reactive<Preferences>({...defaultPreferences}); this.state = reactive<Preferences>({ ...defaultPreferences });
} }
initPreferences = async ({namespace}) => { initPreferences = async ({ namespace }) => {
// 用应用命名空间重新初始化 // 用应用命名空间重新初始化
this.cache = new StorageManager({prefix: namespace}); this.cache = new StorageManager({ prefix: namespace });
// 从缓存加载偏好设置 // 从缓存加载偏好设置
const cached = await this.cache.getItem<Preferences>('preferences'); const cached = await this.cache.getItem<Preferences>('preferences');
@ -406,8 +405,7 @@ class PreferenceManager {
## 注意事项 ## 注意事项
1. **所有方法都是异步的** — 即使底层是同步的 localStorageAPI 也返回 Promise确保切换 Driver 1. **所有方法都是异步的** — 即使底层是同步的 localStorageAPI 也返回 Promise确保切换 Driver 时无需改动调用方。
时无需改动调用方。
2. **TTL 单位是毫秒**`setItem('key', value, 60000)` 表示 60 秒后过期。 2. **TTL 单位是毫秒**`setItem('key', value, 60000)` 表示 60 秒后过期。
@ -415,8 +413,6 @@ class PreferenceManager {
4. **前缀隔离是逻辑隔离** — `clear()` 只清除当前前缀下的数据,不影响其他前缀或无前缀的数据。 4. **前缀隔离是逻辑隔离** — `clear()` 只清除当前前缀下的数据,不影响其他前缀或无前缀的数据。
5. **错误处理** — LocalStorageDriver 在 JSON 解析失败时自动清除损坏数据; 5. **错误处理** — LocalStorageDriver 在 JSON 解析失败时自动清除损坏数据; `PreferenceManager.saveToCache` 内部 try-catch 防止未捕获异常。
`PreferenceManager.saveToCache` 内部 try-catch 防止未捕获异常。
6. **IndexedDB 版本升级** — 如果需要修改 objectStore 结构,需要递增 `dbVersion`。当前实现在 6. **IndexedDB 版本升级** — 如果需要修改 objectStore 结构,需要递增 `dbVersion`。当前实现在 `upgradeneeded` 事件中自动创建 objectStore。
`upgradeneeded` 事件中自动创建 objectStore。

View File

@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import {MemoryStorageDriver} from '../memory-storage-driver'; import { MemoryStorageDriver } from '../memory-storage-driver';
import { StorageManager } from '../storage-manager'; import { StorageManager } from '../storage-manager';
describe('storageManager', () => { describe('storageManager', () => {
@ -15,7 +15,7 @@ describe('storageManager', () => {
}); });
it('should set and get an item', async () => { it('should set and get an item', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}); await storageManager.setItem('user', { age: 30, name: 'John Doe' });
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' }); expect(user).toEqual({ age: 30, name: 'John Doe' });
}); });
@ -29,22 +29,22 @@ describe('storageManager', () => {
}); });
it('should remove an item', async () => { it('should remove an item', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}); await storageManager.setItem('user', { age: 30, name: 'John Doe' });
await storageManager.removeItem('user'); await storageManager.removeItem('user');
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
expect(user).toBeNull(); expect(user).toBeNull();
}); });
it('should clear all items with the prefix', async () => { it('should clear all items with the prefix', async () => {
await storageManager.setItem('user1', {age: 30, name: 'John Doe'}); await storageManager.setItem('user1', { age: 30, name: 'John Doe' });
await storageManager.setItem('user2', {age: 25, name: 'Jane Doe'}); await storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
await storageManager.clear(); await storageManager.clear();
expect(await storageManager.getItem('user1')).toBeNull(); expect(await storageManager.getItem('user1')).toBeNull();
expect(await storageManager.getItem('user2')).toBeNull(); expect(await storageManager.getItem('user2')).toBeNull();
}); });
it('should clear expired items', async () => { it('should clear expired items', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}, 1000); // 1秒过期 await storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
vi.advanceTimersByTime(1001); // 快进时间 vi.advanceTimersByTime(1001); // 快进时间
await storageManager.clearExpiredItems(); await storageManager.clearExpiredItems();
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
@ -52,7 +52,7 @@ describe('storageManager', () => {
}); });
it('should not clear non-expired items', async () => { it('should not clear non-expired items', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}, 10_000); // 10秒过期 await storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
vi.advanceTimersByTime(5000); // 快进时间 vi.advanceTimersByTime(5000); // 快进时间
await storageManager.clearExpiredItems(); await storageManager.clearExpiredItems();
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
@ -65,36 +65,36 @@ describe('storageManager', () => {
}); });
it('should overwrite existing items', async () => { it('should overwrite existing items', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}); await storageManager.setItem('user', { age: 30, name: 'John Doe' });
await storageManager.setItem('user', {age: 25, name: 'Jane Doe'}); await storageManager.setItem('user', { age: 25, name: 'Jane Doe' });
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
expect(user).toEqual({ age: 25, name: 'Jane Doe' }); expect(user).toEqual({ age: 25, name: 'Jane Doe' });
}); });
it('should handle items without expiry correctly', async () => { it('should handle items without expiry correctly', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}); await storageManager.setItem('user', { age: 30, name: 'John Doe' });
vi.advanceTimersByTime(5000); vi.advanceTimersByTime(5000);
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' }); expect(user).toEqual({ age: 30, name: 'John Doe' });
}); });
it('should remove expired items when accessed', async () => { it('should remove expired items when accessed', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}, 1000); // 1秒过期 await storageManager.setItem('user', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
vi.advanceTimersByTime(1001); // 快进时间 vi.advanceTimersByTime(1001); // 快进时间
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
expect(user).toBeNull(); expect(user).toBeNull();
}); });
it('should not remove non-expired items when accessed', async () => { it('should not remove non-expired items when accessed', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}, 10_000); // 10秒过期 await storageManager.setItem('user', { age: 30, name: 'John Doe' }, 10_000); // 10秒过期
vi.advanceTimersByTime(5000); // 快进时间 vi.advanceTimersByTime(5000); // 快进时间
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
expect(user).toEqual({ age: 30, name: 'John Doe' }); expect(user).toEqual({ age: 30, name: 'John Doe' });
}); });
it('should handle multiple items with different expiry times', async () => { it('should handle multiple items with different expiry times', async () => {
await storageManager.setItem('user1', {age: 30, name: 'John Doe'}, 1000); // 1秒过期 await storageManager.setItem('user1', { age: 30, name: 'John Doe' }, 1000); // 1秒过期
await storageManager.setItem('user2', {age: 25, name: 'Jane Doe'}, 2000); // 2秒过期 await storageManager.setItem('user2', { age: 25, name: 'Jane Doe' }, 2000); // 2秒过期
vi.advanceTimersByTime(1500); // 快进时间 vi.advanceTimersByTime(1500); // 快进时间
await storageManager.clearExpiredItems(); await storageManager.clearExpiredItems();
const user1 = await storageManager.getItem('user1'); const user1 = await storageManager.getItem('user1');
@ -104,7 +104,7 @@ describe('storageManager', () => {
}); });
it('should handle items with no expiry', async () => { it('should handle items with no expiry', async () => {
await storageManager.setItem('user', {age: 30, name: 'John Doe'}); await storageManager.setItem('user', { age: 30, name: 'John Doe' });
vi.advanceTimersByTime(10_000); // 快进时间 vi.advanceTimersByTime(10_000); // 快进时间
await storageManager.clearExpiredItems(); await storageManager.clearExpiredItems();
const user = await storageManager.getItem('user'); const user = await storageManager.getItem('user');
@ -112,8 +112,8 @@ describe('storageManager', () => {
}); });
it('should clear all items correctly', async () => { it('should clear all items correctly', async () => {
await storageManager.setItem('user1', {age: 30, name: 'John Doe'}); await storageManager.setItem('user1', { age: 30, name: 'John Doe' });
await storageManager.setItem('user2', {age: 25, name: 'Jane Doe'}); await storageManager.setItem('user2', { age: 25, name: 'Jane Doe' });
await storageManager.clear(); await storageManager.clear();
const user1 = await storageManager.getItem('user1'); const user1 = await storageManager.getItem('user1');
const user2 = await storageManager.getItem('user2'); const user2 = await storageManager.getItem('user2');

View File

@ -1,4 +1,4 @@
import type {IStorageDriver} from './types'; import type { IStorageDriver } from './types';
interface IndexedDBDriverOptions { interface IndexedDBDriverOptions {
/** 数据库名称 */ /** 数据库名称 */
@ -20,10 +20,10 @@ class IndexedDBDriver implements IStorageDriver {
private storeName: string; private storeName: string;
constructor({ constructor({
dbName = 'vben-storage', dbName = 'vben-storage',
dbVersion = 1, dbVersion = 1,
storeName = 'kv-store', storeName = 'kv-store',
}: IndexedDBDriverOptions = {}) { }: IndexedDBDriverOptions = {}) {
this.dbName = dbName; this.dbName = dbName;
this.dbVersion = dbVersion; this.dbVersion = dbVersion;
this.storeName = storeName; this.storeName = storeName;
@ -133,5 +133,5 @@ class IndexedDBDriver implements IStorageDriver {
} }
} }
export {IndexedDBDriver}; export { IndexedDBDriver };
export type {IndexedDBDriverOptions}; export type { IndexedDBDriverOptions };

View File

@ -1,4 +1,4 @@
import type {IStorageDriver} from './types'; import type { IStorageDriver } from './types';
type StorageType = 'localStorage' | 'sessionStorage'; type StorageType = 'localStorage' | 'sessionStorage';
@ -15,8 +15,8 @@ class LocalStorageDriver implements IStorageDriver {
private storage: Storage; private storage: Storage;
constructor({ constructor({
storageType = 'localStorage', storageType = 'localStorage',
}: LocalStorageDriverOptions = {}) { }: LocalStorageDriverOptions = {}) {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
// eslint-disable-next-line unicorn/prefer-type-error -- not a type check, it's an environment check // eslint-disable-next-line unicorn/prefer-type-error -- not a type check, it's an environment check
throw new Error( throw new Error(
@ -67,5 +67,5 @@ class LocalStorageDriver implements IStorageDriver {
} }
} }
export {LocalStorageDriver}; export { LocalStorageDriver };
export type {LocalStorageDriverOptions}; export type { LocalStorageDriverOptions };

View File

@ -1,4 +1,4 @@
import type {IStorageDriver} from './types'; import type { IStorageDriver } from './types';
/** /**
* *
@ -29,4 +29,4 @@ class MemoryStorageDriver implements IStorageDriver {
} }
} }
export {MemoryStorageDriver}; export { MemoryStorageDriver };

View File

@ -4,8 +4,8 @@ import type {
StorageManagerOptions, StorageManagerOptions,
} from './types'; } from './types';
import {LocalStorageDriver} from './local-storage-driver'; import { LocalStorageDriver } from './local-storage-driver';
import {MemoryStorageDriver} from './memory-storage-driver'; import { MemoryStorageDriver } from './memory-storage-driver';
/** /**
* *
@ -17,7 +17,7 @@ class StorageManager {
private driver: IStorageDriver; private driver: IStorageDriver;
private prefix: string; private prefix: string;
constructor({driver, prefix = ''}: StorageManagerOptions = {}) { constructor({ driver, prefix = '' }: StorageManagerOptions = {}) {
this.driver = driver || this.createDefaultDriver(); this.driver = driver || this.createDefaultDriver();
this.prefix = prefix; this.prefix = prefix;
if (!this.prefix && this.driver instanceof LocalStorageDriver) { if (!this.prefix && this.driver instanceof LocalStorageDriver) {

View File

@ -36,4 +36,4 @@ interface StorageManagerOptions {
prefix?: string; prefix?: string;
} }
export type {IStorageDriver, StorageItem, StorageManagerOptions}; export type { IStorageDriver, StorageItem, StorageManagerOptions };

View File

@ -43,7 +43,7 @@ class PreferenceManager {
this.cache = new StorageManager(); this.cache = new StorageManager();
// 构造函数不再同步读取缓存,使用默认值初始化 // 构造函数不再同步读取缓存,使用默认值初始化
// 真正的缓存加载在 initPreferences 中完成(已经是 async // 真正的缓存加载在 initPreferences 中完成(已经是 async
this.state = reactive<Preferences>({...defaultPreferences}); this.state = reactive<Preferences>({ ...defaultPreferences });
this.debouncedSave = useDebounceFn(() => this.saveToCache(), 150); this.debouncedSave = useDebounceFn(() => this.saveToCache(), 150);
} }

View File

@ -12,8 +12,11 @@ defineOptions({
name: 'Page', name: 'Page',
}); });
const { autoContentHeight = false, heightOffset = 0, footerFixed = false } = const {
defineProps<PageProps>(); autoContentHeight = false,
heightOffset = 0,
footerFixed = false,
} = defineProps<PageProps>();
const headerHeight = ref(0); const headerHeight = ref(0);
const footerHeight = ref(0); const footerHeight = ref(0);
@ -40,7 +43,7 @@ async function calcContentHeight() {
await nextTick(); await nextTick();
headerHeight.value = headerRef.value?.offsetHeight || 0; headerHeight.value = headerRef.value?.offsetHeight || 0;
footerHeight.value = footerFixed ? 0 : (footerRef.value?.offsetHeight || 0); footerHeight.value = footerFixed ? 0 : footerRef.value?.offsetHeight || 0;
setTimeout(() => { setTimeout(() => {
shouldAutoHeight.value = true; shouldAutoHeight.value = true;

View File

@ -272,30 +272,30 @@ function createCustomImage(
...this.parent?.(), ...this.parent?.(),
uploadImage: uploadImage:
() => () =>
({ editor: cmdEditor }: { editor: CoreEditor }) => { ({ editor: cmdEditor }: { editor: CoreEditor }) => {
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'file'; input.type = 'file';
input.accept = imageUpload.accept ?? DEFAULT_ACCEPT; input.accept = imageUpload.accept ?? DEFAULT_ACCEPT;
input.style.display = 'none'; input.style.display = 'none';
input.addEventListener('change', () => { input.addEventListener('change', () => {
const file = input.files?.[0]; const file = input.files?.[0];
if (!file) return; if (!file) return;
const error = validateFile(file, imageUpload); const error = validateFile(file, imageUpload);
if (error) { if (error) {
handleUploadError(new Error(error), imageUpload); handleUploadError(new Error(error), imageUpload);
return; return;
} }
createUploadProcess(cmdEditor, file, imageUpload, blobUrlTracker); createUploadProcess(cmdEditor, file, imageUpload, blobUrlTracker);
input.remove(); input.remove();
}); });
document.body.append(input); document.body.append(input);
input.click(); input.click();
return true; return true;
}, },
}; };
}, },
@ -428,20 +428,20 @@ export function createDefaultTiptapExtensions(
}), }),
options.imageUpload options.imageUpload
? createCustomImage( ? createCustomImage(
options.imageUpload, options.imageUpload,
options._blobUrlTracker, options._blobUrlTracker,
).configure({ ).configure({
allowBase64: true, allowBase64: true,
HTMLAttributes: { HTMLAttributes: {
class: 'vben-tiptap__image', class: 'vben-tiptap__image',
}, },
}) })
: Image.configure({ : Image.configure({
allowBase64: true, allowBase64: true,
HTMLAttributes: { HTMLAttributes: {
class: 'vben-tiptap__image', class: 'vben-tiptap__image',
}, },
}), }),
Placeholder.configure({ Placeholder.configure({
placeholder: options.placeholder ?? $t('ui.tiptap.placeholder'), placeholder: options.placeholder ?? $t('ui.tiptap.placeholder'),
}), }),

View File

@ -6,7 +6,7 @@ import type {
} from '@vben-core/form-ui'; } from '@vben-core/form-ui';
import type { VxeGridProps } from './types'; import type { VxeGridProps } from './types';
import type {ViewedRowHelper} from './use-viewed-row'; import type { ViewedRowHelper } from './use-viewed-row';
import { toRaw } from 'vue'; import { toRaw } from 'vue';

View File

@ -75,29 +75,29 @@ interface ViewedRowPersistBase {
*/ */
export type ViewedRowPersistOptions = export type ViewedRowPersistOptions =
| ({ | ({
/** IndexedDB 数据库名称,默认 'viewed-table-db' */ /** IndexedDB 数据库名称,默认 'viewed-table-db' */
dbName?: string; dbName?: string;
/** IndexedDB 数据库版本,默认 1 */ /** IndexedDB 数据库版本,默认 1 */
dbVersion?: number; dbVersion?: number;
/** 存储 key / prefix必传 */ /** 存储 key / prefix必传 */
key: string; key: string;
/** IndexedDB 对象存储名称,默认 'viewed-table-row' */ /** IndexedDB 对象存储名称,默认 'viewed-table-row' */
storeName?: string; storeName?: string;
type: 'indexedDB'; type: 'indexedDB';
} & ViewedRowPersistBase) } & ViewedRowPersistBase)
| ({ | ({
/** 存储 key必传 */ /** 存储 key必传 */
key: string; key: string;
type: 'localStorage' | 'sessionStorage'; type: 'localStorage' | 'sessionStorage';
} & ViewedRowPersistBase) } & ViewedRowPersistBase)
| ({ | ({
/** 自定义存储适配器(必传) */ /** 自定义存储适配器(必传) */
storage: ViewedRowStorageAdapter; storage: ViewedRowStorageAdapter;
type: 'custom'; type: 'custom';
} & ViewedRowPersistBase) } & ViewedRowPersistBase)
| (ViewedRowPersistBase & { | (ViewedRowPersistBase & {
type: 'memory'; type: 'memory';
}); });
/** /**
* row * row

View File

@ -1,4 +1,4 @@
import type {VxeGridProps as VxeTableGridProps} from 'vxe-table'; import type { VxeGridProps as VxeTableGridProps } from 'vxe-table';
import type { import type {
ViewedRowOptions, ViewedRowOptions,
@ -6,9 +6,9 @@ import type {
ViewedRowStorageAdapter, ViewedRowStorageAdapter,
} from './types'; } from './types';
import {isRef, shallowRef, toRaw, triggerRef, watch} from 'vue'; import { isRef, shallowRef, toRaw, triggerRef, watch } from 'vue';
import {isBoolean, isFunction} from '@vben/utils'; import { isBoolean, isFunction } from '@vben/utils';
import { import {
IndexedDBDriver, IndexedDBDriver,
@ -16,7 +16,7 @@ import {
StorageManager, StorageManager,
} from '@vben-core/shared/cache'; } from '@vben-core/shared/cache';
import {useDebounceFn} from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
const DEFAULT_VIEWED_CLASS = 'vxe-row--viewed'; const DEFAULT_VIEWED_CLASS = 'vxe-row--viewed';
@ -32,7 +32,7 @@ function createWebStorageAdapter(
ttl?: number, ttl?: number,
): ViewedRowStorageAdapter { ): ViewedRowStorageAdapter {
const manager = new StorageManager({ const manager = new StorageManager({
driver: new LocalStorageDriver({storageType}), driver: new LocalStorageDriver({ storageType }),
}); });
return { return {
@ -182,11 +182,13 @@ export function useViewedRow<T = any>(
options: ViewedRowOptions<T> & { keyField: string }, options: ViewedRowOptions<T> & { keyField: string },
) { ) {
// ========== 解析持久化配置 ========== // ========== 解析持久化配置 ==========
const persistOpts: null | ViewedRowPersistOptions = options.persist let persistOpts: null | ViewedRowPersistOptions = null;
? (typeof options.persist === 'string' if (options.persist) {
? {key: options.persist, type: 'localStorage'} persistOpts =
: options.persist) typeof options.persist === 'string'
: null; ? { key: options.persist, type: 'localStorage' }
: options.persist;
}
const adapter = createStorageAdapter(options.persist); const adapter = createStorageAdapter(options.persist);
const maxSize = persistOpts?.maxSize ?? 100; const maxSize = persistOpts?.maxSize ?? 100;
@ -341,7 +343,7 @@ export function useViewedRow<T = any>(
function getRowClassName(params: any): string { function getRowClassName(params: any): string {
if (!isViewed(params.row)) return ''; if (!isViewed(params.row)) return '';
const {rowClassName} = options; const { rowClassName } = options;
if (rowClassName === undefined || rowClassName === null) { if (rowClassName === undefined || rowClassName === null) {
return DEFAULT_VIEWED_CLASS; return DEFAULT_VIEWED_CLASS;
} }
@ -358,7 +360,7 @@ export function useViewedRow<T = any>(
function getRowStyle(params: any): any { function getRowStyle(params: any): any {
if (!isViewed(params.row)) return undefined; if (!isViewed(params.row)) return undefined;
const {rowStyle} = options; const { rowStyle } = options;
if (rowStyle === undefined || rowStyle === null) { if (rowStyle === undefined || rowStyle === null) {
return undefined; return undefined;
} }
@ -415,11 +417,11 @@ function wrapColumnsForViewedRow(
return columns.map((column) => { return columns.map((column) => {
if (!column || typeof column !== 'object') return column; if (!column || typeof column !== 'object') return column;
const nextColumn = {...column}; const nextColumn = { ...column };
if (nextColumn.cellRender?.name === 'CellOperation') { if (nextColumn.cellRender?.name === 'CellOperation') {
const cellRender = {...nextColumn.cellRender}; const cellRender = { ...nextColumn.cellRender };
const attrs = {...cellRender.attrs}; const attrs = { ...cellRender.attrs };
const originalOnClick = attrs.onClick; const originalOnClick = attrs.onClick;
attrs.onClick = (params: { code: string; row: any }) => { attrs.onClick = (params: { code: string; row: any }) => {
@ -515,16 +517,16 @@ export function applyViewedRowOptions(
if (!viewedStyle && !originalStyle) return undefined; if (!viewedStyle && !originalStyle) return undefined;
if (!originalStyle) return viewedStyle; if (!originalStyle) return viewedStyle;
if (!viewedStyle) return originalStyle; if (!viewedStyle) return originalStyle;
return {...originalStyle, ...viewedStyle}; return { ...originalStyle, ...viewedStyle };
}; };
// 拦截 CellOperation columns // 拦截 CellOperation columns
const actionCodes = let actionCodes: string[] = [];
!isBoolean(viewedRowConfig) && viewedRowConfig.actionCodes if (!isBoolean(viewedRowConfig) && viewedRowConfig.actionCodes) {
? (Array.isArray(viewedRowConfig.actionCodes) actionCodes = Array.isArray(viewedRowConfig.actionCodes)
? viewedRowConfig.actionCodes ? viewedRowConfig.actionCodes
: [viewedRowConfig.actionCodes]) : [viewedRowConfig.actionCodes];
: []; }
if (actionCodes.length > 0 && Array.isArray(mergedOptions.columns)) { if (actionCodes.length > 0 && Array.isArray(mergedOptions.columns)) {
mergedOptions.columns = wrapColumnsForViewedRow( mergedOptions.columns = wrapColumnsForViewedRow(

View File

@ -44,7 +44,7 @@ import { VxeGrid, VxeUI } from 'vxe-table';
import { extendProxyOptions } from './extends'; import { extendProxyOptions } from './extends';
import { useTableForm } from './init'; import { useTableForm } from './init';
import {applyViewedRowOptions, useViewedRow} from './use-viewed-row'; import { applyViewedRowOptions, useViewedRow } from './use-viewed-row';
import 'vxe-table/styles/cssvar.scss'; import 'vxe-table/styles/cssvar.scss';
import 'vxe-pc-ui/styles/cssvar.scss'; import 'vxe-pc-ui/styles/cssvar.scss';
@ -93,10 +93,10 @@ watch(
if (!cfg) return; if (!cfg) return;
const keyField = (gridOptions.value?.rowConfig as any)?.keyField || 'id'; const keyField = (gridOptions.value?.rowConfig as any)?.keyField || 'id';
const resolved = isBoolean(cfg) ? {keyField} : {keyField, ...cfg}; const resolved = isBoolean(cfg) ? { keyField } : { keyField, ...cfg };
gridApi.viewedRowHelper = useViewedRow(resolved); gridApi.viewedRowHelper = useViewedRow(resolved);
}, },
{immediate: true}, { immediate: true },
); );
const { isMobile } = usePreferences(); const { isMobile } = usePreferences();

View File

@ -1,15 +1,15 @@
<script lang="ts" setup> <script lang="ts" setup>
import type {OnActionClickParams, VxeGridProps} from '#/adapter/vxe-table'; import type { OnActionClickParams, VxeGridProps } from '#/adapter/vxe-table';
import {ref} from 'vue'; import { ref } from 'vue';
import {Page, useVbenModal} from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import {$t} from '@vben/locales'; import { $t } from '@vben/locales';
import {Button, message} from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table'; import { useVbenVxeGrid } from '#/adapter/vxe-table';
import {getExampleTableApi} from '#/api'; import { getExampleTableApi } from '#/api';
interface RowType { interface RowType {
category: string; category: string;
@ -26,12 +26,12 @@ const gridOptions: VxeGridProps<RowType> = {
labelField: 'category', labelField: 'category',
}, },
columns: [ columns: [
{title: '序号', type: 'seq', width: 50}, { title: '序号', type: 'seq', width: 50 },
{field: 'category', sortable: true, title: 'Category'}, { field: 'category', sortable: true, title: 'Category' },
{field: 'color', sortable: true, title: 'Color'}, { field: 'color', sortable: true, title: 'Color' },
{field: 'productName', sortable: true, title: 'Product Name'}, { field: 'productName', sortable: true, title: 'Product Name' },
{field: 'price', sortable: true, title: 'Price'}, { field: 'price', sortable: true, title: 'Price' },
{field: 'releaseDate', formatter: 'formatDateTime', title: 'DateTime'}, { field: 'releaseDate', formatter: 'formatDateTime', title: 'DateTime' },
{ {
align: 'center', align: 'center',
cellRender: { cellRender: {
@ -61,7 +61,7 @@ const gridOptions: VxeGridProps<RowType> = {
keepSource: true, keepSource: true,
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({page, sort}) => { query: async ({ page, sort }) => {
return await getExampleTableApi({ return await getExampleTableApi({
page: page.currentPage, page: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
@ -73,7 +73,7 @@ const gridOptions: VxeGridProps<RowType> = {
sort: true, sort: true,
}, },
sortConfig: { sortConfig: {
defaultSort: {field: 'category', order: 'desc'}, defaultSort: { field: 'category', order: 'desc' },
remote: true, remote: true,
}, },
toolbarConfig: { toolbarConfig: {
@ -103,7 +103,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
}, },
}); });
function onActionClick({code, row}: OnActionClickParams<RowType>) { function onActionClick({ code, row }: OnActionClickParams<RowType>) {
switch (code) { switch (code) {
case 'edit': { case 'edit': {
onEdit(row); onEdit(row);
@ -123,9 +123,9 @@ const editRow = ref<RowType>();
const [Modal, modalApi] = useVbenModal({ const [Modal, modalApi] = useVbenModal({
draggable: true, draggable: true,
onConfirm: () => { onConfirm: () => {
modalApi.setState({loading: true}); modalApi.setState({ loading: true });
editRow.value && gridApi.markRowAsViewed(editRow.value); editRow.value && gridApi.markRowAsViewed(editRow.value);
modalApi.setState({loading: false}); modalApi.setState({ loading: false });
modalApi.close(); modalApi.close();
}, },
}); });
@ -149,7 +149,7 @@ function onStyleSet() {
gridApi.setState({ gridApi.setState({
viewedRowOptions: { viewedRowOptions: {
rowStyle: () => { rowStyle: () => {
return isStyle.value ? {backgroundColor: 'gray'} : ''; return isStyle.value ? { backgroundColor: 'gray' } : '';
}, },
}, },
}); });