镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-20 13:47:47 +08:00
feat(usage-stats): 添加平台使用统计功能
添加 usage-stats 模块用于记录各平台操作成功和失败次数 在文件选择面板执行操作后更新使用统计数据 添加相关单元测试确保功能正确性
这个提交包含在:
父节点
9209ee2d8a
当前提交
104407fe6b
@ -9,6 +9,7 @@ import { BatchResults, ProgressEvent } from '../../types/core';
|
|||||||
import { I18nService } from '../../utils/i18n';
|
import { I18nService } from '../../utils/i18n';
|
||||||
import { parseFileName } from '../../utils/helpers';
|
import { parseFileName } from '../../utils/helpers';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { recordUsageStatsDelta } from '../../utils/usage-stats';
|
||||||
import './config-panel';
|
import './config-panel';
|
||||||
import './file-list-panel';
|
import './file-list-panel';
|
||||||
import './preview-panel';
|
import './preview-panel';
|
||||||
@ -490,6 +491,11 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
this.executionResults = results;
|
this.executionResults = results;
|
||||||
this.executorState = executor.getState();
|
this.executorState = executor.getState();
|
||||||
|
|
||||||
|
await recordUsageStatsDelta(this.adapter.platform, {
|
||||||
|
success: results.success.length,
|
||||||
|
failed: results.failed.length,
|
||||||
|
});
|
||||||
|
|
||||||
this.applyExecutionResults(results);
|
this.applyExecutionResults(results);
|
||||||
void this.syncAfterRename();
|
void this.syncAfterRename();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -697,6 +703,11 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
this.executionResults = results;
|
this.executionResults = results;
|
||||||
this.executorState = executor.getState();
|
this.executorState = executor.getState();
|
||||||
|
|
||||||
|
await recordUsageStatsDelta(this.adapter.platform, {
|
||||||
|
success: results.success.length,
|
||||||
|
failed: results.failed.length,
|
||||||
|
});
|
||||||
|
|
||||||
this.applyExecutionResults(results);
|
this.applyExecutionResults(results);
|
||||||
this.updateSummaryProgress();
|
this.updateSummaryProgress();
|
||||||
void this.syncAfterRename();
|
void this.syncAfterRename();
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
* 1. Edit version in package.json
|
* 1. Edit version in package.json
|
||||||
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
|
* 2. Run: npm run sync:version (or automatically via prebuild/predev)
|
||||||
*
|
*
|
||||||
* Generated at: 2025-12-29T10:57:35.368Z
|
* Generated at: 2025-12-29T11:20:31.860Z
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export class StorageManager {
|
|||||||
async get<T>(key: string): Promise<T | null> {
|
async get<T>(key: string): Promise<T | null> {
|
||||||
try {
|
try {
|
||||||
const result = await chrome.storage.local.get(key);
|
const result = await chrome.storage.local.get(key);
|
||||||
return result[key] || null;
|
return (result as Record<string, T | undefined>)[key] ?? null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[CDR] Failed to read from chrome.storage:', error);
|
console.error('[CDR] Failed to read from chrome.storage:', error);
|
||||||
// 降级到localStorage
|
// 降级到localStorage
|
||||||
|
|||||||
44
src/utils/usage-stats.ts
普通文件
44
src/utils/usage-stats.ts
普通文件
@ -0,0 +1,44 @@
|
|||||||
|
import type { PlatformName } from '../types/platform';
|
||||||
|
import type { PlatformUsageStats } from '../types/stats';
|
||||||
|
import { STORAGE_KEYS } from '../types/stats';
|
||||||
|
import { logger } from './logger';
|
||||||
|
import { storage } from './storage';
|
||||||
|
|
||||||
|
function getUsageStatsKey(platform: PlatformName): string {
|
||||||
|
return STORAGE_KEYS.USAGE_STATS_PREFIX + platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDelta(value: number): number {
|
||||||
|
if (!Number.isFinite(value)) return 0;
|
||||||
|
return Math.max(0, Math.trunc(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function recordUsageStatsDelta(
|
||||||
|
platform: PlatformName,
|
||||||
|
delta: { success: number; failed: number }
|
||||||
|
): Promise<void> {
|
||||||
|
const successDelta = normalizeDelta(delta.success);
|
||||||
|
const failedDelta = normalizeDelta(delta.failed);
|
||||||
|
|
||||||
|
if (successDelta === 0 && failedDelta === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageKey = getUsageStatsKey(platform);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const current = await storage.get<PlatformUsageStats>(storageKey);
|
||||||
|
|
||||||
|
const next: PlatformUsageStats = {
|
||||||
|
platform,
|
||||||
|
successCount: (current?.successCount ?? 0) + successDelta,
|
||||||
|
failedCount: (current?.failedCount ?? 0) + failedDelta,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await storage.set(storageKey, next);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[UsageStats] Failed to update stats for ${platform}`, error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -111,6 +111,20 @@ describe('StorageManager', () => {
|
|||||||
expect(result).toEqual(value);
|
expect(result).toEqual(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('应该正确读取 falsy 值 (false/0/空字符串)', async () => {
|
||||||
|
const cases = [
|
||||||
|
{ key: 'bool-false', value: false },
|
||||||
|
{ key: 'number-zero', value: 0 },
|
||||||
|
{ key: 'empty-string', value: '' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const { key, value } of cases) {
|
||||||
|
mockChromeStorage.get.mockResolvedValueOnce({ [key]: value });
|
||||||
|
const result = await storageManager.get<any>(key);
|
||||||
|
expect(result).toBe(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('应该在数据不存在时返回null', async () => {
|
it('应该在数据不存在时返回null', async () => {
|
||||||
const key = 'non-existent-key';
|
const key = 'non-existent-key';
|
||||||
|
|
||||||
|
|||||||
83
tests/unit/usage-stats.test.ts
普通文件
83
tests/unit/usage-stats.test.ts
普通文件
@ -0,0 +1,83 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import { recordUsageStatsDelta } from '../../src/utils/usage-stats';
|
||||||
|
import { STORAGE_KEYS } from '../../src/types/stats';
|
||||||
|
|
||||||
|
describe('Usage stats', () => {
|
||||||
|
const platform = 'quark' as const;
|
||||||
|
const storageKey = STORAGE_KEYS.USAGE_STATS_PREFIX + platform;
|
||||||
|
|
||||||
|
let mockChromeStorage: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockChromeStorage = {
|
||||||
|
get: vi.fn(() => Promise.resolve({})),
|
||||||
|
set: vi.fn(() => Promise.resolve()),
|
||||||
|
};
|
||||||
|
|
||||||
|
global.chrome = {
|
||||||
|
storage: {
|
||||||
|
local: mockChromeStorage,
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
global.localStorage = {
|
||||||
|
getItem: vi.fn(),
|
||||||
|
setItem: vi.fn(),
|
||||||
|
removeItem: vi.fn(),
|
||||||
|
clear: vi.fn(),
|
||||||
|
key: vi.fn(),
|
||||||
|
length: 0,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
vi.spyOn(Date, 'now').mockReturnValue(1700000000000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create stats for a platform when missing', async () => {
|
||||||
|
mockChromeStorage.get.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
await recordUsageStatsDelta(platform, { success: 2, failed: 1 });
|
||||||
|
|
||||||
|
expect(mockChromeStorage.get).toHaveBeenCalledWith(storageKey);
|
||||||
|
expect(mockChromeStorage.set).toHaveBeenCalledWith({
|
||||||
|
[storageKey]: {
|
||||||
|
platform,
|
||||||
|
successCount: 2,
|
||||||
|
failedCount: 1,
|
||||||
|
lastUpdated: 1700000000000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accumulate stats when existing value present', async () => {
|
||||||
|
mockChromeStorage.get.mockResolvedValueOnce({
|
||||||
|
[storageKey]: {
|
||||||
|
platform,
|
||||||
|
successCount: 5,
|
||||||
|
failedCount: 2,
|
||||||
|
lastUpdated: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await recordUsageStatsDelta(platform, { success: 3, failed: 4 });
|
||||||
|
|
||||||
|
expect(mockChromeStorage.set).toHaveBeenCalledWith({
|
||||||
|
[storageKey]: {
|
||||||
|
platform,
|
||||||
|
successCount: 8,
|
||||||
|
failedCount: 6,
|
||||||
|
lastUpdated: 1700000000000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should no-op when delta is zero', async () => {
|
||||||
|
await recordUsageStatsDelta(platform, { success: 0, failed: 0 });
|
||||||
|
expect(mockChromeStorage.get).not.toHaveBeenCalled();
|
||||||
|
expect(mockChromeStorage.set).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户