🚧 Optimize
This commit is contained in:
@@ -10,6 +10,9 @@
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js";
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as models$0 from "../models/models.js";
|
import * as models$0 from "../models/models.js";
|
||||||
@@ -54,6 +57,11 @@ export function ServiceShutdown(): Promise<void> & { cancel(): void } {
|
|||||||
return $resultPromise;
|
return $resultPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ServiceStartup(options: application$0.ServiceOptions): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2900331732, options) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* StartAutoBackup 启动自动备份定时器
|
* StartAutoBackup 启动自动备份定时器
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,14 +49,6 @@ export function GetDocumentByID(id: number): Promise<models$0.Document | null> &
|
|||||||
return $typingPromise;
|
return $typingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GetFirstDocumentID gets the first active document's ID for frontend initialization
|
|
||||||
*/
|
|
||||||
export function GetFirstDocumentID(): Promise<number> & { cancel(): void } {
|
|
||||||
let $resultPromise = $Call.ByID(2970773833) as any;
|
|
||||||
return $resultPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
* ListAllDocumentsMeta lists all active (non-deleted) document metadata
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {useSystemStore} from '@/stores/systemStore';
|
|||||||
import {useKeybindingStore} from '@/stores/keybindingStore';
|
import {useKeybindingStore} from '@/stores/keybindingStore';
|
||||||
import {useThemeStore} from '@/stores/themeStore';
|
import {useThemeStore} from '@/stores/themeStore';
|
||||||
import {useUpdateStore} from '@/stores/updateStore';
|
import {useUpdateStore} from '@/stores/updateStore';
|
||||||
import {useBackupStore} from '@/stores/backupStore';
|
|
||||||
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
import WindowTitleBar from '@/components/titlebar/WindowTitleBar.vue';
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
@@ -13,7 +12,6 @@ const systemStore = useSystemStore();
|
|||||||
const keybindingStore = useKeybindingStore();
|
const keybindingStore = useKeybindingStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
const backupStore = useBackupStore();
|
|
||||||
|
|
||||||
// 应用启动时加载配置和初始化系统信息
|
// 应用启动时加载配置和初始化系统信息
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -27,10 +25,7 @@ onMounted(async () => {
|
|||||||
// 初始化语言和主题
|
// 初始化语言和主题
|
||||||
await configStore.initializeLanguage();
|
await configStore.initializeLanguage();
|
||||||
themeStore.initializeTheme();
|
themeStore.initializeTheme();
|
||||||
|
|
||||||
// 初始化备份服务
|
|
||||||
await backupStore.initialize();
|
|
||||||
|
|
||||||
// 启动时检查更新
|
// 启动时检查更新
|
||||||
await updateStore.checkOnStartup();
|
await updateStore.checkOnStartup();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
# 异步操作管理器 (AsyncOperationManager)
|
|
||||||
|
|
||||||
一个用于控制异步操作竞态条件的 TypeScript 模块,确保操作的正确性和一致性。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- 🚀 **竞态条件控制**: 自动取消同一资源的过时操作
|
|
||||||
- 🔄 **操作生命周期管理**: 完整的状态跟踪和回调支持
|
|
||||||
- 🎯 **资源隔离**: 基于资源ID的操作隔离机制
|
|
||||||
- ⚡ **并发控制**: 支持最大并发数限制
|
|
||||||
- ⏰ **超时处理**: 可配置的操作超时机制
|
|
||||||
- 🧹 **内存管理**: 自动清理已完成的操作
|
|
||||||
- 🐛 **调试友好**: 内置日志系统
|
|
||||||
|
|
||||||
## 安装和导入
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { AsyncOperationManager } from '@/common/async';
|
|
||||||
// 或者
|
|
||||||
import AsyncOperationManager from '@/common/async';
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本用法
|
|
||||||
|
|
||||||
### 创建管理器实例
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const operationManager = new AsyncOperationManager({
|
|
||||||
timeout: 5000, // 5秒超时
|
|
||||||
autoCleanup: true, // 自动清理已完成操作
|
|
||||||
maxConcurrent: 3, // 最大并发数
|
|
||||||
debug: true // 启用调试日志
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 执行异步操作
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const result = await operationManager.executeOperation(
|
|
||||||
'document-123', // 资源ID
|
|
||||||
async (signal, operationId) => {
|
|
||||||
// 检查操作是否被取消
|
|
||||||
if (signal.aborted) {
|
|
||||||
throw new Error('Operation cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行异步操作
|
|
||||||
const data = await fetchData();
|
|
||||||
|
|
||||||
// 再次检查取消状态
|
|
||||||
if (signal.aborted) {
|
|
||||||
throw new Error('Operation cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
'fetch-data' // 操作类型(可选,用于调试)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('操作成功:', result.data);
|
|
||||||
} else {
|
|
||||||
console.error('操作失败:', result.error);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 文档
|
|
||||||
|
|
||||||
### 构造函数
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
new AsyncOperationManager(config?, callbacks?)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 配置选项 (AsyncOperationManagerConfig)
|
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 描述 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `timeout` | `number` | `0` | 操作超时时间(毫秒),0表示不超时 |
|
|
||||||
| `autoCleanup` | `boolean` | `true` | 是否自动清理已完成的操作 |
|
|
||||||
| `maxConcurrent` | `number` | `0` | 最大并发操作数,0表示无限制 |
|
|
||||||
| `debug` | `boolean` | `false` | 是否启用调试模式 |
|
|
||||||
|
|
||||||
#### 回调函数 (OperationCallbacks)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
onStart?: (operation: OperationInfo) => void; // 操作开始
|
|
||||||
onComplete?: (operation: OperationInfo) => void; // 操作完成
|
|
||||||
onCancel?: (operation: OperationInfo) => void; // 操作取消
|
|
||||||
onError?: (operation: OperationInfo, error: Error) => void; // 操作失败
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 主要方法
|
|
||||||
|
|
||||||
#### executeOperation<T>(resourceId, executor, operationType?)
|
|
||||||
|
|
||||||
执行异步操作的核心方法。
|
|
||||||
|
|
||||||
**参数:**
|
|
||||||
- `resourceId: string | number` - 资源标识符
|
|
||||||
- `executor: OperationExecutor<T>` - 操作执行函数
|
|
||||||
- `operationType?: string` - 操作类型(可选)
|
|
||||||
|
|
||||||
**返回:** `Promise<OperationResult<T>>`
|
|
||||||
|
|
||||||
#### cancelResourceOperations(resourceId, excludeOperationId?)
|
|
||||||
|
|
||||||
取消指定资源的所有操作。
|
|
||||||
|
|
||||||
#### cancelOperation(operationId)
|
|
||||||
|
|
||||||
取消指定的操作。
|
|
||||||
|
|
||||||
#### cancelAllOperations()
|
|
||||||
|
|
||||||
取消所有正在进行的操作。
|
|
||||||
|
|
||||||
#### isOperationValid(operationId, resourceId?)
|
|
||||||
|
|
||||||
检查操作是否仍然有效。
|
|
||||||
|
|
||||||
### 查询方法
|
|
||||||
|
|
||||||
- `getOperation(operationId)` - 获取操作信息
|
|
||||||
- `getCurrentOperationId(resourceId)` - 获取资源的当前操作ID
|
|
||||||
- `getPendingOperations()` - 获取所有待处理操作
|
|
||||||
- `getRunningOperationsCount()` - 获取运行中的操作数量
|
|
||||||
|
|
||||||
## 使用场景
|
|
||||||
|
|
||||||
### 1. 编辑器文档切换
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 防止快速切换文档时的内容混乱
|
|
||||||
const loadDocument = async (documentId: number) => {
|
|
||||||
const result = await operationManager.executeOperation(
|
|
||||||
documentId,
|
|
||||||
async (signal) => {
|
|
||||||
// 保存当前文档
|
|
||||||
await saveCurrentDocument();
|
|
||||||
|
|
||||||
if (signal.aborted) return;
|
|
||||||
|
|
||||||
// 加载新文档
|
|
||||||
const content = await loadDocumentContent(documentId);
|
|
||||||
|
|
||||||
if (signal.aborted) return;
|
|
||||||
|
|
||||||
return content;
|
|
||||||
},
|
|
||||||
'load-document'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
updateEditor(result.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 搜索功能
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 取消过时的搜索请求
|
|
||||||
const search = async (query: string) => {
|
|
||||||
const result = await operationManager.executeOperation(
|
|
||||||
'search',
|
|
||||||
async (signal) => {
|
|
||||||
const results = await searchAPI(query, { signal });
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
'search-query'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
displaySearchResults(result.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 数据加载
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 避免页面切换时的数据竞态
|
|
||||||
const loadPageData = async (pageId: string) => {
|
|
||||||
const result = await operationManager.executeOperation(
|
|
||||||
`page-${pageId}`,
|
|
||||||
async (signal) => {
|
|
||||||
const [userData, pageContent, settings] = await Promise.all([
|
|
||||||
fetchUserData(signal),
|
|
||||||
fetchPageContent(pageId, signal),
|
|
||||||
fetchSettings(signal)
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { userData, pageContent, settings };
|
|
||||||
},
|
|
||||||
'load-page'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
renderPage(result.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 操作状态
|
|
||||||
|
|
||||||
操作在生命周期中会经历以下状态:
|
|
||||||
|
|
||||||
- `PENDING` - 待处理
|
|
||||||
- `RUNNING` - 运行中
|
|
||||||
- `COMPLETED` - 已完成
|
|
||||||
- `CANCELLED` - 已取消
|
|
||||||
- `FAILED` - 失败
|
|
||||||
|
|
||||||
## 最佳实践
|
|
||||||
|
|
||||||
### 1. 合理使用资源ID
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ 好的做法:使用具体的资源标识
|
|
||||||
operationManager.executeOperation('document-123', executor);
|
|
||||||
operationManager.executeOperation('user-456', executor);
|
|
||||||
|
|
||||||
// ❌ 避免:使用过于宽泛的标识
|
|
||||||
operationManager.executeOperation('global', executor);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 及时检查取消状态
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ 在关键点检查取消状态
|
|
||||||
const executor = async (signal) => {
|
|
||||||
const step1 = await longRunningTask1();
|
|
||||||
if (signal.aborted) throw new Error('Cancelled');
|
|
||||||
|
|
||||||
const step2 = await longRunningTask2();
|
|
||||||
if (signal.aborted) throw new Error('Cancelled');
|
|
||||||
|
|
||||||
return processResults(step1, step2);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 使用有意义的操作类型
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ✅ 使用描述性的操作类型
|
|
||||||
operationManager.executeOperation(docId, executor, 'auto-save');
|
|
||||||
operationManager.executeOperation(docId, executor, 'manual-save');
|
|
||||||
operationManager.executeOperation(docId, executor, 'export-pdf');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 适当的错误处理
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const result = await operationManager.executeOperation(resourceId, executor);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
if (result.operation.status === OperationStatus.CANCELLED) {
|
|
||||||
// 操作被取消,通常不需要显示错误
|
|
||||||
console.log('Operation was cancelled');
|
|
||||||
} else {
|
|
||||||
// 真正的错误,需要处理
|
|
||||||
console.error('Operation failed:', result.error);
|
|
||||||
showErrorMessage(result.error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **内存管理**: 启用 `autoCleanup` 以防止内存泄漏
|
|
||||||
2. **并发控制**: 根据应用需求设置合适的 `maxConcurrent` 值
|
|
||||||
3. **超时设置**: 为长时间运行的操作设置合理的超时时间
|
|
||||||
4. **错误处理**: 区分取消和真正的错误,提供适当的用户反馈
|
|
||||||
5. **调试模式**: 在开发环境启用 `debug` 模式以便排查问题
|
|
||||||
|
|
||||||
## 类型定义
|
|
||||||
|
|
||||||
完整的类型定义请参考 `types.ts` 文件。
|
|
||||||
|
|
||||||
## 许可证
|
|
||||||
|
|
||||||
本模块遵循项目的整体许可证。
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* 异步操作管理模块
|
|
||||||
*
|
|
||||||
* 该模块提供了用于管理异步操作竞态条件的完整解决方案。
|
|
||||||
* 主要用于防止同一资源上的并发操作冲突,确保操作的正确性和一致性。
|
|
||||||
*
|
|
||||||
* @fileoverview 异步操作管理模块入口文件
|
|
||||||
* @author VoidRaft Team
|
|
||||||
* @since 1.0.0
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import { AsyncOperationManager, OperationStatus } from '@/common/async';
|
|
||||||
*
|
|
||||||
* // 创建管理器实例
|
|
||||||
* const manager = new AsyncOperationManager({
|
|
||||||
* timeout: 30000,
|
|
||||||
* maxConcurrent: 5,
|
|
||||||
* debug: true
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // 执行异步操作
|
|
||||||
* const result = await manager.executeOperation(
|
|
||||||
* 'document-123',
|
|
||||||
* async (signal, operationId) => {
|
|
||||||
* if (signal.aborted) throw new Error('Cancelled');
|
|
||||||
* return await saveDocument(data);
|
|
||||||
* }
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* if (result.success) {
|
|
||||||
* console.log('Operation completed:', result.data);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出异步操作管理器类
|
|
||||||
*
|
|
||||||
* AsyncOperationManager 是该模块的核心类,提供了完整的异步操作管理功能。
|
|
||||||
*
|
|
||||||
* @see {@link AsyncOperationManager} 异步操作管理器类的详细文档
|
|
||||||
*/
|
|
||||||
export { AsyncOperationManager } from './manager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 导出所有类型定义
|
|
||||||
*
|
|
||||||
* 包括操作状态枚举、接口定义、配置选项等所有相关类型。
|
|
||||||
* 这些类型为使用异步操作管理器提供了完整的 TypeScript 类型支持。
|
|
||||||
*
|
|
||||||
* 导出的类型包括:
|
|
||||||
* - OperationStatus: 操作状态枚举
|
|
||||||
* - OperationInfo: 操作信息接口
|
|
||||||
* - AsyncOperationManagerConfig: 管理器配置接口
|
|
||||||
* - OperationCallbacks: 操作回调函数接口
|
|
||||||
* - OperationExecutor: 操作执行器函数类型
|
|
||||||
* - OperationResult: 操作结果接口
|
|
||||||
*
|
|
||||||
* @see {@link ./types} 类型定义文件
|
|
||||||
*/
|
|
||||||
export * from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认导出异步操作管理器类
|
|
||||||
*
|
|
||||||
* 提供默认导出,方便使用 `import AsyncOperationManager from '@/common/async'` 的方式导入。
|
|
||||||
*
|
|
||||||
* @default AsyncOperationManager
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import AsyncOperationManager from '@/common/async';
|
|
||||||
*
|
|
||||||
* const manager = new AsyncOperationManager();
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export { AsyncOperationManager as default } from './manager';
|
|
||||||
@@ -1,574 +0,0 @@
|
|||||||
import {
|
|
||||||
OperationStatus,
|
|
||||||
OperationInfo,
|
|
||||||
AsyncOperationManagerConfig,
|
|
||||||
OperationCallbacks,
|
|
||||||
OperationExecutor,
|
|
||||||
OperationResult
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 异步操作管理器
|
|
||||||
*
|
|
||||||
* 用于控制异步操作的竞态条件,确保操作的正确性和一致性。
|
|
||||||
* 该管理器提供了操作的生命周期管理、并发控制、超时处理等功能。
|
|
||||||
*
|
|
||||||
* @class AsyncOperationManager
|
|
||||||
* @author VoidRaft Team
|
|
||||||
* @since 1.0.0
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import { AsyncOperationManager } from './manager';
|
|
||||||
*
|
|
||||||
* const manager = new AsyncOperationManager({
|
|
||||||
* timeout: 30000,
|
|
||||||
* maxConcurrent: 5,
|
|
||||||
* debug: true
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // 执行异步操作
|
|
||||||
* const result = await manager.executeOperation(
|
|
||||||
* 'document-123',
|
|
||||||
* async (signal, operationId) => {
|
|
||||||
* // 执行实际的异步操作
|
|
||||||
* return await saveDocument(documentData);
|
|
||||||
* },
|
|
||||||
* 'save-document'
|
|
||||||
* );
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export class AsyncOperationManager {
|
|
||||||
/**
|
|
||||||
* 操作序列号生成器
|
|
||||||
* @private
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
private operationSequence = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 待处理操作映射表
|
|
||||||
* @private
|
|
||||||
* @type {Map<number, OperationInfo>}
|
|
||||||
*/
|
|
||||||
private pendingOperations = new Map<number, OperationInfo>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前资源操作映射表
|
|
||||||
* 记录每个资源当前正在执行的操作ID
|
|
||||||
* @private
|
|
||||||
* @type {Map<string | number, number>}
|
|
||||||
*/
|
|
||||||
private currentResourceOperation = new Map<string | number, number>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理器配置
|
|
||||||
* @private
|
|
||||||
* @type {Required<AsyncOperationManagerConfig>}
|
|
||||||
*/
|
|
||||||
private config: Required<AsyncOperationManagerConfig>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作回调函数集合
|
|
||||||
* @private
|
|
||||||
* @type {OperationCallbacks}
|
|
||||||
*/
|
|
||||||
private callbacks: OperationCallbacks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建异步操作管理器实例
|
|
||||||
*
|
|
||||||
* @param {AsyncOperationManagerConfig} config - 管理器配置选项
|
|
||||||
* @param {OperationCallbacks} callbacks - 操作生命周期回调函数
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const manager = new AsyncOperationManager(
|
|
||||||
* {
|
|
||||||
* timeout: 30000,
|
|
||||||
* autoCleanup: true,
|
|
||||||
* maxConcurrent: 10,
|
|
||||||
* debug: false
|
|
||||||
* },
|
|
||||||
* {
|
|
||||||
* onStart: (op) => console.log(`Operation ${op.id} started`),
|
|
||||||
* onComplete: (op) => console.log(`Operation ${op.id} completed`),
|
|
||||||
* onError: (op, err) => console.error(`Operation ${op.id} failed:`, err)
|
|
||||||
* }
|
|
||||||
* );
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
config: AsyncOperationManagerConfig = {},
|
|
||||||
callbacks: OperationCallbacks = {}
|
|
||||||
) {
|
|
||||||
this.config = {
|
|
||||||
timeout: 0,
|
|
||||||
autoCleanup: true,
|
|
||||||
maxConcurrent: 0,
|
|
||||||
debug: false,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
this.callbacks = callbacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成新的操作ID
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @returns {number} 新的操作ID
|
|
||||||
*/
|
|
||||||
private getNextOperationId(): number {
|
|
||||||
return ++this.operationSequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录调试日志
|
|
||||||
*
|
|
||||||
* 仅在调试模式下输出日志信息。
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {string} message - 日志消息
|
|
||||||
* @param {...any} args - 额外的日志参数
|
|
||||||
*/
|
|
||||||
private log(message: string, ...args: any[]): void {
|
|
||||||
if (this.config.debug) {
|
|
||||||
console.log(`[AsyncOperationManager] ${message}`, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建操作信息对象
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {string | number} resourceId - 资源ID
|
|
||||||
* @param {string} [type] - 操作类型标识
|
|
||||||
* @returns {OperationInfo} 新创建的操作信息
|
|
||||||
*/
|
|
||||||
private createOperation(
|
|
||||||
resourceId: string | number,
|
|
||||||
type?: string
|
|
||||||
): OperationInfo {
|
|
||||||
const operation: OperationInfo = {
|
|
||||||
id: this.getNextOperationId(),
|
|
||||||
resourceId,
|
|
||||||
status: OperationStatus.PENDING,
|
|
||||||
abortController: new AbortController(),
|
|
||||||
createdAt: Date.now(),
|
|
||||||
type
|
|
||||||
};
|
|
||||||
|
|
||||||
this.log(`Created operation ${operation.id} for resource ${resourceId}`, operation);
|
|
||||||
return operation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理已完成的操作
|
|
||||||
*
|
|
||||||
* 自动从内存中移除已完成、已取消或失败的操作,释放内存资源。
|
|
||||||
* 仅在启用自动清理配置时执行。
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private cleanupCompletedOperations(): void {
|
|
||||||
if (!this.config.autoCleanup) return;
|
|
||||||
|
|
||||||
const completedStatuses = [
|
|
||||||
OperationStatus.COMPLETED,
|
|
||||||
OperationStatus.CANCELLED,
|
|
||||||
OperationStatus.FAILED
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [id, operation] of this.pendingOperations.entries()) {
|
|
||||||
if (completedStatuses.includes(operation.status)) {
|
|
||||||
this.pendingOperations.delete(id);
|
|
||||||
this.log(`Cleaned up operation ${id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消指定资源的所有操作
|
|
||||||
*
|
|
||||||
* 取消指定资源上正在运行的所有操作,可以排除指定的操作ID。
|
|
||||||
* 这对于防止竞态条件非常有用,确保同一资源上只有最新的操作在执行。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {string | number} resourceId - 要取消操作的资源ID
|
|
||||||
* @param {number} [excludeOperationId] - 要排除的操作ID(不会被取消)
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // 取消文档 'doc-123' 上的所有操作,除了操作 456
|
|
||||||
* manager.cancelResourceOperations('doc-123', 456);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public cancelResourceOperations(
|
|
||||||
resourceId: string | number,
|
|
||||||
excludeOperationId?: number
|
|
||||||
): void {
|
|
||||||
this.log(`Cancelling operations for resource ${resourceId}, exclude: ${excludeOperationId}`);
|
|
||||||
|
|
||||||
for (const [id, operation] of this.pendingOperations.entries()) {
|
|
||||||
if (
|
|
||||||
operation.resourceId === resourceId &&
|
|
||||||
id !== excludeOperationId &&
|
|
||||||
operation.status === OperationStatus.RUNNING
|
|
||||||
) {
|
|
||||||
this.cancelOperation(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消指定的操作
|
|
||||||
*
|
|
||||||
* 通过操作ID取消正在运行的操作。只有状态为 RUNNING 的操作才能被取消。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {number} operationId - 要取消的操作ID
|
|
||||||
* @returns {boolean} 是否成功取消操作
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const cancelled = manager.cancelOperation(123);
|
|
||||||
* if (cancelled) {
|
|
||||||
* console.log('Operation 123 was cancelled');
|
|
||||||
* } else {
|
|
||||||
* console.log('Operation 123 could not be cancelled (not found or not running)');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public cancelOperation(operationId: number): boolean {
|
|
||||||
const operation = this.pendingOperations.get(operationId);
|
|
||||||
if (!operation) return false;
|
|
||||||
|
|
||||||
if (operation.status === OperationStatus.RUNNING) {
|
|
||||||
operation.abortController.abort();
|
|
||||||
operation.status = OperationStatus.CANCELLED;
|
|
||||||
this.log(`Cancelled operation ${operationId}`);
|
|
||||||
|
|
||||||
this.callbacks.onCancel?.(operation);
|
|
||||||
this.cleanupCompletedOperations();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消所有操作
|
|
||||||
*
|
|
||||||
* 取消管理器中所有正在运行的操作,并清空资源操作映射表。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // 在应用关闭或重置时取消所有操作
|
|
||||||
* manager.cancelAllOperations();
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public cancelAllOperations(): void {
|
|
||||||
this.log('Cancelling all operations');
|
|
||||||
|
|
||||||
for (const [id] of this.pendingOperations.entries()) {
|
|
||||||
this.cancelOperation(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentResourceOperation.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查操作是否仍然有效
|
|
||||||
*
|
|
||||||
* 验证操作是否存在、未被取消、状态为运行中,以及(如果指定了资源ID)
|
|
||||||
* 是否为该资源的当前活跃操作。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {number} operationId - 要检查的操作ID
|
|
||||||
* @param {string | number} [resourceId] - 可选的资源ID,用于验证是否为当前活跃操作
|
|
||||||
* @returns {boolean} 操作是否有效
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // 在长时间运行的操作中定期检查有效性
|
|
||||||
* const saveDocument = async (signal: AbortSignal, operationId: number) => {
|
|
||||||
* for (let i = 0; i < 100; i++) {
|
|
||||||
* if (!manager.isOperationValid(operationId, 'doc-123')) {
|
|
||||||
* throw new Error('Operation is no longer valid');
|
|
||||||
* }
|
|
||||||
* await processChunk(i);
|
|
||||||
* }
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public isOperationValid(
|
|
||||||
operationId: number,
|
|
||||||
resourceId?: string | number
|
|
||||||
): boolean {
|
|
||||||
const operation = this.pendingOperations.get(operationId);
|
|
||||||
|
|
||||||
if (!operation) return false;
|
|
||||||
if (operation.abortController.signal.aborted) return false;
|
|
||||||
if (operation.status !== OperationStatus.RUNNING) return false;
|
|
||||||
|
|
||||||
// 如果指定了资源ID,检查是否为当前资源的活跃操作
|
|
||||||
if (resourceId !== undefined) {
|
|
||||||
const currentOperationId = this.currentResourceOperation.get(resourceId);
|
|
||||||
if (currentOperationId !== operationId) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行异步操作
|
|
||||||
*
|
|
||||||
* 这是管理器的核心方法,用于执行异步操作并管理其生命周期。
|
|
||||||
* 该方法会自动处理并发控制、竞态条件、超时管理等复杂逻辑。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @template T - 操作返回值的类型
|
|
||||||
* @param {string | number} resourceId - 操作关联的资源ID
|
|
||||||
* @param {OperationExecutor<T>} executor - 实际执行操作的函数
|
|
||||||
* @param {string} [operationType] - 操作类型标识,用于调试和日志
|
|
||||||
* @returns {Promise<OperationResult<T>>} 操作执行结果
|
|
||||||
*
|
|
||||||
* @throws {Error} 当达到最大并发限制时抛出错误
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // 执行文档保存操作
|
|
||||||
* const result = await manager.executeOperation(
|
|
||||||
* 'document-123',
|
|
||||||
* async (signal, operationId) => {
|
|
||||||
* // 检查操作是否被取消
|
|
||||||
* if (signal.aborted) {
|
|
||||||
* throw new Error('Operation was cancelled');
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 执行实际的保存逻辑
|
|
||||||
* const saved = await api.saveDocument(documentData);
|
|
||||||
* return saved;
|
|
||||||
* },
|
|
||||||
* 'save-document'
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* if (result.success) {
|
|
||||||
* console.log('Document saved successfully:', result.data);
|
|
||||||
* } else {
|
|
||||||
* console.error('Failed to save document:', result.error);
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public async executeOperation<T = any>(
|
|
||||||
resourceId: string | number,
|
|
||||||
executor: OperationExecutor<T>,
|
|
||||||
operationType?: string
|
|
||||||
): Promise<OperationResult<T>> {
|
|
||||||
// 检查并发限制
|
|
||||||
if (this.config.maxConcurrent > 0) {
|
|
||||||
const runningCount = Array.from(this.pendingOperations.values())
|
|
||||||
.filter(op => op.status === OperationStatus.RUNNING).length;
|
|
||||||
|
|
||||||
if (runningCount >= this.config.maxConcurrent) {
|
|
||||||
throw new Error(`Maximum concurrent operations limit reached: ${this.config.maxConcurrent}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const operation = this.createOperation(resourceId, operationType);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 取消同一资源的其他操作
|
|
||||||
this.cancelResourceOperations(resourceId, operation.id);
|
|
||||||
|
|
||||||
// 设置当前资源的活跃操作
|
|
||||||
this.currentResourceOperation.set(resourceId, operation.id);
|
|
||||||
|
|
||||||
// 添加到待处理操作列表
|
|
||||||
this.pendingOperations.set(operation.id, operation);
|
|
||||||
|
|
||||||
// 设置超时
|
|
||||||
if (this.config.timeout > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.isOperationValid(operation.id)) {
|
|
||||||
this.cancelOperation(operation.id);
|
|
||||||
}
|
|
||||||
}, this.config.timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新状态为运行中
|
|
||||||
operation.status = OperationStatus.RUNNING;
|
|
||||||
this.callbacks.onStart?.(operation);
|
|
||||||
this.log(`Started operation ${operation.id} for resource ${resourceId}`);
|
|
||||||
|
|
||||||
// 执行操作
|
|
||||||
const result = await executor(operation.abortController.signal, operation.id);
|
|
||||||
|
|
||||||
// 检查操作是否仍然有效
|
|
||||||
if (!this.isOperationValid(operation.id, resourceId)) {
|
|
||||||
throw new Error('Operation was cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 操作成功完成
|
|
||||||
operation.status = OperationStatus.COMPLETED;
|
|
||||||
this.callbacks.onComplete?.(operation);
|
|
||||||
this.log(`Completed operation ${operation.id}`);
|
|
||||||
|
|
||||||
this.cleanupCompletedOperations();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result,
|
|
||||||
operation
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
const err = error instanceof Error ? error : new Error(String(error));
|
|
||||||
|
|
||||||
if (operation.abortController.signal.aborted) {
|
|
||||||
operation.status = OperationStatus.CANCELLED;
|
|
||||||
this.log(`Operation ${operation.id} was cancelled`);
|
|
||||||
} else {
|
|
||||||
operation.status = OperationStatus.FAILED;
|
|
||||||
this.callbacks.onError?.(operation, err);
|
|
||||||
this.log(`Operation ${operation.id} failed:`, err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cleanupCompletedOperations();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: err,
|
|
||||||
operation
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
// 清理当前资源操作记录
|
|
||||||
if (this.currentResourceOperation.get(resourceId) === operation.id) {
|
|
||||||
this.currentResourceOperation.delete(resourceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取操作信息
|
|
||||||
*
|
|
||||||
* 根据操作ID获取操作的详细信息。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {number} operationId - 操作ID
|
|
||||||
* @returns {OperationInfo | undefined} 操作信息,如果操作不存在则返回 undefined
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const operation = manager.getOperation(123);
|
|
||||||
* if (operation) {
|
|
||||||
* console.log(`Operation ${operation.id} status: ${operation.status}`);
|
|
||||||
* } else {
|
|
||||||
* console.log('Operation not found');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public getOperation(operationId: number): OperationInfo | undefined {
|
|
||||||
return this.pendingOperations.get(operationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取资源的当前操作ID
|
|
||||||
*
|
|
||||||
* 获取指定资源当前正在执行的操作ID。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @param {string | number} resourceId - 资源ID
|
|
||||||
* @returns {number | undefined} 当前操作ID,如果没有正在执行的操作则返回 undefined
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const currentOpId = manager.getCurrentOperationId('document-123');
|
|
||||||
* if (currentOpId) {
|
|
||||||
* console.log(`Document 123 is currently being processed by operation ${currentOpId}`);
|
|
||||||
* } else {
|
|
||||||
* console.log('No active operation for document 123');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public getCurrentOperationId(resourceId: string | number): number | undefined {
|
|
||||||
return this.currentResourceOperation.get(resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有待处理操作
|
|
||||||
*
|
|
||||||
* 返回管理器中所有待处理操作的列表,包括待执行、正在执行和已完成的操作。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @returns {OperationInfo[]} 所有待处理操作的数组
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const allOperations = manager.getPendingOperations();
|
|
||||||
* console.log(`Total operations: ${allOperations.length}`);
|
|
||||||
*
|
|
||||||
* allOperations.forEach(op => {
|
|
||||||
* console.log(`Operation ${op.id}: ${op.status} (${op.type || 'unknown'})`);
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public getPendingOperations(): OperationInfo[] {
|
|
||||||
return Array.from(this.pendingOperations.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取运行中的操作数量
|
|
||||||
*
|
|
||||||
* 返回当前正在执行的操作数量,用于监控并发情况。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
* @returns {number} 正在运行的操作数量
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const runningCount = manager.getRunningOperationsCount();
|
|
||||||
* console.log(`Currently running operations: ${runningCount}`);
|
|
||||||
*
|
|
||||||
* if (runningCount >= maxConcurrent) {
|
|
||||||
* console.warn('Approaching maximum concurrent operations limit');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public getRunningOperationsCount(): number {
|
|
||||||
return Array.from(this.pendingOperations.values())
|
|
||||||
.filter(op => op.status === OperationStatus.RUNNING).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁管理器
|
|
||||||
*
|
|
||||||
* 清理管理器的所有资源,取消所有正在运行的操作,清空所有映射表。
|
|
||||||
* 通常在应用关闭或组件卸载时调用。
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // 在组件卸载时清理资源
|
|
||||||
* useEffect(() => {
|
|
||||||
* return () => {
|
|
||||||
* manager.destroy();
|
|
||||||
* };
|
|
||||||
* }, []);
|
|
||||||
*
|
|
||||||
* // 或在应用关闭时
|
|
||||||
* window.addEventListener('beforeunload', () => {
|
|
||||||
* manager.destroy();
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
public destroy(): void {
|
|
||||||
this.log('Destroying AsyncOperationManager');
|
|
||||||
this.cancelAllOperations();
|
|
||||||
this.pendingOperations.clear();
|
|
||||||
this.currentResourceOperation.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
/**
|
|
||||||
* 异步操作竞态条件控制相关类型定义
|
|
||||||
*
|
|
||||||
* 该模块提供了用于管理异步操作竞态条件的类型定义,
|
|
||||||
* 包括操作状态、配置选项、回调函数等核心类型。
|
|
||||||
*
|
|
||||||
* @fileoverview 异步操作管理器类型定义
|
|
||||||
* @author VoidRaft Team
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作状态枚举
|
|
||||||
*
|
|
||||||
* 定义异步操作在其生命周期中可能的状态。
|
|
||||||
*
|
|
||||||
* @enum {string}
|
|
||||||
* @readonly
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import { OperationStatus } from './types';
|
|
||||||
*
|
|
||||||
* const status = OperationStatus.RUNNING;
|
|
||||||
* console.log(status); // 'running'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export enum OperationStatus {
|
|
||||||
/** 操作已创建但尚未开始执行 */
|
|
||||||
PENDING = 'pending',
|
|
||||||
/** 操作正在执行中 */
|
|
||||||
RUNNING = 'running',
|
|
||||||
/** 操作已成功完成 */
|
|
||||||
COMPLETED = 'completed',
|
|
||||||
/** 操作已被取消 */
|
|
||||||
CANCELLED = 'cancelled',
|
|
||||||
/** 操作执行失败 */
|
|
||||||
FAILED = 'failed'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作信息接口
|
|
||||||
*
|
|
||||||
* 描述单个异步操作的完整信息,包括标识符、状态、控制器等。
|
|
||||||
*
|
|
||||||
* @interface OperationInfo
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const operation: OperationInfo = {
|
|
||||||
* id: 1,
|
|
||||||
* resourceId: 'document-123',
|
|
||||||
* status: OperationStatus.RUNNING,
|
|
||||||
* abortController: new AbortController(),
|
|
||||||
* createdAt: Date.now(),
|
|
||||||
* type: 'save-document'
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface OperationInfo {
|
|
||||||
/**
|
|
||||||
* 操作的唯一标识符
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关联的资源ID(如文档ID、用户ID等)
|
|
||||||
* 用于标识操作所作用的资源,支持字符串或数字类型
|
|
||||||
* @type {string | number}
|
|
||||||
*/
|
|
||||||
resourceId: string | number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 当前操作状态
|
|
||||||
* @type {OperationStatus}
|
|
||||||
*/
|
|
||||||
status: OperationStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用于取消操作的控制器
|
|
||||||
* 通过调用 abortController.abort() 可以取消正在执行的操作
|
|
||||||
* @type {AbortController}
|
|
||||||
*/
|
|
||||||
abortController: AbortController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作创建时间戳(毫秒)
|
|
||||||
* @type {number}
|
|
||||||
*/
|
|
||||||
createdAt: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作类型标识(可选)
|
|
||||||
* 用于调试和日志记录,帮助识别不同类型的操作
|
|
||||||
* @type {string}
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 异步操作管理器配置接口
|
|
||||||
*
|
|
||||||
* 定义异步操作管理器的配置选项,用于控制操作的行为和限制。
|
|
||||||
*
|
|
||||||
* @interface AsyncOperationManagerConfig
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const config: AsyncOperationManagerConfig = {
|
|
||||||
* timeout: 30000, // 30秒超时
|
|
||||||
* autoCleanup: true, // 自动清理已完成操作
|
|
||||||
* maxConcurrent: 5, // 最多5个并发操作
|
|
||||||
* debug: true // 启用调试模式
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface AsyncOperationManagerConfig {
|
|
||||||
/**
|
|
||||||
* 操作超时时间(毫秒)
|
|
||||||
* 设置为 0 表示不设置超时限制
|
|
||||||
* @type {number}
|
|
||||||
* @default 0
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
timeout?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否自动清理已完成的操作
|
|
||||||
* 启用后会自动从内存中移除已完成、已取消或失败的操作
|
|
||||||
* @type {boolean}
|
|
||||||
* @default true
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
autoCleanup?: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 最大并发操作数
|
|
||||||
* 设置为 0 表示无并发限制
|
|
||||||
* @type {number}
|
|
||||||
* @default 0
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
maxConcurrent?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调试模式开关
|
|
||||||
* 启用后会在控制台输出详细的操作日志
|
|
||||||
* @type {boolean}
|
|
||||||
* @default false
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作回调函数集合接口
|
|
||||||
*
|
|
||||||
* 定义在操作生命周期的不同阶段可以执行的回调函数。
|
|
||||||
*
|
|
||||||
* @interface OperationCallbacks
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const callbacks: OperationCallbacks = {
|
|
||||||
* onStart: (operation) => console.log(`Operation ${operation.id} started`),
|
|
||||||
* onComplete: (operation) => console.log(`Operation ${operation.id} completed`),
|
|
||||||
* onCancel: (operation) => console.log(`Operation ${operation.id} cancelled`),
|
|
||||||
* onError: (operation, error) => console.error(`Operation ${operation.id} failed:`, error)
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface OperationCallbacks {
|
|
||||||
/**
|
|
||||||
* 操作开始时的回调函数
|
|
||||||
* @param {OperationInfo} operation - 操作信息
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
onStart?: (operation: OperationInfo) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作成功完成时的回调函数
|
|
||||||
* @param {OperationInfo} operation - 操作信息
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
onComplete?: (operation: OperationInfo) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作被取消时的回调函数
|
|
||||||
* @param {OperationInfo} operation - 操作信息
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
onCancel?: (operation: OperationInfo) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作执行失败时的回调函数
|
|
||||||
* @param {OperationInfo} operation - 操作信息
|
|
||||||
* @param {Error} error - 错误对象
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
onError?: (operation: OperationInfo, error: Error) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作执行器函数类型
|
|
||||||
*
|
|
||||||
* 定义实际执行异步操作的函数签名。
|
|
||||||
*
|
|
||||||
* @template T - 操作返回值的类型
|
|
||||||
* @param {AbortSignal} signal - 用于检测操作是否被取消的信号
|
|
||||||
* @param {number} operationId - 操作的唯一标识符
|
|
||||||
* @returns {Promise<T>} 操作执行结果的 Promise
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const saveDocument: OperationExecutor<boolean> = async (signal, operationId) => {
|
|
||||||
* // 检查操作是否被取消
|
|
||||||
* if (signal.aborted) {
|
|
||||||
* throw new Error('Operation was cancelled');
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 执行实际的保存操作
|
|
||||||
* const result = await api.saveDocument(documentData);
|
|
||||||
* return result.success;
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export type OperationExecutor<T = any> = (
|
|
||||||
signal: AbortSignal,
|
|
||||||
operationId: number
|
|
||||||
) => Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作执行结果接口
|
|
||||||
*
|
|
||||||
* 封装异步操作的执行结果,包括成功状态、数据和错误信息。
|
|
||||||
*
|
|
||||||
* @template T - 操作结果数据的类型
|
|
||||||
* @interface OperationResult
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // 成功的操作结果
|
|
||||||
* const successResult: OperationResult<string> = {
|
|
||||||
* success: true,
|
|
||||||
* data: 'Operation completed successfully',
|
|
||||||
* operation: operationInfo
|
|
||||||
* };
|
|
||||||
*
|
|
||||||
* // 失败的操作结果
|
|
||||||
* const failureResult: OperationResult<never> = {
|
|
||||||
* success: false,
|
|
||||||
* error: new Error('Operation failed'),
|
|
||||||
* operation: operationInfo
|
|
||||||
* };
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export interface OperationResult<T = any> {
|
|
||||||
/**
|
|
||||||
* 操作是否成功执行
|
|
||||||
* true 表示操作成功完成,false 表示操作失败或被取消
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
success: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作成功时的结果数据
|
|
||||||
* 仅在 success 为 true 时有值
|
|
||||||
* @type {T}
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
data?: T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作失败时的错误信息
|
|
||||||
* 仅在 success 为 false 时有值
|
|
||||||
* @type {Error}
|
|
||||||
* @optional
|
|
||||||
*/
|
|
||||||
error?: Error;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 关联的操作信息
|
|
||||||
* 包含操作的完整元数据
|
|
||||||
* @type {OperationInfo}
|
|
||||||
*/
|
|
||||||
operation: OperationInfo;
|
|
||||||
}
|
|
||||||
119
frontend/src/common/cache/README.md
vendored
119
frontend/src/common/cache/README.md
vendored
@@ -1,119 +0,0 @@
|
|||||||
# 缓存系统
|
|
||||||
|
|
||||||
简洁高效的 LRU 缓存实现,支持泛型和自动清理。
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
|
|
||||||
- 🚀 高性能 LRU 缓存算法
|
|
||||||
- 🔧 TypeScript 泛型支持
|
|
||||||
- 🧹 自动资源清理
|
|
||||||
- 📊 缓存统计信息
|
|
||||||
- ⏰ TTL 过期支持
|
|
||||||
- 🎯 简洁易用的 API
|
|
||||||
- 🔐 多种哈希算法支持
|
|
||||||
- 🏗️ 模块化设计,易于扩展
|
|
||||||
|
|
||||||
## 基础用法
|
|
||||||
|
|
||||||
### 创建缓存
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { LruCache, CacheManager, createCacheItem } from '@/common/cache';
|
|
||||||
|
|
||||||
// 直接创建缓存
|
|
||||||
const cache = new LruCache({
|
|
||||||
maxSize: 100,
|
|
||||||
ttl: 5 * 60 * 1000, // 5 分钟
|
|
||||||
onEvict: (item) => console.log('Evicted:', item)
|
|
||||||
});
|
|
||||||
|
|
||||||
// 使用缓存管理器
|
|
||||||
const manager = new CacheManager();
|
|
||||||
const myCache = manager.createCache('myCache', { maxSize: 50 });
|
|
||||||
```
|
|
||||||
|
|
||||||
### 缓存操作
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 创建缓存项
|
|
||||||
const item = createCacheItem('key1', {
|
|
||||||
name: 'example',
|
|
||||||
data: { foo: 'bar' }
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置缓存
|
|
||||||
cache.set('key1', item);
|
|
||||||
|
|
||||||
// 获取缓存
|
|
||||||
const cached = cache.get('key1');
|
|
||||||
|
|
||||||
// 检查存在
|
|
||||||
if (cache.has('key1')) {
|
|
||||||
// 处理逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除缓存
|
|
||||||
cache.remove('key1');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自动清理资源
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface MyItem extends CacheItem, DisposableCacheItem {
|
|
||||||
resource: SomeResource;
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item: MyItem = {
|
|
||||||
id: 'resource1',
|
|
||||||
lastAccessed: new Date(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
resource: new SomeResource(),
|
|
||||||
dispose() {
|
|
||||||
this.resource.cleanup();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 当项被驱逐或移除时,dispose 方法会自动调用
|
|
||||||
cache.set('resource1', item);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 哈希工具使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createHash, generateCacheKey } from '@/common/cache';
|
|
||||||
|
|
||||||
// 生成简单哈希
|
|
||||||
const hash = createHash('some content');
|
|
||||||
|
|
||||||
// 生成缓存键
|
|
||||||
const key = generateCacheKey('user', userId, 'profile');
|
|
||||||
```
|
|
||||||
|
|
||||||
## API 参考
|
|
||||||
|
|
||||||
### LruCache
|
|
||||||
|
|
||||||
- `get(id)` - 获取缓存项(O(1))
|
|
||||||
- `set(id, item)` - 设置缓存项(O(1))
|
|
||||||
- `remove(id)` - 移除缓存项(O(1))
|
|
||||||
- `has(id)` - 检查是否存在(O(1))
|
|
||||||
- `clear()` - 清空缓存
|
|
||||||
- `size()` - 获取缓存大小
|
|
||||||
- `getStats()` - 获取统计信息
|
|
||||||
- `cleanup()` - 清理过期项
|
|
||||||
|
|
||||||
### CacheManager
|
|
||||||
|
|
||||||
- `getCache(name, config?)` - 获取或创建缓存
|
|
||||||
- `createCache(name, config)` - 创建新缓存
|
|
||||||
- `removeCache(name)` - 删除缓存
|
|
||||||
- `clearAll()` - 清空所有缓存
|
|
||||||
- `getAllStats()` - 获取所有统计信息
|
|
||||||
- `cleanupAll()` - 清理所有缓存的过期项
|
|
||||||
|
|
||||||
## 工具函数
|
|
||||||
|
|
||||||
- `generateCacheKey(...parts)` - 生成缓存键
|
|
||||||
- `createHash(content)` - 创建内容哈希
|
|
||||||
- `createCacheItem(id, data)` - 创建缓存项
|
|
||||||
166
frontend/src/common/cache/doublyLinkedList.ts
vendored
166
frontend/src/common/cache/doublyLinkedList.ts
vendored
@@ -1,166 +0,0 @@
|
|||||||
import type { DoublyLinkedNode } from './interfaces';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 双向链表实现
|
|
||||||
* 用于高效管理LRU缓存的访问顺序,所有操作都是O(1)时间复杂度
|
|
||||||
*
|
|
||||||
* @template T 节点值的类型
|
|
||||||
*/
|
|
||||||
export class DoublyLinkedList<T> {
|
|
||||||
/** 头节点(虚拟节点) */
|
|
||||||
private head: DoublyLinkedNode<T>;
|
|
||||||
/** 尾节点(虚拟节点) */
|
|
||||||
private tail: DoublyLinkedNode<T>;
|
|
||||||
/** 当前节点数量 */
|
|
||||||
private count: number = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* 创建头尾虚拟节点,简化边界处理
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
// 创建虚拟头节点
|
|
||||||
this.head = {
|
|
||||||
value: null as any,
|
|
||||||
key: 'head',
|
|
||||||
prev: null,
|
|
||||||
next: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 创建虚拟尾节点
|
|
||||||
this.tail = {
|
|
||||||
value: null as any,
|
|
||||||
key: 'tail',
|
|
||||||
prev: null,
|
|
||||||
next: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// 连接头尾节点
|
|
||||||
this.head.next = this.tail;
|
|
||||||
this.tail.prev = this.head;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 在头部添加节点
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param node 要添加的节点
|
|
||||||
*/
|
|
||||||
addToHead(node: DoublyLinkedNode<T>): void {
|
|
||||||
node.prev = this.head;
|
|
||||||
node.next = this.head.next;
|
|
||||||
|
|
||||||
if (this.head.next) {
|
|
||||||
this.head.next.prev = node;
|
|
||||||
}
|
|
||||||
this.head.next = node;
|
|
||||||
|
|
||||||
this.count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除指定节点
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param node 要移除的节点
|
|
||||||
*/
|
|
||||||
removeNode(node: DoublyLinkedNode<T>): void {
|
|
||||||
if (node.prev) {
|
|
||||||
node.prev.next = node.next;
|
|
||||||
}
|
|
||||||
if (node.next) {
|
|
||||||
node.next.prev = node.prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.count--;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除尾部节点
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @returns 被移除的节点,如果链表为空则返回null
|
|
||||||
*/
|
|
||||||
removeTail(): DoublyLinkedNode<T> | null {
|
|
||||||
const lastNode = this.tail.prev;
|
|
||||||
|
|
||||||
if (lastNode === this.head) {
|
|
||||||
return null; // 链表为空
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeNode(lastNode!);
|
|
||||||
return lastNode!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将节点移动到头部
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param node 要移动的节点
|
|
||||||
*/
|
|
||||||
moveToHead(node: DoublyLinkedNode<T>): void {
|
|
||||||
this.removeNode(node);
|
|
||||||
this.addToHead(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新节点
|
|
||||||
*
|
|
||||||
* @param key 节点键
|
|
||||||
* @param value 节点值
|
|
||||||
* @returns 新创建的节点
|
|
||||||
*/
|
|
||||||
createNode(key: string | number, value: T): DoublyLinkedNode<T> {
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
prev: null,
|
|
||||||
next: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取链表大小
|
|
||||||
*
|
|
||||||
* @returns 当前节点数量
|
|
||||||
*/
|
|
||||||
size(): number {
|
|
||||||
return this.count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查链表是否为空
|
|
||||||
*
|
|
||||||
* @returns 是否为空
|
|
||||||
*/
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this.count === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空链表
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this.head.next = this.tail;
|
|
||||||
this.tail.prev = this.head;
|
|
||||||
this.count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有节点的值(从头到尾)
|
|
||||||
* 主要用于调试和测试
|
|
||||||
*
|
|
||||||
* @returns 所有节点值的数组
|
|
||||||
*/
|
|
||||||
toArray(): T[] {
|
|
||||||
const result: T[] = [];
|
|
||||||
let current = this.head.next;
|
|
||||||
|
|
||||||
while (current && current !== this.tail) {
|
|
||||||
result.push(current.value);
|
|
||||||
current = current.next;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
21
frontend/src/common/cache/index.ts
vendored
21
frontend/src/common/cache/index.ts
vendored
@@ -1,21 +0,0 @@
|
|||||||
export type {
|
|
||||||
CacheItem,
|
|
||||||
DisposableCacheItem,
|
|
||||||
CacheConfig,
|
|
||||||
CacheStats,
|
|
||||||
CacheStrategy,
|
|
||||||
DoublyLinkedNode
|
|
||||||
} from './interfaces';
|
|
||||||
export { LruCache } from './lruCache';
|
|
||||||
export { CacheManager, globalCacheManager } from './manager';
|
|
||||||
export { DoublyLinkedList } from './doublyLinkedList';
|
|
||||||
export {
|
|
||||||
createHash,
|
|
||||||
generateCacheKey,
|
|
||||||
createCacheItem,
|
|
||||||
calculateHitRate,
|
|
||||||
formatCacheSize,
|
|
||||||
isExpired,
|
|
||||||
debounce,
|
|
||||||
throttle
|
|
||||||
} from './utils';
|
|
||||||
124
frontend/src/common/cache/interfaces.ts
vendored
124
frontend/src/common/cache/interfaces.ts
vendored
@@ -1,124 +0,0 @@
|
|||||||
/**
|
|
||||||
* 缓存项基础接口
|
|
||||||
*/
|
|
||||||
export interface CacheItem {
|
|
||||||
/** 缓存项的唯一标识 */
|
|
||||||
id: string | number;
|
|
||||||
/** 最后访问时间 */
|
|
||||||
lastAccessed: Date;
|
|
||||||
/** 创建时间 */
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 可清理的缓存项接口
|
|
||||||
*/
|
|
||||||
export interface DisposableCacheItem extends CacheItem {
|
|
||||||
/** 清理资源的方法 */
|
|
||||||
dispose(): void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存配置接口
|
|
||||||
*/
|
|
||||||
export interface CacheConfig {
|
|
||||||
/** 最大缓存数量 */
|
|
||||||
maxSize: number;
|
|
||||||
/** 生存时间(毫秒),可选 */
|
|
||||||
ttl?: number;
|
|
||||||
/** 驱逐回调函数,可选 */
|
|
||||||
onEvict?: (item: any) => void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存统计信息接口
|
|
||||||
*/
|
|
||||||
export interface CacheStats {
|
|
||||||
/** 当前缓存项数量 */
|
|
||||||
size: number;
|
|
||||||
/** 最大容量 */
|
|
||||||
maxSize: number;
|
|
||||||
/** 命中次数 */
|
|
||||||
hits: number;
|
|
||||||
/** 未命中次数 */
|
|
||||||
misses: number;
|
|
||||||
/** 命中率 */
|
|
||||||
hitRate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通用缓存策略接口
|
|
||||||
* 所有缓存实现都应该实现这个接口
|
|
||||||
*/
|
|
||||||
export interface CacheStrategy<T extends CacheItem> {
|
|
||||||
/**
|
|
||||||
* 获取缓存项
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @returns 缓存项或null
|
|
||||||
*/
|
|
||||||
get(id: string | number): T | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置缓存项
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @param item 缓存项
|
|
||||||
*/
|
|
||||||
set(id: string | number, item: T): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除缓存项
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @returns 是否成功移除
|
|
||||||
*/
|
|
||||||
remove(id: string | number): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否存在
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @returns 是否存在
|
|
||||||
*/
|
|
||||||
has(id: string | number): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空缓存
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有项
|
|
||||||
* @returns 所有缓存项
|
|
||||||
*/
|
|
||||||
getAll(): T[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存大小
|
|
||||||
* @returns 当前缓存项数量
|
|
||||||
*/
|
|
||||||
size(): number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取统计信息
|
|
||||||
* @returns 缓存统计信息
|
|
||||||
*/
|
|
||||||
getStats(): CacheStats;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理过期项
|
|
||||||
* @returns 清理的项数量
|
|
||||||
*/
|
|
||||||
cleanup(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 双向链表节点接口
|
|
||||||
*/
|
|
||||||
export interface DoublyLinkedNode<T> {
|
|
||||||
/** 节点值 */
|
|
||||||
value: T;
|
|
||||||
/** 节点键 */
|
|
||||||
key: string | number;
|
|
||||||
/** 前一个节点 */
|
|
||||||
prev: DoublyLinkedNode<T> | null;
|
|
||||||
/** 下一个节点 */
|
|
||||||
next: DoublyLinkedNode<T> | null;
|
|
||||||
}
|
|
||||||
276
frontend/src/common/cache/lruCache.ts
vendored
276
frontend/src/common/cache/lruCache.ts
vendored
@@ -1,276 +0,0 @@
|
|||||||
import type { CacheItem, CacheConfig, CacheStats, DisposableCacheItem, CacheStrategy, DoublyLinkedNode } from './interfaces';
|
|
||||||
import { DoublyLinkedList } from './doublyLinkedList';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 高性能LRU缓存实现
|
|
||||||
* 使用双向链表 + Map 的组合,所有核心操作都是O(1)时间复杂度
|
|
||||||
*
|
|
||||||
* @template T 缓存项类型,必须继承自CacheItem
|
|
||||||
*/
|
|
||||||
export class LruCache<T extends CacheItem> implements CacheStrategy<T> {
|
|
||||||
/** 存储缓存项的Map,提供O(1)的查找性能 */
|
|
||||||
private items = new Map<string | number, DoublyLinkedNode<T>>();
|
|
||||||
|
|
||||||
/** 双向链表,管理访问顺序,提供O(1)的插入/删除性能 */
|
|
||||||
private accessList = new DoublyLinkedList<T>();
|
|
||||||
|
|
||||||
/** 缓存配置 */
|
|
||||||
private config: CacheConfig;
|
|
||||||
|
|
||||||
/** 统计信息 */
|
|
||||||
private stats = {
|
|
||||||
hits: 0,
|
|
||||||
misses: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param config 缓存配置
|
|
||||||
*/
|
|
||||||
constructor(config: CacheConfig) {
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存项
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @returns 缓存项或null
|
|
||||||
*/
|
|
||||||
get(id: string | number): T | null {
|
|
||||||
const node = this.items.get(id);
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
this.stats.misses++;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (this.isExpired(node.value)) {
|
|
||||||
this.remove(id);
|
|
||||||
this.stats.misses++;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新访问时间
|
|
||||||
node.value.lastAccessed = new Date();
|
|
||||||
|
|
||||||
// 将节点移动到链表头部(最近访问)
|
|
||||||
this.accessList.moveToHead(node);
|
|
||||||
|
|
||||||
this.stats.hits++;
|
|
||||||
return node.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置缓存项
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @param item 缓存项
|
|
||||||
*/
|
|
||||||
set(id: string | number, item: T): void {
|
|
||||||
const existingNode = this.items.get(id);
|
|
||||||
|
|
||||||
// 如果已存在,更新值并移动到头部
|
|
||||||
if (existingNode) {
|
|
||||||
existingNode.value = item;
|
|
||||||
this.accessList.moveToHead(existingNode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查容量,必要时驱逐最旧的项
|
|
||||||
while (this.items.size >= this.config.maxSize) {
|
|
||||||
this.evictLeastRecentlyUsed();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新节点并添加到头部
|
|
||||||
const newNode = this.accessList.createNode(id, item);
|
|
||||||
this.accessList.addToHead(newNode);
|
|
||||||
this.items.set(id, newNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除缓存项
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @returns 是否成功移除
|
|
||||||
*/
|
|
||||||
remove(id: string | number): boolean {
|
|
||||||
const node = this.items.get(id);
|
|
||||||
if (!node) return false;
|
|
||||||
|
|
||||||
// 从链表中移除
|
|
||||||
this.accessList.removeNode(node);
|
|
||||||
|
|
||||||
// 从Map中移除
|
|
||||||
this.items.delete(id);
|
|
||||||
|
|
||||||
// 调用清理逻辑
|
|
||||||
this.disposeItem(node.value);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否存在
|
|
||||||
* 时间复杂度: O(1)
|
|
||||||
*
|
|
||||||
* @param id 缓存项ID
|
|
||||||
* @returns 是否存在
|
|
||||||
*/
|
|
||||||
has(id: string | number): boolean {
|
|
||||||
const node = this.items.get(id);
|
|
||||||
if (!node) return false;
|
|
||||||
|
|
||||||
if (this.isExpired(node.value)) {
|
|
||||||
this.remove(id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空缓存
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
// 清理所有项
|
|
||||||
for (const node of this.items.values()) {
|
|
||||||
this.disposeItem(node.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.items.clear();
|
|
||||||
this.accessList.clear();
|
|
||||||
this.stats.hits = 0;
|
|
||||||
this.stats.misses = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有项
|
|
||||||
* 按访问顺序返回(最近访问的在前)
|
|
||||||
*
|
|
||||||
* @returns 所有缓存项
|
|
||||||
*/
|
|
||||||
getAll(): T[] {
|
|
||||||
return this.accessList.toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存大小
|
|
||||||
*
|
|
||||||
* @returns 当前缓存项数量
|
|
||||||
*/
|
|
||||||
size(): number {
|
|
||||||
return this.items.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取统计信息
|
|
||||||
*
|
|
||||||
* @returns 缓存统计信息
|
|
||||||
*/
|
|
||||||
getStats(): CacheStats {
|
|
||||||
const total = this.stats.hits + this.stats.misses;
|
|
||||||
return {
|
|
||||||
size: this.items.size,
|
|
||||||
maxSize: this.config.maxSize,
|
|
||||||
hits: this.stats.hits,
|
|
||||||
misses: this.stats.misses,
|
|
||||||
hitRate: total > 0 ? this.stats.hits / total : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理过期项
|
|
||||||
*
|
|
||||||
* @returns 清理的项数量
|
|
||||||
*/
|
|
||||||
cleanup(): number {
|
|
||||||
let cleanedCount = 0;
|
|
||||||
|
|
||||||
if (!this.config.ttl) return cleanedCount;
|
|
||||||
|
|
||||||
// 收集过期的键
|
|
||||||
const expiredKeys: (string | number)[] = [];
|
|
||||||
for (const [id, node] of this.items.entries()) {
|
|
||||||
if (this.isExpired(node.value)) {
|
|
||||||
expiredKeys.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 移除过期项
|
|
||||||
for (const key of expiredKeys) {
|
|
||||||
if (this.remove(key)) {
|
|
||||||
cleanedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 私有方法
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查项是否过期
|
|
||||||
*
|
|
||||||
* @param item 缓存项
|
|
||||||
* @returns 是否过期
|
|
||||||
*/
|
|
||||||
private isExpired(item: T): boolean {
|
|
||||||
if (!this.config.ttl) return false;
|
|
||||||
return Date.now() - item.lastAccessed.getTime() > this.config.ttl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 驱逐最近最少使用的项
|
|
||||||
*/
|
|
||||||
private evictLeastRecentlyUsed(): void {
|
|
||||||
const tailNode = this.accessList.removeTail();
|
|
||||||
if (tailNode) {
|
|
||||||
// 调用驱逐回调
|
|
||||||
if (this.config.onEvict) {
|
|
||||||
this.config.onEvict(tailNode.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从Map中移除
|
|
||||||
this.items.delete(tailNode.key);
|
|
||||||
|
|
||||||
// 清理资源
|
|
||||||
this.disposeItem(tailNode.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理缓存项资源
|
|
||||||
*
|
|
||||||
* @param item 要清理的缓存项
|
|
||||||
*/
|
|
||||||
private disposeItem(item: T): void {
|
|
||||||
if (this.isDisposable(item)) {
|
|
||||||
try {
|
|
||||||
const result = item.dispose();
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
result.catch(error => {
|
|
||||||
console.warn('Failed to dispose cache item:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to dispose cache item:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查项是否可清理
|
|
||||||
*
|
|
||||||
* @param item 缓存项
|
|
||||||
* @returns 是否可清理
|
|
||||||
*/
|
|
||||||
private isDisposable(item: T): item is T & DisposableCacheItem {
|
|
||||||
return 'dispose' in item && typeof (item as any).dispose === 'function';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
263
frontend/src/common/cache/manager.ts
vendored
263
frontend/src/common/cache/manager.ts
vendored
@@ -1,263 +0,0 @@
|
|||||||
import type { CacheItem, CacheConfig, CacheStats, CacheStrategy } from './interfaces';
|
|
||||||
import { LruCache } from './lruCache';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存管理器
|
|
||||||
* 统一管理多个缓存实例,提供全局缓存操作和自动清理功能
|
|
||||||
* 支持不同的缓存策略,默认使用LRU缓存
|
|
||||||
*/
|
|
||||||
export class CacheManager {
|
|
||||||
/** 存储所有缓存实例的Map */
|
|
||||||
private caches = new Map<string, CacheStrategy<any>>();
|
|
||||||
|
|
||||||
/** 自动清理定时器ID */
|
|
||||||
private cleanupInterval?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
*
|
|
||||||
* @param options 配置选项
|
|
||||||
* @param options.cleanupInterval 自动清理间隔(毫秒),默认不启用
|
|
||||||
*/
|
|
||||||
constructor(options?: {
|
|
||||||
/** 自动清理间隔(毫秒),默认 5 分钟 */
|
|
||||||
cleanupInterval?: number;
|
|
||||||
}) {
|
|
||||||
if (options?.cleanupInterval) {
|
|
||||||
this.startAutoCleanup(options.cleanupInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建或获取缓存实例
|
|
||||||
* 如果缓存不存在且提供了配置,则创建新的缓存实例
|
|
||||||
*
|
|
||||||
* @template T 缓存项类型
|
|
||||||
* @param name 缓存名称
|
|
||||||
* @param config 缓存配置(仅在创建新缓存时需要)
|
|
||||||
* @param strategy 缓存策略构造函数,默认使用LruCache
|
|
||||||
* @returns 缓存实例
|
|
||||||
* @throws 如果缓存不存在且未提供配置
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const userCache = manager.getCache<UserCacheItem>('users', {
|
|
||||||
* maxSize: 100,
|
|
||||||
* ttl: 5 * 60 * 1000 // 5分钟
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
getCache<T extends CacheItem>(
|
|
||||||
name: string,
|
|
||||||
config?: CacheConfig,
|
|
||||||
strategy: new (config: CacheConfig) => CacheStrategy<T> = LruCache
|
|
||||||
): CacheStrategy<T> {
|
|
||||||
if (!this.caches.has(name)) {
|
|
||||||
if (!config) {
|
|
||||||
throw new Error(`Cache "${name}" does not exist and no config provided`);
|
|
||||||
}
|
|
||||||
this.caches.set(name, new strategy(config));
|
|
||||||
}
|
|
||||||
return this.caches.get(name)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新的缓存实例
|
|
||||||
* 如果同名缓存已存在,则抛出错误
|
|
||||||
*
|
|
||||||
* @template T 缓存项类型
|
|
||||||
* @param name 缓存名称
|
|
||||||
* @param config 缓存配置
|
|
||||||
* @param strategy 缓存策略构造函数,默认使用LruCache
|
|
||||||
* @returns 新创建的缓存实例
|
|
||||||
* @throws 如果同名缓存已存在
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const productCache = manager.createCache<ProductCacheItem>('products', {
|
|
||||||
* maxSize: 200,
|
|
||||||
* ttl: 10 * 60 * 1000 // 10分钟
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
createCache<T extends CacheItem>(
|
|
||||||
name: string,
|
|
||||||
config: CacheConfig,
|
|
||||||
strategy: new (config: CacheConfig) => CacheStrategy<T> = LruCache
|
|
||||||
): CacheStrategy<T> {
|
|
||||||
if (this.caches.has(name)) {
|
|
||||||
throw new Error(`Cache "${name}" already exists`);
|
|
||||||
}
|
|
||||||
const cache = new strategy(config);
|
|
||||||
this.caches.set(name, cache);
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除缓存实例
|
|
||||||
* 会先清空缓存内容,然后从管理器中移除
|
|
||||||
*
|
|
||||||
* @param name 缓存名称
|
|
||||||
* @returns 是否成功删除
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const removed = manager.removeCache('temp-cache');
|
|
||||||
* console.log(removed); // true 或 false
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
removeCache(name: string): boolean {
|
|
||||||
const cache = this.caches.get(name);
|
|
||||||
if (cache) {
|
|
||||||
cache.clear();
|
|
||||||
this.caches.delete(name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查缓存是否存在
|
|
||||||
*
|
|
||||||
* @param name 缓存名称
|
|
||||||
* @returns 是否存在
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* if (manager.hasCache('users')) {
|
|
||||||
* const userCache = manager.getCache('users');
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
hasCache(name: string): boolean {
|
|
||||||
return this.caches.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有缓存名称
|
|
||||||
*
|
|
||||||
* @returns 缓存名称数组
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const cacheNames = manager.getCacheNames();
|
|
||||||
* console.log('Active caches:', cacheNames);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
getCacheNames(): string[] {
|
|
||||||
return Array.from(this.caches.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空所有缓存
|
|
||||||
* 清空所有缓存实例的内容,但不删除缓存实例本身
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* manager.clearAll(); // 清空所有缓存内容
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
clearAll(): void {
|
|
||||||
for (const cache of this.caches.values()) {
|
|
||||||
cache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有缓存的统计信息
|
|
||||||
*
|
|
||||||
* @returns 包含所有缓存统计信息的对象
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const stats = manager.getAllStats();
|
|
||||||
* console.log('Cache stats:', stats);
|
|
||||||
* // 输出: { users: { size: 50, hits: 100, ... }, products: { ... } }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
getAllStats(): Record<string, CacheStats> {
|
|
||||||
const stats: Record<string, CacheStats> = {};
|
|
||||||
for (const [name, cache] of this.caches.entries()) {
|
|
||||||
stats[name] = cache.getStats();
|
|
||||||
}
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理所有缓存中的过期项
|
|
||||||
*
|
|
||||||
* @returns 包含每个缓存清理项数量的对象
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const results = manager.cleanupAll();
|
|
||||||
* console.log('Cleanup results:', results);
|
|
||||||
* // 输出: { users: 5, products: 2 } // 表示清理的项数量
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
cleanupAll(): Record<string, number> {
|
|
||||||
const results: Record<string, number> = {};
|
|
||||||
for (const [name, cache] of this.caches.entries()) {
|
|
||||||
results[name] = cache.cleanup();
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动自动清理
|
|
||||||
* 定期清理所有缓存中的过期项
|
|
||||||
*
|
|
||||||
* @param interval 清理间隔(毫秒)
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* manager.startAutoCleanup(5 * 60 * 1000); // 每5分钟清理一次
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
startAutoCleanup(interval: number): void {
|
|
||||||
this.stopAutoCleanup();
|
|
||||||
this.cleanupInterval = window.setInterval(() => {
|
|
||||||
this.cleanupAll();
|
|
||||||
}, interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止自动清理
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* manager.stopAutoCleanup();
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
stopAutoCleanup(): void {
|
|
||||||
if (this.cleanupInterval) {
|
|
||||||
clearInterval(this.cleanupInterval);
|
|
||||||
this.cleanupInterval = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁管理器
|
|
||||||
* 停止自动清理,清空所有缓存,并移除所有缓存实例
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* manager.destroy(); // 完全清理管理器
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
this.stopAutoCleanup();
|
|
||||||
this.clearAll();
|
|
||||||
this.caches.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 全局缓存管理器实例
|
|
||||||
* 提供开箱即用的缓存管理功能,默认启用5分钟自动清理
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* import { globalCacheManager } from './cache';
|
|
||||||
*
|
|
||||||
* const userCache = globalCacheManager.getCache('users', {
|
|
||||||
* maxSize: 100,
|
|
||||||
* ttl: 5 * 60 * 1000
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const globalCacheManager = new CacheManager({
|
|
||||||
cleanupInterval: 5 * 60 * 1000 // 5 分钟
|
|
||||||
});
|
|
||||||
180
frontend/src/common/cache/utils.ts
vendored
180
frontend/src/common/cache/utils.ts
vendored
@@ -1,180 +0,0 @@
|
|||||||
import type { CacheItem } from './interfaces';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 简单哈希函数
|
|
||||||
* 使用FNV-1a算法生成哈希值,提供良好的分布性
|
|
||||||
*
|
|
||||||
* @param content 要哈希的内容
|
|
||||||
* @returns 哈希值字符串
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const hash = createHash('some content');
|
|
||||||
* // 结果: 类似 '1a2b3c4d'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function createHash(content: string): string {
|
|
||||||
const FNV_OFFSET_BASIS = 2166136261;
|
|
||||||
const FNV_PRIME = 16777619;
|
|
||||||
|
|
||||||
let hash = FNV_OFFSET_BASIS;
|
|
||||||
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
hash ^= content.charCodeAt(i);
|
|
||||||
hash = (hash * FNV_PRIME) >>> 0; // 无符号32位整数
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash.toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成缓存键
|
|
||||||
* 将多个部分组合成一个缓存键
|
|
||||||
*
|
|
||||||
* @param parts 键的各个部分
|
|
||||||
* @returns 组合后的缓存键
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const key = generateCacheKey('user', 123, 'profile');
|
|
||||||
* // 结果: 'user:123:profile'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function generateCacheKey(...parts: (string | number)[]): string {
|
|
||||||
return parts.join(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建基础缓存项
|
|
||||||
* 为任意数据创建符合CacheItem接口的缓存项
|
|
||||||
*
|
|
||||||
* @template T 数据类型
|
|
||||||
* @param id 缓存项的唯一标识
|
|
||||||
* @param data 要缓存的数据
|
|
||||||
* @returns 包含缓存元数据的缓存项
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const cacheItem = createCacheItem('user:123', { name: 'John', age: 30 });
|
|
||||||
* // 结果包含 id, lastAccessed, createdAt 以及原始数据
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function createCacheItem<T extends Record<string, any>>(
|
|
||||||
id: string | number,
|
|
||||||
data: T
|
|
||||||
): CacheItem & T {
|
|
||||||
const now = new Date();
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
lastAccessed: now,
|
|
||||||
createdAt: now,
|
|
||||||
...data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算缓存命中率
|
|
||||||
* 根据命中次数和未命中次数计算命中率
|
|
||||||
*
|
|
||||||
* @param hits 命中次数
|
|
||||||
* @param misses 未命中次数
|
|
||||||
* @returns 命中率(0-1之间的数值)
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const hitRate = calculateHitRate(80, 20);
|
|
||||||
* // 结果: 0.8 (80% 命中率)
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function calculateHitRate(hits: number, misses: number): number {
|
|
||||||
const total = hits + misses;
|
|
||||||
return total > 0 ? hits / total : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化缓存大小
|
|
||||||
* 将字节数格式化为人类可读的大小字符串
|
|
||||||
*
|
|
||||||
* @param size 大小(字节)
|
|
||||||
* @returns 格式化后的大小字符串
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* formatCacheSize(1024); // '1.0 KB'
|
|
||||||
* formatCacheSize(1048576); // '1.0 MB'
|
|
||||||
* formatCacheSize(500); // '500 B'
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function formatCacheSize(size: number): string {
|
|
||||||
if (size < 1024) return `${size} B`;
|
|
||||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|
||||||
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
||||||
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查项是否过期
|
|
||||||
* 根据最后访问时间和TTL判断缓存项是否过期
|
|
||||||
*
|
|
||||||
* @param item 缓存项
|
|
||||||
* @param ttl 生存时间(毫秒)
|
|
||||||
* @returns 是否过期
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const item = createCacheItem('test', { data: 'value' });
|
|
||||||
* const expired = isExpired(item, 5000); // 5秒TTL
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function isExpired(item: CacheItem, ttl: number): boolean {
|
|
||||||
return Date.now() - item.lastAccessed.getTime() > ttl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 防抖函数,用于缓存操作
|
|
||||||
* 在指定时间内多次调用只执行最后一次
|
|
||||||
*
|
|
||||||
* @template T 函数类型
|
|
||||||
* @param func 要防抖的函数
|
|
||||||
* @param wait 等待时间(毫秒)
|
|
||||||
* @returns 防抖后的函数
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const debouncedSave = debounce(saveToCache, 300);
|
|
||||||
* debouncedSave(data); // 只有在300ms内没有新调用时才执行
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function debounce<T extends (...args: any[]) => any>(
|
|
||||||
func: T,
|
|
||||||
wait: number
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let timeout: number | undefined;
|
|
||||||
|
|
||||||
return (...args: Parameters<T>) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = window.setTimeout(() => func(...args), wait);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节流函数,用于缓存清理
|
|
||||||
* 在指定时间内最多执行一次
|
|
||||||
*
|
|
||||||
* @template T 函数类型
|
|
||||||
* @param func 要节流的函数
|
|
||||||
* @param limit 限制时间间隔(毫秒)
|
|
||||||
* @returns 节流后的函数
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const throttledCleanup = throttle(cleanupCache, 1000);
|
|
||||||
* throttledCleanup(); // 1秒内最多执行一次
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function throttle<T extends (...args: any[]) => any>(
|
|
||||||
func: T,
|
|
||||||
limit: number
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let inThrottle: boolean;
|
|
||||||
|
|
||||||
return (...args: Parameters<T>) => {
|
|
||||||
if (!inThrottle) {
|
|
||||||
func(...args);
|
|
||||||
inThrottle = true;
|
|
||||||
setTimeout(() => inThrottle = false, limit);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@ export type SupportedLocaleType = 'zh-CN' | 'en-US';
|
|||||||
|
|
||||||
// 支持的语言列表
|
// 支持的语言列表
|
||||||
export const SUPPORTED_LOCALES = [
|
export const SUPPORTED_LOCALES = [
|
||||||
{
|
|
||||||
code: 'zh-CN' as SupportedLocaleType,
|
|
||||||
name: '简体中文'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
code: 'en-US' as SupportedLocaleType,
|
code: 'en-US' as SupportedLocaleType,
|
||||||
name: 'English'
|
name: 'English'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'zh-CN' as SupportedLocaleType,
|
||||||
|
name: '简体中文'
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
111
frontend/src/common/utils/debounce.ts
Normal file
111
frontend/src/common/utils/debounce.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* 防抖函数工具类
|
||||||
|
* 用于限制函数的执行频率,在指定时间内只执行最后一次调用
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface DebounceOptions {
|
||||||
|
/** 延迟时间(毫秒),默认 300ms */
|
||||||
|
delay?: number;
|
||||||
|
/** 是否立即执行第一次调用,默认 false */
|
||||||
|
immediate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建防抖函数
|
||||||
|
* @param fn 要防抖的函数
|
||||||
|
* @param options 防抖选项
|
||||||
|
* @returns 返回防抖后的函数和清理函数
|
||||||
|
*/
|
||||||
|
export function createDebounce<T extends (...args: any[]) => any>(
|
||||||
|
fn: T,
|
||||||
|
options: DebounceOptions = {}
|
||||||
|
): {
|
||||||
|
debouncedFn: T;
|
||||||
|
cancel: () => void;
|
||||||
|
flush: () => void;
|
||||||
|
} {
|
||||||
|
const { delay = 300, immediate = false } = options;
|
||||||
|
let timeoutId: number | null = null;
|
||||||
|
let lastArgs: Parameters<T> | null = null;
|
||||||
|
let lastThis: any = null;
|
||||||
|
|
||||||
|
const debouncedFn = function (this: any, ...args: Parameters<T>) {
|
||||||
|
lastArgs = args;
|
||||||
|
lastThis = this;
|
||||||
|
|
||||||
|
const callNow = immediate && !timeoutId;
|
||||||
|
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
timeoutId = null;
|
||||||
|
if (!immediate && lastArgs) {
|
||||||
|
fn.apply(lastThis, lastArgs);
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
if (callNow) {
|
||||||
|
return fn.apply(this, args);
|
||||||
|
}
|
||||||
|
} as T;
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = null;
|
||||||
|
}
|
||||||
|
lastArgs = null;
|
||||||
|
lastThis = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (timeoutId && lastArgs) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
fn.apply(lastThis, lastArgs);
|
||||||
|
timeoutId = null;
|
||||||
|
lastArgs = null;
|
||||||
|
lastThis = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
debouncedFn,
|
||||||
|
cancel,
|
||||||
|
flush
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数
|
||||||
|
* 在指定时间内最多执行一次函数
|
||||||
|
* @param fn 要节流的函数
|
||||||
|
* @param delay 节流时间间隔(毫秒)
|
||||||
|
* @returns 节流后的函数
|
||||||
|
*/
|
||||||
|
export function throttle<T extends (...args: any[]) => any>(
|
||||||
|
fn: T,
|
||||||
|
delay: number = 300
|
||||||
|
): T {
|
||||||
|
let lastExecTime = 0;
|
||||||
|
let timeoutId: number | null = null;
|
||||||
|
|
||||||
|
return ((...args: Parameters<T>) => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (now - lastExecTime >= delay) {
|
||||||
|
lastExecTime = now;
|
||||||
|
fn(...args);
|
||||||
|
} else {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
lastExecTime = Date.now();
|
||||||
|
fn(...args);
|
||||||
|
}, delay - (now - lastExecTime));
|
||||||
|
}
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
162
frontend/src/common/utils/timerUtils.ts
Normal file
162
frontend/src/common/utils/timerUtils.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/**
|
||||||
|
* 定时器管理工具类
|
||||||
|
* 提供安全的定时器创建、清理和管理功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时器管理器接口
|
||||||
|
*/
|
||||||
|
export interface TimerManager {
|
||||||
|
/** 当前定时器 ID */
|
||||||
|
readonly timerId: number | null;
|
||||||
|
/** 清除定时器 */
|
||||||
|
clear(): void;
|
||||||
|
/** 设置定时器 */
|
||||||
|
set(callback: () => void, delay: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建定时器管理器工厂函数
|
||||||
|
* 提供安全的定时器管理,自动处理清理和重置
|
||||||
|
*
|
||||||
|
* @returns 定时器管理器实例
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const timer = createTimerManager();
|
||||||
|
*
|
||||||
|
* // 设置定时器
|
||||||
|
* timer.set(() => {
|
||||||
|
* console.log('Timer executed');
|
||||||
|
* }, 1000);
|
||||||
|
*
|
||||||
|
* // 清除定时器
|
||||||
|
* timer.clear();
|
||||||
|
*
|
||||||
|
* // 检查定时器状态
|
||||||
|
* if (timer.timerId !== null) {
|
||||||
|
* console.log('Timer is running');
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const createTimerManager = (): TimerManager => {
|
||||||
|
let timerId: number | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
get timerId() {
|
||||||
|
return timerId;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
if (timerId !== null) {
|
||||||
|
window.clearTimeout(timerId);
|
||||||
|
timerId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set(callback: () => void, delay: number) {
|
||||||
|
// 先清除现有定时器
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
// 设置新定时器
|
||||||
|
timerId = window.setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
timerId = null; // 执行完成后自动重置
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带有自动清理功能的定时器
|
||||||
|
* 适用于需要在组件卸载时自动清理的场景
|
||||||
|
*
|
||||||
|
* @param onCleanup 清理回调函数,通常在 onScopeDispose 或 onUnmounted 中调用
|
||||||
|
* @returns 定时器管理器实例
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { onScopeDispose } from 'vue';
|
||||||
|
*
|
||||||
|
* const timer = createAutoCleanupTimer(() => {
|
||||||
|
* // 组件卸载时自动清理
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* onScopeDispose(() => {
|
||||||
|
* timer.clear();
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const createAutoCleanupTimer = (onCleanup?: () => void): TimerManager => {
|
||||||
|
const timer = createTimerManager();
|
||||||
|
|
||||||
|
// 如果提供了清理回调,则包装 clear 方法
|
||||||
|
if (onCleanup) {
|
||||||
|
const originalClear = timer.clear.bind(timer);
|
||||||
|
timer.clear = () => {
|
||||||
|
originalClear();
|
||||||
|
onCleanup();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return timer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 延迟执行工具函数
|
||||||
|
* 简化的 Promise 版本延迟执行
|
||||||
|
*
|
||||||
|
* @param delay 延迟时间(毫秒)
|
||||||
|
* @returns Promise
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* await delay(1000); // 延迟 1 秒
|
||||||
|
* console.log('1 second later');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const delay = (delay: number): Promise<void> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可取消的延迟 Promise
|
||||||
|
*
|
||||||
|
* @param delay 延迟时间(毫秒)
|
||||||
|
* @returns 包含 promise 和 cancel 方法的对象
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const { promise, cancel } = createCancelableDelay(1000);
|
||||||
|
*
|
||||||
|
* promise
|
||||||
|
* .then(() => console.log('Executed'))
|
||||||
|
* .catch(() => console.log('Cancelled'));
|
||||||
|
*
|
||||||
|
* // 取消延迟
|
||||||
|
* cancel();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const createCancelableDelay = (delay: number) => {
|
||||||
|
let timeoutId: number;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
timeoutId = window.setTimeout(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { promise, cancel };
|
||||||
|
};
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
|
import {computed, nextTick, onMounted, onUnmounted, ref, watch} from 'vue';
|
||||||
import { SystemService } from '@/../bindings/voidraft/internal/services';
|
import type {MemoryStats} from '@/../bindings/voidraft/internal/services';
|
||||||
import type { MemoryStats } from '@/../bindings/voidraft/internal/services';
|
import {SystemService} from '@/../bindings/voidraft/internal/services';
|
||||||
import { useI18n } from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import { useThemeStore } from '@/stores/themeStore';
|
import {useThemeStore} from '@/stores/themeStore';
|
||||||
import { SystemThemeType } from '@/../bindings/voidraft/internal/models/models';
|
import {SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const {t} = useI18n();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
|
// 响应式状态
|
||||||
const memoryStats = ref<MemoryStats | null>(null);
|
const memoryStats = ref<MemoryStats | null>(null);
|
||||||
const formattedMemory = ref('');
|
const formattedMemory = ref('');
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
||||||
|
|
||||||
// 存储历史数据点 (最近60个数据点)
|
// 存储历史数据点 (最近60个数据点)
|
||||||
const historyData = ref<number[]>([]);
|
const historyData = ref<number[]>([]);
|
||||||
@@ -21,209 +22,188 @@ const maxDataPoints = 60;
|
|||||||
// 动态最大内存值(MB),初始为200MB,会根据实际使用动态调整
|
// 动态最大内存值(MB),初始为200MB,会根据实际使用动态调整
|
||||||
const maxMemoryMB = ref(200);
|
const maxMemoryMB = ref(200);
|
||||||
|
|
||||||
// 使用themeStore获取当前主题
|
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
// 使用 computed 获取当前主题状态
|
||||||
const isDarkTheme = computed(() => {
|
const isDarkTheme = computed(() => {
|
||||||
const theme = themeStore.currentTheme;
|
const {currentTheme} = themeStore;
|
||||||
if (theme === SystemThemeType.SystemThemeAuto) {
|
return currentTheme === SystemThemeType.SystemThemeDark;
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
}
|
|
||||||
return theme === SystemThemeType.SystemThemeDark;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听主题变化,重新绘制图表
|
// 监听主题变化,重新绘制图表
|
||||||
watch(() => themeStore.currentTheme, () => {
|
watch(() => themeStore.currentTheme, () => {
|
||||||
nextTick(() => drawChart());
|
nextTick(drawChart);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 静默错误处理包装器
|
// 格式化内存显示函数
|
||||||
const withSilentErrorHandling = async <T>(
|
const formatMemorySize = (heapMB: number): string => {
|
||||||
operation: () => Promise<T>,
|
if (heapMB < 1) return `${(heapMB * 1024).toFixed(0)}K`;
|
||||||
fallback?: T
|
if (heapMB < 100) return `${heapMB.toFixed(1)}M`;
|
||||||
): Promise<T | undefined> => {
|
return `${heapMB.toFixed(0)}M`;
|
||||||
try {
|
|
||||||
return await operation();
|
|
||||||
} catch (_error) {
|
|
||||||
// 静默处理错误,不输出到控制台
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取内存统计信息
|
// 获取内存统计信息
|
||||||
const fetchMemoryStats = async () => {
|
const fetchMemoryStats = async (): Promise<void> => {
|
||||||
const stats = await withSilentErrorHandling(() => SystemService.GetMemoryStats());
|
try {
|
||||||
|
const stats = await SystemService.GetMemoryStats();
|
||||||
if (!stats) {
|
|
||||||
isLoading.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
memoryStats.value = stats;
|
memoryStats.value = stats;
|
||||||
|
|
||||||
// 格式化内存显示 - 主要显示堆内存使用量
|
// 格式化内存显示 - 主要显示堆内存使用量
|
||||||
const heapMB = (stats.heapInUse / 1024 / 1024);
|
const heapMB = stats.heapInUse / (1024 * 1024);
|
||||||
if (heapMB < 1) {
|
formattedMemory.value = formatMemorySize(heapMB);
|
||||||
formattedMemory.value = `${(heapMB * 1024).toFixed(0)}K`;
|
|
||||||
} else if (heapMB < 100) {
|
|
||||||
formattedMemory.value = `${heapMB.toFixed(1)}M`;
|
|
||||||
} else {
|
|
||||||
formattedMemory.value = `${heapMB.toFixed(0)}M`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动调整最大内存值,确保图表能够显示更大范围
|
// 自动调整最大内存值,确保图表能够显示更大范围
|
||||||
if (heapMB > maxMemoryMB.value * 0.8) {
|
if (heapMB > maxMemoryMB.value * 0.8) {
|
||||||
// 如果内存使用超过当前最大值的80%,则将最大值调整为当前使用值的2倍
|
|
||||||
maxMemoryMB.value = Math.ceil(heapMB * 2);
|
maxMemoryMB.value = Math.ceil(heapMB * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加新数据点到历史记录 - 使用动态最大值计算百分比
|
// 添加新数据点到历史记录 - 使用动态最大值计算百分比
|
||||||
const memoryUsagePercent = Math.min((heapMB / maxMemoryMB.value) * 100, 100);
|
const memoryUsagePercent = Math.min((heapMB / maxMemoryMB.value) * 100, 100);
|
||||||
historyData.value.push(memoryUsagePercent);
|
historyData.value = [...historyData.value, memoryUsagePercent].slice(-maxDataPoints);
|
||||||
|
|
||||||
// 保持最大数据点数量
|
|
||||||
if (historyData.value.length > maxDataPoints) {
|
|
||||||
historyData.value.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新图表
|
// 更新图表
|
||||||
drawChart();
|
drawChart();
|
||||||
|
} catch {
|
||||||
|
// 静默处理错误
|
||||||
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 绘制实时曲线图 - 简化版
|
// 获取主题相关颜色配置
|
||||||
const drawChart = () => {
|
const getThemeColors = () => {
|
||||||
if (!canvasRef.value || historyData.value.length === 0) return;
|
const isDark = isDarkTheme.value;
|
||||||
|
return {
|
||||||
const canvas = canvasRef.value;
|
grid: isDark ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.07)',
|
||||||
const ctx = canvas.getContext('2d');
|
line: isDark ? 'rgba(74, 158, 255, 0.6)' : 'rgba(37, 99, 235, 0.6)',
|
||||||
if (!ctx) return;
|
fill: isDark ? 'rgba(74, 158, 255, 0.05)' : 'rgba(37, 99, 235, 0.05)',
|
||||||
|
point: isDark ? 'rgba(74, 158, 255, 0.8)' : 'rgba(37, 99, 235, 0.8)'
|
||||||
// 设置canvas尺寸
|
};
|
||||||
const rect = canvas.getBoundingClientRect();
|
};
|
||||||
canvas.width = rect.width * window.devicePixelRatio;
|
|
||||||
canvas.height = rect.height * window.devicePixelRatio;
|
// 绘制网格背景
|
||||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
const drawGrid = (ctx: CanvasRenderingContext2D, width: number, height: number, colors: ReturnType<typeof getThemeColors>): void => {
|
||||||
|
ctx.strokeStyle = colors.grid;
|
||||||
const width = rect.width;
|
ctx.lineWidth = 0.5;
|
||||||
const height = rect.height;
|
|
||||||
|
// 水平网格线
|
||||||
// 清除画布
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// 根据主题选择合适的颜色 - 更柔和的颜色
|
|
||||||
const gridColor = isDarkTheme.value ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.07)';
|
|
||||||
const lineColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.6)' : 'rgba(37, 99, 235, 0.6)';
|
|
||||||
const fillColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.05)' : 'rgba(37, 99, 235, 0.05)';
|
|
||||||
const pointColor = isDarkTheme.value ? 'rgba(74, 158, 255, 0.8)' : 'rgba(37, 99, 235, 0.8)';
|
|
||||||
|
|
||||||
// 绘制背景网格 - 更加柔和
|
|
||||||
for (let i = 0; i <= 4; i++) {
|
for (let i = 0; i <= 4; i++) {
|
||||||
const y = (height / 4) * i;
|
const y = (height / 4) * i;
|
||||||
ctx.strokeStyle = gridColor;
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(0, y);
|
ctx.moveTo(0, y);
|
||||||
ctx.lineTo(width, y);
|
ctx.lineTo(width, y);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 垂直网格线
|
// 垂直网格线
|
||||||
for (let i = 0; i <= 6; i++) {
|
for (let i = 0; i <= 6; i++) {
|
||||||
const x = (width / 6) * i;
|
const x = (width / 6) * i;
|
||||||
ctx.strokeStyle = gridColor;
|
|
||||||
ctx.lineWidth = 0.5;
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x, 0);
|
ctx.moveTo(x, 0);
|
||||||
ctx.lineTo(x, height);
|
ctx.lineTo(x, height);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制平滑曲线路径
|
||||||
|
const drawSmoothPath = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
data: number[],
|
||||||
|
startX: number,
|
||||||
|
stepX: number,
|
||||||
|
height: number,
|
||||||
|
fillArea = false
|
||||||
|
): void => {
|
||||||
|
if (data.length < 2) return;
|
||||||
|
|
||||||
|
const firstY = height - (data[0] / 100) * height;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
if (fillArea) ctx.moveTo(startX, height);
|
||||||
|
ctx.moveTo(startX, firstY);
|
||||||
|
|
||||||
|
// 使用二次贝塞尔曲线绘制平滑路径
|
||||||
|
for (let i = 1; i < data.length; i++) {
|
||||||
|
const x = startX + i * stepX;
|
||||||
|
const y = height - (data[i] / 100) * height;
|
||||||
|
|
||||||
|
if (i === 1) {
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
} else {
|
||||||
|
const prevX = startX + (i - 1) * stepX;
|
||||||
|
const prevY = height - (data[i - 1] / 100) * height;
|
||||||
|
const cpX = (prevX + x) / 2;
|
||||||
|
const cpY = (prevY + y) / 2;
|
||||||
|
ctx.quadraticCurveTo(cpX, cpY, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillArea) {
|
||||||
|
const lastX = startX + (data.length - 1) * stepX;
|
||||||
|
ctx.lineTo(lastX, height);
|
||||||
|
ctx.closePath();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制实时曲线图
|
||||||
|
const drawChart = (): void => {
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
if (!canvas || historyData.value.length === 0) return;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// 设置canvas尺寸
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const dpr = window.devicePixelRatio;
|
||||||
|
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = rect.height * dpr;
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
const {width, height} = rect;
|
||||||
|
|
||||||
|
// 清除画布
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// 获取主题颜色
|
||||||
|
const colors = getThemeColors();
|
||||||
|
|
||||||
|
// 绘制背景网格
|
||||||
|
drawGrid(ctx, width, height, colors);
|
||||||
|
|
||||||
if (historyData.value.length < 2) return;
|
if (historyData.value.length < 2) return;
|
||||||
|
|
||||||
// 计算数据点位置
|
// 计算数据点位置
|
||||||
const dataLength = historyData.value.length;
|
const dataLength = historyData.value.length;
|
||||||
const stepX = width / (maxDataPoints - 1);
|
const stepX = width / (maxDataPoints - 1);
|
||||||
const startX = width - (dataLength - 1) * stepX;
|
const startX = width - (dataLength - 1) * stepX;
|
||||||
|
|
||||||
// 绘制填充区域 - 更柔和的填充
|
// 绘制填充区域
|
||||||
ctx.beginPath();
|
drawSmoothPath(ctx, historyData.value, startX, stepX, height, true);
|
||||||
ctx.moveTo(startX, height);
|
ctx.fillStyle = colors.fill;
|
||||||
|
|
||||||
// 移动到第一个数据点
|
|
||||||
const firstY = height - (historyData.value[0] / 100) * height;
|
|
||||||
ctx.lineTo(startX, firstY);
|
|
||||||
|
|
||||||
// 绘制数据点路径 - 使用曲线连接点,确保连续性
|
|
||||||
for (let i = 1; i < dataLength; i++) {
|
|
||||||
const x = startX + i * stepX;
|
|
||||||
const y = height - (historyData.value[i] / 100) * height;
|
|
||||||
|
|
||||||
// 使用贝塞尔曲线平滑连接
|
|
||||||
if (i < dataLength - 1) {
|
|
||||||
const nextX = startX + (i + 1) * stepX;
|
|
||||||
const nextY = height - (historyData.value[i + 1] / 100) * height;
|
|
||||||
const cpX1 = x - stepX / 4;
|
|
||||||
const cpY1 = y;
|
|
||||||
const cpX2 = x + stepX / 4;
|
|
||||||
const cpY2 = nextY;
|
|
||||||
|
|
||||||
// 使用三次贝塞尔曲线平滑连接点
|
|
||||||
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
|
|
||||||
i++; // 跳过下一个点,因为已经在曲线中处理了
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完成填充路径
|
|
||||||
const lastX = startX + (dataLength - 1) * stepX;
|
|
||||||
ctx.lineTo(lastX, height);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.fillStyle = fillColor;
|
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 绘制主曲线 - 平滑连续的曲线
|
// 绘制主曲线
|
||||||
ctx.beginPath();
|
drawSmoothPath(ctx, historyData.value, startX, stepX, height);
|
||||||
ctx.moveTo(startX, firstY);
|
ctx.strokeStyle = colors.line;
|
||||||
|
|
||||||
// 重新绘制曲线路径,但这次只绘制线条
|
|
||||||
for (let i = 1; i < dataLength; i++) {
|
|
||||||
const x = startX + i * stepX;
|
|
||||||
const y = height - (historyData.value[i] / 100) * height;
|
|
||||||
|
|
||||||
// 使用贝塞尔曲线平滑连接
|
|
||||||
if (i < dataLength - 1) {
|
|
||||||
const nextX = startX + (i + 1) * stepX;
|
|
||||||
const nextY = height - (historyData.value[i + 1] / 100) * height;
|
|
||||||
const cpX1 = x - stepX / 4;
|
|
||||||
const cpY1 = y;
|
|
||||||
const cpX2 = x + stepX / 4;
|
|
||||||
const cpY2 = nextY;
|
|
||||||
|
|
||||||
// 使用三次贝塞尔曲线平滑连接点
|
|
||||||
ctx.bezierCurveTo(cpX1, cpY1, cpX2, cpY2, nextX, nextY);
|
|
||||||
i++; // 跳过下一个点,因为已经在曲线中处理了
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.strokeStyle = lineColor;
|
|
||||||
ctx.lineWidth = 1.5;
|
ctx.lineWidth = 1.5;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 绘制当前值的高亮点
|
// 绘制当前值的高亮点
|
||||||
|
const lastX = startX + (dataLength - 1) * stepX;
|
||||||
const lastY = height - (historyData.value[dataLength - 1] / 100) * height;
|
const lastY = height - (historyData.value[dataLength - 1] / 100) * height;
|
||||||
|
|
||||||
// 外圈
|
// 外圈
|
||||||
ctx.fillStyle = pointColor;
|
ctx.fillStyle = colors.point;
|
||||||
ctx.globalAlpha = 0.4;
|
ctx.globalAlpha = 0.4;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
|
ctx.arc(lastX, lastY, 3, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 内圈
|
// 内圈
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -232,72 +212,32 @@ const drawChart = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 手动触发GC
|
// 手动触发GC
|
||||||
const triggerGC = async () => {
|
const triggerGC = async (): Promise<void> => {
|
||||||
const success = await withSilentErrorHandling(() => SystemService.TriggerGC());
|
try {
|
||||||
|
await SystemService.TriggerGC();
|
||||||
if (success) {
|
} catch (error) {
|
||||||
// 延迟一下再获取新的统计信息
|
console.error("Failed to trigger GC: ", error);
|
||||||
setTimeout(fetchMemoryStats, 100);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理窗口大小变化
|
|
||||||
const handleResize = () => {
|
|
||||||
if (historyData.value.length > 0) {
|
|
||||||
nextTick(() => drawChart());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 仅监听系统主题变化
|
|
||||||
const setupSystemThemeListener = () => {
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
const handleSystemThemeChange = () => {
|
|
||||||
// 仅当设置为auto时才响应系统主题变化
|
|
||||||
if (themeStore.currentTheme === SystemThemeType.SystemThemeAuto) {
|
|
||||||
nextTick(() => drawChart());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加监听器
|
|
||||||
if (mediaQuery.addEventListener) {
|
|
||||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回清理函数
|
|
||||||
return () => {
|
|
||||||
if (mediaQuery.removeEventListener) {
|
|
||||||
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMemoryStats();
|
fetchMemoryStats();
|
||||||
// 每1秒更新一次内存信息
|
// 每3秒更新一次内存信息
|
||||||
intervalId = setInterval(fetchMemoryStats, 3000);
|
intervalId = setInterval(fetchMemoryStats, 3000);
|
||||||
|
});
|
||||||
// 监听窗口大小变化
|
|
||||||
window.addEventListener('resize', handleResize);
|
onUnmounted(() => {
|
||||||
|
intervalId && clearInterval(intervalId);
|
||||||
// 设置系统主题监听器(仅用于auto模式)
|
|
||||||
const cleanupThemeListener = setupSystemThemeListener();
|
|
||||||
|
|
||||||
// 在卸载时清理
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (intervalId) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
}
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
cleanupThemeListener();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="memory-monitor" @click="triggerGC" :title="`${t('monitor.memory')}: ${formattedMemory} | ${t('monitor.clickToClean')}`">
|
<div class="memory-monitor" @click="triggerGC"
|
||||||
|
:title="`${t('monitor.memory')}: ${formattedMemory} | ${t('monitor.clickToClean')}`">
|
||||||
<div class="monitor-info">
|
<div class="monitor-info">
|
||||||
<div class="memory-label">
|
<div class="memory-label">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{{ t('monitor.memory') }}</span>
|
<span>{{ t('monitor.memory') }}</span>
|
||||||
@@ -306,10 +246,10 @@ onMounted(() => {
|
|||||||
<div class="memory-loading" v-else>--</div>
|
<div class="memory-loading" v-else>--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-area">
|
<div class="chart-area">
|
||||||
<canvas
|
<canvas
|
||||||
ref="canvasRef"
|
ref="canvasRef"
|
||||||
class="memory-chart"
|
class="memory-chart"
|
||||||
:class="{ 'loading': isLoading }"
|
:class="{ 'loading': isLoading }"
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -323,28 +263,28 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.monitor-info {
|
.monitor-info {
|
||||||
.memory-label {
|
.memory-label {
|
||||||
color: var(--selection-text);
|
color: var(--selection-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-value {
|
.memory-value {
|
||||||
color: var(--toolbar-text);
|
color: var(--toolbar-text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-area .memory-chart {
|
.chart-area .memory-chart {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitor-info {
|
.monitor-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
.memory-label {
|
.memory-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -353,18 +293,18 @@ onMounted(() => {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-value, .memory-loading {
|
.memory-value, .memory-loading {
|
||||||
color: var(--toolbar-text-secondary);
|
color: var(--toolbar-text-secondary);
|
||||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||||
@@ -372,26 +312,26 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: color 0.2s ease;
|
transition: color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memory-loading {
|
.memory-loading {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-area {
|
.chart-area {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
.memory-chart {
|
.memory-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
@@ -407,4 +347,4 @@ onMounted(() => {
|
|||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick, shallowRef, readonly, effectScope, onScopeDispose } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useEditorStore } from '@/stores/editorStore';
|
import { useEditorStore } from '@/stores/editorStore';
|
||||||
import { type SupportedLanguage } from '@/views/editor/extensions/codeblock/types';
|
import { type SupportedLanguage } from '@/views/editor/extensions/codeblock/types';
|
||||||
@@ -8,56 +8,46 @@ import { getActiveNoteBlock } from '@/views/editor/extensions/codeblock/state';
|
|||||||
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
|
import { changeCurrentBlockLanguage } from '@/views/editor/extensions/codeblock/commands';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const editorStore = useEditorStore();
|
const editorStore = readonly(useEditorStore());
|
||||||
|
|
||||||
// 组件状态
|
// 组件状态
|
||||||
const showLanguageMenu = ref(false);
|
const showLanguageMenu = shallowRef(false);
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const searchInputRef = ref<HTMLInputElement>();
|
const searchInputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
// 支持的语言列表
|
// 优化语言数据处理
|
||||||
const supportedLanguages = getAllSupportedLanguages();
|
const languageData = computed(() => {
|
||||||
|
const supportedLanguages = getAllSupportedLanguages();
|
||||||
// 动态生成语言显示名称映射
|
|
||||||
const languageNames = computed(() => {
|
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
auto: 'Auto',
|
auto: 'Auto',
|
||||||
text: 'Plain Text'
|
text: 'Plain Text'
|
||||||
};
|
};
|
||||||
|
|
||||||
LANGUAGES.forEach(lang => {
|
|
||||||
names[lang.token] = lang.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 动态生成语言别名映射
|
|
||||||
const languageAliases = computed(() => {
|
|
||||||
const aliases: Record<string, string> = {
|
const aliases: Record<string, string> = {
|
||||||
auto: 'auto',
|
auto: 'auto',
|
||||||
text: 'txt'
|
text: 'txt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 一次遍历完成所有映射
|
||||||
LANGUAGES.forEach(lang => {
|
LANGUAGES.forEach(lang => {
|
||||||
// 使用语言名称的小写作为别名
|
names[lang.token] = lang.name;
|
||||||
aliases[lang.token] = lang.name.toLowerCase();
|
aliases[lang.token] = lang.name.toLowerCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
return aliases;
|
return {
|
||||||
|
supportedLanguages,
|
||||||
|
names,
|
||||||
|
aliases
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当前活动块的语言信息
|
// 当前活动块的语言信息
|
||||||
const currentBlockLanguage = ref<{ name: SupportedLanguage; auto: boolean }>({
|
const currentBlockLanguage = shallowRef<{ name: SupportedLanguage; auto: boolean }>({
|
||||||
name: 'text',
|
name: 'text',
|
||||||
auto: false
|
auto: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// 事件监听器引用
|
// 事件监听器管理
|
||||||
const eventListeners = ref<{
|
let editorScope: ReturnType<typeof effectScope> | null = null;
|
||||||
updateListener?: () => void;
|
|
||||||
selectionUpdateListener?: () => void;
|
|
||||||
}>({});
|
|
||||||
|
|
||||||
// 更新当前块语言信息
|
// 更新当前块语言信息
|
||||||
const updateCurrentBlockLanguage = () => {
|
const updateCurrentBlockLanguage = () => {
|
||||||
@@ -81,7 +71,7 @@ const updateCurrentBlockLanguage = () => {
|
|||||||
currentBlockLanguage.value = newLanguage;
|
currentBlockLanguage.value = newLanguage;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto !== false) {
|
if (currentBlockLanguage.value.name !== 'text' || currentBlockLanguage.value.auto) {
|
||||||
currentBlockLanguage.value = { name: 'text', auto: false };
|
currentBlockLanguage.value = { name: 'text', auto: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,57 +83,47 @@ const updateCurrentBlockLanguage = () => {
|
|||||||
|
|
||||||
// 清理事件监听器
|
// 清理事件监听器
|
||||||
const cleanupEventListeners = () => {
|
const cleanupEventListeners = () => {
|
||||||
if (editorStore.editorView?.dom && eventListeners.value.updateListener) {
|
if (editorScope) {
|
||||||
const dom = editorStore.editorView.dom;
|
editorScope.stop();
|
||||||
dom.removeEventListener('click', eventListeners.value.updateListener);
|
editorScope = null;
|
||||||
dom.removeEventListener('keyup', eventListeners.value.updateListener);
|
|
||||||
dom.removeEventListener('keydown', eventListeners.value.updateListener);
|
|
||||||
dom.removeEventListener('focus', eventListeners.value.updateListener);
|
|
||||||
dom.removeEventListener('mouseup', eventListeners.value.updateListener);
|
|
||||||
|
|
||||||
if (eventListeners.value.selectionUpdateListener) {
|
|
||||||
dom.removeEventListener('selectionchange', eventListeners.value.selectionUpdateListener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
eventListeners.value = {};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置事件监听器
|
// 设置事件监听器 - 使用 effectScope 管理
|
||||||
const setupEventListeners = (view: any) => {
|
const setupEventListeners = (view: any) => {
|
||||||
cleanupEventListeners();
|
cleanupEventListeners();
|
||||||
|
|
||||||
// 监听编辑器状态更新
|
editorScope = effectScope();
|
||||||
const updateListener = () => {
|
editorScope.run(() => {
|
||||||
// 使用 requestAnimationFrame 确保在下一帧更新,性能更好
|
// 监听编辑器状态更新
|
||||||
requestAnimationFrame(() => {
|
const updateListener = () => {
|
||||||
updateCurrentBlockLanguage();
|
// 使用 requestAnimationFrame 确保在下一帧更新
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
updateCurrentBlockLanguage();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听关键事件:光标位置变化、文档变化、焦点变化
|
||||||
|
view.dom.addEventListener('click', updateListener);
|
||||||
|
view.dom.addEventListener('keyup', updateListener);
|
||||||
|
view.dom.addEventListener('keydown', updateListener);
|
||||||
|
view.dom.addEventListener('focus', updateListener);
|
||||||
|
view.dom.addEventListener('mouseup', updateListener);
|
||||||
|
view.dom.addEventListener('selectionchange', updateListener);
|
||||||
|
|
||||||
|
// 在 scope 销毁时清理
|
||||||
|
onScopeDispose(() => {
|
||||||
|
view.dom.removeEventListener('click', updateListener);
|
||||||
|
view.dom.removeEventListener('keyup', updateListener);
|
||||||
|
view.dom.removeEventListener('keydown', updateListener);
|
||||||
|
view.dom.removeEventListener('focus', updateListener);
|
||||||
|
view.dom.removeEventListener('mouseup', updateListener);
|
||||||
|
view.dom.removeEventListener('selectionchange', updateListener);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
// 立即更新一次当前状态
|
||||||
// 监听选择变化
|
updateCurrentBlockLanguage();
|
||||||
const selectionUpdateListener = () => {
|
});
|
||||||
requestAnimationFrame(() => {
|
|
||||||
updateCurrentBlockLanguage();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存监听器引用
|
|
||||||
eventListeners.value = { updateListener, selectionUpdateListener };
|
|
||||||
|
|
||||||
// 监听关键事件:光标位置变化、文档变化、焦点变化
|
|
||||||
view.dom.addEventListener('click', updateListener);
|
|
||||||
view.dom.addEventListener('keyup', updateListener);
|
|
||||||
view.dom.addEventListener('keydown', updateListener);
|
|
||||||
view.dom.addEventListener('focus', updateListener);
|
|
||||||
view.dom.addEventListener('mouseup', updateListener); // 鼠标选择结束
|
|
||||||
|
|
||||||
// 监听编辑器的选择变化事件
|
|
||||||
if (view.dom.addEventListener) {
|
|
||||||
view.dom.addEventListener('selectionchange', selectionUpdateListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即更新一次当前状态
|
|
||||||
updateCurrentBlockLanguage();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听编辑器状态变化
|
// 监听编辑器状态变化
|
||||||
@@ -159,16 +139,18 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 过滤后的语言列表
|
// 过滤后的语言列表 - 优化搜索性能
|
||||||
const filteredLanguages = computed(() => {
|
const filteredLanguages = computed(() => {
|
||||||
|
const { supportedLanguages, names, aliases } = languageData.value;
|
||||||
|
|
||||||
if (!searchQuery.value) {
|
if (!searchQuery.value) {
|
||||||
return supportedLanguages;
|
return supportedLanguages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
const query = searchQuery.value.toLowerCase();
|
||||||
return supportedLanguages.filter(langId => {
|
return supportedLanguages.filter(langId => {
|
||||||
const name = languageNames.value[langId];
|
const name = names[langId];
|
||||||
const alias = languageAliases.value[langId];
|
const alias = aliases[langId];
|
||||||
return langId.toLowerCase().includes(query) ||
|
return langId.toLowerCase().includes(query) ||
|
||||||
(name && name.toLowerCase().includes(query)) ||
|
(name && name.toLowerCase().includes(query)) ||
|
||||||
(alias && alias.toLowerCase().includes(query));
|
(alias && alias.toLowerCase().includes(query));
|
||||||
@@ -191,7 +173,7 @@ const closeLanguageMenu = () => {
|
|||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选择语言
|
// 选择语言 - 优化性能
|
||||||
const selectLanguage = (languageId: SupportedLanguage) => {
|
const selectLanguage = (languageId: SupportedLanguage) => {
|
||||||
if (!editorStore.editorView) {
|
if (!editorStore.editorView) {
|
||||||
closeLanguageMenu();
|
closeLanguageMenu();
|
||||||
@@ -203,18 +185,9 @@ const selectLanguage = (languageId: SupportedLanguage) => {
|
|||||||
const state = view.state;
|
const state = view.state;
|
||||||
const dispatch = view.dispatch;
|
const dispatch = view.dispatch;
|
||||||
|
|
||||||
let targetLanguage: string;
|
const [targetLanguage, autoDetect] = languageId === 'auto'
|
||||||
let autoDetect: boolean;
|
? ['text', true]
|
||||||
|
: [languageId, false];
|
||||||
if (languageId === 'auto') {
|
|
||||||
// 设置为自动检测
|
|
||||||
targetLanguage = 'text';
|
|
||||||
autoDetect = true;
|
|
||||||
} else {
|
|
||||||
// 设置为指定语言,关闭自动检测
|
|
||||||
targetLanguage = languageId;
|
|
||||||
autoDetect = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用修复后的函数来更改语言
|
// 使用修复后的函数来更改语言
|
||||||
const success = changeCurrentBlockLanguage(state as any, dispatch, targetLanguage, autoDetect);
|
const success = changeCurrentBlockLanguage(state as any, dispatch, targetLanguage, autoDetect);
|
||||||
@@ -231,72 +204,75 @@ const selectLanguage = (languageId: SupportedLanguage) => {
|
|||||||
closeLanguageMenu();
|
closeLanguageMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 点击外部关闭
|
// 全局事件处理器 - 使用 effectScope 管理
|
||||||
|
const globalScope = effectScope();
|
||||||
|
|
||||||
|
// 点击外部关闭 - 只在菜单打开时处理
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (!showLanguageMenu.value) return;
|
||||||
|
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (!target.closest('.block-language-selector')) {
|
if (!target.closest('.block-language-selector')) {
|
||||||
closeLanguageMenu();
|
closeLanguageMenu();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 键盘事件处理
|
// 键盘事件处理 - 只在菜单打开时处理
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (!showLanguageMenu.value) return;
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
closeLanguageMenu();
|
closeLanguageMenu();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
// 在 setup 阶段就设置全局事件监听器
|
||||||
|
globalScope.run(() => {
|
||||||
document.addEventListener('click', handleClickOutside);
|
document.addEventListener('click', handleClickOutside);
|
||||||
document.addEventListener('keydown', handleKeydown);
|
document.addEventListener('keydown', handleKeydown);
|
||||||
|
|
||||||
|
onScopeDispose(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleKeydown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
// 立即更新一次当前语言状态
|
// 立即更新一次当前语言状态
|
||||||
updateCurrentBlockLanguage();
|
updateCurrentBlockLanguage();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside);
|
globalScope.stop();
|
||||||
document.removeEventListener('keydown', handleKeydown);
|
|
||||||
cleanupEventListeners();
|
cleanupEventListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取当前语言的显示名称
|
// 优化计算属性
|
||||||
const getCurrentLanguageName = computed(() => {
|
const languageDisplayInfo = computed(() => {
|
||||||
const lang = currentBlockLanguage.value;
|
const lang = currentBlockLanguage.value;
|
||||||
if (lang.auto) {
|
const displayName = lang.auto ? `${lang.name} (auto)` : lang.name;
|
||||||
return `${lang.name} (auto)`;
|
const displayLanguage = lang.auto ? 'auto' : lang.name;
|
||||||
}
|
|
||||||
return lang.name;
|
return {
|
||||||
|
name: displayName,
|
||||||
|
language: displayLanguage
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取当前显示的语言选项
|
// 滚动到当前选择的语言 - 优化性能
|
||||||
const getCurrentDisplayLanguage = computed(() => {
|
|
||||||
const lang = currentBlockLanguage.value;
|
|
||||||
if (lang.auto) {
|
|
||||||
return 'auto';
|
|
||||||
}
|
|
||||||
return lang.name;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 滚动到当前选择的语言
|
|
||||||
const scrollToCurrentLanguage = () => {
|
const scrollToCurrentLanguage = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const currentLang = getCurrentDisplayLanguage.value;
|
const currentLang = languageDisplayInfo.value.language;
|
||||||
const selectorElement = document.querySelector('.block-language-selector');
|
const activeOption = document.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
|
||||||
|
|
||||||
if (!selectorElement) return;
|
if (activeOption) {
|
||||||
|
// 使用 scrollIntoView 进行平滑滚动
|
||||||
const languageList = selectorElement.querySelector('.language-list') as HTMLElement;
|
activeOption.scrollIntoView({
|
||||||
const activeOption = selectorElement.querySelector(`.language-option[data-language="${currentLang}"]`) as HTMLElement;
|
behavior: 'auto',
|
||||||
|
block: 'nearest',
|
||||||
if (languageList && activeOption) {
|
inline: 'nearest'
|
||||||
// 使用 scrollIntoView 进行平滑滚动
|
});
|
||||||
activeOption.scrollIntoView({
|
}
|
||||||
behavior: 'auto',
|
|
||||||
block: 'nearest',
|
|
||||||
inline: 'nearest'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -314,7 +290,7 @@ const scrollToCurrentLanguage = () => {
|
|||||||
<polyline points="8 6 2 12 8 18"></polyline>
|
<polyline points="8 6 2 12 8 18"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="language-name">{{ getCurrentLanguageName }}</span>
|
<span class="language-name">{{ languageDisplayInfo.name }}</span>
|
||||||
<span class="arrow" :class="{ 'open': showLanguageMenu }">▲</span>
|
<span class="arrow" :class="{ 'open': showLanguageMenu }">▲</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -341,11 +317,11 @@ const scrollToCurrentLanguage = () => {
|
|||||||
v-for="language in filteredLanguages"
|
v-for="language in filteredLanguages"
|
||||||
:key="language"
|
:key="language"
|
||||||
class="language-option"
|
class="language-option"
|
||||||
:class="{ 'active': getCurrentDisplayLanguage === language }"
|
:class="{ 'active': languageDisplayInfo.language === language }"
|
||||||
:data-language="language"
|
:data-language="language"
|
||||||
@click="selectLanguage(language)"
|
@click="selectLanguage(language)"
|
||||||
>
|
>
|
||||||
<span class="language-name">{{ languageNames[language] || language }}</span>
|
<span class="language-name">{{ languageData.names[language] || language }}</span>
|
||||||
<span class="language-alias">{{ language }}</span>
|
<span class="language-alias">{{ language }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -517,4 +493,4 @@ const scrollToCurrentLanguage = () => {
|
|||||||
background-color: var(--text-muted);
|
background-color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -82,7 +82,7 @@ const formatTime = (dateString: string | null) => {
|
|||||||
// 核心操作
|
// 核心操作
|
||||||
const openMenu = async () => {
|
const openMenu = async () => {
|
||||||
documentStore.openDocumentSelector();
|
documentStore.openDocumentSelector();
|
||||||
await documentStore.updateDocuments();
|
await documentStore.getDocumentMetaList();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
inputRef.value?.focus();
|
inputRef.value?.focus();
|
||||||
};
|
};
|
||||||
@@ -158,7 +158,7 @@ const saveEdit = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||||
await documentStore.updateDocuments();
|
await documentStore.getDocumentMetaList();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update document:', error);
|
console.error('Failed to update document:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -191,7 +191,7 @@ const handleDelete = async (doc: Document, event: Event) => {
|
|||||||
|
|
||||||
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
||||||
if (deleteSuccess) {
|
if (deleteSuccess) {
|
||||||
await documentStore.updateDocuments();
|
await documentStore.getDocumentMetaList();
|
||||||
// 如果删除的是当前文档,切换到第一个文档
|
// 如果删除的是当前文档,切换到第一个文档
|
||||||
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
if (documentStore.currentDocument?.id === doc.id && documentStore.documentList.length > 0) {
|
||||||
const firstDoc = documentStore.documentList[0];
|
const firstDoc = documentStore.documentList[0];
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
|
import {computed, onMounted, onUnmounted, ref, watch, shallowRef, readonly, toRefs, effectScope, onScopeDispose} from 'vue';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {useEditorStore} from '@/stores/editorStore';
|
import {useEditorStore} from '@/stores/editorStore';
|
||||||
import {useUpdateStore} from '@/stores/updateStore';
|
import {useUpdateStore} from '@/stores/updateStore';
|
||||||
import {useWindowStore} from '@/stores/windowStore';
|
import {useWindowStore} from '@/stores/windowStore';
|
||||||
import {useSystemStore} from '@/stores/systemStore';
|
import {useSystemStore} from '@/stores/systemStore';
|
||||||
import * as runtime from '@wailsio/runtime';
|
|
||||||
import {useRouter} from 'vue-router';
|
import {useRouter} from 'vue-router';
|
||||||
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
import BlockLanguageSelector from './BlockLanguageSelector.vue';
|
||||||
import DocumentSelector from './DocumentSelector.vue';
|
import DocumentSelector from './DocumentSelector.vue';
|
||||||
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
|
import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state';
|
||||||
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
|
import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages';
|
||||||
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
|
import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode';
|
||||||
|
import {createDebounce} from '@/common/utils/debounce';
|
||||||
|
|
||||||
const editorStore = useEditorStore();
|
const editorStore = readonly(useEditorStore());
|
||||||
const configStore = useConfigStore();
|
const configStore = readonly(useConfigStore());
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = readonly(useUpdateStore());
|
||||||
const windowStore = useWindowStore();
|
const windowStore = readonly(useWindowStore());
|
||||||
const systemStore = useSystemStore();
|
const systemStore = readonly(useSystemStore());
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 当前块是否支持格式化的响应式状态
|
|
||||||
const canFormatCurrentBlock = ref(false);
|
const canFormatCurrentBlock = ref(false);
|
||||||
|
const isLoaded = shallowRef(false);
|
||||||
|
|
||||||
// 窗口置顶状态 - 合并配置和临时状态
|
const { documentStats } = toRefs(editorStore);
|
||||||
|
const { config } = toRefs(configStore);
|
||||||
|
|
||||||
|
// 窗口置顶状态
|
||||||
const isCurrentWindowOnTop = computed(() => {
|
const isCurrentWindowOnTop = computed(() => {
|
||||||
return configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
|
return config.value.general.alwaysOnTop || systemStore.isWindowOnTop;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换窗口置顶状态
|
// 切换窗口置顶状态
|
||||||
const toggleAlwaysOnTop = async () => {
|
const toggleAlwaysOnTop = async () => {
|
||||||
const currentlyOnTop = isCurrentWindowOnTop.value;
|
const currentlyOnTop = isCurrentWindowOnTop.value;
|
||||||
|
|
||||||
if (currentlyOnTop) {
|
if (currentlyOnTop) {
|
||||||
// 如果当前是置顶状态,彻底关闭所有置顶
|
// 如果当前是置顶状态,彻底关闭所有置顶
|
||||||
if (configStore.config.general.alwaysOnTop) {
|
if (config.value.general.alwaysOnTop) {
|
||||||
await configStore.setAlwaysOnTop(false);
|
await configStore.setAlwaysOnTop(false);
|
||||||
}
|
}
|
||||||
await systemStore.setWindowOnTop(false);
|
await systemStore.setWindowOnTop(false);
|
||||||
@@ -57,9 +60,8 @@ const formatCurrentBlock = () => {
|
|||||||
formatBlockContent(editorStore.editorView);
|
formatBlockContent(editorStore.editorView);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化按钮状态更新
|
// 格式化按钮状态更新 - 使用更高效的检查逻辑
|
||||||
const updateFormatButtonState = () => {
|
const updateFormatButtonState = () => {
|
||||||
// 安全检查
|
|
||||||
const view = editorStore.editorView;
|
const view = editorStore.editorView;
|
||||||
if (!view) {
|
if (!view) {
|
||||||
canFormatCurrentBlock.value = false;
|
canFormatCurrentBlock.value = false;
|
||||||
@@ -67,156 +69,158 @@ const updateFormatButtonState = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取活动块和语言信息
|
|
||||||
const state = view.state;
|
const state = view.state;
|
||||||
const activeBlock = getActiveNoteBlock(state as any);
|
const activeBlock = getActiveNoteBlock(state as any);
|
||||||
|
|
||||||
|
// 提前返回,减少不必要的计算
|
||||||
|
if (!activeBlock) {
|
||||||
|
canFormatCurrentBlock.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查块和语言格式化支持
|
const language = getLanguage(activeBlock.language.name as any);
|
||||||
canFormatCurrentBlock.value = !!(
|
canFormatCurrentBlock.value = Boolean(language?.prettier);
|
||||||
activeBlock &&
|
|
||||||
getLanguage(activeBlock.language.name as any)?.prettier
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error checking format capability:', error);
|
console.warn('Error checking format capability:', error);
|
||||||
canFormatCurrentBlock.value = false;
|
canFormatCurrentBlock.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建带300ms防抖的更新函数
|
// 创建带1s防抖的更新函数
|
||||||
const debouncedUpdateFormatButton = (() => {
|
const { debouncedFn: debouncedUpdateFormat, cancel: cancelDebounce } = createDebounce(
|
||||||
let timeout: number | null = null;
|
updateFormatButtonState,
|
||||||
|
{ delay: 1000 }
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
// 使用 effectScope 管理编辑器事件监听器
|
||||||
if (timeout) clearTimeout(timeout);
|
const editorScope = effectScope();
|
||||||
timeout = window.setTimeout(() => {
|
let cleanupListeners: (() => void)[] = [];
|
||||||
updateFormatButtonState();
|
|
||||||
timeout = null;
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// 编辑器事件管理
|
// 优化的事件监听器管理
|
||||||
const setupEditorListeners = (view: any) => {
|
const setupEditorListeners = (view: any) => {
|
||||||
if (!view?.dom) return [];
|
if (!view?.dom) return [];
|
||||||
|
|
||||||
const events = [
|
// 使用对象缓存事件处理器,避免重复创建
|
||||||
{type: 'click', handler: updateFormatButtonState},
|
const eventHandlers = {
|
||||||
{type: 'keyup', handler: debouncedUpdateFormatButton},
|
click: updateFormatButtonState,
|
||||||
{type: 'focus', handler: updateFormatButtonState}
|
keyup: debouncedUpdateFormat,
|
||||||
];
|
focus: updateFormatButtonState
|
||||||
|
} as const;
|
||||||
|
|
||||||
// 注册所有事件
|
const events = Object.entries(eventHandlers).map(([type, handler]) => ({
|
||||||
events.forEach(event => view.dom.addEventListener(event.type, event.handler));
|
type,
|
||||||
|
handler,
|
||||||
|
cleanup: () => view.dom.removeEventListener(type, handler)
|
||||||
|
}));
|
||||||
|
|
||||||
// 返回清理函数数组
|
// 批量注册事件
|
||||||
return events.map(event =>
|
events.forEach(event => view.dom.addEventListener(event.type, event.handler, { passive: true }));
|
||||||
() => view.dom.removeEventListener(event.type, event.handler)
|
|
||||||
);
|
return events.map(event => event.cleanup);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听编辑器视图变化
|
// 监听编辑器视图变化
|
||||||
let cleanupListeners: (() => void)[] = [];
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => editorStore.editorView,
|
() => editorStore.editorView,
|
||||||
(newView) => {
|
(newView) => {
|
||||||
// 清理旧监听器
|
// 在 scope 中管理副作用
|
||||||
cleanupListeners.forEach(cleanup => cleanup());
|
editorScope.run(() => {
|
||||||
cleanupListeners = [];
|
// 清理旧监听器
|
||||||
|
cleanupListeners.forEach(cleanup => cleanup());
|
||||||
|
cleanupListeners = [];
|
||||||
|
|
||||||
if (newView) {
|
if (newView) {
|
||||||
// 初始更新状态
|
// 初始更新状态
|
||||||
updateFormatButtonState();
|
updateFormatButtonState();
|
||||||
// 设置新监听器
|
// 设置新监听器
|
||||||
cleanupListeners = setupEditorListeners(newView);
|
cleanupListeners = setupEditorListeners(newView);
|
||||||
} else {
|
} else {
|
||||||
canFormatCurrentBlock.value = false;
|
canFormatCurrentBlock.value = false;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
{immediate: true}
|
{ immediate: true, flush: 'post' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 组件生命周期
|
// 组件生命周期
|
||||||
const isLoaded = ref(false);
|
onMounted(async () => {
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
isLoaded.value = true;
|
isLoaded.value = true;
|
||||||
// 首次更新格式化状态
|
// 首次更新格式化状态
|
||||||
updateFormatButtonState();
|
updateFormatButtonState();
|
||||||
|
await systemStore.setWindowOnTop(isCurrentWindowOnTop.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 onScopeDispose 确保 scope 清理
|
||||||
|
onScopeDispose(() => {
|
||||||
|
cleanupListeners.forEach(cleanup => cleanup());
|
||||||
|
cleanupListeners = [];
|
||||||
|
cancelDebounce();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 清理所有事件监听器
|
// 停止 effect scope
|
||||||
cleanupListeners.forEach(cleanup => cleanup());
|
editorScope.stop();
|
||||||
cleanupListeners = [];
|
// 清理防抖函数
|
||||||
|
cancelDebounce();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 组件加载后初始化置顶状态
|
// 更新按钮处理
|
||||||
watch(isLoaded, async (loaded) => {
|
|
||||||
if (loaded) {
|
|
||||||
// 应用合并后的置顶状态
|
|
||||||
const shouldBeOnTop = configStore.config.general.alwaysOnTop || systemStore.isWindowOnTop;
|
|
||||||
try {
|
|
||||||
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to apply window pin state:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听配置变化,同步窗口状态
|
|
||||||
watch(
|
|
||||||
() => isCurrentWindowOnTop.value,
|
|
||||||
async (shouldBeOnTop) => {
|
|
||||||
try {
|
|
||||||
await runtime.Window.SetAlwaysOnTop(shouldBeOnTop);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to sync window pin state:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdateButtonClick = async () => {
|
const handleUpdateButtonClick = async () => {
|
||||||
if (updateStore.hasUpdate && !updateStore.isUpdating && !updateStore.updateSuccess) {
|
const { hasUpdate, isUpdating, updateSuccess } = updateStore;
|
||||||
// 开始下载更新
|
|
||||||
|
if (hasUpdate && !isUpdating && !updateSuccess) {
|
||||||
await updateStore.applyUpdate();
|
await updateStore.applyUpdate();
|
||||||
} else if (updateStore.updateSuccess) {
|
} else if (updateSuccess) {
|
||||||
// 更新成功后,点击重启
|
|
||||||
await updateStore.restartApplication();
|
await updateStore.restartApplication();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新按钮标题计算属性
|
// 更新按钮标题计算属性
|
||||||
const updateButtonTitle = computed(() => {
|
const updateButtonTitle = computed(() => {
|
||||||
if (updateStore.isChecking) return t('settings.checking');
|
const { isChecking, isUpdating, updateSuccess, hasUpdate, updateResult } = updateStore;
|
||||||
if (updateStore.isUpdating) return t('settings.updating');
|
|
||||||
if (updateStore.updateSuccess) return t('settings.updateSuccessRestartRequired');
|
if (isChecking) return t('settings.checking');
|
||||||
if (updateStore.hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateStore.updateResult?.latestVersion || ''}`;
|
if (isUpdating) return t('settings.updating');
|
||||||
|
if (updateSuccess) return t('settings.updateSuccessRestartRequired');
|
||||||
|
if (hasUpdate) return `${t('settings.newVersionAvailable')}: ${updateResult?.latestVersion || ''}`;
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 统计数据的计算属性
|
||||||
|
const statsData = computed(() => ({
|
||||||
|
lines: documentStats.value.lines,
|
||||||
|
characters: documentStats.value.characters,
|
||||||
|
selectedCharacters: documentStats.value.selectedCharacters
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="toolbar-container">
|
<div class="toolbar-container">
|
||||||
<div class="statistics">
|
<div class="statistics">
|
||||||
<span class="stat-item" :title="t('toolbar.editor.lines')">{{ t('toolbar.editor.lines') }}: <span
|
<span class="stat-item" :title="t('toolbar.editor.lines')">
|
||||||
class="stat-value">{{
|
{{ t('toolbar.editor.lines') }}:
|
||||||
editorStore.documentStats.lines
|
<span class="stat-value">{{ statsData.lines }}</span>
|
||||||
}}</span></span>
|
</span>
|
||||||
<span class="stat-item" :title="t('toolbar.editor.characters')">{{ t('toolbar.editor.characters') }}: <span
|
<span class="stat-item" :title="t('toolbar.editor.characters')">
|
||||||
class="stat-value">{{
|
{{ t('toolbar.editor.characters') }}:
|
||||||
editorStore.documentStats.characters
|
<span class="stat-value">{{ statsData.characters }}</span>
|
||||||
}}</span></span>
|
</span>
|
||||||
<span class="stat-item" :title="t('toolbar.editor.selected')"
|
<span
|
||||||
v-if="editorStore.documentStats.selectedCharacters > 0">
|
v-if="statsData.selectedCharacters > 0"
|
||||||
{{ t('toolbar.editor.selected') }}: <span class="stat-value">{{
|
class="stat-item"
|
||||||
editorStore.documentStats.selectedCharacters
|
:title="t('toolbar.editor.selected')"
|
||||||
}}</span>
|
>
|
||||||
|
{{ t('toolbar.editor.selected') }}:
|
||||||
|
<span class="stat-value">{{ statsData.selectedCharacters }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<span class="font-size" :title="t('toolbar.fontSizeTooltip')" @click="() => configStore.resetFontSize()">
|
<span
|
||||||
{{ configStore.config.editing.fontSize }}px
|
class="font-size"
|
||||||
|
:title="t('toolbar.fontSizeTooltip')"
|
||||||
|
@click="configStore.resetFontSize"
|
||||||
|
>
|
||||||
|
{{ config.editing.fontSize }}px
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- 文档选择器 -->
|
<!-- 文档选择器 -->
|
||||||
@@ -302,7 +306,6 @@ const updateButtonTitle = computed(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button v-if="windowStore.isMainWindow" class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
|
<button v-if="windowStore.isMainWindow" class="settings-btn" :title="t('toolbar.settings')" @click="goToSettings">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|||||||
@@ -1,123 +1,175 @@
|
|||||||
import {defineStore} from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import {computed, readonly, ref} from 'vue';
|
import { computed, readonly, ref, shallowRef, watchEffect, onScopeDispose } from 'vue';
|
||||||
import type {GitBackupConfig} from '@/../bindings/voidraft/internal/models';
|
import type { GitBackupConfig } from '@/../bindings/voidraft/internal/models';
|
||||||
import {BackupService} from '@/../bindings/voidraft/internal/services';
|
import { BackupService } from '@/../bindings/voidraft/internal/services';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
|
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||||
|
|
||||||
|
// 备份状态枚举
|
||||||
|
export enum BackupStatus {
|
||||||
|
IDLE = 'idle',
|
||||||
|
PUSHING = 'pushing',
|
||||||
|
SUCCESS = 'success',
|
||||||
|
ERROR = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备份操作结果类型
|
||||||
|
export interface BackupResult {
|
||||||
|
status: BackupStatus;
|
||||||
|
message?: string;
|
||||||
|
timestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型守卫函数
|
||||||
|
const isBackupError = (error: unknown): error is Error => {
|
||||||
|
return error instanceof Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 工具类型:提取错误消息
|
||||||
|
type ErrorMessage<T> = T extends Error ? string : string;
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimalist Backup Store
|
|
||||||
*/
|
|
||||||
export const useBackupStore = defineStore('backup', () => {
|
export const useBackupStore = defineStore('backup', () => {
|
||||||
// Core state
|
// === 核心状态 ===
|
||||||
const config = ref<GitBackupConfig | null>(null);
|
const config = shallowRef<GitBackupConfig | null>(null);
|
||||||
const isPushing = ref(false);
|
|
||||||
const error = ref<string | null>(null);
|
// 统一的备份结果状态
|
||||||
const isInitialized = ref(false);
|
const backupResult = ref<BackupResult>({
|
||||||
|
status: BackupStatus.IDLE
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 定时器管理 ===
|
||||||
|
const statusTimer = createTimerManager();
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onScopeDispose(() => {
|
||||||
|
statusTimer.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 外部依赖 ===
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
// === 计算属性 ===
|
||||||
|
const isEnabled = computed(() => configStore.config.backup.enabled);
|
||||||
|
const isConfigured = computed(() => Boolean(configStore.config.backup.repo_url?.trim()));
|
||||||
|
|
||||||
|
// 派生状态计算属性
|
||||||
|
const isPushing = computed(() => backupResult.value.status === BackupStatus.PUSHING);
|
||||||
|
const isSuccess = computed(() => backupResult.value.status === BackupStatus.SUCCESS);
|
||||||
|
const isError = computed(() => backupResult.value.status === BackupStatus.ERROR);
|
||||||
|
const errorMessage = computed(() =>
|
||||||
|
backupResult.value.status === BackupStatus.ERROR ? backupResult.value.message : null
|
||||||
|
);
|
||||||
|
|
||||||
|
// === 状态管理方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置备份状态
|
||||||
|
* @param status 备份状态
|
||||||
|
* @param message 可选消息
|
||||||
|
* @param autoHide 是否自动隐藏(毫秒)
|
||||||
|
*/
|
||||||
|
const setBackupStatus = <T extends BackupStatus>(
|
||||||
|
status: T,
|
||||||
|
message?: T extends BackupStatus.ERROR ? string : string,
|
||||||
|
autoHide?: number
|
||||||
|
): void => {
|
||||||
|
statusTimer.clear();
|
||||||
|
|
||||||
// Backup result states
|
backupResult.value = {
|
||||||
const pushSuccess = ref(false);
|
status,
|
||||||
const pushError = ref(false);
|
message,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动隐藏逻辑
|
||||||
|
if (autoHide && (status === BackupStatus.SUCCESS || status === BackupStatus.ERROR)) {
|
||||||
|
statusTimer.set(() => {
|
||||||
|
if (backupResult.value.status === status) {
|
||||||
|
backupResult.value = { status: BackupStatus.IDLE };
|
||||||
|
}
|
||||||
|
}, autoHide);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除当前状态
|
||||||
|
*/
|
||||||
|
const clearStatus = (): void => {
|
||||||
|
statusTimer.clear();
|
||||||
|
backupResult.value = { status: BackupStatus.IDLE };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理错误的通用方法
|
||||||
|
*/
|
||||||
|
const handleError = (error: unknown): void => {
|
||||||
|
const message: ErrorMessage<typeof error> = isBackupError(error)
|
||||||
|
? error.message
|
||||||
|
: 'Backup operation failed';
|
||||||
|
|
||||||
// Timers for auto-hiding status icons and error messages
|
setBackupStatus(BackupStatus.ERROR, message, 5000);
|
||||||
let pushStatusTimer: number | null = null;
|
};
|
||||||
let errorTimer: number | null = null;
|
|
||||||
|
|
||||||
// 获取configStore
|
// === 业务逻辑方法 ===
|
||||||
const configStore = useConfigStore();
|
|
||||||
|
/**
|
||||||
|
* 推送到远程仓库
|
||||||
|
* 使用现代 async/await 和错误处理
|
||||||
|
*/
|
||||||
|
const pushToRemote = async (): Promise<void> => {
|
||||||
|
// 前置条件检查
|
||||||
|
if (isPushing.value || !isConfigured.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Computed properties
|
try {
|
||||||
const isEnabled = computed(() => configStore.config.backup.enabled);
|
setBackupStatus(BackupStatus.PUSHING);
|
||||||
const isConfigured = computed(() => configStore.config.backup.repo_url);
|
|
||||||
|
await BackupService.PushToRemote();
|
||||||
|
|
||||||
|
setBackupStatus(BackupStatus.SUCCESS, 'Backup completed successfully', 3000);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 清除状态显示
|
/**
|
||||||
const clearPushStatus = () => {
|
* 重试备份操作
|
||||||
if (pushStatusTimer !== null) {
|
*/
|
||||||
window.clearTimeout(pushStatusTimer);
|
const retryBackup = async (): Promise<void> => {
|
||||||
pushStatusTimer = null;
|
if (isError.value) {
|
||||||
}
|
await pushToRemote();
|
||||||
pushSuccess.value = false;
|
}
|
||||||
pushError.value = false;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// 清除错误信息和错误图标
|
// === 响应式副作用 ===
|
||||||
const clearError = () => {
|
|
||||||
if (errorTimer !== null) {
|
// 监听配置变化,自动清除错误状态
|
||||||
window.clearTimeout(errorTimer);
|
watchEffect(() => {
|
||||||
errorTimer = null;
|
if (isEnabled.value && isConfigured.value && isError.value) {
|
||||||
}
|
// 配置修复后清除错误状态
|
||||||
error.value = null;
|
clearStatus();
|
||||||
pushError.value = false;
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// 设置错误信息和错误图标并自动清除
|
// === 返回的 API ===
|
||||||
const setErrorWithAutoHide = (errorMessage: string, hideAfter: number = 3000) => {
|
return {
|
||||||
clearError();
|
// 只读状态
|
||||||
clearPushStatus();
|
config: readonly(config),
|
||||||
error.value = errorMessage;
|
backupResult: readonly(backupResult),
|
||||||
pushError.value = true;
|
|
||||||
errorTimer = window.setTimeout(() => {
|
|
||||||
error.value = null;
|
|
||||||
pushError.value = false;
|
|
||||||
errorTimer = null;
|
|
||||||
}, hideAfter);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Push to remote repository
|
// 计算属性
|
||||||
const pushToRemote = async () => {
|
isEnabled,
|
||||||
if (isPushing.value || !isConfigured.value) return;
|
isConfigured,
|
||||||
|
isPushing,
|
||||||
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
errorMessage,
|
||||||
|
|
||||||
isPushing.value = true;
|
// 方法
|
||||||
clearError(); // 清除之前的错误信息
|
pushToRemote,
|
||||||
clearPushStatus();
|
retryBackup,
|
||||||
|
clearStatus
|
||||||
try {
|
} as const;
|
||||||
await BackupService.PushToRemote();
|
|
||||||
// 显示成功状态,并设置3秒后自动消失
|
|
||||||
pushSuccess.value = true;
|
|
||||||
pushStatusTimer = window.setTimeout(() => {
|
|
||||||
pushSuccess.value = false;
|
|
||||||
pushStatusTimer = null;
|
|
||||||
}, 3000);
|
|
||||||
} catch (err: any) {
|
|
||||||
setErrorWithAutoHide(err?.message || 'Backup operation failed');
|
|
||||||
} finally {
|
|
||||||
isPushing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化备份服务
|
|
||||||
const initialize = async () => {
|
|
||||||
if (!isEnabled.value) return;
|
|
||||||
|
|
||||||
// 避免重复初始化
|
|
||||||
if (isInitialized.value) return;
|
|
||||||
|
|
||||||
clearError(); // 清除之前的错误信息
|
|
||||||
try {
|
|
||||||
await BackupService.Initialize();
|
|
||||||
isInitialized.value = true;
|
|
||||||
} catch (err: any) {
|
|
||||||
setErrorWithAutoHide(err?.message || 'Failed to initialize backup service');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
// State
|
|
||||||
config: readonly(config),
|
|
||||||
isPushing: readonly(isPushing),
|
|
||||||
error: readonly(error),
|
|
||||||
isInitialized: readonly(isInitialized),
|
|
||||||
pushSuccess: readonly(pushSuccess),
|
|
||||||
pushError: readonly(pushError),
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
isEnabled,
|
|
||||||
isConfigured,
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
pushToRemote,
|
|
||||||
initialize,
|
|
||||||
clearError
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {ConfigUtils} from '@/common/utils/configUtils';
|
import {ConfigUtils} from '@/common/utils/configUtils';
|
||||||
import {FONT_OPTIONS} from '@/common/constant/fonts';
|
import {FONT_OPTIONS} from '@/common/constant/fonts';
|
||||||
import {SupportedLocaleType, SUPPORTED_LOCALES} from '@/common/constant/locales';
|
import {SUPPORTED_LOCALES} from '@/common/constant/locales';
|
||||||
import {
|
import {
|
||||||
NumberConfigKey,
|
NumberConfigKey,
|
||||||
GENERAL_CONFIG_KEY_MAP,
|
GENERAL_CONFIG_KEY_MAP,
|
||||||
@@ -29,19 +29,6 @@ import {
|
|||||||
} from '@/common/constant/config';
|
} from '@/common/constant/config';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
|
||||||
// 获取浏览器的默认语言
|
|
||||||
const getBrowserLanguage = (): SupportedLocaleType => {
|
|
||||||
const browserLang = navigator.language;
|
|
||||||
const langCode = browserLang.split('-')[0];
|
|
||||||
|
|
||||||
// 检查是否支持此语言
|
|
||||||
const supportedLang = SUPPORTED_LOCALES.find(locale =>
|
|
||||||
locale.code.startsWith(langCode) || locale.code.split('-')[0] === langCode
|
|
||||||
);
|
|
||||||
|
|
||||||
return supportedLang?.code || 'zh-CN';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const {locale} = useI18n();
|
const {locale} = useI18n();
|
||||||
|
|
||||||
@@ -231,7 +218,7 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language);
|
const frontendLocale = ConfigUtils.backendLanguageToFrontend(state.config.appearance.language);
|
||||||
locale.value = frontendLocale as any;
|
locale.value = frontendLocale as any;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
const browserLang = getBrowserLanguage();
|
const browserLang = SUPPORTED_LOCALES[0].code;
|
||||||
locale.value = browserLang as any;
|
locale.value = browserLang as any;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {Document} from '@/../bindings/voidraft/internal/models/models';
|
|||||||
|
|
||||||
export const useDocumentStore = defineStore('document', () => {
|
export const useDocumentStore = defineStore('document', () => {
|
||||||
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
|
const DEFAULT_DOCUMENT_ID = ref<number>(1); // 默认草稿文档ID
|
||||||
|
|
||||||
// === 核心状态 ===
|
// === 核心状态 ===
|
||||||
const documents = ref<Record<number, Document>>({});
|
const documents = ref<Record<number, Document>>({});
|
||||||
const currentDocumentId = ref<number | null>(null);
|
const currentDocumentId = ref<number | null>(null);
|
||||||
@@ -34,7 +34,7 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
|
|
||||||
// === 错误处理 ===
|
// === 错误处理 ===
|
||||||
const setError = (docId: number, message: string) => {
|
const setError = (docId: number, message: string) => {
|
||||||
selectorError.value = { docId, message };
|
selectorError.value = {docId, message};
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearError = () => {
|
const clearError = () => {
|
||||||
@@ -88,25 +88,8 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存新文档
|
// 获取文档列表
|
||||||
const saveNewDocument = async (title: string, content: string): Promise<Document | null> => {
|
const getDocumentMetaList = async () => {
|
||||||
try {
|
|
||||||
const doc = await DocumentService.CreateDocument(title);
|
|
||||||
if (doc) {
|
|
||||||
await DocumentService.UpdateDocumentContent(doc.id, content);
|
|
||||||
doc.content = content;
|
|
||||||
documents.value[doc.id] = doc;
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save new document:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新文档列表
|
|
||||||
const updateDocuments = async () => {
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
const docs = await DocumentService.ListAllDocumentsMeta();
|
const docs = await DocumentService.ListAllDocumentsMeta();
|
||||||
@@ -199,7 +182,7 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
// === 初始化 ===
|
// === 初始化 ===
|
||||||
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
await updateDocuments();
|
await getDocumentMetaList();
|
||||||
|
|
||||||
// 优先使用URL参数中的文档ID
|
// 优先使用URL参数中的文档ID
|
||||||
if (urlDocumentId && documents.value[urlDocumentId]) {
|
if (urlDocumentId && documents.value[urlDocumentId]) {
|
||||||
@@ -208,11 +191,8 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
// 如果URL中没有指定文档ID,则使用持久化的文档ID
|
// 如果URL中没有指定文档ID,则使用持久化的文档ID
|
||||||
await openDocument(currentDocumentId.value);
|
await openDocument(currentDocumentId.value);
|
||||||
} else {
|
} else {
|
||||||
// 否则获取第一个文档ID并打开
|
// 否则打开默认文档
|
||||||
const firstDocId = await DocumentService.GetFirstDocumentID();
|
await openDocument(DEFAULT_DOCUMENT_ID.value);
|
||||||
if (firstDocId && documents.value[firstDocId]) {
|
|
||||||
await openDocument(firstDocId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize document store:', error);
|
console.error('Failed to initialize document store:', error);
|
||||||
@@ -231,11 +211,10 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
updateDocuments,
|
getDocumentMetaList,
|
||||||
openDocument,
|
openDocument,
|
||||||
openDocumentInNewWindow,
|
openDocumentInNewWindow,
|
||||||
createNewDocument,
|
createNewDocument,
|
||||||
saveNewDocument,
|
|
||||||
updateDocumentMetadata,
|
updateDocumentMetadata,
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
openDocumentSelector,
|
openDocumentSelector,
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
import { defineStore } from 'pinia';
|
|
||||||
import { computed, shallowRef, type ShallowRef } from 'vue';
|
|
||||||
import { EditorView } from '@codemirror/view';
|
|
||||||
import { ensureSyntaxTree } from '@codemirror/language';
|
|
||||||
import { LruCache, type CacheItem, type DisposableCacheItem, createHash } from '@/common/cache';
|
|
||||||
import { removeExtensionManagerView } from '@/views/editor/manager';
|
|
||||||
|
|
||||||
/** 语法树缓存信息 */
|
|
||||||
interface SyntaxTreeCache {
|
|
||||||
readonly lastDocLength: number;
|
|
||||||
readonly lastContentHash: string;
|
|
||||||
readonly lastParsed: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 编辑器状态 */
|
|
||||||
interface EditorState {
|
|
||||||
content: string;
|
|
||||||
isDirty: boolean;
|
|
||||||
lastModified: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 编辑器缓存项 */
|
|
||||||
export interface EditorCacheItem extends CacheItem, DisposableCacheItem {
|
|
||||||
readonly view: EditorView;
|
|
||||||
readonly documentId: number;
|
|
||||||
state: EditorState;
|
|
||||||
autoSaveTimer: number | null;
|
|
||||||
syntaxTreeCache: SyntaxTreeCache | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === 缓存配置 ===
|
|
||||||
const CACHE_CONFIG = {
|
|
||||||
maxSize: 5,
|
|
||||||
syntaxTreeExpireTime: 30000, // 30秒
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const useEditorCacheStore = defineStore('editorCache', () => {
|
|
||||||
// === 状态 ===
|
|
||||||
const containerElement: ShallowRef<HTMLElement | null> = shallowRef(null);
|
|
||||||
|
|
||||||
// === 内部方法 ===
|
|
||||||
const cleanupEditor = (item: EditorCacheItem): void => {
|
|
||||||
try {
|
|
||||||
// 清除自动保存定时器
|
|
||||||
if (item.autoSaveTimer) {
|
|
||||||
clearTimeout(item.autoSaveTimer);
|
|
||||||
item.autoSaveTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从扩展管理器中移除视图
|
|
||||||
removeExtensionManagerView(item.documentId);
|
|
||||||
|
|
||||||
// 移除DOM元素
|
|
||||||
item.view.dom?.remove();
|
|
||||||
|
|
||||||
// 销毁编辑器
|
|
||||||
item.view.destroy?.();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to cleanup editor ${item.documentId}:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const createEditorCacheItem = (
|
|
||||||
documentId: number,
|
|
||||||
view: EditorView,
|
|
||||||
content: string
|
|
||||||
): EditorCacheItem => {
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
const item: EditorCacheItem = {
|
|
||||||
id: documentId,
|
|
||||||
lastAccessed: now,
|
|
||||||
createdAt: now,
|
|
||||||
view,
|
|
||||||
documentId,
|
|
||||||
state: {
|
|
||||||
content,
|
|
||||||
isDirty: false,
|
|
||||||
lastModified: now
|
|
||||||
},
|
|
||||||
autoSaveTimer: null,
|
|
||||||
syntaxTreeCache: null,
|
|
||||||
dispose: () => cleanupEditor(item)
|
|
||||||
};
|
|
||||||
|
|
||||||
return item;
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldRebuildSyntaxTree = (
|
|
||||||
item: EditorCacheItem,
|
|
||||||
docLength: number,
|
|
||||||
contentHash: string
|
|
||||||
): boolean => {
|
|
||||||
const { syntaxTreeCache } = item;
|
|
||||||
if (!syntaxTreeCache) return true;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const isExpired = (now - syntaxTreeCache.lastParsed.getTime()) > CACHE_CONFIG.syntaxTreeExpireTime;
|
|
||||||
const isContentChanged = syntaxTreeCache.lastDocLength !== docLength ||
|
|
||||||
syntaxTreeCache.lastContentHash !== contentHash;
|
|
||||||
|
|
||||||
return isExpired || isContentChanged;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildSyntaxTree = (view: EditorView, item: EditorCacheItem): void => {
|
|
||||||
const docLength = view.state.doc.length;
|
|
||||||
const content = view.state.doc.toString();
|
|
||||||
const contentHash = createHash(content);
|
|
||||||
|
|
||||||
if (!shouldRebuildSyntaxTree(item, docLength, contentHash)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
ensureSyntaxTree(view.state, docLength, 5000);
|
|
||||||
|
|
||||||
item.syntaxTreeCache = {
|
|
||||||
lastDocLength: docLength,
|
|
||||||
lastContentHash: contentHash,
|
|
||||||
lastParsed: new Date()
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to build syntax tree for editor ${item.documentId}:`, error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// === 缓存实例 ===
|
|
||||||
const cache = new LruCache<EditorCacheItem>({
|
|
||||||
maxSize: CACHE_CONFIG.maxSize,
|
|
||||||
onEvict: cleanupEditor
|
|
||||||
});
|
|
||||||
|
|
||||||
// === 计算属性 ===
|
|
||||||
const cacheSize = computed(() => cache.size());
|
|
||||||
const cacheStats = computed(() => cache.getStats());
|
|
||||||
const allEditors = computed(() => cache.getAll());
|
|
||||||
const dirtyEditors = computed(() =>
|
|
||||||
allEditors.value.filter(item => item.state.isDirty)
|
|
||||||
);
|
|
||||||
|
|
||||||
// === 公共方法 ===
|
|
||||||
|
|
||||||
// 容器管理
|
|
||||||
const setContainer = (element: HTMLElement | null): void => {
|
|
||||||
containerElement.value = element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getContainer = (): HTMLElement | null => containerElement.value;
|
|
||||||
|
|
||||||
// 基础缓存操作
|
|
||||||
const addEditor = (documentId: number, view: EditorView, content: string): void => {
|
|
||||||
const item = createEditorCacheItem(documentId, view, content);
|
|
||||||
cache.set(documentId, item);
|
|
||||||
|
|
||||||
// 初始化语法树缓存
|
|
||||||
buildSyntaxTree(view, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getEditor = (documentId: number): EditorCacheItem | null => {
|
|
||||||
return cache.get(documentId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasEditor = (documentId: number): boolean => {
|
|
||||||
return cache.has(documentId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEditor = (documentId: number): boolean => {
|
|
||||||
return cache.remove(documentId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAll = (): void => {
|
|
||||||
cache.clear();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑器状态管理
|
|
||||||
const updateEditorContent = (documentId: number, content: string): boolean => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
item.state.content = content;
|
|
||||||
item.state.isDirty = false;
|
|
||||||
item.state.lastModified = new Date();
|
|
||||||
item.syntaxTreeCache = null; // 清理语法树缓存
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const markEditorDirty = (documentId: number): boolean => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
item.state.isDirty = true;
|
|
||||||
item.state.lastModified = new Date();
|
|
||||||
item.syntaxTreeCache = null; // 清理语法树缓存
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自动保存管理
|
|
||||||
const setAutoSaveTimer = (documentId: number, timer: number): boolean => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (item.autoSaveTimer) {
|
|
||||||
clearTimeout(item.autoSaveTimer);
|
|
||||||
}
|
|
||||||
item.autoSaveTimer = timer;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearAutoSaveTimer = (documentId: number): boolean => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (!item || !item.autoSaveTimer) return false;
|
|
||||||
|
|
||||||
clearTimeout(item.autoSaveTimer);
|
|
||||||
item.autoSaveTimer = null;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 语法树管理
|
|
||||||
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (!item) return;
|
|
||||||
|
|
||||||
buildSyntaxTree(view, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanupExpiredSyntaxTrees = (): void => {
|
|
||||||
const now = Date.now();
|
|
||||||
allEditors.value.forEach(item => {
|
|
||||||
if (item.syntaxTreeCache &&
|
|
||||||
(now - item.syntaxTreeCache.lastParsed.getTime()) > CACHE_CONFIG.syntaxTreeExpireTime) {
|
|
||||||
item.syntaxTreeCache = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 容器管理
|
|
||||||
setContainer,
|
|
||||||
getContainer,
|
|
||||||
|
|
||||||
// 基础缓存操作
|
|
||||||
addEditor,
|
|
||||||
getEditor,
|
|
||||||
hasEditor,
|
|
||||||
removeEditor,
|
|
||||||
clearAll,
|
|
||||||
|
|
||||||
// 编辑器状态管理
|
|
||||||
updateEditorContent,
|
|
||||||
markEditorDirty,
|
|
||||||
|
|
||||||
// 自动保存管理
|
|
||||||
setAutoSaveTimer,
|
|
||||||
clearAutoSaveTimer,
|
|
||||||
|
|
||||||
// 语法树管理
|
|
||||||
ensureSyntaxTreeCached,
|
|
||||||
cleanupExpiredSyntaxTrees,
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
cacheSize,
|
|
||||||
cacheStats,
|
|
||||||
allEditors,
|
|
||||||
dirtyEditors
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {computed, nextTick, ref, watch} from 'vue';
|
import {nextTick, ref, watch} from 'vue';
|
||||||
import {EditorView} from '@codemirror/view';
|
import {EditorView} from '@codemirror/view';
|
||||||
import {EditorState, Extension} from '@codemirror/state';
|
import {EditorState, Extension} from '@codemirror/state';
|
||||||
import {useConfigStore} from './configStore';
|
import {useConfigStore} from './configStore';
|
||||||
import {useDocumentStore} from './documentStore';
|
import {useDocumentStore} from './documentStore';
|
||||||
import {useThemeStore} from './themeStore';
|
import {useThemeStore} from './themeStore';
|
||||||
import {useEditorCacheStore} from './editorCacheStore';
|
|
||||||
import {ExtensionID, SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
import {ExtensionID, SystemThemeType} from '@/../bindings/voidraft/internal/models/models';
|
||||||
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
|
import {DocumentService, ExtensionService} from '@/../bindings/voidraft/internal/services';
|
||||||
|
import {ensureSyntaxTree} from "@codemirror/language";
|
||||||
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
import {createBasicSetup} from '@/views/editor/basic/basicSetup';
|
||||||
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
import {createThemeExtension, updateEditorTheme} from '@/views/editor/basic/themeExtension';
|
||||||
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
import {getTabExtensions, updateTabConfig} from '@/views/editor/basic/tabExtension';
|
||||||
@@ -15,15 +15,11 @@ import {createFontExtensionFromBackend, updateFontConfig} from '@/views/editor/b
|
|||||||
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
import {createStatsUpdateExtension} from '@/views/editor/basic/statsExtension';
|
||||||
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
import {createContentChangePlugin} from '@/views/editor/basic/contentChangeExtension';
|
||||||
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
import {createDynamicKeymapExtension, updateKeymapExtension} from '@/views/editor/keymap';
|
||||||
import {
|
import {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
||||||
createDynamicExtensions,
|
|
||||||
getExtensionManager,
|
|
||||||
removeExtensionManagerView,
|
|
||||||
setExtensionManagerView
|
|
||||||
} from '@/views/editor/manager';
|
|
||||||
import {useExtensionStore} from './extensionStore';
|
import {useExtensionStore} from './extensionStore';
|
||||||
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
import createCodeBlockExtension from "@/views/editor/extensions/codeblock";
|
||||||
import {AsyncOperationManager} from '@/common/async';
|
|
||||||
|
const NUM_EDITOR_INSTANCES = 5; // 最多缓存5个编辑器实例
|
||||||
|
|
||||||
export interface DocumentStats {
|
export interface DocumentStats {
|
||||||
lines: number;
|
lines: number;
|
||||||
@@ -31,48 +27,136 @@ export interface DocumentStats {
|
|||||||
selectedCharacters: number;
|
selectedCharacters: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditorInstance {
|
||||||
|
view: EditorView;
|
||||||
|
documentId: number;
|
||||||
|
content: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
lastModified: Date;
|
||||||
|
autoSaveTimer: number | null;
|
||||||
|
syntaxTreeCache: {
|
||||||
|
lastDocLength: number;
|
||||||
|
lastContentHash: string;
|
||||||
|
lastParsed: Date;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const useEditorStore = defineStore('editor', () => {
|
export const useEditorStore = defineStore('editor', () => {
|
||||||
// === 依赖store ===
|
// === 依赖store ===
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
const extensionStore = useExtensionStore();
|
const extensionStore = useExtensionStore();
|
||||||
const editorCacheStore = useEditorCacheStore();
|
|
||||||
|
|
||||||
// === 核心状态 ===
|
// === 核心状态 ===
|
||||||
|
const editorCache = ref<{
|
||||||
|
lru: number[];
|
||||||
|
instances: Record<number, EditorInstance>;
|
||||||
|
containerElement: HTMLElement | null;
|
||||||
|
}>({
|
||||||
|
lru: [],
|
||||||
|
instances: {},
|
||||||
|
containerElement: null
|
||||||
|
});
|
||||||
|
|
||||||
const currentEditor = ref<EditorView | null>(null);
|
const currentEditor = ref<EditorView | null>(null);
|
||||||
const documentStats = ref<DocumentStats>({
|
const documentStats = ref<DocumentStats>({
|
||||||
lines: 0,
|
lines: 0,
|
||||||
characters: 0,
|
characters: 0,
|
||||||
selectedCharacters: 0
|
selectedCharacters: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// 编辑器加载状态
|
// 编辑器加载状态
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// 异步操作管理器
|
// 异步操作竞态条件控制
|
||||||
const operationManager = new AsyncOperationManager({
|
const operationSequence = ref(0);
|
||||||
debug: false,
|
const pendingOperations = ref(new Map<number, AbortController>());
|
||||||
autoCleanup: true
|
const currentLoadingDocumentId = ref<number | null>(null);
|
||||||
});
|
|
||||||
|
|
||||||
// 自动保存设置
|
// 自动保存设置 - 从配置动态获取
|
||||||
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay;
|
||||||
|
|
||||||
|
// 生成新的操作序列号
|
||||||
|
const getNextOperationId = () => ++operationSequence.value;
|
||||||
|
|
||||||
|
// 取消之前的操作
|
||||||
|
const cancelPreviousOperations = (excludeId?: number) => {
|
||||||
|
pendingOperations.value.forEach((controller, id) => {
|
||||||
|
if (id !== excludeId) {
|
||||||
|
controller.abort();
|
||||||
|
pendingOperations.value.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查操作是否仍然有效
|
||||||
|
const isOperationValid = (operationId: number, documentId: number) => {
|
||||||
|
return (
|
||||||
|
pendingOperations.value.has(operationId) &&
|
||||||
|
!pendingOperations.value.get(operationId)?.signal.aborted &&
|
||||||
|
currentLoadingDocumentId.value === documentId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// === 私有方法 ===
|
// === 私有方法 ===
|
||||||
|
|
||||||
|
// 生成内容哈希
|
||||||
|
const generateContentHash = (content: string): string => {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
|
const char = content.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 缓存化的语法树确保方法
|
||||||
|
const ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||||
|
const instance = editorCache.value.instances[documentId];
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
|
const docLength = view.state.doc.length;
|
||||||
|
const content = view.state.doc.toString();
|
||||||
|
const contentHash = generateContentHash(content);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// 检查是否需要重新构建语法树
|
||||||
|
const cache = instance.syntaxTreeCache;
|
||||||
|
const shouldRebuild = !cache ||
|
||||||
|
cache.lastDocLength !== docLength ||
|
||||||
|
cache.lastContentHash !== contentHash ||
|
||||||
|
(now.getTime() - cache.lastParsed.getTime()) > 30000; // 30秒过期
|
||||||
|
|
||||||
|
if (shouldRebuild) {
|
||||||
|
try {
|
||||||
|
ensureSyntaxTree(view.state, docLength, 5000);
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
instance.syntaxTreeCache = {
|
||||||
|
lastDocLength: docLength,
|
||||||
|
lastContentHash: contentHash,
|
||||||
|
lastParsed: now
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to ensure syntax tree:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 创建编辑器实例
|
// 创建编辑器实例
|
||||||
const createEditorInstance = async (
|
const createEditorInstance = async (
|
||||||
content: string,
|
content: string,
|
||||||
signal: AbortSignal,
|
operationId: number,
|
||||||
documentId: number
|
documentId: number
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
if (!editorCacheStore.getContainer()) {
|
if (!editorCache.value.containerElement) {
|
||||||
throw new Error('Editor container not set');
|
throw new Error('Editor container not set');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查操作是否被取消
|
// 检查操作是否仍然有效
|
||||||
if (signal.aborted) {
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,24 +195,24 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
enableAutoDetection: true
|
enableAutoDetection: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// 再次检查操作是否被取消
|
// 再次检查操作有效性
|
||||||
if (signal.aborted) {
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快捷键扩展
|
// 快捷键扩展
|
||||||
const keymapExtension = await createDynamicKeymapExtension();
|
const keymapExtension = await createDynamicKeymapExtension();
|
||||||
|
|
||||||
// 检查操作是否被取消
|
// 检查操作有效性
|
||||||
if (signal.aborted) {
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
// 动态扩展,传递文档ID以便扩展管理器可以预初始化
|
||||||
const dynamicExtensions = await createDynamicExtensions(documentId);
|
const dynamicExtensions = await createDynamicExtensions(documentId);
|
||||||
|
|
||||||
// 最终检查操作是否被取消
|
// 最终检查操作有效性
|
||||||
if (signal.aborted) {
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,43 +250,91 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加编辑器到缓存
|
||||||
|
const addEditorToCache = (documentId: number, view: EditorView, content: string) => {
|
||||||
|
// 如果缓存已满,移除最少使用的编辑器
|
||||||
|
if (editorCache.value.lru.length >= NUM_EDITOR_INSTANCES) {
|
||||||
|
const oldestId = editorCache.value.lru.shift();
|
||||||
|
if (oldestId && editorCache.value.instances[oldestId]) {
|
||||||
|
const oldInstance = editorCache.value.instances[oldestId];
|
||||||
|
// 清除自动保存定时器
|
||||||
|
if (oldInstance.autoSaveTimer) {
|
||||||
|
clearTimeout(oldInstance.autoSaveTimer);
|
||||||
|
}
|
||||||
|
// 移除DOM元素
|
||||||
|
if (oldInstance.view.dom.parentElement) {
|
||||||
|
oldInstance.view.dom.remove();
|
||||||
|
}
|
||||||
|
oldInstance.view.destroy();
|
||||||
|
delete editorCache.value.instances[oldestId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的编辑器实例
|
||||||
|
editorCache.value.instances[documentId] = {
|
||||||
|
view,
|
||||||
|
documentId,
|
||||||
|
content,
|
||||||
|
isDirty: false,
|
||||||
|
lastModified: new Date(),
|
||||||
|
autoSaveTimer: null,
|
||||||
|
syntaxTreeCache: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到LRU列表
|
||||||
|
editorCache.value.lru.push(documentId);
|
||||||
|
|
||||||
|
// 初始化语法树缓存
|
||||||
|
ensureSyntaxTreeCached(view, documentId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新LRU
|
||||||
|
const updateLRU = (documentId: number) => {
|
||||||
|
const lru = editorCache.value.lru;
|
||||||
|
const index = lru.indexOf(documentId);
|
||||||
|
if (index > -1) {
|
||||||
|
lru.splice(index, 1);
|
||||||
|
}
|
||||||
|
lru.push(documentId);
|
||||||
|
};
|
||||||
|
|
||||||
// 获取或创建编辑器
|
// 获取或创建编辑器
|
||||||
const getOrCreateEditor = async (
|
const getOrCreateEditor = async (
|
||||||
documentId: number,
|
documentId: number,
|
||||||
content: string,
|
content: string,
|
||||||
signal: AbortSignal
|
operationId: number
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
const cached = editorCacheStore.getEditor(documentId);
|
const cached = editorCache.value.instances[documentId];
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
updateLRU(documentId);
|
||||||
return cached.view;
|
return cached.view;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查操作是否被取消
|
// 检查操作是否仍然有效
|
||||||
if (signal.aborted) {
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的编辑器实例
|
// 创建新的编辑器实例
|
||||||
const view = await createEditorInstance(content, signal, documentId);
|
const view = await createEditorInstance(content, operationId, documentId);
|
||||||
|
|
||||||
// 最终检查操作是否被取消
|
// 最终检查操作有效性
|
||||||
if (signal.aborted) {
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
// 如果操作已取消,清理创建的实例
|
// 如果操作已取消,清理创建的实例
|
||||||
view.destroy();
|
view.destroy();
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到缓存
|
addEditorToCache(documentId, view, content);
|
||||||
editorCacheStore.addEditor(documentId, view, content);
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 显示编辑器
|
// 显示编辑器
|
||||||
const showEditor = (documentId: number) => {
|
const showEditor = (documentId: number) => {
|
||||||
const instance = editorCacheStore.getEditor(documentId);
|
const instance = editorCache.value.instances[documentId];
|
||||||
if (!instance || !editorCacheStore.getContainer()) return;
|
if (!instance || !editorCache.value.containerElement) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 移除当前编辑器DOM
|
// 移除当前编辑器DOM
|
||||||
@@ -211,19 +343,18 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确保容器为空
|
// 确保容器为空
|
||||||
const container = editorCacheStore.getContainer();
|
editorCache.value.containerElement.innerHTML = '';
|
||||||
if (container) {
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
// 将目标编辑器DOM添加到容器
|
|
||||||
container.appendChild(instance.view.dom);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 将目标编辑器DOM添加到容器
|
||||||
|
editorCache.value.containerElement.appendChild(instance.view.dom);
|
||||||
currentEditor.value = instance.view;
|
currentEditor.value = instance.view;
|
||||||
|
|
||||||
// 设置扩展管理器视图
|
// 设置扩展管理器视图
|
||||||
setExtensionManagerView(instance.view, documentId);
|
setExtensionManagerView(instance.view, documentId);
|
||||||
|
|
||||||
|
// 更新LRU
|
||||||
|
updateLRU(documentId);
|
||||||
|
|
||||||
// 重新测量和聚焦编辑器
|
// 重新测量和聚焦编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 将光标定位到文档末尾并滚动到该位置
|
// 将光标定位到文档末尾并滚动到该位置
|
||||||
@@ -232,12 +363,12 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
selection: {anchor: docLength, head: docLength},
|
selection: {anchor: docLength, head: docLength},
|
||||||
scrollIntoView: true
|
scrollIntoView: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// 滚动到文档底部
|
// 滚动到文档底部(将光标位置滚动到可见区域)
|
||||||
instance.view.focus();
|
instance.view.focus();
|
||||||
|
|
||||||
// 使用缓存的语法树确保方法
|
// 使用缓存的语法树确保方法
|
||||||
editorCacheStore.ensureSyntaxTreeCached(instance.view, documentId);
|
ensureSyntaxTreeCached(instance.view, documentId);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error showing editor:', error);
|
console.error('Error showing editor:', error);
|
||||||
@@ -246,19 +377,20 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 保存编辑器内容
|
// 保存编辑器内容
|
||||||
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
const saveEditorContent = async (documentId: number): Promise<boolean> => {
|
||||||
const instance = editorCacheStore.getEditor(documentId);
|
const instance = editorCache.value.instances[documentId];
|
||||||
if (!instance || !instance.state.isDirty) return true;
|
if (!instance || !instance.isDirty) return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = instance.view.state.doc.toString();
|
const content = instance.view.state.doc.toString();
|
||||||
const lastModified = instance.state.lastModified;
|
const lastModified = instance.lastModified;
|
||||||
|
|
||||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||||
|
|
||||||
// 检查在保存期间内容是否又被修改了
|
// 检查在保存期间内容是否又被修改了
|
||||||
if (instance.state.lastModified === lastModified) {
|
if (instance.lastModified === lastModified) {
|
||||||
editorCacheStore.updateEditorContent(documentId, content);
|
instance.content = content;
|
||||||
// isDirty 已在 updateEditorContent 中设置为 false
|
instance.isDirty = false;
|
||||||
|
instance.lastModified = new Date();
|
||||||
}
|
}
|
||||||
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
||||||
|
|
||||||
@@ -271,23 +403,31 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 内容变化处理
|
// 内容变化处理
|
||||||
const onContentChange = (documentId: number) => {
|
const onContentChange = (documentId: number) => {
|
||||||
const instance = editorCacheStore.getEditor(documentId);
|
const instance = editorCache.value.instances[documentId];
|
||||||
if (!instance) return;
|
if (!instance) return;
|
||||||
|
|
||||||
editorCacheStore.markEditorDirty(documentId);
|
instance.isDirty = true;
|
||||||
|
instance.lastModified = new Date();
|
||||||
|
|
||||||
|
// 清理语法树缓存,下次访问时重新构建
|
||||||
|
instance.syntaxTreeCache = null;
|
||||||
|
|
||||||
// 清除之前的定时器并设置新的自动保存定时器
|
// 清除之前的定时器
|
||||||
const timer = setTimeout(() => {
|
if (instance.autoSaveTimer) {
|
||||||
|
clearTimeout(instance.autoSaveTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的自动保存定时器
|
||||||
|
instance.autoSaveTimer = window.setTimeout(() => {
|
||||||
saveEditorContent(documentId);
|
saveEditorContent(documentId);
|
||||||
}, getAutoSaveDelay());
|
}, getAutoSaveDelay());
|
||||||
editorCacheStore.setAutoSaveTimer(documentId, timer as unknown as number);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// === 公共API ===
|
// === 公共API ===
|
||||||
|
|
||||||
// 设置编辑器容器
|
// 设置编辑器容器
|
||||||
const setEditorContainer = (container: HTMLElement | null) => {
|
const setEditorContainer = (container: HTMLElement | null) => {
|
||||||
editorCacheStore.setContainer(container);
|
editorCache.value.containerElement = container;
|
||||||
|
|
||||||
// 如果设置容器时已有当前文档,立即加载编辑器
|
// 如果设置容器时已有当前文档,立即加载编辑器
|
||||||
if (container && documentStore.currentDocument) {
|
if (container && documentStore.currentDocument) {
|
||||||
@@ -299,6 +439,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const loadEditor = async (documentId: number, content: string) => {
|
const loadEditor = async (documentId: number, content: string) => {
|
||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
// 生成新的操作ID
|
||||||
|
const operationId = getNextOperationId();
|
||||||
|
const abortController = new AbortController();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 验证参数
|
// 验证参数
|
||||||
@@ -306,69 +449,72 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
throw new Error('Invalid parameters for loadEditor');
|
throw new Error('Invalid parameters for loadEditor');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用异步操作管理器执行加载操作
|
// 取消之前的操作并设置当前操作
|
||||||
const result = await operationManager.executeOperation(
|
cancelPreviousOperations();
|
||||||
documentId,
|
currentLoadingDocumentId.value = documentId;
|
||||||
async (signal) => {
|
pendingOperations.value.set(operationId, abortController);
|
||||||
// 保存当前编辑器内容
|
|
||||||
if (currentEditor.value) {
|
|
||||||
const currentDocId = documentStore.currentDocumentId;
|
|
||||||
if (currentDocId && currentDocId !== documentId) {
|
|
||||||
await saveEditorContent(currentDocId);
|
|
||||||
|
|
||||||
// 检查操作是否被取消
|
// 保存当前编辑器内容
|
||||||
if (signal.aborted) {
|
if (currentEditor.value) {
|
||||||
throw new Error('Operation cancelled');
|
const currentDocId = documentStore.currentDocumentId;
|
||||||
}
|
if (currentDocId && currentDocId !== documentId) {
|
||||||
}
|
await saveEditorContent(currentDocId);
|
||||||
|
|
||||||
|
// 检查操作是否仍然有效
|
||||||
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取或创建编辑器
|
|
||||||
const view = await getOrCreateEditor(documentId, content, signal);
|
|
||||||
|
|
||||||
// 检查操作是否被取消
|
|
||||||
if (signal.aborted) {
|
|
||||||
throw new Error('Operation cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新内容
|
|
||||||
const instance = editorCacheStore.getEditor(documentId);
|
|
||||||
if (instance && instance.state.content !== content) {
|
|
||||||
// 确保编辑器视图有效
|
|
||||||
if (view && view.state && view.dispatch) {
|
|
||||||
view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: 0,
|
|
||||||
to: view.state.doc.length,
|
|
||||||
insert: content
|
|
||||||
}
|
|
||||||
});
|
|
||||||
editorCacheStore.updateEditorContent(documentId, content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终检查操作是否被取消
|
|
||||||
if (signal.aborted) {
|
|
||||||
throw new Error('Operation cancelled');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示编辑器
|
|
||||||
showEditor(documentId);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
},
|
|
||||||
'loadEditor'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
if (result.error?.message !== 'Operation cancelled') {
|
|
||||||
console.error('Failed to load editor:', result.error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取或创建编辑器
|
||||||
|
const view = await getOrCreateEditor(documentId, content, operationId);
|
||||||
|
|
||||||
|
// 检查操作是否仍然有效
|
||||||
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新内容(如果需要)
|
||||||
|
const instance = editorCache.value.instances[documentId];
|
||||||
|
if (instance && instance.content !== content) {
|
||||||
|
// 确保编辑器视图有效
|
||||||
|
if (view && view.state && view.dispatch) {
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.state.doc.length,
|
||||||
|
insert: content
|
||||||
|
}
|
||||||
|
});
|
||||||
|
instance.content = content;
|
||||||
|
instance.isDirty = false;
|
||||||
|
// 清理语法树缓存,因为内容已更新
|
||||||
|
instance.syntaxTreeCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最终检查操作有效性
|
||||||
|
if (!isOperationValid(operationId, documentId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑器
|
||||||
|
showEditor(documentId);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load editor:', error);
|
if (error instanceof Error && error.message === 'Operation cancelled') {
|
||||||
|
console.log(`Editor loading cancelled for document ${documentId}`);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load editor:', error);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
// 清理操作记录
|
||||||
|
pendingOperations.value.delete(operationId);
|
||||||
|
if (currentLoadingDocumentId.value === documentId) {
|
||||||
|
currentLoadingDocumentId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 延迟一段时间后再取消加载状态
|
// 延迟一段时间后再取消加载状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@@ -378,20 +524,49 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 移除编辑器
|
// 移除编辑器
|
||||||
const removeEditor = (documentId: number) => {
|
const removeEditor = (documentId: number) => {
|
||||||
// 取消该文档的所有操作
|
const instance = editorCache.value.instances[documentId];
|
||||||
operationManager.cancelResourceOperations(documentId);
|
if (instance) {
|
||||||
|
try {
|
||||||
|
// 如果正在加载这个文档,取消操作
|
||||||
|
if (currentLoadingDocumentId.value === documentId) {
|
||||||
|
cancelPreviousOperations();
|
||||||
|
currentLoadingDocumentId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 从扩展管理器中移除视图
|
// 清除自动保存定时器
|
||||||
removeExtensionManagerView(documentId);
|
if (instance.autoSaveTimer) {
|
||||||
|
clearTimeout(instance.autoSaveTimer);
|
||||||
|
instance.autoSaveTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 清除当前编辑器引用
|
// 从扩展管理器中移除视图
|
||||||
const instance = editorCacheStore.getEditor(documentId);
|
removeExtensionManagerView(documentId);
|
||||||
if (instance && currentEditor.value === instance.view) {
|
|
||||||
currentEditor.value = null;
|
// 移除DOM元素
|
||||||
|
if (instance.view && instance.view.dom && instance.view.dom.parentElement) {
|
||||||
|
instance.view.dom.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 销毁编辑器
|
||||||
|
if (instance.view && instance.view.destroy) {
|
||||||
|
instance.view.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理引用
|
||||||
|
if (currentEditor.value === instance.view) {
|
||||||
|
currentEditor.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete editorCache.value.instances[documentId];
|
||||||
|
|
||||||
|
const lruIndex = editorCache.value.lru.indexOf(documentId);
|
||||||
|
if (lruIndex > -1) {
|
||||||
|
editorCache.value.lru.splice(lruIndex, 1);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing editor:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从缓存中移除编辑器
|
|
||||||
editorCacheStore.removeEditor(documentId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新文档统计
|
// 更新文档统计
|
||||||
@@ -401,7 +576,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用字体设置
|
// 应用字体设置
|
||||||
const applyFontSettings = () => {
|
const applyFontSettings = () => {
|
||||||
editorCacheStore.allEditors.forEach(instance => {
|
Object.values(editorCache.value.instances).forEach(instance => {
|
||||||
updateFontConfig(instance.view, {
|
updateFontConfig(instance.view, {
|
||||||
fontFamily: configStore.config.editing.fontFamily,
|
fontFamily: configStore.config.editing.fontFamily,
|
||||||
fontSize: configStore.config.editing.fontSize,
|
fontSize: configStore.config.editing.fontSize,
|
||||||
@@ -413,7 +588,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用主题设置
|
// 应用主题设置
|
||||||
const applyThemeSettings = () => {
|
const applyThemeSettings = () => {
|
||||||
editorCacheStore.allEditors.forEach(instance => {
|
Object.values(editorCache.value.instances).forEach(instance => {
|
||||||
updateEditorTheme(instance.view,
|
updateEditorTheme(instance.view,
|
||||||
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
||||||
);
|
);
|
||||||
@@ -422,7 +597,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用Tab设置
|
// 应用Tab设置
|
||||||
const applyTabSettings = () => {
|
const applyTabSettings = () => {
|
||||||
editorCacheStore.allEditors.forEach(instance => {
|
Object.values(editorCache.value.instances).forEach(instance => {
|
||||||
updateTabConfig(
|
updateTabConfig(
|
||||||
instance.view,
|
instance.view,
|
||||||
configStore.config.editing.tabSize,
|
configStore.config.editing.tabSize,
|
||||||
@@ -436,21 +611,37 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const applyKeymapSettings = async () => {
|
const applyKeymapSettings = async () => {
|
||||||
// 确保所有编辑器实例的快捷键都更新
|
// 确保所有编辑器实例的快捷键都更新
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
editorCacheStore.allEditors.map(instance =>
|
Object.values(editorCache.value.instances).map(instance =>
|
||||||
updateKeymapExtension(instance.view)
|
updateKeymapExtension(instance.view)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理所有编辑器
|
// 清空所有编辑器
|
||||||
const clearAllEditors = () => {
|
const clearAllEditors = () => {
|
||||||
// 取消所有挂起的操作
|
// 取消所有挂起的操作
|
||||||
operationManager.cancelAllOperations();
|
cancelPreviousOperations();
|
||||||
|
currentLoadingDocumentId.value = null;
|
||||||
|
|
||||||
|
Object.values(editorCache.value.instances).forEach(instance => {
|
||||||
|
// 清除自动保存定时器
|
||||||
|
if (instance.autoSaveTimer) {
|
||||||
|
clearTimeout(instance.autoSaveTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从扩展管理器移除
|
||||||
|
removeExtensionManagerView(instance.documentId);
|
||||||
|
|
||||||
|
// 移除DOM元素
|
||||||
|
if (instance.view.dom.parentElement) {
|
||||||
|
instance.view.dom.remove();
|
||||||
|
}
|
||||||
|
// 销毁编辑器
|
||||||
|
instance.view.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
// 清理所有编辑器
|
editorCache.value.instances = {};
|
||||||
editorCacheStore.clearAll();
|
editorCache.value.lru = [];
|
||||||
|
|
||||||
// 清除当前编辑器引用
|
|
||||||
currentEditor.value = null;
|
currentEditor.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -479,37 +670,9 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
await applyKeymapSettings();
|
await applyKeymapSettings();
|
||||||
};
|
};
|
||||||
|
|
||||||
// === 配置监听相关的 computed 属性 ===
|
|
||||||
|
|
||||||
// 字体相关配置的 computed 属性
|
|
||||||
const fontSettings = computed(() => ({
|
|
||||||
fontSize: configStore.config.editing.fontSize,
|
|
||||||
fontFamily: configStore.config.editing.fontFamily,
|
|
||||||
lineHeight: configStore.config.editing.lineHeight,
|
|
||||||
fontWeight: configStore.config.editing.fontWeight
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Tab相关配置的 computed 属性
|
|
||||||
const tabSettings = computed(() => ({
|
|
||||||
tabSize: configStore.config.editing.tabSize,
|
|
||||||
enableTabIndent: configStore.config.editing.enableTabIndent,
|
|
||||||
tabType: configStore.config.editing.tabType
|
|
||||||
}));
|
|
||||||
|
|
||||||
// === 配置监听器 ===
|
|
||||||
|
|
||||||
// 监听字体配置变化
|
|
||||||
watch(fontSettings, applyFontSettings, { deep: true });
|
|
||||||
|
|
||||||
// 监听Tab配置变化
|
|
||||||
watch(tabSettings, applyTabSettings, { deep: true });
|
|
||||||
|
|
||||||
// 监听主题变化
|
|
||||||
watch(() => themeStore.currentTheme, applyThemeSettings);
|
|
||||||
|
|
||||||
// 监听文档切换
|
// 监听文档切换
|
||||||
watch(() => documentStore.currentDocument, (newDoc) => {
|
watch(() => documentStore.currentDocument, (newDoc) => {
|
||||||
if (newDoc && editorCacheStore.getContainer()) {
|
if (newDoc && editorCache.value.containerElement) {
|
||||||
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
// 使用 nextTick 确保DOM更新完成后再加载编辑器
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
loadEditor(newDoc.id, newDoc.content);
|
loadEditor(newDoc.id, newDoc.content);
|
||||||
@@ -517,6 +680,15 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
watch(() => configStore.config.editing.fontSize, applyFontSettings);
|
||||||
|
watch(() => configStore.config.editing.fontFamily, applyFontSettings);
|
||||||
|
watch(() => configStore.config.editing.lineHeight, applyFontSettings);
|
||||||
|
watch(() => configStore.config.editing.fontWeight, applyFontSettings);
|
||||||
|
watch(() => configStore.config.editing.tabSize, applyTabSettings);
|
||||||
|
watch(() => configStore.config.editing.enableTabIndent, applyTabSettings);
|
||||||
|
watch(() => configStore.config.editing.tabType, applyTabSettings);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
currentEditor,
|
currentEditor,
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
import {defineStore} from 'pinia';
|
|
||||||
import {computed, ref, watch} from 'vue';
|
|
||||||
import {useDocumentStore} from './documentStore';
|
|
||||||
import {useEditorCacheStore} from './editorCacheStore';
|
|
||||||
import type {Document} from '@/../bindings/voidraft/internal/models/models';
|
|
||||||
|
|
||||||
/** 标签页信息 */
|
|
||||||
export interface TabInfo {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
isActive: boolean;
|
|
||||||
lastAccessed: Date;
|
|
||||||
document?: Document;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTabStore = defineStore('tab', () => {
|
|
||||||
// === 依赖的 Store ===
|
|
||||||
const documentStore = useDocumentStore();
|
|
||||||
const editorCacheStore = useEditorCacheStore();
|
|
||||||
|
|
||||||
// === 状态 ===
|
|
||||||
const openTabIds = ref<number[]>([]);
|
|
||||||
const activeTabId = ref<number | null>(null);
|
|
||||||
|
|
||||||
// === 计算属性 ===
|
|
||||||
|
|
||||||
// 获取所有打开的标签页信息
|
|
||||||
const openTabs = computed((): TabInfo[] => {
|
|
||||||
return openTabIds.value.map(id => {
|
|
||||||
const document = documentStore.documents[id];
|
|
||||||
const editorItem = editorCacheStore.getEditor(id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title: document?.title || `Document ${id}`,
|
|
||||||
isDirty: editorItem?.state.isDirty || false,
|
|
||||||
isActive: id === activeTabId.value,
|
|
||||||
lastAccessed: editorItem?.lastAccessed || new Date(),
|
|
||||||
document
|
|
||||||
};
|
|
||||||
}).sort((a, b) => {
|
|
||||||
// 按最后访问时间排序,最近访问的在前
|
|
||||||
return b.lastAccessed.getTime() - a.lastAccessed.getTime();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取当前活跃的标签页
|
|
||||||
const activeTab = computed((): TabInfo | null => {
|
|
||||||
if (!activeTabId.value) return null;
|
|
||||||
return openTabs.value.find(tab => tab.id === activeTabId.value) || null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 标签页数量
|
|
||||||
const tabCount = computed(() => openTabIds.value.length);
|
|
||||||
|
|
||||||
// 是否有标签页打开
|
|
||||||
const hasTabs = computed(() => tabCount.value > 0);
|
|
||||||
|
|
||||||
// === 私有方法 ===
|
|
||||||
|
|
||||||
// 添加标签页到列表
|
|
||||||
const addTabToList = (documentId: number): void => {
|
|
||||||
if (!openTabIds.value.includes(documentId)) {
|
|
||||||
openTabIds.value.push(documentId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 从列表中移除标签页
|
|
||||||
const removeTabFromList = (documentId: number): void => {
|
|
||||||
const index = openTabIds.value.indexOf(documentId);
|
|
||||||
if (index > -1) {
|
|
||||||
openTabIds.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// === 公共方法 ===
|
|
||||||
|
|
||||||
// 打开标签页
|
|
||||||
const openTab = async (documentId: number): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
// 使用 documentStore 的 openDocument 方法
|
|
||||||
const success = await documentStore.openDocument(documentId);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// 添加到标签页列表
|
|
||||||
addTabToList(documentId);
|
|
||||||
// 设置为活跃标签页
|
|
||||||
activeTabId.value = documentId;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to open tab:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切换到指定标签页
|
|
||||||
const switchToTab = async (documentId: number): Promise<boolean> => {
|
|
||||||
// 如果标签页已经是活跃状态,直接返回
|
|
||||||
if (activeTabId.value === documentId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果标签页不在打开列表中,先打开它
|
|
||||||
if (!openTabIds.value.includes(documentId)) {
|
|
||||||
return await openTab(documentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换到已打开的标签页
|
|
||||||
try {
|
|
||||||
const success = await documentStore.openDocument(documentId);
|
|
||||||
if (success) {
|
|
||||||
activeTabId.value = documentId;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to switch tab:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭标签页
|
|
||||||
const closeTab = async (documentId: number): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
// 检查是否有未保存的更改
|
|
||||||
const editorItem = editorCacheStore.getEditor(documentId);
|
|
||||||
if (editorItem?.state.isDirty) {
|
|
||||||
// 这里可以添加确认对话框逻辑
|
|
||||||
console.warn(`Document ${documentId} has unsaved changes`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从标签页列表中移除
|
|
||||||
removeTabFromList(documentId);
|
|
||||||
|
|
||||||
// 如果关闭的是当前活跃标签页,需要切换到其他标签页
|
|
||||||
if (activeTabId.value === documentId) {
|
|
||||||
if (openTabIds.value.length > 0) {
|
|
||||||
// 切换到最近访问的标签页
|
|
||||||
const nextTab = openTabs.value[0];
|
|
||||||
if (nextTab) {
|
|
||||||
await switchToTab(nextTab.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 没有其他标签页了
|
|
||||||
activeTabId.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从编辑器缓存中移除(可选,取决于是否要保持缓存)
|
|
||||||
// editorCacheStore.removeEditor(documentId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to close tab:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭所有标签页
|
|
||||||
const closeAllTabs = async (): Promise<boolean> => {
|
|
||||||
// 清空标签页列表
|
|
||||||
openTabIds.value = [];
|
|
||||||
activeTabId.value = null;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭其他标签页(保留指定标签页)
|
|
||||||
const closeOtherTabs = async (keepDocumentId: number): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const tabsToClose = openTabIds.value.filter(id => id !== keepDocumentId);
|
|
||||||
|
|
||||||
// 检查其他标签页是否有未保存的更改
|
|
||||||
const dirtyOtherTabs = tabsToClose.filter(id => {
|
|
||||||
const editorItem = editorCacheStore.getEditor(id);
|
|
||||||
return editorItem?.state.isDirty;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dirtyOtherTabs.length > 0) {
|
|
||||||
console.warn(`${dirtyOtherTabs.length} other tabs have unsaved changes`);
|
|
||||||
// 这里可以添加确认对话框逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只保留指定的标签页
|
|
||||||
openTabIds.value = [keepDocumentId];
|
|
||||||
|
|
||||||
// 如果保留的标签页不是当前活跃的,切换到它
|
|
||||||
if (activeTabId.value !== keepDocumentId) {
|
|
||||||
await switchToTab(keepDocumentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to close other tabs:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取标签页信息
|
|
||||||
const getTabInfo = (documentId: number): TabInfo | null => {
|
|
||||||
return openTabs.value.find(tab => tab.id === documentId) || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查标签页是否打开
|
|
||||||
const isTabOpen = (documentId: number): boolean => {
|
|
||||||
return openTabIds.value.includes(documentId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// === 监听器 ===
|
|
||||||
|
|
||||||
// 监听 documentStore 的当前文档变化,同步标签页状态
|
|
||||||
watch(
|
|
||||||
() => documentStore.currentDocument,
|
|
||||||
(newDoc) => {
|
|
||||||
if (newDoc) {
|
|
||||||
// 确保当前文档在标签页列表中
|
|
||||||
addTabToList(newDoc.id);
|
|
||||||
activeTabId.value = newDoc.id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{immediate: true}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 状态
|
|
||||||
openTabIds,
|
|
||||||
activeTabId,
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
openTabs,
|
|
||||||
activeTab,
|
|
||||||
tabCount,
|
|
||||||
hasTabs,
|
|
||||||
|
|
||||||
// 方法
|
|
||||||
openTab,
|
|
||||||
switchToTab,
|
|
||||||
closeTab,
|
|
||||||
closeAllTabs,
|
|
||||||
closeOtherTabs,
|
|
||||||
getTabInfo,
|
|
||||||
isTabOpen,
|
|
||||||
};
|
|
||||||
}, {
|
|
||||||
persist: {
|
|
||||||
key: 'voidraft-tabs',
|
|
||||||
storage: localStorage,
|
|
||||||
pick: ['openTabIds', 'activeTabId']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -133,7 +133,7 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新编辑器主题(在主题颜色更改后调用)
|
// 刷新编辑器主题
|
||||||
const refreshEditorTheme = () => {
|
const refreshEditorTheme = () => {
|
||||||
// 使用当前主题重新应用DOM主题
|
// 使用当前主题重新应用DOM主题
|
||||||
const theme = currentTheme.value;
|
const theme = currentTheme.value;
|
||||||
|
|||||||
@@ -149,7 +149,6 @@ export const useTranslationStore = defineStore('translation', () => {
|
|||||||
if (defaultTargetLang.value) {
|
if (defaultTargetLang.value) {
|
||||||
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
const validatedLang = validateLanguage(defaultTargetLang.value, defaultTranslator.value);
|
||||||
if (validatedLang !== defaultTargetLang.value) {
|
if (validatedLang !== defaultTargetLang.value) {
|
||||||
console.log(`目标语言 ${defaultTargetLang.value} 不受支持,已切换到 ${validatedLang}`);
|
|
||||||
defaultTargetLang.value = validatedLang;
|
defaultTargetLang.value = validatedLang;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,236 @@
|
|||||||
import {defineStore} from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import {computed, ref} from 'vue';
|
import { computed, readonly, ref, shallowRef, onScopeDispose } from 'vue';
|
||||||
import {CheckForUpdates, ApplyUpdate, RestartApplication} from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
import { CheckForUpdates, ApplyUpdate, RestartApplication } from '@/../bindings/voidraft/internal/services/selfupdateservice';
|
||||||
import {SelfUpdateResult} from '@/../bindings/voidraft/internal/services/models';
|
import { SelfUpdateResult } from '@/../bindings/voidraft/internal/services/models';
|
||||||
import {useConfigStore} from './configStore';
|
import { useConfigStore } from './configStore';
|
||||||
|
import { createTimerManager } from '@/common/utils/timerUtils';
|
||||||
import * as runtime from "@wailsio/runtime";
|
import * as runtime from "@wailsio/runtime";
|
||||||
|
|
||||||
|
// 更新状态枚举
|
||||||
|
export enum UpdateStatus {
|
||||||
|
IDLE = 'idle',
|
||||||
|
CHECKING = 'checking',
|
||||||
|
UPDATE_AVAILABLE = 'update_available',
|
||||||
|
UPDATING = 'updating',
|
||||||
|
UPDATE_SUCCESS = 'update_success',
|
||||||
|
ERROR = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新操作结果类型
|
||||||
|
export interface UpdateOperationResult {
|
||||||
|
status: UpdateStatus;
|
||||||
|
result?: SelfUpdateResult;
|
||||||
|
message?: string;
|
||||||
|
timestamp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 类型守卫函数
|
||||||
|
const isUpdateError = (error: unknown): error is Error => {
|
||||||
|
return error instanceof Error;
|
||||||
|
};
|
||||||
|
|
||||||
export const useUpdateStore = defineStore('update', () => {
|
export const useUpdateStore = defineStore('update', () => {
|
||||||
// 状态
|
// === 核心状态 ===
|
||||||
const isChecking = ref(false);
|
const hasCheckedOnStartup = ref(false);
|
||||||
const isUpdating = ref(false);
|
|
||||||
const updateResult = ref<SelfUpdateResult | null>(null);
|
// 统一的更新操作结果状态
|
||||||
const hasCheckedOnStartup = ref(false);
|
const updateOperation = ref<UpdateOperationResult>({
|
||||||
const updateSuccess = ref(false);
|
status: UpdateStatus.IDLE
|
||||||
const errorMessage = ref('');
|
});
|
||||||
|
|
||||||
|
// === 定时器管理 ===
|
||||||
|
const statusTimer = createTimerManager();
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
onScopeDispose(() => {
|
||||||
|
statusTimer.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
// === 外部依赖 ===
|
||||||
|
const configStore = useConfigStore();
|
||||||
|
|
||||||
|
// === 计算属性 ===
|
||||||
|
|
||||||
|
// 派生状态计算属性
|
||||||
|
const isChecking = computed(() => updateOperation.value.status === UpdateStatus.CHECKING);
|
||||||
|
const isUpdating = computed(() => updateOperation.value.status === UpdateStatus.UPDATING);
|
||||||
|
const hasUpdate = computed(() => updateOperation.value.status === UpdateStatus.UPDATE_AVAILABLE);
|
||||||
|
const updateSuccess = computed(() => updateOperation.value.status === UpdateStatus.UPDATE_SUCCESS);
|
||||||
|
const isError = computed(() => updateOperation.value.status === UpdateStatus.ERROR);
|
||||||
|
|
||||||
|
// 数据访问计算属性
|
||||||
|
const updateResult = computed(() => updateOperation.value.result || undefined);
|
||||||
|
const errorMessage = computed(() =>
|
||||||
|
updateOperation.value.status === UpdateStatus.ERROR ? updateOperation.value.message : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
// === 状态管理方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置更新状态
|
||||||
|
* @param status 更新状态
|
||||||
|
* @param result 可选的更新结果
|
||||||
|
* @param message 可选消息
|
||||||
|
* @param autoHide 是否自动隐藏(毫秒)
|
||||||
|
*/
|
||||||
|
const setUpdateStatus = <T extends UpdateStatus>(
|
||||||
|
status: T,
|
||||||
|
result?: SelfUpdateResult,
|
||||||
|
message?: string,
|
||||||
|
autoHide?: number
|
||||||
|
): void => {
|
||||||
|
updateOperation.value = {
|
||||||
|
status,
|
||||||
|
result,
|
||||||
|
message,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自动隐藏功能
|
||||||
|
if (autoHide && autoHide > 0) {
|
||||||
|
statusTimer.set(() => {
|
||||||
|
if (updateOperation.value.status === status) {
|
||||||
|
updateOperation.value = { status: UpdateStatus.IDLE };
|
||||||
|
}
|
||||||
|
}, autoHide);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除状态
|
||||||
|
*/
|
||||||
|
const clearStatus = (): void => {
|
||||||
|
statusTimer.clear();
|
||||||
|
updateOperation.value = { status: UpdateStatus.IDLE };
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 业务方法 ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查更新
|
||||||
|
* @returns Promise<boolean> 是否成功检查
|
||||||
|
*/
|
||||||
|
const checkForUpdates = async (): Promise<boolean> => {
|
||||||
|
if (isChecking.value) return false;
|
||||||
|
|
||||||
|
setUpdateStatus(UpdateStatus.CHECKING);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await CheckForUpdates();
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setUpdateStatus(UpdateStatus.ERROR, result, result.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.hasUpdate) {
|
||||||
|
setUpdateStatus(UpdateStatus.UPDATE_AVAILABLE, result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有更新,设置为空闲状态
|
||||||
|
setUpdateStatus(UpdateStatus.IDLE, result || undefined);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const message = isUpdateError(error) ? error.message : 'Network error';
|
||||||
|
setUpdateStatus(UpdateStatus.ERROR, undefined, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用更新
|
||||||
|
* @returns Promise<boolean> 是否成功应用更新
|
||||||
|
*/
|
||||||
|
const applyUpdate = async (): Promise<boolean> => {
|
||||||
|
if (isUpdating.value) return false;
|
||||||
|
|
||||||
|
setUpdateStatus(UpdateStatus.UPDATING);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await ApplyUpdate();
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
setUpdateStatus(UpdateStatus.ERROR, result || undefined, result.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.updateApplied) {
|
||||||
|
setUpdateStatus(UpdateStatus.UPDATE_SUCCESS, result || undefined);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpdateStatus(UpdateStatus.ERROR, result || undefined, 'Update failed');
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const message = isUpdateError(error) ? error.message : 'Update failed';
|
||||||
|
setUpdateStatus(UpdateStatus.ERROR, undefined, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启应用
|
||||||
|
* @returns Promise<boolean> 是否成功重启
|
||||||
|
*/
|
||||||
|
const restartApplication = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
await RestartApplication();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
const message = isUpdateError(error) ? error.message : 'Restart failed';
|
||||||
|
setUpdateStatus(UpdateStatus.ERROR, undefined, message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动时检查更新
|
||||||
|
*/
|
||||||
|
const checkOnStartup = async (): Promise<void> => {
|
||||||
|
if (hasCheckedOnStartup.value) return;
|
||||||
|
|
||||||
|
if (configStore.config.updates.autoUpdate) {
|
||||||
|
await checkForUpdates();
|
||||||
|
}
|
||||||
|
hasCheckedOnStartup.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开发布页面
|
||||||
|
*/
|
||||||
|
const openReleaseURL = async (): Promise<void> => {
|
||||||
|
const result = updateResult.value;
|
||||||
|
if (result?.assetURL) {
|
||||||
|
await runtime.Browser.OpenURL(result.assetURL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 公共接口 ===
|
||||||
|
return {
|
||||||
|
// 只读状态
|
||||||
|
hasCheckedOnStartup: readonly(hasCheckedOnStartup),
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const hasUpdate = computed(() => updateResult.value?.hasUpdate || false);
|
isChecking,
|
||||||
|
isUpdating,
|
||||||
|
hasUpdate,
|
||||||
|
updateSuccess,
|
||||||
|
isError,
|
||||||
|
updateResult,
|
||||||
|
errorMessage,
|
||||||
|
|
||||||
// 检查更新
|
// 方法
|
||||||
const checkForUpdates = async (): Promise<boolean> => {
|
checkForUpdates,
|
||||||
if (isChecking.value) return false;
|
applyUpdate,
|
||||||
|
restartApplication,
|
||||||
// 重置错误信息
|
checkOnStartup,
|
||||||
errorMessage.value = '';
|
openReleaseURL,
|
||||||
isChecking.value = true;
|
clearStatus,
|
||||||
try {
|
|
||||||
const result = await CheckForUpdates();
|
// 内部状态管理
|
||||||
if (result) {
|
setUpdateStatus
|
||||||
updateResult.value = result;
|
};
|
||||||
if (result.error) {
|
});
|
||||||
errorMessage.value = result.error;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error instanceof Error ? error.message : 'Network error';
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isChecking.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 应用更新
|
|
||||||
const applyUpdate = async (): Promise<boolean> => {
|
|
||||||
if (isUpdating.value) return false;
|
|
||||||
|
|
||||||
// 重置错误信息
|
|
||||||
errorMessage.value = '';
|
|
||||||
isUpdating.value = true;
|
|
||||||
try {
|
|
||||||
const result = await ApplyUpdate();
|
|
||||||
if (result) {
|
|
||||||
updateResult.value = result;
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
errorMessage.value = result.error;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.updateApplied) {
|
|
||||||
updateSuccess.value = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error instanceof Error ? error.message : 'Update failed';
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isUpdating.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重启应用
|
|
||||||
const restartApplication = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await RestartApplication();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
errorMessage.value = error instanceof Error ? error.message : 'Restart failed';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 启动时检查更新
|
|
||||||
const checkOnStartup = async () => {
|
|
||||||
if (hasCheckedOnStartup.value) return;
|
|
||||||
const configStore = useConfigStore();
|
|
||||||
|
|
||||||
if (configStore.config.updates.autoUpdate) {
|
|
||||||
await checkForUpdates();
|
|
||||||
}
|
|
||||||
hasCheckedOnStartup.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开发布页面
|
|
||||||
const openReleaseURL = async () => {
|
|
||||||
if (updateResult.value?.assetURL) {
|
|
||||||
await runtime.Browser.OpenURL(updateResult.value.assetURL);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 重置状态
|
|
||||||
const reset = () => {
|
|
||||||
updateResult.value = null;
|
|
||||||
isChecking.value = false;
|
|
||||||
isUpdating.value = false;
|
|
||||||
updateSuccess.value = false;
|
|
||||||
errorMessage.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
// 状态
|
|
||||||
isChecking,
|
|
||||||
isUpdating,
|
|
||||||
updateResult,
|
|
||||||
hasCheckedOnStartup,
|
|
||||||
updateSuccess,
|
|
||||||
errorMessage,
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
hasUpdate,
|
|
||||||
|
|
||||||
// 方法
|
|
||||||
checkForUpdates,
|
|
||||||
applyUpdate,
|
|
||||||
restartApplication,
|
|
||||||
checkOnStartup,
|
|
||||||
openReleaseURL,
|
|
||||||
reset
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
import {computed} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {IsDocumentWindowOpen} from "@/../bindings/voidraft/internal/services/windowservice";
|
import {IsDocumentWindowOpen} from "@/../bindings/voidraft/internal/services/windowservice";
|
||||||
|
|
||||||
|
|
||||||
export const useWindowStore = defineStore('window', () => {
|
export const useWindowStore = defineStore('window', () => {
|
||||||
|
|
||||||
|
const DOCUMENT_ID_KEY = ref<string>('documentId');
|
||||||
|
|
||||||
// 判断是否为主窗口
|
// 判断是否为主窗口
|
||||||
const isMainWindow = computed(() => {
|
const isMainWindow = computed(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
return !urlParams.has('documentId');
|
return !urlParams.has(DOCUMENT_ID_KEY.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取当前窗口的documentId
|
// 获取当前窗口的documentId
|
||||||
const currentDocumentId = computed(() => {
|
const currentDocumentId = computed(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
return urlParams.get('documentId');
|
return urlParams.get(DOCUMENT_ID_KEY.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断文档窗口是否打开
|
* 判断文档窗口是否打开
|
||||||
* @param documentId 文档ID
|
* @param documentId 文档ID
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {useConfigStore} from '@/stores/configStore';
|
import {useConfigStore} from '@/stores/configStore';
|
||||||
import {useBackupStore} from '@/stores/backupStore';
|
import {useBackupStore} from '@/stores/backupStore';
|
||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import {computed, onMounted, onUnmounted} from 'vue';
|
import {computed, onUnmounted} from 'vue';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||||
@@ -13,14 +13,8 @@ const {t} = useI18n();
|
|||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const backupStore = useBackupStore();
|
const backupStore = useBackupStore();
|
||||||
|
|
||||||
// 确保配置已加载
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!configStore.configLoaded) {
|
|
||||||
await configStore.initConfig();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
backupStore.clearError();
|
backupStore.clearStatus();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 认证方式选项
|
// 认证方式选项
|
||||||
@@ -142,6 +136,11 @@ const pushToRemote = async () => {
|
|||||||
await backupStore.pushToRemote();
|
await backupStore.pushToRemote();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 重试备份
|
||||||
|
const retryBackup = async () => {
|
||||||
|
await backupStore.retryBackup();
|
||||||
|
};
|
||||||
|
|
||||||
// 选择SSH密钥文件
|
// 选择SSH密钥文件
|
||||||
const selectSshKeyFile = async () => {
|
const selectSshKeyFile = async () => {
|
||||||
// 使用DialogService选择文件
|
// 使用DialogService选择文件
|
||||||
@@ -311,8 +310,8 @@ const selectSshKeyFile = async () => {
|
|||||||
>
|
>
|
||||||
<div class="backup-operation-container">
|
<div class="backup-operation-container">
|
||||||
<div class="backup-status-icons">
|
<div class="backup-status-icons">
|
||||||
<span v-if="backupStore.pushSuccess" class="success-icon">✓</span>
|
<span v-if="backupStore.isSuccess" class="success-icon">✓</span>
|
||||||
<span v-if="backupStore.pushError" class="error-icon">✗</span>
|
<span v-if="backupStore.isError" class="error-icon">✗</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="push-button"
|
class="push-button"
|
||||||
@@ -323,10 +322,18 @@ const selectSshKeyFile = async () => {
|
|||||||
<span v-if="backupStore.isPushing" class="loading-spinner"></span>
|
<span v-if="backupStore.isPushing" class="loading-spinner"></span>
|
||||||
{{ backupStore.isPushing ? t('settings.backup.pushing') : t('settings.backup.actions.push') }}
|
{{ backupStore.isPushing ? t('settings.backup.pushing') : t('settings.backup.actions.push') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="backupStore.isError"
|
||||||
|
class="retry-button"
|
||||||
|
@click="() => retryBackup()"
|
||||||
|
:disabled="backupStore.isPushing"
|
||||||
|
>
|
||||||
|
{{ t('settings.backup.actions.retry') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<div v-if="backupStore.error" class="error-message-row">
|
<div v-if="backupStore.errorMessage" class="error-message-row">
|
||||||
{{ backupStore.error }}
|
{{ backupStore.errorMessage }}
|
||||||
</div>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,7 +435,8 @@ const selectSshKeyFile = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 按钮样式
|
// 按钮样式
|
||||||
.push-button {
|
.push-button,
|
||||||
|
.retry-button {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background-color: var(--settings-input-bg);
|
background-color: var(--settings-input-bg);
|
||||||
border: 1px solid var(--settings-input-border);
|
border: 1px solid var(--settings-input-border);
|
||||||
@@ -472,6 +480,17 @@ const selectSshKeyFile = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retry-button {
|
||||||
|
background-color: #ff9800;
|
||||||
|
border-color: #ff9800;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: #f57c00;
|
||||||
|
border-color: #f57c00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 错误信息行样式
|
// 错误信息行样式
|
||||||
.error-message-row {
|
.error-message-row {
|
||||||
color: #f44336;
|
color: #f44336;
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import {
|
|||||||
MigrationService,
|
MigrationService,
|
||||||
MigrationStatus
|
MigrationStatus
|
||||||
} from '@/../bindings/voidraft/internal/services';
|
} from '@/../bindings/voidraft/internal/services';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import {useSystemStore} from "@/stores/systemStore";
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
|
const systemStore = useSystemStore();
|
||||||
|
|
||||||
// 迁移进度状态
|
// 迁移进度状态
|
||||||
const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
|
const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
|
||||||
@@ -148,8 +149,7 @@ const alwaysOnTop = computed({
|
|||||||
set: async (value: boolean) => {
|
set: async (value: boolean) => {
|
||||||
// 先更新配置
|
// 先更新配置
|
||||||
await configStore.setAlwaysOnTop(value);
|
await configStore.setAlwaysOnTop(value);
|
||||||
// 然后立即应用窗口置顶状态
|
await systemStore.setWindowOnTop(value);
|
||||||
await runtime.Window.SetAlwaysOnTop(value);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useI18n} from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {computed} from 'vue';
|
import { computed, onUnmounted } from 'vue';
|
||||||
import {useConfigStore} from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import {useUpdateStore} from '@/stores/updateStore';
|
import { useUpdateStore } from '@/stores/updateStore';
|
||||||
import SettingSection from '../components/SettingSection.vue';
|
import SettingSection from '../components/SettingSection.vue';
|
||||||
import SettingItem from '../components/SettingItem.vue';
|
import SettingItem from '../components/SettingItem.vue';
|
||||||
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
import ToggleSwitch from '../components/ToggleSwitch.vue';
|
||||||
import { Remarkable } from 'remarkable';
|
import { Remarkable } from 'remarkable';
|
||||||
|
|
||||||
const {t} = useI18n();
|
const { t } = useI18n();
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const updateStore = useUpdateStore();
|
const updateStore = useUpdateStore();
|
||||||
|
|
||||||
|
// 清理状态
|
||||||
|
onUnmounted(() => {
|
||||||
|
updateStore.clearStatus();
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化Remarkable实例并配置
|
// 初始化Remarkable实例并配置
|
||||||
const md = new Remarkable({
|
const md = new Remarkable({
|
||||||
html: true, // 允许HTML
|
html: true, // 允许HTML
|
||||||
@@ -93,13 +98,21 @@ const currentVersion = computed(() => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
<!-- 检查结果 -->
|
<!-- 检查结果 -->
|
||||||
<div class="check-results" v-if="updateStore.updateResult || updateStore.errorMessage">
|
<div class="check-results" v-if="updateStore.updateResult || updateStore.isError">
|
||||||
<!-- 错误信息 -->
|
<!-- 错误信息 -->
|
||||||
<div v-if="updateStore.errorMessage" class="result-item error-result">
|
<div v-if="updateStore.isError" class="result-item error-result">
|
||||||
<div class="result-text">
|
<div class="result-text">
|
||||||
<span class="result-icon">⚠️</span>
|
<span class="result-icon">⚠️</span>
|
||||||
<div class="result-message">{{ updateStore.errorMessage }}</div>
|
<div class="result-message">{{ updateStore.errorMessage }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="updateStore.isError"
|
||||||
|
class="retry-button"
|
||||||
|
@click="updateStore.checkForUpdates"
|
||||||
|
:disabled="updateStore.isChecking"
|
||||||
|
>
|
||||||
|
{{ t('common.retry') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 更新成功 -->
|
<!-- 更新成功 -->
|
||||||
@@ -128,7 +141,7 @@ const currentVersion = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已是最新版本 -->
|
<!-- 已是最新版本 -->
|
||||||
<div v-else-if="updateStore.updateResult && !updateStore.hasUpdate && !updateStore.errorMessage"
|
<div v-else-if="updateStore.updateResult && !updateStore.hasUpdate && !updateStore.isError"
|
||||||
class="result-item latest-version">
|
class="result-item latest-version">
|
||||||
<div class="result-text">
|
<div class="result-text">
|
||||||
<span class="result-icon">✓</span>
|
<span class="result-icon">✓</span>
|
||||||
@@ -232,6 +245,28 @@ const currentVersion = computed(() => {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
padding-right: 8px; // 添加右侧内边距,防止文本贴近容器边缘
|
padding-right: 8px; // 添加右侧内边距,防止文本贴近容器边缘
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retry-button {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: #ff9800;
|
||||||
|
border: 1px solid #ff9800;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background-color: #f57c00;
|
||||||
|
border-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,4 +456,4 @@ const currentVersion = computed(() => {
|
|||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -45,6 +47,13 @@ func NewBackupService(configService *ConfigService, dbService *DatabaseService,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ds *BackupService) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||||
|
if err := ds.Initialize(); err != nil {
|
||||||
|
return fmt.Errorf("initializing backup service: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize 初始化备份服务
|
// Initialize 初始化备份服务
|
||||||
func (s *BackupService) Initialize() error {
|
func (s *BackupService) Initialize() error {
|
||||||
config, repoPath, err := s.getConfigAndPath()
|
config, repoPath, err := s.getConfigAndPath()
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ func (ds *DocumentService) LockDocument(id int64) error {
|
|||||||
return errors.New("database service not available")
|
return errors.New("database service not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先检查文档是否存在且未删除(不加锁避免死锁)
|
// 先检查文档是否存在且未删除
|
||||||
doc, err := ds.GetDocumentByID(id)
|
doc, err := ds.GetDocumentByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get document: %w", err)
|
return fmt.Errorf("failed to get document: %w", err)
|
||||||
@@ -245,7 +245,7 @@ func (ds *DocumentService) UnlockDocument(id int64) error {
|
|||||||
return errors.New("database service not available")
|
return errors.New("database service not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先检查文档是否存在(不加锁避免死锁)
|
// 先检查文档是否存在
|
||||||
doc, err := ds.GetDocumentByID(id)
|
doc, err := ds.GetDocumentByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get document: %w", err)
|
return fmt.Errorf("failed to get document: %w", err)
|
||||||
@@ -438,24 +438,3 @@ func (ds *DocumentService) ListDeletedDocumentsMeta() ([]*models.Document, error
|
|||||||
|
|
||||||
return documents, nil
|
return documents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFirstDocumentID gets the first active document's ID for frontend initialization
|
|
||||||
func (ds *DocumentService) GetFirstDocumentID() (int64, error) {
|
|
||||||
ds.mu.RLock()
|
|
||||||
defer ds.mu.RUnlock()
|
|
||||||
|
|
||||||
if ds.databaseService == nil || ds.databaseService.db == nil {
|
|
||||||
return 0, errors.New("database service not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
var id int64
|
|
||||||
err := ds.databaseService.db.QueryRow(sqlGetFirstDocumentID).Scan(&id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return 0, nil // No documents exist
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("failed to get first document ID: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user