♻️ Refactor document selector and cache management logic
This commit is contained in:
@@ -119,6 +119,42 @@ export enum MigrationStatus {
|
|||||||
MigrationStatusFailed = "failed",
|
MigrationStatusFailed = "failed",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSInfo 操作系统信息
|
||||||
|
*/
|
||||||
|
export class OSInfo {
|
||||||
|
"id": string;
|
||||||
|
"name": string;
|
||||||
|
"version": string;
|
||||||
|
"branding": string;
|
||||||
|
|
||||||
|
/** Creates a new OSInfo instance. */
|
||||||
|
constructor($$source: Partial<OSInfo> = {}) {
|
||||||
|
if (!("id" in $$source)) {
|
||||||
|
this["id"] = "";
|
||||||
|
}
|
||||||
|
if (!("name" in $$source)) {
|
||||||
|
this["name"] = "";
|
||||||
|
}
|
||||||
|
if (!("version" in $$source)) {
|
||||||
|
this["version"] = "";
|
||||||
|
}
|
||||||
|
if (!("branding" in $$source)) {
|
||||||
|
this["branding"] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new OSInfo instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): OSInfo {
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
return new OSInfo($$parsedSource as Partial<OSInfo>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SelfUpdateResult 自我更新结果
|
* SelfUpdateResult 自我更新结果
|
||||||
*/
|
*/
|
||||||
@@ -202,6 +238,54 @@ export class SelfUpdateResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SystemInfo 系统信息
|
||||||
|
*/
|
||||||
|
export class SystemInfo {
|
||||||
|
"os": string;
|
||||||
|
"arch": string;
|
||||||
|
"debug": boolean;
|
||||||
|
"osInfo": OSInfo | null;
|
||||||
|
"platformInfo": { [_: string]: any };
|
||||||
|
|
||||||
|
/** Creates a new SystemInfo instance. */
|
||||||
|
constructor($$source: Partial<SystemInfo> = {}) {
|
||||||
|
if (!("os" in $$source)) {
|
||||||
|
this["os"] = "";
|
||||||
|
}
|
||||||
|
if (!("arch" in $$source)) {
|
||||||
|
this["arch"] = "";
|
||||||
|
}
|
||||||
|
if (!("debug" in $$source)) {
|
||||||
|
this["debug"] = false;
|
||||||
|
}
|
||||||
|
if (!("osInfo" in $$source)) {
|
||||||
|
this["osInfo"] = null;
|
||||||
|
}
|
||||||
|
if (!("platformInfo" in $$source)) {
|
||||||
|
this["platformInfo"] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(this, $$source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new SystemInfo instance from a string or object.
|
||||||
|
*/
|
||||||
|
static createFrom($$source: any = {}): SystemInfo {
|
||||||
|
const $$createField3_0 = $$createType1;
|
||||||
|
const $$createField4_0 = $$createType2;
|
||||||
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
|
if ("osInfo" in $$parsedSource) {
|
||||||
|
$$parsedSource["osInfo"] = $$createField3_0($$parsedSource["osInfo"]);
|
||||||
|
}
|
||||||
|
if ("platformInfo" in $$parsedSource) {
|
||||||
|
$$parsedSource["platformInfo"] = $$createField4_0($$parsedSource["platformInfo"]);
|
||||||
|
}
|
||||||
|
return new SystemInfo($$parsedSource as Partial<SystemInfo>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WindowInfo 窗口信息(简化版)
|
* WindowInfo 窗口信息(简化版)
|
||||||
*/
|
*/
|
||||||
@@ -229,7 +313,7 @@ export class WindowInfo {
|
|||||||
* Creates a new WindowInfo instance from a string or object.
|
* Creates a new WindowInfo instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): WindowInfo {
|
static createFrom($$source: any = {}): WindowInfo {
|
||||||
const $$createField0_0 = $$createType1;
|
const $$createField0_0 = $$createType4;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("Window" in $$parsedSource) {
|
if ("Window" in $$parsedSource) {
|
||||||
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
$$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]);
|
||||||
@@ -259,5 +343,8 @@ export class WindowSnapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = application$0.WebviewWindow.createFrom;
|
const $$createType0 = OSInfo.createFrom;
|
||||||
const $$createType1 = $Create.Nullable($$createType0);
|
const $$createType1 = $Create.Nullable($$createType0);
|
||||||
|
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||||
|
const $$createType3 = application$0.WebviewWindow.createFrom;
|
||||||
|
const $$createType4 = $Create.Nullable($$createType3);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
// @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 from "./models.js";
|
import * as $models from "./models.js";
|
||||||
@@ -34,6 +38,26 @@ export function GetMemoryStats(): Promise<$models.MemoryStats> & { cancel(): voi
|
|||||||
return $typingPromise;
|
return $typingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetSystemInfo 获取系统环境信息
|
||||||
|
*/
|
||||||
|
export function GetSystemInfo(): Promise<$models.SystemInfo | null> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2629436820) as any;
|
||||||
|
let $typingPromise = $resultPromise.then(($result: any) => {
|
||||||
|
return $$createType2($result);
|
||||||
|
}) as any;
|
||||||
|
$typingPromise.cancel = $resultPromise.cancel.bind($resultPromise);
|
||||||
|
return $typingPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetAppReferences 设置应用引用
|
||||||
|
*/
|
||||||
|
export function SetAppReferences(app: application$0.App | null): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(3873053414, app) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TriggerGC 手动触发垃圾回收
|
* TriggerGC 手动触发垃圾回收
|
||||||
*/
|
*/
|
||||||
@@ -44,3 +68,5 @@ export function TriggerGC(): Promise<void> & { cancel(): void } {
|
|||||||
|
|
||||||
// Private type creation functions
|
// Private type creation functions
|
||||||
const $$createType0 = $models.MemoryStats.createFrom;
|
const $$createType0 = $models.MemoryStats.createFrom;
|
||||||
|
const $$createType1 = $models.SystemInfo.createFrom;
|
||||||
|
const $$createType2 = $Create.Nullable($$createType1);
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export { AsyncOperationManager } from './manager';
|
|
||||||
export * from './types';
|
|
||||||
export { AsyncOperationManager as default } from './manager';
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
import {
|
|
||||||
OperationStatus,
|
|
||||||
OperationInfo,
|
|
||||||
AsyncOperationManagerConfig,
|
|
||||||
OperationCallbacks,
|
|
||||||
OperationExecutor,
|
|
||||||
OperationResult
|
|
||||||
} from './types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 异步操作管理器
|
|
||||||
* 用于控制异步操作的竞态条件,确保操作的正确性和一致性
|
|
||||||
*/
|
|
||||||
export class AsyncOperationManager {
|
|
||||||
private operationSequence = 0;
|
|
||||||
private pendingOperations = new Map<number, OperationInfo>();
|
|
||||||
private currentResourceOperation = new Map<string | number, number>();
|
|
||||||
private config: Required<AsyncOperationManagerConfig>;
|
|
||||||
private callbacks: OperationCallbacks;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
config: AsyncOperationManagerConfig = {},
|
|
||||||
callbacks: OperationCallbacks = {}
|
|
||||||
) {
|
|
||||||
this.config = {
|
|
||||||
timeout: 0,
|
|
||||||
autoCleanup: true,
|
|
||||||
maxConcurrent: 0,
|
|
||||||
debug: false,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
this.callbacks = callbacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成新的操作ID
|
|
||||||
*/
|
|
||||||
private getNextOperationId(): number {
|
|
||||||
return ++this.operationSequence;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录日志(调试模式下)
|
|
||||||
*/
|
|
||||||
private log(message: string, ...args: any[]): void {
|
|
||||||
if (this.config.debug) {
|
|
||||||
console.log(`[AsyncOperationManager] ${message}`, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建操作信息
|
|
||||||
*/
|
|
||||||
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 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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消指定操作
|
|
||||||
*/
|
|
||||||
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 cancelAllOperations(): void {
|
|
||||||
this.log('Cancelling all operations');
|
|
||||||
|
|
||||||
for (const [id] of this.pendingOperations.entries()) {
|
|
||||||
this.cancelOperation(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.currentResourceOperation.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查操作是否仍然有效
|
|
||||||
*/
|
|
||||||
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 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取操作信息
|
|
||||||
*/
|
|
||||||
public getOperation(operationId: number): OperationInfo | undefined {
|
|
||||||
return this.pendingOperations.get(operationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取资源的当前操作ID
|
|
||||||
*/
|
|
||||||
public getCurrentOperationId(resourceId: string | number): number | undefined {
|
|
||||||
return this.currentResourceOperation.get(resourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有待处理操作
|
|
||||||
*/
|
|
||||||
public getPendingOperations(): OperationInfo[] {
|
|
||||||
return Array.from(this.pendingOperations.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取运行中的操作数量
|
|
||||||
*/
|
|
||||||
public getRunningOperationsCount(): number {
|
|
||||||
return Array.from(this.pendingOperations.values())
|
|
||||||
.filter(op => op.status === OperationStatus.RUNNING).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁管理器,取消所有操作
|
|
||||||
*/
|
|
||||||
public destroy(): void {
|
|
||||||
this.log('Destroying AsyncOperationManager');
|
|
||||||
this.cancelAllOperations();
|
|
||||||
this.pendingOperations.clear();
|
|
||||||
this.currentResourceOperation.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* 异步操作竞态条件控制相关类型定义
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作状态枚举
|
|
||||||
*/
|
|
||||||
export enum OperationStatus {
|
|
||||||
PENDING = 'pending',
|
|
||||||
RUNNING = 'running',
|
|
||||||
COMPLETED = 'completed',
|
|
||||||
CANCELLED = 'cancelled',
|
|
||||||
FAILED = 'failed'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作信息接口
|
|
||||||
*/
|
|
||||||
export interface OperationInfo {
|
|
||||||
/** 操作ID */
|
|
||||||
id: number;
|
|
||||||
/** 资源ID(如文档ID、用户ID等) */
|
|
||||||
resourceId: string | number;
|
|
||||||
/** 操作状态 */
|
|
||||||
status: OperationStatus;
|
|
||||||
/** 取消控制器 */
|
|
||||||
abortController: AbortController;
|
|
||||||
/** 创建时间 */
|
|
||||||
createdAt: number;
|
|
||||||
/** 操作类型(可选,用于调试) */
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作管理器配置
|
|
||||||
*/
|
|
||||||
export interface AsyncOperationManagerConfig {
|
|
||||||
/** 操作超时时间(毫秒),0表示不超时 */
|
|
||||||
timeout?: number;
|
|
||||||
/** 是否自动清理已完成的操作 */
|
|
||||||
autoCleanup?: boolean;
|
|
||||||
/** 最大并发操作数,0表示无限制 */
|
|
||||||
maxConcurrent?: number;
|
|
||||||
/** 调试模式 */
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作回调函数类型
|
|
||||||
*/
|
|
||||||
export interface OperationCallbacks {
|
|
||||||
/** 操作开始回调 */
|
|
||||||
onStart?: (operation: OperationInfo) => void;
|
|
||||||
/** 操作完成回调 */
|
|
||||||
onComplete?: (operation: OperationInfo) => void;
|
|
||||||
/** 操作取消回调 */
|
|
||||||
onCancel?: (operation: OperationInfo) => void;
|
|
||||||
/** 操作失败回调 */
|
|
||||||
onError?: (operation: OperationInfo, error: Error) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作执行器函数类型
|
|
||||||
*/
|
|
||||||
export type OperationExecutor<T = any> = (
|
|
||||||
signal: AbortSignal,
|
|
||||||
operationId: number
|
|
||||||
) => Promise<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 操作结果类型
|
|
||||||
*/
|
|
||||||
export interface OperationResult<T = any> {
|
|
||||||
/** 操作是否成功 */
|
|
||||||
success: boolean;
|
|
||||||
/** 操作结果数据 */
|
|
||||||
data?: T;
|
|
||||||
/** 错误信息 */
|
|
||||||
error?: Error;
|
|
||||||
/** 操作信息 */
|
|
||||||
operation: OperationInfo;
|
|
||||||
}
|
|
||||||
285
frontend/src/common/async/README.md
Normal file
285
frontend/src/common/async/README.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# 异步操作管理器 (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` 文件。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本模块遵循项目的整体许可证。
|
||||||
78
frontend/src/common/async/index.ts
Normal file
78
frontend/src/common/async/index.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* 异步操作管理模块
|
||||||
|
*
|
||||||
|
* 该模块提供了用于管理异步操作竞态条件的完整解决方案。
|
||||||
|
* 主要用于防止同一资源上的并发操作冲突,确保操作的正确性和一致性。
|
||||||
|
*
|
||||||
|
* @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';
|
||||||
574
frontend/src/common/async/manager.ts
Normal file
574
frontend/src/common/async/manager.ts
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
289
frontend/src/common/async/types.ts
Normal file
289
frontend/src/common/async/types.ts
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* 异步操作竞态条件控制相关类型定义
|
||||||
|
*
|
||||||
|
* 该模块提供了用于管理异步操作竞态条件的类型定义,
|
||||||
|
* 包括操作状态、配置选项、回调函数等核心类型。
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
35
frontend/src/common/cache/README.md
vendored
35
frontend/src/common/cache/README.md
vendored
@@ -10,16 +10,18 @@
|
|||||||
- 📊 缓存统计信息
|
- 📊 缓存统计信息
|
||||||
- ⏰ TTL 过期支持
|
- ⏰ TTL 过期支持
|
||||||
- 🎯 简洁易用的 API
|
- 🎯 简洁易用的 API
|
||||||
|
- 🔐 多种哈希算法支持
|
||||||
|
- 🏗️ 模块化设计,易于扩展
|
||||||
|
|
||||||
## 基础用法
|
## 基础用法
|
||||||
|
|
||||||
### 创建缓存
|
### 创建缓存
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { LRUCache, CacheManager, createCacheItem } from '@/common/cache';
|
import { LruCache, CacheManager, createCacheItem } from '@/common/cache';
|
||||||
|
|
||||||
// 直接创建缓存
|
// 直接创建缓存
|
||||||
const cache = new LRUCache({
|
const cache = new LruCache({
|
||||||
maxSize: 100,
|
maxSize: 100,
|
||||||
ttl: 5 * 60 * 1000, // 5 分钟
|
ttl: 5 * 60 * 1000, // 5 分钟
|
||||||
onEvict: (item) => console.log('Evicted:', item)
|
onEvict: (item) => console.log('Evicted:', item)
|
||||||
@@ -76,14 +78,26 @@ const item: MyItem = {
|
|||||||
cache.set('resource1', item);
|
cache.set('resource1', item);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 哈希工具使用
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createHash, generateCacheKey } from '@/common/cache';
|
||||||
|
|
||||||
|
// 生成简单哈希
|
||||||
|
const hash = createHash('some content');
|
||||||
|
|
||||||
|
// 生成缓存键
|
||||||
|
const key = generateCacheKey('user', userId, 'profile');
|
||||||
|
```
|
||||||
|
|
||||||
## API 参考
|
## API 参考
|
||||||
|
|
||||||
### LRUCache
|
### LruCache
|
||||||
|
|
||||||
- `get(id)` - 获取缓存项
|
- `get(id)` - 获取缓存项(O(1))
|
||||||
- `set(id, item)` - 设置缓存项
|
- `set(id, item)` - 设置缓存项(O(1))
|
||||||
- `remove(id)` - 移除缓存项
|
- `remove(id)` - 移除缓存项(O(1))
|
||||||
- `has(id)` - 检查是否存在
|
- `has(id)` - 检查是否存在(O(1))
|
||||||
- `clear()` - 清空缓存
|
- `clear()` - 清空缓存
|
||||||
- `size()` - 获取缓存大小
|
- `size()` - 获取缓存大小
|
||||||
- `getStats()` - 获取统计信息
|
- `getStats()` - 获取统计信息
|
||||||
@@ -96,11 +110,10 @@ cache.set('resource1', item);
|
|||||||
- `removeCache(name)` - 删除缓存
|
- `removeCache(name)` - 删除缓存
|
||||||
- `clearAll()` - 清空所有缓存
|
- `clearAll()` - 清空所有缓存
|
||||||
- `getAllStats()` - 获取所有统计信息
|
- `getAllStats()` - 获取所有统计信息
|
||||||
|
- `cleanupAll()` - 清理所有缓存的过期项
|
||||||
|
|
||||||
## 工具函数
|
## 工具函数
|
||||||
|
|
||||||
- `generateCacheKey(...parts)` - 生成缓存键
|
- `generateCacheKey(...parts)` - 生成缓存键
|
||||||
- `createCacheItem(id, data)` - 创建缓存项
|
- `createHash(content)` - 创建内容哈希
|
||||||
- `createContentHash(content)` - 创建内容哈希
|
- `createCacheItem(id, data)` - 创建缓存项
|
||||||
- `debounce(func, wait)` - 防抖函数
|
|
||||||
- `throttle(func, limit)` - 节流函数
|
|
||||||
133
frontend/src/common/cache/cache-manager.ts
vendored
133
frontend/src/common/cache/cache-manager.ts
vendored
@@ -1,133 +0,0 @@
|
|||||||
import type { CacheItem, CacheConfig, CacheStats } from './types';
|
|
||||||
import { LRUCache } from './lru-cache';
|
|
||||||
|
|
||||||
export class CacheManager {
|
|
||||||
private caches = new Map<string, LRUCache<any>>();
|
|
||||||
private cleanupInterval?: number;
|
|
||||||
|
|
||||||
constructor(options?: {
|
|
||||||
/** 自动清理间隔(毫秒),默认 5 分钟 */
|
|
||||||
cleanupInterval?: number;
|
|
||||||
}) {
|
|
||||||
if (options?.cleanupInterval) {
|
|
||||||
this.startAutoCleanup(options.cleanupInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建或获取缓存实例
|
|
||||||
*/
|
|
||||||
getCache<T extends CacheItem>(name: string, config?: CacheConfig): LRUCache<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 LRUCache<T>(config));
|
|
||||||
}
|
|
||||||
return this.caches.get(name)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建新的缓存实例
|
|
||||||
*/
|
|
||||||
createCache<T extends CacheItem>(name: string, config: CacheConfig): LRUCache<T> {
|
|
||||||
if (this.caches.has(name)) {
|
|
||||||
throw new Error(`Cache "${name}" already exists`);
|
|
||||||
}
|
|
||||||
const cache = new LRUCache<T>(config);
|
|
||||||
this.caches.set(name, cache);
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除缓存实例
|
|
||||||
*/
|
|
||||||
removeCache(name: string): boolean {
|
|
||||||
const cache = this.caches.get(name);
|
|
||||||
if (cache) {
|
|
||||||
cache.clear();
|
|
||||||
this.caches.delete(name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查缓存是否存在
|
|
||||||
*/
|
|
||||||
hasCache(name: string): boolean {
|
|
||||||
return this.caches.has(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有缓存名称
|
|
||||||
*/
|
|
||||||
getCacheNames(): string[] {
|
|
||||||
return Array.from(this.caches.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空所有缓存
|
|
||||||
*/
|
|
||||||
clearAll(): void {
|
|
||||||
for (const cache of this.caches.values()) {
|
|
||||||
cache.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有缓存的统计信息
|
|
||||||
*/
|
|
||||||
getAllStats(): Record<string, CacheStats> {
|
|
||||||
const stats: Record<string, CacheStats> = {};
|
|
||||||
for (const [name, cache] of this.caches.entries()) {
|
|
||||||
stats[name] = cache.getStats();
|
|
||||||
}
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理所有缓存中的过期项
|
|
||||||
*/
|
|
||||||
cleanupAll(): Record<string, number> {
|
|
||||||
const results: Record<string, number> = {};
|
|
||||||
for (const [name, cache] of this.caches.entries()) {
|
|
||||||
results[name] = cache.cleanup();
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动自动清理
|
|
||||||
*/
|
|
||||||
startAutoCleanup(interval: number): void {
|
|
||||||
this.stopAutoCleanup();
|
|
||||||
this.cleanupInterval = window.setInterval(() => {
|
|
||||||
this.cleanupAll();
|
|
||||||
}, interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止自动清理
|
|
||||||
*/
|
|
||||||
stopAutoCleanup(): void {
|
|
||||||
if (this.cleanupInterval) {
|
|
||||||
clearInterval(this.cleanupInterval);
|
|
||||||
this.cleanupInterval = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁管理器
|
|
||||||
*/
|
|
||||||
destroy(): void {
|
|
||||||
this.stopAutoCleanup();
|
|
||||||
this.clearAll();
|
|
||||||
this.caches.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局缓存管理器实例
|
|
||||||
export const globalCacheManager = new CacheManager({
|
|
||||||
cleanupInterval: 5 * 60 * 1000 // 5 分钟
|
|
||||||
});
|
|
||||||
166
frontend/src/common/cache/doublyLinkedList.ts
vendored
Normal file
166
frontend/src/common/cache/doublyLinkedList.ts
vendored
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
frontend/src/common/cache/index.ts
vendored
28
frontend/src/common/cache/index.ts
vendored
@@ -1,19 +1,21 @@
|
|||||||
export type {
|
export type {
|
||||||
CacheItem,
|
CacheItem,
|
||||||
DisposableCacheItem,
|
DisposableCacheItem,
|
||||||
CacheConfig,
|
CacheConfig,
|
||||||
CacheStats
|
CacheStats,
|
||||||
} from './types';
|
CacheStrategy,
|
||||||
export { LRUCache } from './lru-cache';
|
DoublyLinkedNode
|
||||||
export { CacheManager, globalCacheManager } from './cache-manager';
|
} from './interfaces';
|
||||||
|
export { LruCache } from './lruCache';
|
||||||
export {
|
export { CacheManager, globalCacheManager } from './manager';
|
||||||
|
export { DoublyLinkedList } from './doublyLinkedList';
|
||||||
|
export {
|
||||||
|
createHash,
|
||||||
generateCacheKey,
|
generateCacheKey,
|
||||||
createCacheItem,
|
createCacheItem,
|
||||||
calculateHitRate,
|
calculateHitRate,
|
||||||
formatCacheSize,
|
formatCacheSize,
|
||||||
isExpired,
|
isExpired,
|
||||||
createContentHash,
|
|
||||||
debounce,
|
debounce,
|
||||||
throttle
|
throttle
|
||||||
} from './utils';
|
} from './utils';
|
||||||
124
frontend/src/common/cache/interfaces.ts
vendored
Normal file
124
frontend/src/common/cache/interfaces.ts
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* 缓存项基础接口
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
200
frontend/src/common/cache/lru-cache.ts
vendored
200
frontend/src/common/cache/lru-cache.ts
vendored
@@ -1,200 +0,0 @@
|
|||||||
import type { CacheItem, CacheConfig, CacheStats, DisposableCacheItem } from './types';
|
|
||||||
|
|
||||||
export class LRUCache<T extends CacheItem> {
|
|
||||||
private items = new Map<string | number, T>();
|
|
||||||
private accessOrder: (string | number)[] = [];
|
|
||||||
private config: CacheConfig;
|
|
||||||
private stats = {
|
|
||||||
hits: 0,
|
|
||||||
misses: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(config: CacheConfig) {
|
|
||||||
this.config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存项
|
|
||||||
*/
|
|
||||||
get(id: string | number): T | null {
|
|
||||||
const item = this.items.get(id);
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
this.stats.misses++;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (this.isExpired(item)) {
|
|
||||||
this.remove(id);
|
|
||||||
this.stats.misses++;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新访问时间和顺序
|
|
||||||
item.lastAccessed = new Date();
|
|
||||||
this.updateAccessOrder(id);
|
|
||||||
this.stats.hits++;
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置缓存项
|
|
||||||
*/
|
|
||||||
set(id: string | number, item: T): void {
|
|
||||||
// 如果已存在,先移除旧的
|
|
||||||
if (this.items.has(id)) {
|
|
||||||
this.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查容量,必要时驱逐最旧的项
|
|
||||||
while (this.items.size >= this.config.maxSize) {
|
|
||||||
const oldestId = this.accessOrder.shift();
|
|
||||||
if (oldestId !== undefined) {
|
|
||||||
this.evict(oldestId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加新项
|
|
||||||
this.items.set(id, item);
|
|
||||||
this.accessOrder.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除缓存项
|
|
||||||
*/
|
|
||||||
remove(id: string | number): boolean {
|
|
||||||
const item = this.items.get(id);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
this.items.delete(id);
|
|
||||||
const index = this.accessOrder.indexOf(id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.accessOrder.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用清理逻辑
|
|
||||||
this.disposeItem(item);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否存在
|
|
||||||
*/
|
|
||||||
has(id: string | number): boolean {
|
|
||||||
const item = this.items.get(id);
|
|
||||||
if (!item) return false;
|
|
||||||
|
|
||||||
if (this.isExpired(item)) {
|
|
||||||
this.remove(id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空缓存
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
for (const item of this.items.values()) {
|
|
||||||
this.disposeItem(item);
|
|
||||||
}
|
|
||||||
this.items.clear();
|
|
||||||
this.accessOrder = [];
|
|
||||||
this.stats.hits = 0;
|
|
||||||
this.stats.misses = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有项
|
|
||||||
*/
|
|
||||||
getAll(): T[] {
|
|
||||||
return Array.from(this.items.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取缓存大小
|
|
||||||
*/
|
|
||||||
size(): number {
|
|
||||||
return this.items.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取统计信息
|
|
||||||
*/
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理过期项
|
|
||||||
*/
|
|
||||||
cleanup(): number {
|
|
||||||
let cleanedCount = 0;
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (!this.config.ttl) return cleanedCount;
|
|
||||||
|
|
||||||
for (const [id, item] of this.items.entries()) {
|
|
||||||
if (now - item.lastAccessed.getTime() > this.config.ttl) {
|
|
||||||
this.remove(id);
|
|
||||||
cleanedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 私有方法
|
|
||||||
|
|
||||||
private isExpired(item: T): boolean {
|
|
||||||
if (!this.config.ttl) return false;
|
|
||||||
return Date.now() - item.lastAccessed.getTime() > this.config.ttl;
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateAccessOrder(id: string | number): void {
|
|
||||||
const index = this.accessOrder.indexOf(id);
|
|
||||||
if (index > -1) {
|
|
||||||
this.accessOrder.splice(index, 1);
|
|
||||||
}
|
|
||||||
this.accessOrder.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private evict(id: string | number): void {
|
|
||||||
const item = this.items.get(id);
|
|
||||||
if (item) {
|
|
||||||
if (this.config.onEvict) {
|
|
||||||
this.config.onEvict(item);
|
|
||||||
}
|
|
||||||
this.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isDisposable(item: T): item is T & DisposableCacheItem {
|
|
||||||
return 'dispose' in item && typeof (item as any).dispose === 'function';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
276
frontend/src/common/cache/lruCache.ts
vendored
Normal file
276
frontend/src/common/cache/lruCache.ts
vendored
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
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
Normal file
263
frontend/src/common/cache/manager.ts
vendored
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
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 分钟
|
||||||
|
});
|
||||||
39
frontend/src/common/cache/types.ts
vendored
39
frontend/src/common/cache/types.ts
vendored
@@ -1,39 +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;
|
|
||||||
}
|
|
||||||
116
frontend/src/common/cache/utils.ts
vendored
116
frontend/src/common/cache/utils.ts
vendored
@@ -1,7 +1,42 @@
|
|||||||
import type { CacheItem } from './types';
|
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 {
|
export function generateCacheKey(...parts: (string | number)[]): string {
|
||||||
return parts.join(':');
|
return parts.join(':');
|
||||||
@@ -9,6 +44,17 @@ export function generateCacheKey(...parts: (string | number)[]): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建基础缓存项
|
* 创建基础缓存项
|
||||||
|
* 为任意数据创建符合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>>(
|
export function createCacheItem<T extends Record<string, any>>(
|
||||||
id: string | number,
|
id: string | number,
|
||||||
@@ -25,6 +71,16 @@ export function createCacheItem<T extends Record<string, any>>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算缓存命中率
|
* 计算缓存命中率
|
||||||
|
* 根据命中次数和未命中次数计算命中率
|
||||||
|
*
|
||||||
|
* @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 {
|
export function calculateHitRate(hits: number, misses: number): number {
|
||||||
const total = hits + misses;
|
const total = hits + misses;
|
||||||
@@ -33,6 +89,16 @@ export function calculateHitRate(hits: number, misses: number): number {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化缓存大小
|
* 格式化缓存大小
|
||||||
|
* 将字节数格式化为人类可读的大小字符串
|
||||||
|
*
|
||||||
|
* @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 {
|
export function formatCacheSize(size: number): string {
|
||||||
if (size < 1024) return `${size} B`;
|
if (size < 1024) return `${size} B`;
|
||||||
@@ -43,29 +109,34 @@ export function formatCacheSize(size: number): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查项是否过期
|
* 检查项是否过期
|
||||||
|
* 根据最后访问时间和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 {
|
export function isExpired(item: CacheItem, ttl: number): boolean {
|
||||||
return Date.now() - item.lastAccessed.getTime() > ttl;
|
return Date.now() - item.lastAccessed.getTime() > ttl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建内容哈希(简单实现)
|
|
||||||
*/
|
|
||||||
export function createContentHash(content: string): string {
|
|
||||||
let hash = 0;
|
|
||||||
if (content.length === 0) return hash.toString();
|
|
||||||
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
const char = content.charCodeAt(i);
|
|
||||||
hash = ((hash << 5) - hash) + char;
|
|
||||||
hash = hash & hash; // 转换为 32 位整数
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.abs(hash).toString(36);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 防抖函数,用于缓存操作
|
* 防抖函数,用于缓存操作
|
||||||
|
* 在指定时间内多次调用只执行最后一次
|
||||||
|
*
|
||||||
|
* @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>(
|
export function debounce<T extends (...args: any[]) => any>(
|
||||||
func: T,
|
func: T,
|
||||||
@@ -81,6 +152,17 @@ export function debounce<T extends (...args: any[]) => any>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 节流函数,用于缓存清理
|
* 节流函数,用于缓存清理
|
||||||
|
* 在指定时间内最多执行一次
|
||||||
|
*
|
||||||
|
* @template T 函数类型
|
||||||
|
* @param func 要节流的函数
|
||||||
|
* @param limit 限制时间间隔(毫秒)
|
||||||
|
* @returns 节流后的函数
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const throttledCleanup = throttle(cleanupCache, 1000);
|
||||||
|
* throttledCleanup(); // 1秒内最多执行一次
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function throttle<T extends (...args: any[]) => any>(
|
export function throttle<T extends (...args: any[]) => any>(
|
||||||
func: T,
|
func: T,
|
||||||
|
|||||||
233
frontend/src/components/tabs/TabContainer.vue
Normal file
233
frontend/src/components/tabs/TabContainer.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tab-container" style="--wails-draggable:no-drag">
|
||||||
|
<div class="tab-bar" ref="tabBarRef">
|
||||||
|
<div class="tab-scroll-wrapper" ref="tabScrollWrapperRef" style="--wails-draggable:no-drag" @wheel.prevent.stop="onWheelScroll">
|
||||||
|
<div class="tab-list" ref="tabListRef">
|
||||||
|
<TabItem
|
||||||
|
v-for="tab in mockTabs"
|
||||||
|
:key="tab.id"
|
||||||
|
:tab="tab"
|
||||||
|
:is-active="tab.id === activeTabId"
|
||||||
|
@click="switchToTab(tab.id)"
|
||||||
|
@close="closeTab(tab.id)"
|
||||||
|
@dragstart="onDragStart($event, tab.id)"
|
||||||
|
@dragover="onDragOver"
|
||||||
|
@drop="onDrop($event, tab.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右键菜单占位 -->
|
||||||
|
<div v-if="showContextMenu" class="context-menu-placeholder">
|
||||||
|
<!-- 这里将来会放置 TabContextMenu 组件 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
||||||
|
import TabItem from './TabItem.vue';
|
||||||
|
|
||||||
|
// 模拟数据接口
|
||||||
|
interface MockTab {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
documentId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟标签页数据
|
||||||
|
const mockTabs = ref<MockTab[]>([
|
||||||
|
{ id: 1, title: 'Document 1', isDirty: false, isActive: true, documentId: 1 },
|
||||||
|
{ id: 2, title: 'Long Document Name Example', isDirty: true, isActive: false, documentId: 2 },
|
||||||
|
{ id: 3, title: 'README.md', isDirty: false, isActive: false, documentId: 3 },
|
||||||
|
{ id: 4, title: 'config.json', isDirty: true, isActive: false, documentId: 4 },
|
||||||
|
{ id: 5, title: 'Another Very Long Document Title', isDirty: false, isActive: false, documentId: 5 },
|
||||||
|
{ id: 6, title: 'package.json', isDirty: false, isActive: false, documentId: 6 },
|
||||||
|
{ id: 7, title: 'index.html', isDirty: true, isActive: false, documentId: 7 },
|
||||||
|
{ id: 8, title: 'styles.css', isDirty: false, isActive: false, documentId: 8 },
|
||||||
|
{ id: 9, title: 'main.js', isDirty: false, isActive: false, documentId: 9 },
|
||||||
|
{ id: 10, title: 'utils.ts', isDirty: true, isActive: false, documentId: 10 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeTabId = ref(1);
|
||||||
|
const showContextMenu = ref(false);
|
||||||
|
|
||||||
|
// DOM 引用
|
||||||
|
const tabBarRef = ref<HTMLElement>();
|
||||||
|
const tabListRef = ref<HTMLElement>();
|
||||||
|
// 新增:滚动容器引用
|
||||||
|
const tabScrollWrapperRef = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
let draggedTabId: number | null = null;
|
||||||
|
|
||||||
|
// 切换标签页
|
||||||
|
const switchToTab = (tabId: number) => {
|
||||||
|
activeTabId.value = tabId;
|
||||||
|
mockTabs.value.forEach(tab => {
|
||||||
|
tab.isActive = tab.id === tabId;
|
||||||
|
});
|
||||||
|
console.log('Switch to tab:', tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭标签页
|
||||||
|
const closeTab = (tabId: number) => {
|
||||||
|
const index = mockTabs.value.findIndex(tab => tab.id === tabId);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
mockTabs.value.splice(index, 1);
|
||||||
|
|
||||||
|
// 如果关闭的是活跃标签页,切换到其他标签页
|
||||||
|
if (activeTabId.value === tabId && mockTabs.value.length > 0) {
|
||||||
|
const nextTab = mockTabs.value[Math.max(0, index - 1)];
|
||||||
|
switchToTab(nextTab.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Close tab:', tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽开始
|
||||||
|
const onDragStart = (event: DragEvent, tabId: number) => {
|
||||||
|
draggedTabId = tabId;
|
||||||
|
event.dataTransfer?.setData('text/plain', tabId.toString());
|
||||||
|
console.log('Drag start:', tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽悬停
|
||||||
|
const onDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽放置
|
||||||
|
const onDrop = (event: DragEvent, targetTabId: number) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (draggedTabId && draggedTabId !== targetTabId) {
|
||||||
|
const draggedIndex = mockTabs.value.findIndex(tab => tab.id === draggedTabId);
|
||||||
|
const targetIndex = mockTabs.value.findIndex(tab => tab.id === targetTabId);
|
||||||
|
|
||||||
|
if (draggedIndex !== -1 && targetIndex !== -1) {
|
||||||
|
const draggedTab = mockTabs.value.splice(draggedIndex, 1)[0];
|
||||||
|
mockTabs.value.splice(targetIndex, 0, draggedTab);
|
||||||
|
console.log('Reorder tabs:', draggedTabId, 'to position of', targetTabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draggedTabId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const onWheelScroll = (event: WheelEvent) => {
|
||||||
|
const el = tabScrollWrapperRef.value;
|
||||||
|
if (!el) return;
|
||||||
|
const delta = event.deltaY || event.deltaX || 0;
|
||||||
|
el.scrollLeft += delta;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 组件挂载时的初始化逻辑
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 组件卸载时的清理逻辑
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tab-container {
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--toolbar-bg);
|
||||||
|
min-width: 0; /* 允许子项收缩,确保产生横向溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-scroll-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; // 关键:允许作为 flex 子项收缩,从而产生横向溢出
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
position: relative;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-list {
|
||||||
|
display: flex;
|
||||||
|
width: max-content; /* 令宽度等于所有子项总宽度,必定溢出 */
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: var(--toolbar-bg);
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0 1px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--toolbar-text);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--toolbar-button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tab-action-btn {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
273
frontend/src/components/tabs/TabItem.vue
Normal file
273
frontend/src/components/tabs/TabItem.vue
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{
|
||||||
|
active: isActive,
|
||||||
|
dragging: isDragging
|
||||||
|
}"
|
||||||
|
style="--wails-draggable:no-drag"
|
||||||
|
draggable="true"
|
||||||
|
@click="handleClick"
|
||||||
|
@dragstart="handleDragStart"
|
||||||
|
@dragover="handleDragOver"
|
||||||
|
@drop="handleDrop"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
@contextmenu="handleContextMenu"
|
||||||
|
>
|
||||||
|
<!-- 文档图标 -->
|
||||||
|
<div class="tab-icon">
|
||||||
|
<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="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
|
||||||
|
<polyline points="14,2 14,8 20,8"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签页标题 -->
|
||||||
|
<div class="tab-title" :title="tab.title">
|
||||||
|
{{ displayTitle }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 关闭按钮 -->
|
||||||
|
<div
|
||||||
|
class="tab-close"
|
||||||
|
@click.stop="handleClose"
|
||||||
|
:title="'关闭标签页'"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 拖拽指示器 -->
|
||||||
|
<div v-if="isDragging" class="drag-indicator"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
// 组件属性
|
||||||
|
interface TabProps {
|
||||||
|
tab: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
isDirty: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
documentId: number;
|
||||||
|
};
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<TabProps>();
|
||||||
|
|
||||||
|
// 组件事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [tabId: number];
|
||||||
|
close: [tabId: number];
|
||||||
|
dragstart: [event: DragEvent, tabId: number];
|
||||||
|
dragover: [event: DragEvent];
|
||||||
|
drop: [event: DragEvent, tabId: number];
|
||||||
|
contextmenu: [event: MouseEvent, tabId: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 组件状态
|
||||||
|
const isDragging = ref(false);
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const displayTitle = computed(() => {
|
||||||
|
const title = props.tab.title;
|
||||||
|
// 限制标题长度,超过15个字符显示省略号
|
||||||
|
return title.length > 15 ? title.substring(0, 15) + '...' : title;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
const handleClick = () => {
|
||||||
|
emit('click', props.tab.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('close', props.tab.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragStart = (event: DragEvent) => {
|
||||||
|
isDragging.value = true;
|
||||||
|
emit('dragstart', event, props.tab.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
emit('dragover', event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent) => {
|
||||||
|
isDragging.value = false;
|
||||||
|
emit('drop', event, props.tab.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragEnd = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
emit('contextmenu', event, props.tab.id);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 200px;
|
||||||
|
height: 32px; // 适配标题栏高度
|
||||||
|
padding: 0 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
border-right: 1px solid var(--toolbar-border);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box; // 防止激活态的边框影响整体高度
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--toolbar-button-hover);
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: var(--toolbar-button-hover);
|
||||||
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
color: var(--toolbar-text);
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
color: var(--toolbar-text);
|
||||||
|
/* 不用加粗,避免抖动 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: rotate(2deg);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 文档图标 */
|
||||||
|
.tab-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 6px;
|
||||||
|
color: var(--toolbar-text);
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--toolbar-text);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
color: var(--toolbar-text);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--error-color);
|
||||||
|
color: white;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(45deg,
|
||||||
|
transparent 25%,
|
||||||
|
rgba(var(--accent-color-rgb), 0.1) 25%,
|
||||||
|
rgba(var(--accent-color-rgb), 0.1) 50%,
|
||||||
|
transparent 50%,
|
||||||
|
transparent 75%,
|
||||||
|
rgba(var(--accent-color-rgb), 0.1) 75%
|
||||||
|
);
|
||||||
|
background-size: 8px 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 活跃标签页在拖拽时的特殊样式 */
|
||||||
|
.tab-item.active.dragging {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tab-item {
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 150px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,7 +4,11 @@
|
|||||||
<div class="titlebar-icon">
|
<div class="titlebar-icon">
|
||||||
<img src="/appicon.png" alt="voidraft"/>
|
<img src="/appicon.png" alt="voidraft"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="titlebar-title">{{ titleText }}</div>
|
<!-- <div class="titlebar-title">{{ titleText }}</div>-->
|
||||||
|
<!-- 标签页容器区域 -->
|
||||||
|
<div class="titlebar-tabs" style="--wails-draggable:no-drag">
|
||||||
|
<TabContainer />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
<div class="titlebar-controls" style="--wails-draggable:no-drag" @contextmenu.prevent>
|
||||||
@@ -40,6 +44,7 @@ import {computed, onMounted, ref} from 'vue';
|
|||||||
import {useI18n} from 'vue-i18n';
|
import {useI18n} from 'vue-i18n';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import {useDocumentStore} from '@/stores/documentStore';
|
||||||
|
import TabContainer from '@/components/tabs/TabContainer.vue';
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const isMaximized = ref(false);
|
const isMaximized = ref(false);
|
||||||
@@ -118,6 +123,7 @@ onMounted(async () => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
min-width: 0; /* 允许内容收缩 */
|
||||||
|
|
||||||
-webkit-context-menu: none;
|
-webkit-context-menu: none;
|
||||||
-moz-context-menu: none;
|
-moz-context-menu: none;
|
||||||
@@ -127,6 +133,7 @@ onMounted(async () => {
|
|||||||
.titlebar-content .titlebar-icon {
|
.titlebar-content .titlebar-icon {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -135,9 +142,14 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.titlebar-title {
|
.titlebar-tabs {
|
||||||
font-size: 12px;
|
flex: 1;
|
||||||
color: var(--toolbar-text);
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
//margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titlebar-controls {
|
.titlebar-controls {
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, nextTick, onMounted, onUnmounted, ref} from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import {useDocumentStore} from '@/stores/documentStore';
|
import { useDocumentStore } from '@/stores/documentStore';
|
||||||
import {useI18n} from 'vue-i18n';
|
import { useWindowStore } from '@/stores/windowStore';
|
||||||
import type {Document} from '@/../bindings/voidraft/internal/models/models';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {useWindowStore} from "@/stores/windowStore";
|
import type { Document } from '@/../bindings/voidraft/internal/models/models';
|
||||||
|
|
||||||
const documentStore = useDocumentStore();
|
const documentStore = useDocumentStore();
|
||||||
const windowStore = useWindowStore();
|
const windowStore = useWindowStore();
|
||||||
const {t} = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// 组件状态
|
// 组件状态
|
||||||
const showMenu = ref(false);
|
|
||||||
const inputValue = ref('');
|
const inputValue = ref('');
|
||||||
const inputRef = ref<HTMLInputElement>();
|
const inputRef = ref<HTMLInputElement>();
|
||||||
const editingId = ref<number | null>(null);
|
const editingId = ref<number | null>(null);
|
||||||
const editingTitle = ref('');
|
const editingTitle = ref('');
|
||||||
const editInputRef = ref<HTMLInputElement>();
|
const editInputRef = ref<HTMLInputElement>();
|
||||||
const deleteConfirmId = ref<number | null>(null);
|
const deleteConfirmId = ref<number | null>(null);
|
||||||
// 添加错误提示状态
|
|
||||||
const alreadyOpenDocId = ref<number | null>(null);
|
|
||||||
const errorMessageTimer = ref<number | null>(null);
|
|
||||||
|
|
||||||
// 过滤后的文档列表 + 创建选项
|
// 常量
|
||||||
|
const MAX_TITLE_LENGTH = 50;
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const currentDocName = computed(() => {
|
||||||
|
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
|
||||||
|
const title = documentStore.currentDocument.title;
|
||||||
|
return title.length > 12 ? title.substring(0, 12) + '...' : title;
|
||||||
|
});
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
const docs = documentStore.documentList;
|
const docs = documentStore.documentList;
|
||||||
const query = inputValue.value.trim();
|
const query = inputValue.value.trim();
|
||||||
|
|
||||||
if (!query) {
|
if (!query) return docs;
|
||||||
return docs;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤匹配的文档
|
|
||||||
const filtered = docs.filter(doc =>
|
const filtered = docs.filter(doc =>
|
||||||
doc.title.toLowerCase().includes(query.toLowerCase())
|
doc.title.toLowerCase().includes(query.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
// 如果输入的不是已存在文档的完整标题,添加创建选项
|
// 如果输入的不是已存在文档的完整标题,添加创建选项
|
||||||
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase());
|
const exactMatch = docs.some(doc => doc.title.toLowerCase() === query.toLowerCase());
|
||||||
if (!exactMatch && query.length > 0) {
|
if (!exactMatch && query.length > 0) {
|
||||||
return [
|
return [
|
||||||
{id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true} as any,
|
{ id: -1, title: t('toolbar.createDocument') + ` "${query}"`, isCreateOption: true } as any,
|
||||||
...filtered
|
...filtered
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -47,164 +49,125 @@ const filteredItems = computed(() => {
|
|||||||
return filtered;
|
return filtered;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 当前文档显示名称
|
// 工具函数
|
||||||
const currentDocName = computed(() => {
|
|
||||||
if (!documentStore.currentDocument) return t('toolbar.selectDocument');
|
|
||||||
const title = documentStore.currentDocument.title;
|
|
||||||
return title.length > 12 ? title.substring(0, 12) + '...' : title;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 打开菜单
|
|
||||||
const openMenu = async () => {
|
|
||||||
showMenu.value = true;
|
|
||||||
await documentStore.updateDocuments();
|
|
||||||
nextTick(() => {
|
|
||||||
inputRef.value?.focus();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭菜单
|
|
||||||
const closeMenu = () => {
|
|
||||||
showMenu.value = false;
|
|
||||||
inputValue.value = '';
|
|
||||||
editingId.value = null;
|
|
||||||
editingTitle.value = '';
|
|
||||||
deleteConfirmId.value = null;
|
|
||||||
|
|
||||||
// 清除错误状态和定时器
|
|
||||||
clearErrorMessage();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清除错误提示和定时器
|
|
||||||
const clearErrorMessage = () => {
|
|
||||||
if (errorMessageTimer.value) {
|
|
||||||
clearTimeout(errorMessageTimer.value);
|
|
||||||
errorMessageTimer.value = null;
|
|
||||||
}
|
|
||||||
alreadyOpenDocId.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切换菜单
|
|
||||||
const toggleMenu = () => {
|
|
||||||
if (showMenu.value) {
|
|
||||||
closeMenu();
|
|
||||||
} else {
|
|
||||||
openMenu();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 选择文档或创建文档
|
|
||||||
const selectItem = async (item: any) => {
|
|
||||||
if (item.isCreateOption) {
|
|
||||||
// 创建新文档
|
|
||||||
await createDoc(inputValue.value.trim());
|
|
||||||
} else {
|
|
||||||
// 选择现有文档
|
|
||||||
await selectDoc(item);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 选择文档
|
|
||||||
const selectDoc = async (doc: Document) => {
|
|
||||||
try {
|
|
||||||
// 如果选择的就是当前文档,直接关闭菜单
|
|
||||||
if (documentStore.currentDocument?.id === doc.id) {
|
|
||||||
closeMenu();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
|
||||||
if (hasOpen) {
|
|
||||||
// 设置错误状态并启动定时器
|
|
||||||
alreadyOpenDocId.value = doc.id;
|
|
||||||
|
|
||||||
// 清除之前的定时器(如果存在)
|
|
||||||
if (errorMessageTimer.value) {
|
|
||||||
clearTimeout(errorMessageTimer.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的定时器,3秒后清除错误信息
|
|
||||||
errorMessageTimer.value = window.setTimeout(() => {
|
|
||||||
alreadyOpenDocId.value = null;
|
|
||||||
errorMessageTimer.value = null;
|
|
||||||
}, 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const success = await documentStore.openDocument(doc.id);
|
|
||||||
if (success) {
|
|
||||||
closeMenu();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to switch documents:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 文档名称长度限制
|
|
||||||
const MAX_TITLE_LENGTH = 50;
|
|
||||||
|
|
||||||
// 验证文档名称
|
|
||||||
const validateTitle = (title: string): string | null => {
|
const validateTitle = (title: string): string | null => {
|
||||||
if (!title.trim()) {
|
if (!title.trim()) return t('toolbar.documentNameRequired');
|
||||||
return t('toolbar.documentNameRequired');
|
|
||||||
}
|
|
||||||
if (title.trim().length > MAX_TITLE_LENGTH) {
|
if (title.trim().length > MAX_TITLE_LENGTH) {
|
||||||
return t('toolbar.documentNameTooLong', {max: MAX_TITLE_LENGTH});
|
return t('toolbar.documentNameTooLong', { max: MAX_TITLE_LENGTH });
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建文档
|
const formatTime = (dateString: string | null) => {
|
||||||
const createDoc = async (title: string) => {
|
if (!dateString) return t('toolbar.unknownTime');
|
||||||
const trimmedTitle = title.trim();
|
|
||||||
const error = validateTitle(trimmedTitle);
|
try {
|
||||||
if (error) {
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
|
||||||
|
|
||||||
|
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
|
||||||
|
return date.toLocaleString(locale, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return t('toolbar.timeError');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 核心操作
|
||||||
|
const openMenu = async () => {
|
||||||
|
documentStore.openDocumentSelector();
|
||||||
|
await documentStore.updateDocuments();
|
||||||
|
await nextTick();
|
||||||
|
inputRef.value?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
documentStore.closeDocumentSelector();
|
||||||
|
inputValue.value = '';
|
||||||
|
editingId.value = null;
|
||||||
|
editingTitle.value = '';
|
||||||
|
deleteConfirmId.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectDoc = async (doc: Document) => {
|
||||||
|
// 如果选择的就是当前文档,直接关闭菜单
|
||||||
|
if (documentStore.currentDocument?.id === doc.id) {
|
||||||
|
closeMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
|
if (hasOpen) {
|
||||||
|
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await documentStore.openDocument(doc.id);
|
||||||
|
if (success) closeMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDoc = async (title: string) => {
|
||||||
|
const trimmedTitle = title.trim();
|
||||||
|
const error = validateTitle(trimmedTitle);
|
||||||
|
if (error) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newDoc = await documentStore.createNewDocument(trimmedTitle);
|
const newDoc = await documentStore.createNewDocument(trimmedTitle);
|
||||||
if (newDoc) {
|
if (newDoc) await selectDoc(newDoc);
|
||||||
await selectDoc(newDoc);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create document:', error);
|
console.error('Failed to create document:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始重命名
|
const selectItem = async (item: any) => {
|
||||||
|
if (item.isCreateOption) {
|
||||||
|
await createDoc(inputValue.value.trim());
|
||||||
|
} else {
|
||||||
|
await selectDoc(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑操作
|
||||||
const startRename = (doc: Document, event: Event) => {
|
const startRename = (doc: Document, event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
editingId.value = doc.id;
|
editingId.value = doc.id;
|
||||||
editingTitle.value = doc.title;
|
editingTitle.value = doc.title;
|
||||||
deleteConfirmId.value = null; // 清除删除确认状态
|
deleteConfirmId.value = null;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
editInputRef.value?.focus();
|
editInputRef.value?.focus();
|
||||||
editInputRef.value?.select();
|
editInputRef.value?.select();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存编辑
|
|
||||||
const saveEdit = async () => {
|
const saveEdit = async () => {
|
||||||
if (editingId.value && editingTitle.value.trim()) {
|
if (!editingId.value || !editingTitle.value.trim()) {
|
||||||
const trimmedTitle = editingTitle.value.trim();
|
editingId.value = null;
|
||||||
const error = validateTitle(trimmedTitle);
|
editingTitle.value = '';
|
||||||
if (error) {
|
return;
|
||||||
|
}
|
||||||
return;
|
|
||||||
}
|
const trimmedTitle = editingTitle.value.trim();
|
||||||
|
const error = validateTitle(trimmedTitle);
|
||||||
try {
|
if (error) return;
|
||||||
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
|
||||||
await documentStore.updateDocuments();
|
try {
|
||||||
} catch (_error) {
|
await documentStore.updateDocumentMetadata(editingId.value, trimmedTitle);
|
||||||
return;
|
await documentStore.updateDocuments();
|
||||||
}
|
} catch (error) {
|
||||||
|
console.error('Failed to update document:', error);
|
||||||
|
} finally {
|
||||||
|
editingId.value = null;
|
||||||
|
editingTitle.value = '';
|
||||||
}
|
}
|
||||||
editingId.value = null;
|
|
||||||
editingTitle.value = '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 在新窗口打开文档
|
// 其他操作
|
||||||
const openInNewWindow = async (doc: Document, event: Event) => {
|
const openInNewWindow = async (doc: Document, event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
try {
|
try {
|
||||||
@@ -214,56 +177,32 @@ const openInNewWindow = async (doc: Document, event: Event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理删除
|
|
||||||
const handleDelete = async (doc: Document, event: Event) => {
|
const handleDelete = async (doc: Document, event: Event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (deleteConfirmId.value === doc.id) {
|
if (deleteConfirmId.value === doc.id) {
|
||||||
// 确认删除前检查文档是否在其他窗口打开
|
// 确认删除前检查文档是否在其他窗口打开
|
||||||
try {
|
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
||||||
const hasOpen = await windowStore.isDocumentWindowOpen(doc.id);
|
if (hasOpen) {
|
||||||
if (hasOpen) {
|
documentStore.setError(doc.id, t('toolbar.alreadyOpenInNewWindow'));
|
||||||
// 设置错误状态并启动定时器
|
deleteConfirmId.value = null;
|
||||||
alreadyOpenDocId.value = doc.id;
|
return;
|
||||||
|
}
|
||||||
// 清除之前的定时器(如果存在)
|
|
||||||
if (errorMessageTimer.value) {
|
|
||||||
clearTimeout(errorMessageTimer.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置新的定时器,3秒后清除错误信息
|
|
||||||
errorMessageTimer.value = window.setTimeout(() => {
|
|
||||||
alreadyOpenDocId.value = null;
|
|
||||||
errorMessageTimer.value = null;
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
// 取消删除确认状态
|
|
||||||
deleteConfirmId.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
|
||||||
|
|
||||||
if (!deleteSuccess) {
|
const deleteSuccess = await documentStore.deleteDocument(doc.id);
|
||||||
return;
|
if (deleteSuccess) {
|
||||||
}
|
|
||||||
|
|
||||||
await documentStore.updateDocuments();
|
await documentStore.updateDocuments();
|
||||||
// 如果删除的是当前文档,切换到第一个文档
|
// 如果删除的是当前文档,切换到第一个文档
|
||||||
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];
|
||||||
if (firstDoc) {
|
if (firstDoc) await selectDoc(firstDoc);
|
||||||
await selectDoc(firstDoc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('deleted failed:', error);
|
|
||||||
}
|
}
|
||||||
deleteConfirmId.value = null;
|
deleteConfirmId.value = null;
|
||||||
} else {
|
} else {
|
||||||
// 进入确认状态
|
// 进入确认状态
|
||||||
deleteConfirmId.value = doc.id;
|
deleteConfirmId.value = doc.id;
|
||||||
editingId.value = null; // 清除编辑状态
|
editingId.value = null;
|
||||||
|
|
||||||
// 3秒后自动取消确认状态
|
// 3秒后自动取消确认状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -274,32 +213,18 @@ const handleDelete = async (doc: Document, event: Event) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化时间
|
// 键盘事件处理
|
||||||
const formatTime = (dateString: string | null) => {
|
const createKeyHandler = (handlers: Record<string, () => void>) => (event: KeyboardEvent) => {
|
||||||
if (!dateString) return t('toolbar.unknownTime');
|
const handler = handlers[event.key];
|
||||||
|
if (handler) {
|
||||||
try {
|
event.preventDefault();
|
||||||
const date = new Date(dateString);
|
event.stopPropagation();
|
||||||
if (isNaN(date.getTime())) return t('toolbar.invalidDate');
|
handler();
|
||||||
|
|
||||||
// 根据当前语言显示时间格式
|
|
||||||
const locale = t('locale') === 'zh-CN' ? 'zh-CN' : 'en-US';
|
|
||||||
return date.toLocaleString(locale, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
} catch (_error) {
|
|
||||||
return t('toolbar.timeError');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 键盘事件
|
const handleGlobalKeydown = createKeyHandler({
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
Escape: () => {
|
||||||
if (event.key === 'Escape') {
|
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
editingId.value = null;
|
editingId.value = null;
|
||||||
editingTitle.value = '';
|
editingTitle.value = '';
|
||||||
@@ -309,38 +234,25 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
// 输入框键盘事件
|
const handleInputKeydown = createKeyHandler({
|
||||||
const handleInputKeydown = (event: KeyboardEvent) => {
|
Enter: () => {
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
const query = inputValue.value.trim();
|
const query = inputValue.value.trim();
|
||||||
if (query) {
|
if (query && filteredItems.value.length > 0) {
|
||||||
// 如果有匹配的项目,选择第一个
|
selectItem(filteredItems.value[0]);
|
||||||
if (filteredItems.value.length > 0) {
|
|
||||||
selectItem(filteredItems.value[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (event.key === 'Escape') {
|
},
|
||||||
event.preventDefault();
|
Escape: closeMenu
|
||||||
closeMenu();
|
});
|
||||||
}
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 编辑键盘事件
|
const handleEditKeydown = createKeyHandler({
|
||||||
const handleEditKeydown = (event: KeyboardEvent) => {
|
Enter: saveEdit,
|
||||||
if (event.key === 'Enter') {
|
Escape: () => {
|
||||||
event.preventDefault();
|
|
||||||
saveEdit();
|
|
||||||
} else if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
editingId.value = null;
|
editingId.value = null;
|
||||||
editingTitle.value = '';
|
editingTitle.value = '';
|
||||||
}
|
}
|
||||||
event.stopPropagation();
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// 点击外部关闭
|
// 点击外部关闭
|
||||||
const handleClickOutside = (event: Event) => {
|
const handleClickOutside = (event: Event) => {
|
||||||
@@ -353,15 +265,18 @@ const handleClickOutside = (event: Event) => {
|
|||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleClickOutside);
|
document.addEventListener('click', handleClickOutside);
|
||||||
document.addEventListener('keydown', handleKeydown);
|
document.addEventListener('keydown', handleGlobalKeydown);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener('click', handleClickOutside);
|
document.removeEventListener('click', handleClickOutside);
|
||||||
document.removeEventListener('keydown', handleKeydown);
|
document.removeEventListener('keydown', handleGlobalKeydown);
|
||||||
// 清理定时器
|
});
|
||||||
if (errorMessageTimer.value) {
|
|
||||||
clearTimeout(errorMessageTimer.value);
|
// 监听菜单状态变化
|
||||||
|
watch(() => documentStore.showDocumentSelector, (isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
openMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -369,7 +284,7 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="document-selector">
|
<div class="document-selector">
|
||||||
<!-- 选择器按钮 -->
|
<!-- 选择器按钮 -->
|
||||||
<button class="doc-btn" @click="toggleMenu">
|
<button class="doc-btn" @click="documentStore.toggleDocumentSelector">
|
||||||
<span class="doc-icon">
|
<span class="doc-icon">
|
||||||
<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="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
|
||||||
@@ -377,21 +292,21 @@ onUnmounted(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="doc-name">{{ currentDocName }}</span>
|
<span class="doc-name">{{ currentDocName }}</span>
|
||||||
<span class="arrow" :class="{ open: showMenu }">▲</span>
|
<span class="arrow" :class="{ open: documentStore.showDocumentSelector }">▲</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- 菜单 -->
|
<!-- 菜单 -->
|
||||||
<div v-if="showMenu" class="doc-menu">
|
<div v-if="documentStore.showDocumentSelector" class="doc-menu">
|
||||||
<!-- 输入框 -->
|
<!-- 输入框 -->
|
||||||
<div class="input-box">
|
<div class="input-box">
|
||||||
<input
|
<input
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="inputValue"
|
v-model="inputValue"
|
||||||
type="text"
|
type="text"
|
||||||
class="main-input"
|
class="main-input"
|
||||||
:placeholder="t('toolbar.searchOrCreateDocument')"
|
:placeholder="t('toolbar.searchOrCreateDocument')"
|
||||||
:maxlength="MAX_TITLE_LENGTH"
|
:maxlength="MAX_TITLE_LENGTH"
|
||||||
@keydown="handleInputKeydown"
|
@keydown="handleInputKeydown"
|
||||||
/>
|
/>
|
||||||
<svg class="input-icon" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24"
|
<svg class="input-icon" 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">
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -403,14 +318,14 @@ onUnmounted(() => {
|
|||||||
<!-- 项目列表 -->
|
<!-- 项目列表 -->
|
||||||
<div class="item-list">
|
<div class="item-list">
|
||||||
<div
|
<div
|
||||||
v-for="item in filteredItems"
|
v-for="item in filteredItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
:class="{
|
:class="{
|
||||||
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
|
'active': !item.isCreateOption && documentStore.currentDocument?.id === item.id,
|
||||||
'create-item': item.isCreateOption
|
'create-item': item.isCreateOption
|
||||||
}"
|
}"
|
||||||
@click="selectItem(item)"
|
@click="selectItem(item)"
|
||||||
>
|
>
|
||||||
<!-- 创建选项 -->
|
<!-- 创建选项 -->
|
||||||
<div v-if="item.isCreateOption" class="create-option">
|
<div v-if="item.isCreateOption" class="create-option">
|
||||||
@@ -428,23 +343,23 @@ onUnmounted(() => {
|
|||||||
<div v-if="editingId !== item.id" class="doc-info">
|
<div v-if="editingId !== item.id" class="doc-info">
|
||||||
<div class="doc-title">{{ item.title }}</div>
|
<div class="doc-title">{{ item.title }}</div>
|
||||||
<!-- 根据状态显示错误信息或时间 -->
|
<!-- 根据状态显示错误信息或时间 -->
|
||||||
<div v-if="alreadyOpenDocId === item.id" class="doc-error">
|
<div v-if="documentStore.selectorError?.docId === item.id" class="doc-error">
|
||||||
{{ t('toolbar.alreadyOpenInNewWindow') }}
|
{{ documentStore.selectorError?.message }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
<div v-else class="doc-date">{{ formatTime(item.updatedAt) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 编辑状态 -->
|
<!-- 编辑状态 -->
|
||||||
<div v-else class="doc-edit">
|
<div v-else class="doc-edit">
|
||||||
<input
|
<input
|
||||||
:ref="el => editInputRef = el as HTMLInputElement"
|
:ref="el => editInputRef = el as HTMLInputElement"
|
||||||
v-model="editingTitle"
|
v-model="editingTitle"
|
||||||
type="text"
|
type="text"
|
||||||
class="edit-input"
|
class="edit-input"
|
||||||
:maxlength="MAX_TITLE_LENGTH"
|
:maxlength="MAX_TITLE_LENGTH"
|
||||||
@keydown="handleEditKeydown"
|
@keydown="handleEditKeydown"
|
||||||
@blur="saveEdit"
|
@blur="saveEdit"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -452,17 +367,17 @@ onUnmounted(() => {
|
|||||||
<div v-if="editingId !== item.id" class="doc-actions">
|
<div v-if="editingId !== item.id" class="doc-actions">
|
||||||
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
|
<!-- 只有非当前文档才显示在新窗口打开按钮 -->
|
||||||
<button
|
<button
|
||||||
v-if="documentStore.currentDocument?.id !== item.id"
|
v-if="documentStore.currentDocument?.id !== item.id"
|
||||||
class="action-btn"
|
class="action-btn"
|
||||||
@click="openInNewWindow(item, $event)"
|
@click="openInNewWindow(item, $event)"
|
||||||
:title="t('toolbar.openInNewWindow')"
|
:title="t('toolbar.openInNewWindow')"
|
||||||
>
|
>
|
||||||
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
<svg width="12" height="12" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="currentColor">
|
fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
|
d="M172.8 1017.6c-89.6 0-166.4-70.4-166.4-166.4V441.6c0-89.6 70.4-166.4 166.4-166.4h416c89.6 0 166.4 70.4 166.4 166.4v416c0 89.6-70.4 166.4-166.4 166.4l-416-6.4z m0-659.2c-51.2 0-89.6 38.4-89.6 89.6v416c0 51.2 38.4 89.6 89.6 89.6h416c51.2 0 89.6-38.4 89.6-89.6V441.6c0-51.2-38.4-89.6-89.6-89.6H172.8z"></path>
|
||||||
<path
|
<path
|
||||||
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
|
d="M851.2 19.2H435.2C339.2 19.2 268.8 96 268.8 185.6v25.6h70.4v-25.6c0-51.2 38.4-89.6 89.6-89.6h409.6c51.2 0 89.6 38.4 89.6 89.6v409.6c0 51.2-38.4 89.6-89.6 89.6h-38.4V768h51.2c96 0 166.4-76.8 166.4-166.4V185.6c0-96-76.8-166.4-166.4-166.4z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')">
|
<button class="action-btn" @click="startRename(item, $event)" :title="t('toolbar.rename')">
|
||||||
@@ -472,11 +387,11 @@ onUnmounted(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
v-if="documentStore.documentList.length > 1 && item.id !== 1"
|
||||||
class="action-btn delete-btn"
|
class="action-btn delete-btn"
|
||||||
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
|
:class="{ 'delete-confirm': deleteConfirmId === item.id }"
|
||||||
@click="handleDelete(item, $event)"
|
@click="handleDelete(item, $event)"
|
||||||
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
|
:title="deleteConfirmId === item.id ? t('toolbar.confirmDelete') : t('toolbar.delete')"
|
||||||
>
|
>
|
||||||
<svg v-if="deleteConfirmId !== item.id" xmlns="http://www.w3.org/2000/svg" width="12" height="12"
|
<svg v-if="deleteConfirmId !== item.id" 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"
|
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
@@ -709,37 +624,25 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 20px;
|
|
||||||
min-height: 20px;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--border-color);
|
background-color: var(--bg-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.delete-btn:hover {
|
&.delete-btn {
|
||||||
color: var(--text-danger);
|
&:hover {
|
||||||
}
|
color: var(--text-danger);
|
||||||
|
|
||||||
&.delete-confirm {
|
|
||||||
background-color: var(--text-danger);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
.confirm-text {
|
|
||||||
font-size: 10px;
|
|
||||||
padding: 0 4px;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&.delete-confirm {
|
||||||
background-color: var(--text-danger);
|
background-color: var(--text-danger);
|
||||||
color: white !important; // 确保确认状态下文字始终为白色
|
color: white;
|
||||||
opacity: 0.8;
|
|
||||||
|
.confirm-text {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -752,44 +655,19 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty, .loading {
|
.empty, .loading {
|
||||||
padding: 12px 8px;
|
padding: 16px 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自定义滚动条
|
|
||||||
.item-list {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background-color: var(--border-color);
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--text-muted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInOut {
|
@keyframes fadeInOut {
|
||||||
0% {
|
0% { opacity: 0; }
|
||||||
opacity: 1;
|
10% { opacity: 1; }
|
||||||
}
|
90% { opacity: 1; }
|
||||||
70% {
|
100% { opacity: 0; }
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -4,45 +4,27 @@ import {DocumentService} from '@/../bindings/voidraft/internal/services';
|
|||||||
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice';
|
||||||
import {Document} from '@/../bindings/voidraft/internal/models/models';
|
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 recentDocumentIds = ref<number[]>([DEFAULT_DOCUMENT_ID.value]);
|
|
||||||
const currentDocumentId = ref<number | null>(null);
|
const currentDocumentId = ref<number | null>(null);
|
||||||
const currentDocument = ref<Document | null>(null);
|
const currentDocument = ref<Document | null>(null);
|
||||||
|
|
||||||
// === UI状态 ===
|
// === UI状态 ===
|
||||||
const showDocumentSelector = ref(false);
|
const showDocumentSelector = ref(false);
|
||||||
|
const selectorError = ref<{ docId: number; message: string } | null>(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// === 计算属性 ===
|
// === 计算属性 ===
|
||||||
const documentList = computed(() =>
|
const documentList = computed(() =>
|
||||||
Object.values(documents.value).sort((a, b) => {
|
Object.values(documents.value).sort((a, b) => {
|
||||||
const aIndex = recentDocumentIds.value.indexOf(a.id);
|
|
||||||
const bIndex = recentDocumentIds.value.indexOf(b.id);
|
|
||||||
|
|
||||||
// 按最近使用排序
|
|
||||||
if (aIndex !== -1 && bIndex !== -1) {
|
|
||||||
return aIndex - bIndex;
|
|
||||||
}
|
|
||||||
if (aIndex !== -1) return -1;
|
|
||||||
if (bIndex !== -1) return 1;
|
|
||||||
|
|
||||||
// 然后按更新时间排序
|
|
||||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// === 私有方法 ===
|
// === 私有方法 ===
|
||||||
const addRecentDocument = (docId: number) => {
|
|
||||||
const recent = recentDocumentIds.value.filter(id => id !== docId);
|
|
||||||
recent.unshift(docId);
|
|
||||||
recentDocumentIds.value = recent.slice(0, 100); // 保留最近100个
|
|
||||||
};
|
|
||||||
|
|
||||||
const setDocuments = (docs: Document[]) => {
|
const setDocuments = (docs: Document[]) => {
|
||||||
documents.value = {};
|
documents.value = {};
|
||||||
docs.forEach(doc => {
|
docs.forEach(doc => {
|
||||||
@@ -50,7 +32,35 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// === 公共API ===
|
// === 错误处理 ===
|
||||||
|
const setError = (docId: number, message: string) => {
|
||||||
|
selectorError.value = { docId, message };
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
selectorError.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// === UI控制方法 ===
|
||||||
|
const openDocumentSelector = () => {
|
||||||
|
showDocumentSelector.value = true;
|
||||||
|
clearError();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeDocumentSelector = () => {
|
||||||
|
showDocumentSelector.value = false;
|
||||||
|
clearError();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleDocumentSelector = () => {
|
||||||
|
if (showDocumentSelector.value) {
|
||||||
|
closeDocumentSelector();
|
||||||
|
} else {
|
||||||
|
openDocumentSelector();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// === 文档操作方法 ===
|
||||||
|
|
||||||
// 在新窗口中打开文档
|
// 在新窗口中打开文档
|
||||||
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
const openDocumentInNewWindow = async (docId: number): Promise<boolean> => {
|
||||||
@@ -63,22 +73,57 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 创建新文档
|
||||||
|
const createNewDocument = async (title: string): Promise<Document | null> => {
|
||||||
|
try {
|
||||||
|
const doc = await DocumentService.CreateDocument(title);
|
||||||
|
if (doc) {
|
||||||
|
documents.value[doc.id] = doc;
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create document:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存新文档
|
||||||
|
const saveNewDocument = async (title: string, content: string): Promise<Document | null> => {
|
||||||
|
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 () => {
|
const updateDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
const docs = await DocumentService.ListAllDocumentsMeta();
|
const docs = await DocumentService.ListAllDocumentsMeta();
|
||||||
if (docs) {
|
if (docs) {
|
||||||
setDocuments(docs.filter((doc): doc is Document => doc !== null));
|
setDocuments(docs.filter((doc): doc is Document => doc !== null));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to update documents:', error);
|
console.error('Failed to update documents:', error);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开文档
|
// 打开文档
|
||||||
const openDocument = async (docId: number): Promise<boolean> => {
|
const openDocument = async (docId: number): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
closeDialog();
|
closeDocumentSelector();
|
||||||
|
|
||||||
// 获取完整文档数据
|
// 获取完整文档数据
|
||||||
const doc = await DocumentService.GetDocumentByID(docId);
|
const doc = await DocumentService.GetDocumentByID(docId);
|
||||||
@@ -88,7 +133,6 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
|
|
||||||
currentDocumentId.value = docId;
|
currentDocumentId.value = docId;
|
||||||
currentDocument.value = doc;
|
currentDocument.value = doc;
|
||||||
addRecentDocument(docId);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -97,41 +141,6 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建新文档
|
|
||||||
const createNewDocument = async (title: string): Promise<Document | null> => {
|
|
||||||
try {
|
|
||||||
const newDoc = await DocumentService.CreateDocument(title);
|
|
||||||
if (!newDoc) {
|
|
||||||
throw new Error('Failed to create document');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新文档列表
|
|
||||||
documents.value[newDoc.id] = newDoc;
|
|
||||||
|
|
||||||
return newDoc;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to create document:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存新文档
|
|
||||||
const saveNewDocument = async (title: string, content: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const newDoc = await createNewDocument(title);
|
|
||||||
if (!newDoc) return false;
|
|
||||||
|
|
||||||
// 更新内容
|
|
||||||
await DocumentService.UpdateDocumentContent(newDoc.id, content);
|
|
||||||
newDoc.content = content;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save new document:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新文档元数据
|
// 更新文档元数据
|
||||||
const updateDocumentMetadata = async (docId: number, title: string): Promise<boolean> => {
|
const updateDocumentMetadata = async (docId: number, title: string): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
@@ -168,7 +177,6 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
|
|
||||||
// 更新本地状态
|
// 更新本地状态
|
||||||
delete documents.value[docId];
|
delete documents.value[docId];
|
||||||
recentDocumentIds.value = recentDocumentIds.value.filter(id => id !== docId);
|
|
||||||
|
|
||||||
// 如果删除的是当前文档,切换到第一个可用文档
|
// 如果删除的是当前文档,切换到第一个可用文档
|
||||||
if (currentDocumentId.value === docId) {
|
if (currentDocumentId.value === docId) {
|
||||||
@@ -188,16 +196,6 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// === UI控制 ===
|
|
||||||
const openDocumentSelector = () => {
|
|
||||||
closeDialog();
|
|
||||||
showDocumentSelector.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeDialog = () => {
|
|
||||||
showDocumentSelector.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// === 初始化 ===
|
// === 初始化 ===
|
||||||
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
const initialize = async (urlDocumentId?: number): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -226,10 +224,10 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
// 状态
|
// 状态
|
||||||
documents,
|
documents,
|
||||||
documentList,
|
documentList,
|
||||||
recentDocumentIds,
|
|
||||||
currentDocumentId,
|
currentDocumentId,
|
||||||
currentDocument,
|
currentDocument,
|
||||||
showDocumentSelector,
|
showDocumentSelector,
|
||||||
|
selectorError,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
@@ -241,13 +239,16 @@ export const useDocumentStore = defineStore('document', () => {
|
|||||||
updateDocumentMetadata,
|
updateDocumentMetadata,
|
||||||
deleteDocument,
|
deleteDocument,
|
||||||
openDocumentSelector,
|
openDocumentSelector,
|
||||||
closeDialog,
|
closeDocumentSelector,
|
||||||
|
toggleDocumentSelector,
|
||||||
|
setError,
|
||||||
|
clearError,
|
||||||
initialize,
|
initialize,
|
||||||
};
|
};
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: {
|
||||||
key: 'voidraft-document',
|
key: 'voidraft-document',
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
pick: ['currentDocumentId']
|
pick: ['currentDocumentId', 'documents']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,28 +1,45 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { computed, shallowRef, type ShallowRef } from 'vue';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { ensureSyntaxTree } from '@codemirror/language';
|
import { ensureSyntaxTree } from '@codemirror/language';
|
||||||
import { LRUCache, type CacheItem, type DisposableCacheItem, createContentHash } from '@/common/cache';
|
import { LruCache, type CacheItem, type DisposableCacheItem, createHash } from '@/common/cache';
|
||||||
import { removeExtensionManagerView } from '@/views/editor/manager';
|
import { removeExtensionManagerView } from '@/views/editor/manager';
|
||||||
|
|
||||||
// 编辑器缓存项接口
|
/** 语法树缓存信息 */
|
||||||
export interface EditorCacheItem extends CacheItem, DisposableCacheItem {
|
interface SyntaxTreeCache {
|
||||||
view: EditorView;
|
readonly lastDocLength: number;
|
||||||
documentId: number;
|
readonly lastContentHash: string;
|
||||||
|
readonly lastParsed: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑器状态 */
|
||||||
|
interface EditorState {
|
||||||
content: string;
|
content: string;
|
||||||
isDirty: boolean;
|
isDirty: boolean;
|
||||||
lastModified: Date;
|
lastModified: Date;
|
||||||
autoSaveTimer: number | null;
|
|
||||||
syntaxTreeCache: {
|
|
||||||
lastDocLength: number;
|
|
||||||
lastContentHash: string;
|
|
||||||
lastParsed: Date;
|
|
||||||
} | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 编辑器缓存项 */
|
||||||
|
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', () => {
|
export const useEditorCacheStore = defineStore('editorCache', () => {
|
||||||
// 清理编辑器实例的函数
|
// === 状态 ===
|
||||||
const cleanupEditorInstance = (item: EditorCacheItem) => {
|
const containerElement: ShallowRef<HTMLElement | null> = shallowRef(null);
|
||||||
|
|
||||||
|
// === 内部方法 ===
|
||||||
|
const cleanupEditor = (item: EditorCacheItem): void => {
|
||||||
try {
|
try {
|
||||||
// 清除自动保存定时器
|
// 清除自动保存定时器
|
||||||
if (item.autoSaveTimer) {
|
if (item.autoSaveTimer) {
|
||||||
@@ -34,20 +51,15 @@ export const useEditorCacheStore = defineStore('editorCache', () => {
|
|||||||
removeExtensionManagerView(item.documentId);
|
removeExtensionManagerView(item.documentId);
|
||||||
|
|
||||||
// 移除DOM元素
|
// 移除DOM元素
|
||||||
if (item.view && item.view.dom && item.view.dom.parentElement) {
|
item.view.dom?.remove();
|
||||||
item.view.dom.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 销毁编辑器
|
// 销毁编辑器
|
||||||
if (item.view && item.view.destroy) {
|
item.view.destroy?.();
|
||||||
item.view.destroy();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error cleaning up editor instance:', error);
|
console.error(`Failed to cleanup editor ${item.documentId}:`, error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建编辑器缓存项
|
|
||||||
const createEditorCacheItem = (
|
const createEditorCacheItem = (
|
||||||
documentId: number,
|
documentId: number,
|
||||||
view: EditorView,
|
view: EditorView,
|
||||||
@@ -61,171 +73,166 @@ export const useEditorCacheStore = defineStore('editorCache', () => {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
view,
|
view,
|
||||||
documentId,
|
documentId,
|
||||||
content,
|
state: {
|
||||||
isDirty: false,
|
content,
|
||||||
lastModified: now,
|
isDirty: false,
|
||||||
|
lastModified: now
|
||||||
|
},
|
||||||
autoSaveTimer: null,
|
autoSaveTimer: null,
|
||||||
syntaxTreeCache: null,
|
syntaxTreeCache: null,
|
||||||
dispose: () => cleanupEditorInstance(item)
|
dispose: () => cleanupEditor(item)
|
||||||
};
|
};
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 编辑器缓存配置
|
const shouldRebuildSyntaxTree = (
|
||||||
const EDITOR_CACHE_CONFIG = {
|
item: EditorCacheItem,
|
||||||
maxSize: 5, // 最多缓存5个编辑器实例
|
docLength: number,
|
||||||
onEvict: (item: EditorCacheItem) => {
|
contentHash: string
|
||||||
// 清理被驱逐的编辑器实例
|
): boolean => {
|
||||||
cleanupEditorInstance(item);
|
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>(EDITOR_CACHE_CONFIG);
|
|
||||||
|
|
||||||
// 容器元素
|
|
||||||
const containerElement = ref<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
// 设置容器元素
|
// === 缓存实例 ===
|
||||||
const setContainer = (element: HTMLElement | null) => {
|
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;
|
containerElement.value = element;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取容器元素
|
const getContainer = (): HTMLElement | null => containerElement.value;
|
||||||
const getContainer = () => containerElement.value;
|
|
||||||
|
|
||||||
// 添加编辑器到缓存
|
// 基础缓存操作
|
||||||
const addEditor = (documentId: number, view: EditorView, content: string) => {
|
const addEditor = (documentId: number, view: EditorView, content: string): void => {
|
||||||
const item = createEditorCacheItem(documentId, view, content);
|
const item = createEditorCacheItem(documentId, view, content);
|
||||||
cache.set(documentId, item);
|
cache.set(documentId, item);
|
||||||
|
|
||||||
// 初始化语法树缓存
|
// 初始化语法树缓存
|
||||||
ensureSyntaxTreeCached(view, documentId);
|
buildSyntaxTree(view, item);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取编辑器实例
|
|
||||||
const getEditor = (documentId: number): EditorCacheItem | null => {
|
const getEditor = (documentId: number): EditorCacheItem | null => {
|
||||||
return cache.get(documentId);
|
return cache.get(documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查编辑器是否存在
|
|
||||||
const hasEditor = (documentId: number): boolean => {
|
const hasEditor = (documentId: number): boolean => {
|
||||||
return cache.has(documentId);
|
return cache.has(documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 移除编辑器
|
|
||||||
const removeEditor = (documentId: number): boolean => {
|
const removeEditor = (documentId: number): boolean => {
|
||||||
return cache.remove(documentId);
|
return cache.remove(documentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取所有编辑器实例
|
const clearAll = (): void => {
|
||||||
const getAllEditors = (): EditorCacheItem[] => {
|
|
||||||
return cache.getAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清空所有编辑器
|
|
||||||
const clearAll = () => {
|
|
||||||
cache.clear();
|
cache.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取缓存大小
|
// 编辑器状态管理
|
||||||
const size = (): number => {
|
const updateEditorContent = (documentId: number, content: string): boolean => {
|
||||||
return cache.size();
|
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 getStats = () => {
|
const item = cache.get(documentId);
|
||||||
return cache.getStats();
|
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 ensureSyntaxTreeCached = (view: EditorView, documentId: number): void => {
|
||||||
const item = cache.get(documentId);
|
const item = cache.get(documentId);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
const docLength = view.state.doc.length;
|
buildSyntaxTree(view, item);
|
||||||
const content = view.state.doc.toString();
|
|
||||||
const contentHash = createContentHash(content);
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
// 检查是否需要重新构建语法树
|
|
||||||
const syntaxCache = item.syntaxTreeCache;
|
|
||||||
const shouldRebuild = !syntaxCache ||
|
|
||||||
syntaxCache.lastDocLength !== docLength ||
|
|
||||||
syntaxCache.lastContentHash !== contentHash ||
|
|
||||||
(now.getTime() - syntaxCache.lastParsed.getTime()) > 30000; // 30秒过期
|
|
||||||
|
|
||||||
if (shouldRebuild) {
|
|
||||||
try {
|
|
||||||
ensureSyntaxTree(view.state, docLength, 5000);
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
item.syntaxTreeCache = {
|
|
||||||
lastDocLength: docLength,
|
|
||||||
lastContentHash: contentHash,
|
|
||||||
lastParsed: now
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to ensure syntax tree:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新编辑器内容
|
const cleanupExpiredSyntaxTrees = (): void => {
|
||||||
const updateEditorContent = (documentId: number, content: string) => {
|
const now = Date.now();
|
||||||
const item = cache.get(documentId);
|
allEditors.value.forEach(item => {
|
||||||
if (item) {
|
|
||||||
item.content = content;
|
|
||||||
item.isDirty = false;
|
|
||||||
item.lastModified = new Date();
|
|
||||||
// 清理语法树缓存,因为内容已更新
|
|
||||||
item.syntaxTreeCache = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 标记编辑器为脏状态
|
|
||||||
const markEditorDirty = (documentId: number) => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (item) {
|
|
||||||
item.isDirty = true;
|
|
||||||
item.lastModified = new Date();
|
|
||||||
// 清理语法树缓存,下次访问时重新构建
|
|
||||||
item.syntaxTreeCache = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置自动保存定时器
|
|
||||||
const setAutoSaveTimer = (documentId: number, timer: number) => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (item) {
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (item.autoSaveTimer) {
|
|
||||||
clearTimeout(item.autoSaveTimer);
|
|
||||||
}
|
|
||||||
item.autoSaveTimer = timer;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清除自动保存定时器
|
|
||||||
const clearAutoSaveTimer = (documentId: number) => {
|
|
||||||
const item = cache.get(documentId);
|
|
||||||
if (item && item.autoSaveTimer) {
|
|
||||||
clearTimeout(item.autoSaveTimer);
|
|
||||||
item.autoSaveTimer = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取脏状态的编辑器
|
|
||||||
const getDirtyEditors = (): EditorCacheItem[] => {
|
|
||||||
return cache.getAll().filter(item => item.isDirty);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清理过期的语法树缓存
|
|
||||||
const cleanupExpiredSyntaxTrees = () => {
|
|
||||||
const now = new Date();
|
|
||||||
cache.getAll().forEach(item => {
|
|
||||||
if (item.syntaxTreeCache &&
|
if (item.syntaxTreeCache &&
|
||||||
(now.getTime() - item.syntaxTreeCache.lastParsed.getTime()) > 30000) {
|
(now - item.syntaxTreeCache.lastParsed.getTime()) > CACHE_CONFIG.syntaxTreeExpireTime) {
|
||||||
item.syntaxTreeCache = null;
|
item.syntaxTreeCache = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -241,17 +248,24 @@ export const useEditorCacheStore = defineStore('editorCache', () => {
|
|||||||
getEditor,
|
getEditor,
|
||||||
hasEditor,
|
hasEditor,
|
||||||
removeEditor,
|
removeEditor,
|
||||||
getAllEditors,
|
|
||||||
clearAll,
|
clearAll,
|
||||||
size,
|
|
||||||
getStats,
|
// 编辑器状态管理
|
||||||
|
|
||||||
ensureSyntaxTreeCached,
|
|
||||||
updateEditorContent,
|
updateEditorContent,
|
||||||
markEditorDirty,
|
markEditorDirty,
|
||||||
|
|
||||||
|
// 自动保存管理
|
||||||
setAutoSaveTimer,
|
setAutoSaveTimer,
|
||||||
clearAutoSaveTimer,
|
clearAutoSaveTimer,
|
||||||
getDirtyEditors,
|
|
||||||
cleanupExpiredSyntaxTrees
|
// 语法树管理
|
||||||
|
ensureSyntaxTreeCached,
|
||||||
|
cleanupExpiredSyntaxTrees,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
cacheSize,
|
||||||
|
cacheStats,
|
||||||
|
allEditors,
|
||||||
|
dirtyEditors
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {nextTick, ref, watch} from 'vue';
|
import {computed, 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';
|
||||||
@@ -15,10 +15,15 @@ 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 {createDynamicExtensions, getExtensionManager, setExtensionManagerView, removeExtensionManagerView} from '@/views/editor/manager';
|
import {
|
||||||
|
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-operation';
|
import {AsyncOperationManager} from '@/common/async';
|
||||||
|
|
||||||
export interface DocumentStats {
|
export interface DocumentStats {
|
||||||
lines: number;
|
lines: number;
|
||||||
@@ -41,7 +46,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
characters: 0,
|
characters: 0,
|
||||||
selectedCharacters: 0
|
selectedCharacters: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// 编辑器加载状态
|
// 编辑器加载状态
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
@@ -58,7 +63,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 创建编辑器实例
|
// 创建编辑器实例
|
||||||
const createEditorInstance = async (
|
const createEditorInstance = async (
|
||||||
content: string,
|
content: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
documentId: number
|
documentId: number
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
@@ -163,8 +168,8 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 获取或创建编辑器
|
// 获取或创建编辑器
|
||||||
const getOrCreateEditor = async (
|
const getOrCreateEditor = async (
|
||||||
documentId: number,
|
documentId: number,
|
||||||
content: string,
|
content: string,
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
): Promise<EditorView> => {
|
): Promise<EditorView> => {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
@@ -180,7 +185,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 创建新的编辑器实例
|
// 创建新的编辑器实例
|
||||||
const view = await createEditorInstance(content, signal, documentId);
|
const view = await createEditorInstance(content, signal, documentId);
|
||||||
|
|
||||||
// 最终检查操作是否被取消
|
// 最终检查操作是否被取消
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
// 如果操作已取消,清理创建的实例
|
// 如果操作已取消,清理创建的实例
|
||||||
@@ -209,11 +214,11 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const container = editorCacheStore.getContainer();
|
const container = editorCacheStore.getContainer();
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
// 将目标编辑器DOM添加到容器
|
// 将目标编辑器DOM添加到容器
|
||||||
container.appendChild(instance.view.dom);
|
container.appendChild(instance.view.dom);
|
||||||
}
|
}
|
||||||
|
|
||||||
currentEditor.value = instance.view;
|
currentEditor.value = instance.view;
|
||||||
|
|
||||||
// 设置扩展管理器视图
|
// 设置扩展管理器视图
|
||||||
@@ -227,10 +232,10 @@ 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);
|
editorCacheStore.ensureSyntaxTreeCached(instance.view, documentId);
|
||||||
});
|
});
|
||||||
@@ -242,18 +247,18 @@ 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 = editorCacheStore.getEditor(documentId);
|
||||||
if (!instance || !instance.isDirty) return true;
|
if (!instance || !instance.state.isDirty) return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = instance.view.state.doc.toString();
|
const content = instance.view.state.doc.toString();
|
||||||
const lastModified = instance.lastModified;
|
const lastModified = instance.state.lastModified;
|
||||||
|
|
||||||
await DocumentService.UpdateDocumentContent(documentId, content);
|
await DocumentService.UpdateDocumentContent(documentId, content);
|
||||||
|
|
||||||
// 检查在保存期间内容是否又被修改了
|
// 检查在保存期间内容是否又被修改了
|
||||||
if (instance.lastModified === lastModified) {
|
if (instance.state.lastModified === lastModified) {
|
||||||
editorCacheStore.updateEditorContent(documentId, content);
|
editorCacheStore.updateEditorContent(documentId, content);
|
||||||
// isDirty 已在 updateEditorContent 中设置为 false
|
// isDirty 已在 updateEditorContent 中设置为 false
|
||||||
}
|
}
|
||||||
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
// 如果内容在保存期间被修改了,保持 isDirty 状态
|
||||||
|
|
||||||
@@ -310,7 +315,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const currentDocId = documentStore.currentDocumentId;
|
const currentDocId = documentStore.currentDocumentId;
|
||||||
if (currentDocId && currentDocId !== documentId) {
|
if (currentDocId && currentDocId !== documentId) {
|
||||||
await saveEditorContent(currentDocId);
|
await saveEditorContent(currentDocId);
|
||||||
|
|
||||||
// 检查操作是否被取消
|
// 检查操作是否被取消
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
throw new Error('Operation cancelled');
|
throw new Error('Operation cancelled');
|
||||||
@@ -328,7 +333,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 更新内容
|
// 更新内容
|
||||||
const instance = editorCacheStore.getEditor(documentId);
|
const instance = editorCacheStore.getEditor(documentId);
|
||||||
if (instance && instance.content !== content) {
|
if (instance && instance.state.content !== content) {
|
||||||
// 确保编辑器视图有效
|
// 确保编辑器视图有效
|
||||||
if (view && view.state && view.dispatch) {
|
if (view && view.state && view.dispatch) {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
@@ -396,7 +401,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用字体设置
|
// 应用字体设置
|
||||||
const applyFontSettings = () => {
|
const applyFontSettings = () => {
|
||||||
editorCacheStore.getAllEditors().forEach(instance => {
|
editorCacheStore.allEditors.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,
|
||||||
@@ -408,7 +413,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用主题设置
|
// 应用主题设置
|
||||||
const applyThemeSettings = () => {
|
const applyThemeSettings = () => {
|
||||||
editorCacheStore.getAllEditors().forEach(instance => {
|
editorCacheStore.allEditors.forEach(instance => {
|
||||||
updateEditorTheme(instance.view,
|
updateEditorTheme(instance.view,
|
||||||
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
themeStore.currentTheme || SystemThemeType.SystemThemeAuto
|
||||||
);
|
);
|
||||||
@@ -417,7 +422,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
|
|
||||||
// 应用Tab设置
|
// 应用Tab设置
|
||||||
const applyTabSettings = () => {
|
const applyTabSettings = () => {
|
||||||
editorCacheStore.getAllEditors().forEach(instance => {
|
editorCacheStore.allEditors.forEach(instance => {
|
||||||
updateTabConfig(
|
updateTabConfig(
|
||||||
instance.view,
|
instance.view,
|
||||||
configStore.config.editing.tabSize,
|
configStore.config.editing.tabSize,
|
||||||
@@ -431,7 +436,7 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const applyKeymapSettings = async () => {
|
const applyKeymapSettings = async () => {
|
||||||
// 确保所有编辑器实例的快捷键都更新
|
// 确保所有编辑器实例的快捷键都更新
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
editorCacheStore.getAllEditors().map(instance =>
|
editorCacheStore.allEditors.map(instance =>
|
||||||
updateKeymapExtension(instance.view)
|
updateKeymapExtension(instance.view)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -441,10 +446,10 @@ export const useEditorStore = defineStore('editor', () => {
|
|||||||
const clearAllEditors = () => {
|
const clearAllEditors = () => {
|
||||||
// 取消所有挂起的操作
|
// 取消所有挂起的操作
|
||||||
operationManager.cancelAllOperations();
|
operationManager.cancelAllOperations();
|
||||||
|
|
||||||
// 清理所有编辑器
|
// 清理所有编辑器
|
||||||
editorCacheStore.clearAll();
|
editorCacheStore.clearAll();
|
||||||
|
|
||||||
// 清除当前编辑器引用
|
// 清除当前编辑器引用
|
||||||
currentEditor.value = null;
|
currentEditor.value = null;
|
||||||
};
|
};
|
||||||
@@ -474,6 +479,34 @@ 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 && editorCacheStore.getContainer()) {
|
||||||
@@ -484,16 +517,6 @@ 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);
|
|
||||||
watch(() => themeStore.currentTheme, applyThemeSettings);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
currentEditor,
|
currentEditor,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {computed, ref} from 'vue';
|
import {computed, ref} from 'vue';
|
||||||
|
import {GetSystemInfo} from '@/../bindings/voidraft/internal/services/systemservice';
|
||||||
|
import type {SystemInfo} from '@/../bindings/voidraft/internal/services/models';
|
||||||
import * as runtime from '@wailsio/runtime';
|
import * as runtime from '@wailsio/runtime';
|
||||||
|
|
||||||
export interface SystemEnvironment {
|
export interface SystemEnvironment {
|
||||||
@@ -19,7 +21,7 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
// 状态
|
// 状态
|
||||||
const environment = ref<SystemEnvironment | null>(null);
|
const environment = ref<SystemEnvironment | null>(null);
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
// 窗口置顶状态管理
|
// 窗口置顶状态管理
|
||||||
const isWindowOnTop = ref<boolean>(false);
|
const isWindowOnTop = ref<boolean>(false);
|
||||||
|
|
||||||
@@ -42,7 +44,24 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
environment.value = await runtime.System.Environment();
|
const systemInfo: SystemInfo | null = await GetSystemInfo();
|
||||||
|
|
||||||
|
if (systemInfo) {
|
||||||
|
environment.value = {
|
||||||
|
OS: systemInfo.os,
|
||||||
|
Arch: systemInfo.arch,
|
||||||
|
Debug: systemInfo.debug,
|
||||||
|
OSInfo: {
|
||||||
|
Name: systemInfo.osInfo?.name || '',
|
||||||
|
Branding: systemInfo.osInfo?.branding || '',
|
||||||
|
Version: systemInfo.osInfo?.version || '',
|
||||||
|
ID: systemInfo.osInfo?.id || '',
|
||||||
|
},
|
||||||
|
PlatformInfo: systemInfo.platformInfo || {},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
environment.value = null;
|
||||||
|
}
|
||||||
} catch (_err) {
|
} catch (_err) {
|
||||||
environment.value = null;
|
environment.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -94,4 +113,4 @@ export const useSystemStore = defineStore('system', () => {
|
|||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
pick: ['isWindowOnTop']
|
pick: ['isWindowOnTop']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
253
frontend/src/stores/tabStore.ts
Normal file
253
frontend/src/stores/tabStore.ts
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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']
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -266,3 +266,8 @@ func (sm *ServiceManager) GetBadgeService() *badge.BadgeService {
|
|||||||
func (sm *ServiceManager) GetNotificationService() *notifications.NotificationService {
|
func (sm *ServiceManager) GetNotificationService() *notifications.NotificationService {
|
||||||
return sm.notificationService
|
return sm.notificationService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSystemService 获取系统服务实例
|
||||||
|
func (sm *ServiceManager) GetSystemService() *SystemService {
|
||||||
|
return sm.systemService
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
"github.com/wailsapp/wails/v3/pkg/services/log"
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SystemService 系统监控服务
|
// SystemService 系统监控服务
|
||||||
type SystemService struct {
|
type SystemService struct {
|
||||||
logger *log.LogService
|
logger *log.LogService
|
||||||
|
app *application.App
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryStats 内存统计信息
|
// MemoryStats 内存统计信息
|
||||||
@@ -28,6 +30,23 @@ type MemoryStats struct {
|
|||||||
NumGoroutine int `json:"numGoroutine"`
|
NumGoroutine int `json:"numGoroutine"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemInfo 系统信息
|
||||||
|
type SystemInfo struct {
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Debug bool `json:"debug"`
|
||||||
|
OSInfo *OSInfo `json:"osInfo"`
|
||||||
|
PlatformInfo map[string]interface{} `json:"platformInfo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSInfo 操作系统信息
|
||||||
|
type OSInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Branding string `json:"branding"`
|
||||||
|
}
|
||||||
|
|
||||||
// NewSystemService 创建新的系统服务实例
|
// NewSystemService 创建新的系统服务实例
|
||||||
func NewSystemService(logger *log.LogService) *SystemService {
|
func NewSystemService(logger *log.LogService) *SystemService {
|
||||||
return &SystemService{
|
return &SystemService{
|
||||||
@@ -35,6 +54,11 @@ func NewSystemService(logger *log.LogService) *SystemService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAppReferences 设置应用引用
|
||||||
|
func (ss *SystemService) SetAppReferences(app *application.App) {
|
||||||
|
ss.app = app
|
||||||
|
}
|
||||||
|
|
||||||
// GetMemoryStats 获取当前内存统计信息
|
// GetMemoryStats 获取当前内存统计信息
|
||||||
func (ss *SystemService) GetMemoryStats() MemoryStats {
|
func (ss *SystemService) GetMemoryStats() MemoryStats {
|
||||||
var m runtime.MemStats
|
var m runtime.MemStats
|
||||||
@@ -50,6 +74,34 @@ func (ss *SystemService) GetMemoryStats() MemoryStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSystemInfo 获取系统环境信息
|
||||||
|
func (ss *SystemService) GetSystemInfo() (*SystemInfo, error) {
|
||||||
|
if ss.app == nil {
|
||||||
|
return nil, fmt.Errorf("app reference not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
envInfo := ss.app.Env.Info()
|
||||||
|
|
||||||
|
systemInfo := &SystemInfo{
|
||||||
|
OS: envInfo.OS,
|
||||||
|
Arch: envInfo.Arch,
|
||||||
|
Debug: envInfo.Debug,
|
||||||
|
PlatformInfo: envInfo.PlatformInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换OSInfo
|
||||||
|
if envInfo.OSInfo != nil {
|
||||||
|
systemInfo.OSInfo = &OSInfo{
|
||||||
|
ID: envInfo.OSInfo.ID,
|
||||||
|
Name: envInfo.OSInfo.Name,
|
||||||
|
Version: envInfo.OSInfo.Version,
|
||||||
|
Branding: envInfo.OSInfo.Branding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FormatBytes 格式化字节数为人类可读的格式
|
// FormatBytes 格式化字节数为人类可读的格式
|
||||||
func (ss *SystemService) FormatBytes(bytes uint64) string {
|
func (ss *SystemService) FormatBytes(bytes uint64) string {
|
||||||
const unit = 1024
|
const unit = 1024
|
||||||
|
|||||||
5
main.go
5
main.go
@@ -64,7 +64,6 @@ func main() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create a new window with the necessary options.
|
// Create a new window with the necessary options.
|
||||||
// 'Title' is the title of the window.
|
// 'Title' is the title of the window.
|
||||||
// 'Mac' options tailor the window when running on macOS.
|
// 'Mac' options tailor the window when running on macOS.
|
||||||
@@ -92,6 +91,10 @@ func main() {
|
|||||||
mainWindow.Center()
|
mainWindow.Center()
|
||||||
window = mainWindow
|
window = mainWindow
|
||||||
|
|
||||||
|
// 获取系统服务并设置应用引用
|
||||||
|
systemService := serviceManager.GetSystemService()
|
||||||
|
systemService.SetAppReferences(app)
|
||||||
|
|
||||||
// 获取托盘服务并设置应用引用
|
// 获取托盘服务并设置应用引用
|
||||||
trayService := serviceManager.GetTrayService()
|
trayService := serviceManager.GetTrayService()
|
||||||
trayService.SetAppReferences(app, mainWindow)
|
trayService.SetAppReferences(app, mainWindow)
|
||||||
|
|||||||
Reference in New Issue
Block a user