Develop (#9)
* feat: optimize popup standalone window layout and enhance storage cleaner synchronization * docs: 更新README文档并删除过时文件 - 更新README文档,添加项目结构、功能特性和路由系统等详细信息 - 删除不再使用的文档文件,包括CLAUDE.md、GEMINI.md和多个设计规范文档 - 清理项目中的过时配置文件和计划文档 * feat: 添加 Vitest 测试框架和组件测试 - 添加 Vitest 配置 (vitest.config.ts, vitest.setup.ts) - 创建组件测试: Button, ToolCard, GlobalSnackbar, TopBar, RouterContainer, StorageCleanerConfirm - 创建工具测试: routes, storageCleaner - 修复 background.ts 监听器参数问题 - 修复 options/App.tsx 硬编码默认值 - 更新 lint-staged.config.mjs (添加 .mjs 支持, 添加 --no-warn-ignored) - 更新 tsconfig.json (添加测试类型支持, 移除测试文件排除) - 更新 package.json (添加测试脚本和依赖) * fix: 修复 StorageCleanerPage Chrome API 监听器内存泄漏 使用 useRef 模式存储 loadInfo 函数引用,避免依赖数组变化导致的监听器重复注册问题 * refactor(popup): 优化 OpenUrl 页面样式和导航逻辑 重构 OpenUrl 页面输入框样式,改进聚焦状态效果 移除 RouterProvider 依赖,直接通过存储设置侧边栏路由 在 OpenUrlViewer 页面添加加载状态指示器和错误处理 监听存储变化实现 URL 自动更新 * feat(ui): 优化存储清理页面UI和交互效果 重构存储清理页面组件,增强视觉层次和交互体验: - 使用新的错误提示样式和布局 - 改进选项卡片样式,增加悬停动画和选中状态 - 调整整体间距和排版,提升视觉一致性 - 添加微交互效果如悬停缩放和阴影 - 优化颜色方案和过渡动画 - 统一组件尺寸和字体层级 * feat: 添加二维码工具页面,支持URL转二维码和二维码解析功能 * chore: update package-lock.json (npm audit fix) * refactor(主题): 将页面样式抽离到统一配置文件 将各页面的颜色和样式配置抽离到config/pageTheme.ts中统一管理 优化测试用例中使用each替代forEach 更新路由测试以包含新的qrCode页面 * feat(二维码页面): 添加复制二维码功能并优化样式 添加复制二维码到剪贴板的功能,并调整按钮布局和样式。同时将 ContentCopyIcon 导入位置调整到其他图标导入之后,并修复缩进问题。在 tsconfig.json 中添加 vitest/globals 类型支持。 * feat(theme): 为所有页面添加统一的背景色和卡片背景色 为应用中的所有页面添加了统一的浅灰色背景(#f5f5f5)和白色卡片背景(#ffffff),以保持视觉一致性。修改了ToolCard组件以支持自定义卡片背景色,并更新了所有相关页面使用新的主题配置。 * feat: 添加复制按钮组件并优化现有复制功能 refactor(utils): 创建剪贴板工具函数 feat(components): 新增可复用的CopyButton组件 refactor(pages): 在QrCodePage和TimestampPage中使用CopyButton style: 格式化代码并调整部分样式 * refactor(存储): 统一qrCode相关存储键名 将'qrCode/expanded'重命名为'qrCode/qrExpanded'以保持命名一致性 * feat: 添加二维码工具功能并更新项目配置 - 新增二维码工具页面及相关组件和工具函数 - 添加 MIT 许可证文件 - 更新 package.json 配置为公开项目 - 更新 README 文档说明新功能
This commit is contained in:
@@ -1,880 +0,0 @@
|
||||
# Storage Cleaner Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a storage cleaner feature to the browser extension popup that allows users to clear localStorage, sessionStorage, IndexedDB, Cookies, Cache Storage, andress Workers for the current page.
|
||||
|
||||
**Architecture:** Add new StorageCleanerPage component with tab switching in the popup, using chrome.cookies API for cookies and script injection for other storage types. User preferences are persisted using Chrome Storage.
|
||||
|
||||
**Tech Stack:** React 19 + TypeScript, Material UI, Chrome Extension APIs
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
entrypoints/popup/
|
||||
├── App.tsx (modify: add tab switching)
|
||||
└── pages/
|
||||
├── TimestampPage.tsx (no change)
|
||||
└── StorageCleanerPage.tsx (create: new storage cleaner page)
|
||||
|
||||
types/
|
||||
└── storage.d.ts (modify: add storage cleaner types)
|
||||
|
||||
utils/
|
||||
└── storageCleaner.ts (create: storage cleaning utilities)
|
||||
|
||||
wxt.config.ts (modify: add cookies permission)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Add TypeScript Types for Storage Cleaner
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `types/storage.d.ts`
|
||||
|
||||
- [ ] **Step 1: Add storage cleaner types to StorageSchema and interfaces**
|
||||
|
||||
```typescript
|
||||
export interface StorageSchema {
|
||||
'app/lastRoute': string;
|
||||
'app/theme': string;
|
||||
'storageCleaner/preferences': StorageCleanerPreferences;
|
||||
}
|
||||
|
||||
export interface StorageCleanerPreferences {
|
||||
autoRefresh: boolean;
|
||||
selectedTypes: StorageCleanerOptions;
|
||||
}
|
||||
|
||||
export interface StorageCleanerOptions {
|
||||
localStorage: boolean;
|
||||
sessionStorage: boolean;
|
||||
indexedDB: boolean;
|
||||
cookies: boolean;
|
||||
cacheStorage: boolean;
|
||||
serviceWorkers: boolean;
|
||||
}
|
||||
|
||||
export type StorageCleanResult =
|
||||
| {
|
||||
success: true;
|
||||
count: number;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export interface CleaningResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
localStorage?: StorageCleanResult;
|
||||
sessionStorage?: StorageCleanResult;
|
||||
indexedDB?: StorageCleanResult;
|
||||
cookies?: StorageCleanResult;
|
||||
cacheStorage?: StorageCleanResult;
|
||||
serviceWorkers?: StorageCleanResult;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit TypeScript types**
|
||||
|
||||
```bash
|
||||
git add types/storage.d.ts
|
||||
git commit -m "feat: add TypeScript types for storage cleaner"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Add Cookies Permission to Manifest
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `wxt.config.ts:10-18`
|
||||
|
||||
- [ ] **Step 1: Add 'cookies' permission to manifest**
|
||||
|
||||
```typescript
|
||||
permissions: [
|
||||
'storage',
|
||||
'unlimitedStorage',
|
||||
'clipboardWrite',
|
||||
'activeTab',
|
||||
'scripting',
|
||||
'tabs',
|
||||
'debugger',
|
||||
'cookies', // Add this line
|
||||
],
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Test build to ensure manifest is valid**
|
||||
|
||||
Run: `npm run compile`
|
||||
Expected: No TypeScript errors
|
||||
|
||||
- [ ] **Step 3: Commit manifest changes**
|
||||
|
||||
```bash
|
||||
git add wxt.config.ts
|
||||
git commit -m "feat: add cookies permission to manifest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Create Storage Cleaning Utilities
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `utils/storageCleaner.ts`
|
||||
|
||||
- [ ] **Step 1: Create storage cleaning utility file with helper functions**
|
||||
|
||||
```typescript
|
||||
import type { StorageCleanerOptions, CleaningResult, StorageCleanResult } from 'types/storage';
|
||||
|
||||
const RESTRICTED_PROTOCOLS = [
|
||||
'chrome:',
|
||||
'chrome-extension:',
|
||||
'about:',
|
||||
'edge:',
|
||||
'view-source:',
|
||||
'file:',
|
||||
'data:',
|
||||
] as const;
|
||||
|
||||
export async function getCurrentTab() {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
return tab;
|
||||
}
|
||||
|
||||
export function isRestrictedUrl(url?: string): boolean {
|
||||
if (!url) return true;
|
||||
return RESTRICTED_PROTOCOLS.some((p) => url.startsWith(p));
|
||||
}
|
||||
|
||||
export async function clearCookies(url: string): Promise<StorageCleanResult> {
|
||||
try {
|
||||
const cookies = await chrome.cookies.getAll({ url });
|
||||
for (const cookie of cookies) {
|
||||
await chrome.cookies.remove({
|
||||
url,
|
||||
name: cookie.name,
|
||||
storeId: cookie.storeId,
|
||||
});
|
||||
}
|
||||
return { success: true, count: cookies.length };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function injectClearLocalStorage(tabId: number): Promise<StorageCleanResult> {
|
||||
try {
|
||||
const result = await chrome.scripting.executeScript<{ count: number }>({
|
||||
target: { tabId },
|
||||
func: () => {
|
||||
const count = localStorage.length;
|
||||
localStorage.clear();
|
||||
return { count };
|
||||
},
|
||||
});
|
||||
if (result.result) {
|
||||
return { success: true, count: result.result.count };
|
||||
}
|
||||
return { success: false, error: 'No result returned' };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function injectClearSessionStorage(tabId: number): Promise<StorageCleanResult> {
|
||||
try {
|
||||
const result = await chrome.scripting.executeScript<{ count: number }>({
|
||||
target: { tabId },
|
||||
func: () => {
|
||||
const count = sessionStorage.length;
|
||||
sessionStorage.clear();
|
||||
return { count };
|
||||
},
|
||||
});
|
||||
if (result.result) {
|
||||
return { success: true, count: result.result.count };
|
||||
}
|
||||
return { success: false, error: 'No result returned' };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function injectClearIndexedDB(tabId: number): Promise<StorageCleanResult> {
|
||||
try {
|
||||
const result = await chrome.scripting.executeScript<{ count: number } | { error: string }>({
|
||||
target: { tabId },
|
||||
func: () => {
|
||||
if (typeof indexedDB.databases === 'function') {
|
||||
return indexedDB.databases().then(async (databases) => {
|
||||
let count = 0;
|
||||
for (const db of databases) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const deleteReq = indexedDB.deleteDatabase(db.name);
|
||||
deleteReq.onblocked = () => {
|
||||
console.warn('IndexedDB delete blocked:', db.name);
|
||||
};
|
||||
deleteReq.onsuccess = () => resolve();
|
||||
deleteReq.onerror = () => reject();
|
||||
});
|
||||
count++;
|
||||
}
|
||||
return { count };
|
||||
});
|
||||
}
|
||||
return { error: 'databases_api_unavailable' };
|
||||
},
|
||||
});
|
||||
if (result.result) {
|
||||
if ('error' in result.result) {
|
||||
return { success: false, error: result.result.error };
|
||||
}
|
||||
return { success: true, count: result.result.count };
|
||||
}
|
||||
return { success: false, error: 'No result returned' };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function injectClearCacheStorage(tabId: number): Promise<StorageCleanResult> {
|
||||
try {
|
||||
const result = await chrome.scripting.executeScript<{ count: number }>({
|
||||
target: { tabId },
|
||||
func: async () => {
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
for (const name of cacheNames) {
|
||||
await caches.delete(name);
|
||||
}
|
||||
return { count: cacheNames.length };
|
||||
}
|
||||
return { count: 0 };
|
||||
},
|
||||
});
|
||||
if (result.result) {
|
||||
return { success: true, count: result.result.count };
|
||||
}
|
||||
return { success: false, error: 'No result returned' };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function injectUnregisterServiceWorkers(tabId: number): Promise<StorageCleanResult> {
|
||||
try {
|
||||
const result = await chrome.scripting.executeScript<{ count: number }>({
|
||||
target: { tabId },
|
||||
func: async () => {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (const registration of registrations) {
|
||||
await registration.unregister();
|
||||
}
|
||||
return { count: registrations.length };
|
||||
}
|
||||
return { count: 0 };
|
||||
},
|
||||
});
|
||||
if (result.result) {
|
||||
return { success: true, count: result.result.count };
|
||||
}
|
||||
return { success: false, error: 'No result returned' };
|
||||
} catch (error) {
|
||||
return { success: false, error: String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearStorage(
|
||||
tabId: number,
|
||||
url: string,
|
||||
options: StorageCleanerOptions,
|
||||
): Promise<CleaningResult> {
|
||||
const result: CleaningResult = { success: true };
|
||||
|
||||
if (options.localStorage) {
|
||||
result.localStorage = await injectClearLocalStorage(tabId);
|
||||
}
|
||||
|
||||
if (options.sessionStorage) {
|
||||
result.sessionStorage = await injectClearSessionStorage(tabId);
|
||||
}
|
||||
|
||||
if (options.indexedDB) {
|
||||
result.indexedDB = await injectClearIndexedDB(tabId);
|
||||
}
|
||||
|
||||
if (options.cookies) {
|
||||
result.cookies = await clearCookies(url);
|
||||
}
|
||||
|
||||
if (options.cacheStorage) {
|
||||
result.cacheStorage = await injectClearCacheStorage(tabId);
|
||||
}
|
||||
|
||||
if (options.serviceWorkers) {
|
||||
result.serviceWorkers = await injectUnregisterServiceWorkers(tabId);
|
||||
}
|
||||
|
||||
// Check if any operation failed
|
||||
const failures = Object.values(result).filter(
|
||||
(r): r is StorageCleanResult => r?.success === false,
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
result.success = false;
|
||||
result.error = '部分清理失败';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function formatCleaningResult(result: CleaningResult): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (result.localStorage?.success) {
|
||||
parts.push(`${result.localStorage.count} 个 localStorage`);
|
||||
}
|
||||
if (result.sessionStorage?.success) {
|
||||
parts.push(`${result.sessionStorage.count} 个 sessionStorage`);
|
||||
}
|
||||
if (result.indexedDB?.success) {
|
||||
parts.push(`${result.indexedDB.count} 个 IndexedDB`);
|
||||
}
|
||||
if (result.cookies?.success) {
|
||||
parts.push(`${result.cookies.count} 个 Cookies`);
|
||||
}
|
||||
if (result.cacheStorage?.success) {
|
||||
parts.push(`${result.cacheStorage.count} 个 Cache`);
|
||||
}
|
||||
if (result.serviceWorkers?.success) {
|
||||
parts.push(`${result.serviceWorkers.count} 个 Service Workers`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return '该页面没有可清理的存储数据';
|
||||
}
|
||||
|
||||
return `清理了 ${parts.join(', ')}`;
|
||||
}
|
||||
|
||||
export function isEmptyResult(result: CleaningResult): boolean {
|
||||
const values = Object.values(result).filter(
|
||||
(r): r is StorageCleanResult => r?.success === true && r.count > 0,
|
||||
);
|
||||
return values.length === 0;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit storage cleaning utilities**
|
||||
|
||||
```bash
|
||||
git add utils/storageCleaner.ts
|
||||
git commit -m "feat: add storage cleaning utility functions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Create StorageCleanerPage Component
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `entrypoints/popup/pages/StorageCleanerPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Create StorageCleanerPage component with UI and logic**
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Checkbox,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
Alert,
|
||||
Snackbar,
|
||||
} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import type { StorageCleanerOptions, CleaningResult, StorageCleanerPreferences } from 'types/storage';
|
||||
import {
|
||||
getCurrentTab,
|
||||
isRestrictedUrl,
|
||||
clearStorage,
|
||||
formatCleaningResult,
|
||||
isEmptyResult,
|
||||
} from '@/utils/storageCleaner';
|
||||
|
||||
const DEFAULT_OPTIONS: StorageCleanerOptions = {
|
||||
localStorage: true,
|
||||
sessionStorage: true,
|
||||
indexedDB: true,
|
||||
cookies: true,
|
||||
cacheStorage: true,
|
||||
serviceWorkers: true,
|
||||
};
|
||||
|
||||
const DEFAULT_PREFERENCES: StorageCleanerPreferences = {
|
||||
autoRefresh: true,
|
||||
selectedTypes: DEFAULT_OPTIONS,
|
||||
};
|
||||
|
||||
export default function StorageCleanerPage() {
|
||||
const [domain, setDomain] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [options, setOptions] = useState<StorageCleanerOptions>(DEFAULT_OPTIONS);
|
||||
const [autoRefresh, setAutoRefresh] = useState<boolean>(true);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [result, setResult] = useState<CleaningResult | null>(null);
|
||||
const [showConfirm, setShowConfirm] = useState<boolean>(false);
|
||||
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string }>({
|
||||
open: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
// Load tab info and user preferences
|
||||
useEffect(() => {
|
||||
const loadInfo = async () => {
|
||||
const tab = await getCurrentTab();
|
||||
|
||||
if (!tab || !tab.url) {
|
||||
setError('无法获取当前标签页');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRestrictedUrl(tab.url)) {
|
||||
setError('存储清理功能不支持此页面');
|
||||
return;
|
||||
}
|
||||
|
||||
setDomain(new URL(tab.url).hostname);
|
||||
|
||||
// Load user preferences
|
||||
const prefs = await storageUtil.get(
|
||||
'storageCleaner/preferences',
|
||||
DEFAULT_PREFERENCES,
|
||||
);
|
||||
setAutoRefresh(prefs.autoRefresh);
|
||||
setOptions(prefs.selectedTypes);
|
||||
};
|
||||
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
const handleOptionChange = useCallback((key: keyof StorageCleanerOptions) => {
|
||||
setOptions((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
}, []);
|
||||
|
||||
const handleClean = useCallback(async () => {
|
||||
const tab = await getCurrentTab();
|
||||
|
||||
if (!tab || !tab.id || !tab.url) {
|
||||
setSnackbar({ open: true, message: '无法获取当前标签页' });
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const cleaningResult = await clearStorage(tab.id, tab.url, options);
|
||||
setResult(cleaningResult);
|
||||
|
||||
// Save user preferences
|
||||
await storageUtil.set('storageCleaner/preferences', {
|
||||
autoRefresh,
|
||||
selectedTypes: options,
|
||||
});
|
||||
|
||||
// Auto refresh if enabled
|
||||
if (autoRefresh && cleaningResult.success) {
|
||||
setSnackbar({ open: true, message: '页面即将刷新,Popup 将关闭' });
|
||||
setTimeout(() => {
|
||||
chrome.tabs.reload(tab.id);
|
||||
}, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
setSnackbar({ open: true, message: `清理失败: ${String(err)}` });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowConfirm(false);
|
||||
}
|
||||
}, [options, autoRefresh]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
const tab = await getCurrentTab();
|
||||
if (tab?.id) {
|
||||
setSnackbar({ open: true, message: '页面即将刷新,Popup 将关闭' });
|
||||
setTimeout(() => {
|
||||
chrome.tabs.reload(tab.id);
|
||||
}, 1500);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Paper sx={{ p: 2, m: 1, borderRadius: 2 }}>
|
||||
<Alert severity="error" icon={<WarningIcon />}>
|
||||
{error}
|
||||
</Alert>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2, m: 1, borderRadius: 2 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ textAlign: 'center', mb: 2 }}>
|
||||
<Typography variant="h5" component="h1" sx={{ mb: 1 }}>
|
||||
存储清理
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
当前页面: {domain || '加载中...'}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Storage Type Options */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="subtitle1" sx={{ mb: 1 }}>
|
||||
选择要清理的存储类型:
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={options.localStorage}
|
||||
onChange={() => handleOptionChange('localStorage')}
|
||||
/>
|
||||
}
|
||||
label="localStorage"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={options.sessionStorage}
|
||||
onChange={() => handleOptionChange('sessionStorage')}
|
||||
/>
|
||||
}
|
||||
label="sessionStorage"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={options.indexedDB}
|
||||
onChange={() => handleOptionChange('indexedDB')}
|
||||
/>
|
||||
}
|
||||
label="IndexedDB"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={options.cookies}
|
||||
onChange={() => handleOptionChange('cookies')}
|
||||
/>
|
||||
}
|
||||
label="Cookies"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={options.cacheStorage}
|
||||
onChange={() => handleOptionChange('cacheStorage')}
|
||||
/>
|
||||
}
|
||||
label="Cache Storage"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={options.serviceWorkers}
|
||||
onChange={() => handleOptionChange('serviceWorkers')}
|
||||
/>
|
||||
}
|
||||
label="Service Workers"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Auto Refresh Option */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => setAutoRefresh(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="清理完成后自动刷新页面"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setShowConfirm(true)}
|
||||
disabled={loading}
|
||||
fullWidth
|
||||
>
|
||||
{loading ? '清理中...' : '清理'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Result Display */}
|
||||
{result && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Alert
|
||||
severity={result.success ? 'success' : 'error'}
|
||||
sx={{ mb: !autoRefresh && result.success ? 1 : 0 }}
|
||||
>
|
||||
{result.success ? formatCleaningResult(result) : result.error || '清理失败'}
|
||||
</Alert>
|
||||
{!autoRefresh && result.success && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleRefresh}
|
||||
fullWidth
|
||||
>
|
||||
刷新页面
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
{showConfirm && (
|
||||
<Paper
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
bgcolor: 'rgba(255, 255, 255, 0.95)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">确认清理</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', mb: 1 }}>
|
||||
将清理以下存储类型:
|
||||
</Typography>
|
||||
<Box sx={{ mb: 1 }}>
|
||||
{options.localStorage && (
|
||||
<Typography variant="body2">- localStorage</Typography>
|
||||
)}
|
||||
{options.sessionStorage && (
|
||||
<Typography variant="body2">- sessionStorage</Typography>
|
||||
)}
|
||||
{options.indexedDB && <Typography variant="body2">- IndexedDB</Typography>}
|
||||
{options.cookies && <Typography variant="body2">- Cookies</Typography>}
|
||||
{options.cacheStorage && (
|
||||
<Typography variant="body2">- Cache Storage</Typography>
|
||||
)}
|
||||
{options.serviceWorkers && (
|
||||
<Typography variant="body2">- Service Workers</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', mb: 1 }}
|
||||
>
|
||||
此操作不可撤销。
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button variant="outlined" onClick={() => setShowConfirm(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="contained" color="error" onClick={handleClean}>
|
||||
确认清理
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{/* Snackbar */}
|
||||
<Snackbar
|
||||
open={snackbar.open}
|
||||
autoHideDuration={3000}
|
||||
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
||||
>
|
||||
<Alert severity="info" variant="filled">
|
||||
{snackbar.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit StorageCleanerPage component**
|
||||
|
||||
```bash
|
||||
git add entrypoints/popup/pages/StorageCleanerPage.tsx
|
||||
git commit -m "feat: add StorageCleanerPage component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Update App.tsx with Tab Switching
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `entrypoints/popup/App.tsx`
|
||||
|
||||
- [ ] **Step 1: Add tab switching logic to App.tsx**
|
||||
|
||||
```typescript
|
||||
import { useState } from 'react';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import TimestampPage from './pages/TimestampPage';
|
||||
import StorageCleanerPage from './pages/StorageCleanerPage';
|
||||
import './App.css';
|
||||
|
||||
type PageType = 'timestamp' | 'storageCleaner';
|
||||
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState<PageType>('timestamp');
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 2 }}>
|
||||
<Button
|
||||
variant={currentPage === 'timestamp' ? 'contained' : 'outlined'}
|
||||
onClick={() => setCurrentPage('timestamp')}
|
||||
>
|
||||
时间戳
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentPage === 'storageCleaner' ? 'contained' : 'outlined'}
|
||||
onClick={() => setCurrentPage('storageCleaner')}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
存储清理
|
||||
</Button>
|
||||
</Box>
|
||||
{currentPage === 'timestamp' && <TimestampPage />}
|
||||
{currentPage === 'storageCleaner' && <StorageCleanerPage />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run type check**
|
||||
|
||||
Run: `npm run compile`
|
||||
Expected: No TypeScript errors
|
||||
|
||||
- [ ] **Step 3: Commit App.tsx changes**
|
||||
|
||||
```bash
|
||||
git add entrypoints/popup/App.tsx
|
||||
git commit -m "feat: add tab switching to App component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Build and Test
|
||||
|
||||
**Files:**
|
||||
|
||||
- No file changes
|
||||
|
||||
- [ ] **Step 1: Build the extension**
|
||||
|
||||
Run: `npm run build`
|
||||
Expected: Build succeeds with no errors
|
||||
|
||||
- [ ] **Step 2: Run lint check**
|
||||
|
||||
Run: `npm run lint`
|
||||
Expected: No linting errors
|
||||
|
||||
- [ ] **Step 3: Load extension in Chrome for manual testing**
|
||||
|
||||
Instructions:
|
||||
|
||||
1. Open Chrome and navigate to `chrome://extensions/`
|
||||
2. Enable Developer Mode
|
||||
3. Click "Load unpacked"
|
||||
4. Select `.output/chrome-mv3` directory
|
||||
5. Test on a regular web page (e.g., example.com)
|
||||
|
||||
- [ ] **Step 4: Commit successful implementation**
|
||||
|
||||
```bash
|
||||
git commit --allow-empty -m "feat: complete storage cleaner feature implementation"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After implementation, verify:
|
||||
|
||||
- [ ] Tab switching works between timestamp and storage cleaner
|
||||
- [ ] Current domain displays correctly
|
||||
- [ ] All storage type checkboxes toggle correctly
|
||||
- [ ] Auto refresh checkbox persists across sessions
|
||||
- [ ] Clear confirmation dialog appears
|
||||
- [ ] Confirmation dialog shows selected storage types
|
||||
- [ ] localStorage clears successfully
|
||||
- [ ] sessionStorage clears successfully
|
||||
- [ ] IndexedDB clears successfully (or shows error if unavailable)
|
||||
- [ ] Cookies clear successfully
|
||||
- [ ] Clear httponly and secure cookies
|
||||
- [ ] Cache Storage clears successfully
|
||||
- [ ] Service Workers unregister successfully
|
||||
- [ ] Result message displays correctly
|
||||
- [ ] Empty state shows friendly message
|
||||
- [ ] Auto refresh works
|
||||
- [ ] Manual refresh button appears when auto-refresh is off
|
||||
- [ ] Restricted pages show error message
|
||||
- [ ] Snackbar notifications appear correctly
|
||||
- [ ] Test on localhost
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues occur during testing:
|
||||
|
||||
1. Revert to before implementation:
|
||||
|
||||
```bash
|
||||
git reset --hard <commit-before-start>
|
||||
```
|
||||
|
||||
2. Or revert specific files:
|
||||
```bash
|
||||
git checkout HEAD -- types/storage.d.ts wxt.config.ts utils/storageCleaner.ts entrypoints/popup/App.tsx entrypoints/popup/pages/StorageCleanerPage.tsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- The popup closes automatically when the page is refreshed - this is expected behavior
|
||||
- IndexedDB.databases() may not be available in all browser versions; the fallback handles this
|
||||
- Chrome Cookies API requires explicit permission, which is added to the manifest
|
||||
- User preferences are persisted using the existing chromeStorage.ts utility
|
||||
@@ -1,32 +0,0 @@
|
||||
# 实施计划:修复 Popup 布局与滚动条样式
|
||||
|
||||
## 1. 目标
|
||||
|
||||
按照设计规范实施固定高度布局与极简滚动条,确保扩展弹窗显示稳定且美观。
|
||||
|
||||
## 2. 实施步骤
|
||||
|
||||
### 2.1 CSS 核心样式更新 (`entrypoints/popup/App.css`)
|
||||
|
||||
1. **视口锁定**: 更新 `html`, `body` 样式,固定 `width: 400px`, `height: 600px`。
|
||||
2. **根容器改造**:
|
||||
- 将 `.app` 修改为 Flex 容器:`display: flex; flex-direction: column; height: 100%; overflow: hidden;`。
|
||||
- 移除 `margin: 0 auto;` 和 `min-height: 100%;`。
|
||||
3. **滚动条变量与全局注入**:
|
||||
- 在 `:root` 中定义 `--sb-` 开头的滚动条样式变量。
|
||||
- 使用 `*::-webkit-scrollbar` 系列伪元素定义全局极简滚动条。
|
||||
|
||||
### 2.2 React 结构重构 (`entrypoints/popup/App.tsx`)
|
||||
|
||||
1. **注入滚动容器**: 在 `nav-container` 之后,将所有的页面渲染(`TimestampPage`, `StorageCleanerPage`)包裹在一个统一的 `Box` 中。
|
||||
2. **设置容器样式**: 给该 `Box` 设置 `sx={{ flex: 1, overflowY: 'auto', scrollbarGutter: 'stable' }}`。
|
||||
|
||||
### 2.3 质量保障
|
||||
|
||||
1. **Lint 检测**: 运行 `npm run lint` 检查样式变量引用和 JSX 结构。
|
||||
2. **类型检查**: 运行 `npm run compile` 验证 MUI 组件属性。
|
||||
|
||||
## 3. 验证计划
|
||||
|
||||
- 手动切换导航栏,观察窗口尺寸是否维持在 600px。
|
||||
- 在“存储清理”页面展开所有选项,观察右侧是否出现极细滚动条,且不会挤压内容。
|
||||
@@ -1,387 +0,0 @@
|
||||
# 存储清理功能设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
为浏览器扩展添加一个存储清理功能,允许用户快速清理当前页面的各种存储数据,包括 localStorage、sessionStorage、IndexedDB、Cookies、Cache Storage 和 Service Workers。
|
||||
|
||||
## 目标
|
||||
|
||||
- 提供便捷的页面存储清理功能
|
||||
- 支持多种存储类型清理
|
||||
- 提供清理结果反馈
|
||||
- 支持清理后自动刷新页面
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 组件结构
|
||||
|
||||
```
|
||||
entrypoints/popup/pages/
|
||||
├── TimestampPage.tsx (现有:时间戳转换页面)
|
||||
└── StorageCleanerPage.tsx (新增:存储清理页面)
|
||||
```
|
||||
|
||||
### 页面布局
|
||||
|
||||
在弹窗中添加标签页切换功能,用户可以在时间戳转换和存储清理之间切换。
|
||||
|
||||
**路由实现方案:**
|
||||
|
||||
使用简单的状态管理进行页面切换:
|
||||
|
||||
```typescript
|
||||
// App.tsx
|
||||
type PageType = 'timestamp' | 'storageCleaner';
|
||||
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState<PageType>('timestamp');
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mb: 2 }}>
|
||||
<Button
|
||||
variant={currentPage === 'timestamp' ? 'contained' : 'outlined'}
|
||||
onClick={() => setCurrentPage('timestamp')}
|
||||
>
|
||||
时间戳
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentPage === 'storageCleaner' ? 'contained' : 'outlined'}
|
||||
onClick={() => setCurrentPage('storageCleaner')}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
存储清理
|
||||
</Button>
|
||||
</Box>
|
||||
{currentPage === 'timestamp' && <TimestampPage />}
|
||||
{currentPage === 'storageCleaner' && <StorageCleanerPage />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 用户界面设计
|
||||
|
||||
### 页面组成
|
||||
|
||||
1. **头部区域**
|
||||
- 标题:"存储清理"
|
||||
- 当前域名显示(自动从活动标签页获取)
|
||||
|
||||
2. **存储类型选择区域**
|
||||
- 勾选框:localStorage
|
||||
- 勾选框:sessionStorage
|
||||
- 勾选框:IndexedDB
|
||||
- 勾选框:Cookies
|
||||
- 勾选框:Cache Storage
|
||||
- 勾选框:Service Workers
|
||||
|
||||
3. **自动刷新选项**
|
||||
- 复选框:清理完成后自动刷新页面(默认勾选)
|
||||
|
||||
4. **操作区域**
|
||||
- 清理按钮
|
||||
|
||||
5. **结果显示区域**
|
||||
- 清理成功/失败提示
|
||||
- 清理详情统计(如:"清理了 5 个 localStorage, 3 个 cookies")
|
||||
- 刷新页面按钮(当未勾选自动刷新时显示)
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 获取当前标签页域名
|
||||
|
||||
使用 Chrome Tabs API 获取当前活动标签页,并过滤受限页面:
|
||||
|
||||
```typescript
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
// 检查受限页面
|
||||
const restrictedProtocols = [
|
||||
'chrome:',
|
||||
'chrome-extension:',
|
||||
'about:',
|
||||
'edge:',
|
||||
'view-source:',
|
||||
'file:',
|
||||
'data:',
|
||||
];
|
||||
|
||||
if (!tab?.url || restrictedProtocols.some((p) => tab.url!.startsWith(p))) {
|
||||
throw new Error('存储清理功能不支持此页面');
|
||||
}
|
||||
|
||||
const domain = new URL(tab.url).hostname;
|
||||
```
|
||||
|
||||
### 清理 Cookies(使用 chrome.cookies API)
|
||||
|
||||
在扩展环境中直接执行,不需要注入页面:
|
||||
|
||||
```typescript
|
||||
const cookies = await chrome.cookies.getAll({ url: tab.url });
|
||||
let count = 0;
|
||||
for (const cookie of cookies) {
|
||||
await chrome.cookies.remove({
|
||||
url: tab.url,
|
||||
name: cookie.name,
|
||||
storeId: cookie.storeId,
|
||||
});
|
||||
count++;
|
||||
}
|
||||
```
|
||||
|
||||
### 注入脚本清理其他存储
|
||||
|
||||
使用 `chrome.scripting.executeScript` 注入清理脚本:
|
||||
|
||||
```typescript
|
||||
const result = await chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
func: () => {
|
||||
// 清理逻辑在页面上下文中执行
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
需要注入到页面执行的存储清理逻辑:
|
||||
|
||||
#### 清理 localStorage
|
||||
|
||||
```javascript
|
||||
const count = localStorage.length;
|
||||
localStorage.clear();
|
||||
return count;
|
||||
```
|
||||
|
||||
#### 清理 sessionStorage
|
||||
|
||||
```javascript
|
||||
const count = sessionStorage.length;
|
||||
sessionStorage.clear();
|
||||
return count;
|
||||
```
|
||||
|
||||
#### 清理 IndexedDB
|
||||
|
||||
```javascript
|
||||
// 检查 indexedDB.databases 方法是否可用
|
||||
if (typeof indexedDB.databases === 'function') {
|
||||
const databases = await indexedDB.databases();
|
||||
let count = 0;
|
||||
for (const db of databases) {
|
||||
const deleteReq = indexedDB.deleteDatabase(db.name);
|
||||
deleteReq.onblocked = () => {
|
||||
console.warn('IndexedDB delete blocked:', db.name);
|
||||
};
|
||||
await new Promise((resolve, reject) => {
|
||||
deleteReq.onsuccess = resolve;
|
||||
deleteReq.onerror = reject;
|
||||
});
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
// 降级方案:由于无法获取所有数据库名称,提示返回特殊值表示需要手动操作
|
||||
return { error: 'databases_api_unavailable' };
|
||||
```
|
||||
|
||||
#### 清理 Cache Storage
|
||||
|
||||
```javascript
|
||||
if ('caches' in window) {
|
||||
const cacheNames = await caches.keys();
|
||||
for (const name of cacheNames) {
|
||||
await caches.delete(name);
|
||||
}
|
||||
return cacheNames.length;
|
||||
}
|
||||
return 0;
|
||||
```
|
||||
|
||||
#### 注销 Service Workers
|
||||
|
||||
```javascript
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
let count = 0;
|
||||
for (const registration of registrations) {
|
||||
await registration.unregister();
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
return 0;
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
1. 页面加载时获取当前标签页 URL 并显示域名
|
||||
2. 检查是否为受限页面(chrome://, about:// 等),如果是则显示错误提示
|
||||
3. 用户勾选要清理的存储类型
|
||||
4. 用户选择是否自动刷新页面
|
||||
5. 用户点击清理按钮
|
||||
6. 弹出确认对话框询问用户确认
|
||||
7. 确认后执行清理:
|
||||
- 如果选择 Cookies:直接使用 chrome.cookies API 删除
|
||||
- 其他存储类型:向页面注入清理脚本
|
||||
8. 收集所有清理结果并统计
|
||||
9. 显示清理结果
|
||||
10. 如果勾选"自动刷新"或用户点击"刷新页面"按钮,执行页面刷新
|
||||
|
||||
**注意:** 当触发页面刷新时,popup 会自动关闭。需要在刷新前显示提示信息。
|
||||
|
||||
### 页面刷新
|
||||
|
||||
```typescript
|
||||
// 显示刷新提示
|
||||
setRefreshing(true);
|
||||
setTimeout(async () => {
|
||||
await chrome.tabs.reload(tab.id);
|
||||
}, 1500); // 1.5秒延迟让用户看到提示信息
|
||||
```
|
||||
|
||||
### 空状态处理
|
||||
|
||||
当所有存储类型清理返回 0 时,显示友好的提示:
|
||||
|
||||
```
|
||||
该页面没有可清理的存储数据
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
| ------------------------------- | ---------------------------------------- |
|
||||
| 无法获取当前标签页 | 显示错误提示:"无法获取当前标签页" |
|
||||
| 受限页面(chrome://, about://) | 显示错误提示:"存储清理功能不支持此页面" |
|
||||
| 无法访问页面 URL | 显示错误提示:"无法访问此页面" |
|
||||
| IndexedDB onblocked | 显示警告但继续执行其他清理 |
|
||||
| IndexedDB.databases 不可用 | 使用降级方案或提示用户手动清除 |
|
||||
| 清理失败 | 显示具体错误信息 |
|
||||
| Cookies 删除失败 | 记录错误,显示清理失败提示 |
|
||||
| 无权限 | 提示用户刷新扩展或检查权限 |
|
||||
| 脚本注入失败 | 显示错误提示:"无法注入清理脚本" |
|
||||
|
||||
**Popup 生命周期说明:**
|
||||
|
||||
- Popup 在页面失去焦点时会关闭
|
||||
- 刷新页面后 Popup 会自动关闭
|
||||
- 需要在刷新前显示提示:"页面即将刷新,Popup 将关闭"
|
||||
|
||||
## 用户偏好持久化
|
||||
|
||||
使用现有的 `chromeStorage.ts` 工具保存用户偏好:
|
||||
|
||||
```typescript
|
||||
// 保存用户偏好
|
||||
await storageUtil.set('storageCleaner/preferences', {
|
||||
autoRefresh: true, // 默认勾选自动刷新
|
||||
selectedTypes: {
|
||||
// 可以保存用户上次选择的存储类型
|
||||
localStorage: true,
|
||||
sessionStorage: true,
|
||||
indexedDB: true,
|
||||
cookies: true,
|
||||
cacheStorage: true,
|
||||
serviceWorkers: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 读取用户偏好
|
||||
const preferences = await storageUtil.get('storageCleaner/preferences', {
|
||||
autoRefresh: true,
|
||||
selectedTypes: {
|
||||
localStorage: true,
|
||||
sessionStorage: true,
|
||||
indexedDB: true,
|
||||
cookies: true,
|
||||
cacheStorage: true,
|
||||
serviceWorkers: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 权限需求
|
||||
|
||||
需要在 manifest 中添加 `cookies` 权限:
|
||||
|
||||
```typescript
|
||||
permissions: [
|
||||
'storage',
|
||||
'unlimitedStorage',
|
||||
'clipboardWrite',
|
||||
'activeTab',
|
||||
'scripting',
|
||||
'tabs',
|
||||
'debugger',
|
||||
'cookies', // 新增
|
||||
],
|
||||
```
|
||||
|
||||
## TypeScript 类型定义
|
||||
|
||||
在现有 `types/storage.d.ts` 中添加存储清理相关的类型:
|
||||
|
||||
```typescript
|
||||
export interface StorageSchema {
|
||||
'app/lastRoute': string;
|
||||
'app/theme': string;
|
||||
'storageCleaner/preferences': StorageCleanerPreferences;
|
||||
}
|
||||
|
||||
export interface StorageCleanerPreferences {
|
||||
autoRefresh: boolean;
|
||||
selectedTypes: StorageCleanerOptions;
|
||||
}
|
||||
|
||||
export interface StorageCleanerOptions {
|
||||
localStorage: boolean;
|
||||
sessionStorage: boolean;
|
||||
indexedDB: boolean;
|
||||
cookies: boolean;
|
||||
cacheStorage: boolean;
|
||||
serviceWorkers: boolean;
|
||||
}
|
||||
|
||||
export type StorageCleanResult =
|
||||
| {
|
||||
success: true;
|
||||
count: number;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export interface CleaningResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
localStorage?: StorageCleanResult;
|
||||
sessionStorage?: StorageCleanResult;
|
||||
indexedDB?: StorageCleanResult;
|
||||
cookies?: StorageCleanResult;
|
||||
cacheStorage?: StorageCleanResult;
|
||||
serviceWorkers?: StorageCleanResult;
|
||||
}
|
||||
```
|
||||
|
||||
## 测试计划
|
||||
|
||||
1. 测试各种存储类型的单独清理
|
||||
2. 测试同时清理多种存储类型
|
||||
3. 测试自动刷新功能
|
||||
4. 测试手动刷新按钮
|
||||
5. 测试无存储数据时的清理(显示空状态提示)
|
||||
6. 测试无法访问页面的错误处理
|
||||
7. 测试受限页面(chrome://, about://, file://, data://)
|
||||
8. 测试 IndexedDB onblocked 场景
|
||||
9. 测试 httponly 和 secure cookies 清理
|
||||
10. 测试本地开发环境(localhost)
|
||||
11. 测试用户偏好持久化
|
||||
|
||||
## 后续优化
|
||||
|
||||
- 显示清理前的存储使用情况
|
||||
- 支持批量清理多个标签页
|
||||
- 支持自定义域名清理
|
||||
@@ -1,97 +0,0 @@
|
||||
# CLAUDE.md Reorganization Design
|
||||
|
||||
**Date**: 2026-03-25
|
||||
**Status**: Approved & Implemented
|
||||
**Related Files**: `/CLAUDE.md`
|
||||
|
||||
## Overview
|
||||
|
||||
Reorganized the existing CLAUDE.md file to improve clarity, flow, and usability for future Claude Code instances working with this browser extension project.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The existing CLAUDE.md file contained comprehensive information but had organizational issues:
|
||||
- Mixed development commands, architecture, and implementation details
|
||||
- Redundant information in multiple sections
|
||||
- Lack of clear logical flow from setup to development to reference
|
||||
- Missing some technical details (path aliases, messaging system explanation)
|
||||
|
||||
## Design Goals
|
||||
|
||||
1. **Improve logical flow**: Structure content in order of developer needs
|
||||
2. **Reduce redundancy**: Eliminate duplicate information
|
||||
3. **Enhance readability**: Use clearer headings and organization
|
||||
4. **Maintain completeness**: Preserve all essential information
|
||||
5. **Add missing context**: Include path aliases and other technical specifics
|
||||
|
||||
## Solution Design
|
||||
|
||||
### Reorganized Structure
|
||||
|
||||
1. **Quick Start** - Essential commands and setup (first thing developers need)
|
||||
2. **Architecture Overview** - Tech stack and high-level structure (context before diving in)
|
||||
3. **Core Features** - What the extension does (timestamp conversion, storage cleaning)
|
||||
4. **Development Workflow** - How to work with the codebase (browser compatibility, code quality tools)
|
||||
5. **Configuration & Implementation** - Reference details (wxt.config.ts, manifest permissions)
|
||||
6. **CI/CD & Project Context** - Background information (GitHub Actions, project history)
|
||||
|
||||
### Key Improvements
|
||||
|
||||
1. **Command Table**: Replaced bullet list with markdown table for better readability
|
||||
2. **Simplified Directory Structure**: Removed excessive detail while maintaining clarity
|
||||
3. **Logical Grouping**: Related information placed together (e.g., all storage cleaning details)
|
||||
4. **Added Missing Information**: Path aliases (`@/`), TypeScript configuration highlights
|
||||
5. **Clearer Section Titles**: More descriptive headings that indicate content purpose
|
||||
|
||||
### Content Preservation
|
||||
|
||||
All essential information from the original CLAUDE.md was preserved:
|
||||
- All npm commands and their purposes
|
||||
- Tech stack details
|
||||
- Directory structure (simplified but complete)
|
||||
- Core feature descriptions
|
||||
- Storage cleaning implementation details
|
||||
- Manifest permissions
|
||||
- CI/CD workflow information
|
||||
- Project history context
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### File Changes
|
||||
- **CLAUDE.md**: Complete rewrite with reorganized structure
|
||||
- **No other files modified**: Only documentation changes
|
||||
|
||||
### Structural Changes
|
||||
1. **Moved commands to front**: Developers need these immediately
|
||||
2. **Grouped related topics**: All storage-related information together
|
||||
3. **Separated workflow from reference**: Development process vs. configuration details
|
||||
4. **Added visual hierarchy**: Clear section headings and subheadings
|
||||
|
||||
### Content Additions
|
||||
1. **Path aliases section**: Explains `@/` import pattern
|
||||
2. **TypeScript configuration highlights**: Key settings called out
|
||||
3. **Better cross-references**: Links between related sections
|
||||
|
||||
## Validation
|
||||
|
||||
The reorganized CLAUDE.md was validated against:
|
||||
- ✅ All original commands preserved
|
||||
- ✅ All architectural information maintained
|
||||
- ✅ All feature descriptions included
|
||||
- ✅ All configuration details retained
|
||||
- ✅ Improved readability and flow
|
||||
- ✅ Added missing technical context
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Quick access to commands**: Developers can find essential npm scripts immediately
|
||||
2. **Clear understanding of architecture**: Tech stack and structure explained upfront
|
||||
3. **Logical information flow**: Follows natural developer workflow
|
||||
4. **Complete reference**: All necessary information preserved and organized
|
||||
5. **Improved usability**: Easier for Claude Code instances to understand and work with the project
|
||||
|
||||
## Future Considerations
|
||||
|
||||
1. **Regular updates**: CLAUDE.md should be updated when project structure changes
|
||||
2. **User feedback**: Monitor if the reorganization improves developer experience
|
||||
3. **Additional context**: Consider adding troubleshooting tips or common issues section if needed
|
||||
@@ -1,48 +0,0 @@
|
||||
# 设计规范:Popup 弹窗固定高度与极简滚动条
|
||||
|
||||
## 1. 目标
|
||||
|
||||
解决 Chrome 扩展弹窗高度“只增不减”的布局问题,提供稳定的 600px 固定高度体验,并实现符合极简主义设计规范的可复用滚动条样式。
|
||||
|
||||
## 2. 核心架构方案 (方案 A)
|
||||
|
||||
采用 **Flex 布局 + 视口锁定** 的策略,将弹窗尺寸固定在 `400px * 600px`。
|
||||
|
||||
### 2.1 容器层级设计
|
||||
|
||||
- **`html`, `body`**: 锁定为 `400px * 600px`,并设置 `overflow: hidden` 防止出现双滚动条。
|
||||
- **`.app` (根容器)**:
|
||||
- 设置为 `display: flex; flex-direction: column;`。
|
||||
- 高度撑满父级 (`100%`)。
|
||||
- 锁定 `overflow: hidden`。
|
||||
- **`nav-container` (导航栏)**:
|
||||
- 位于顶部,高度固定,不参与 Flex 缩放。
|
||||
- **内容展示区 (Page Container)**:
|
||||
- 设置 `flex: 1`,自动填充剩余空间。
|
||||
- 设置 `overflow-y: auto`,启用局部滚动。
|
||||
- 设置 `scrollbar-gutter: stable`,预留滚动条空间,防止布局抖动。
|
||||
|
||||
## 3. 极简滚动条设计规范
|
||||
|
||||
为了确保在所有页面和组件中表现一致,采用全局 Webkit 伪元素定制方案。
|
||||
|
||||
### 3.1 变量定义
|
||||
|
||||
在 `:root` 中定义 CSS 变量以增强兼容性和可维护性:
|
||||
|
||||
- `--sb-width`: `6px` (极细)
|
||||
- `--sb-thumb-color`: `rgba(0, 0, 0, 0.1)` (浅灰色半透明)
|
||||
- `--sb-thumb-hover`: `rgba(0, 0, 0, 0.18)` (悬停时微深)
|
||||
- `--sb-track-color`: `transparent` (背景完全透明)
|
||||
|
||||
### 3.2 样式表现
|
||||
|
||||
- **滑块 (Thumb)**: 胶囊状圆角 (`10px`)。
|
||||
- **背景剪裁 (Background Clip)**: 使用 `content-box` 结合透明边框来实现滑块与边缘的微距感。
|
||||
- **自动应用**: 使用通配符 `*::-webkit-scrollbar` 确保全局所有溢出容器均自动继承此样式。
|
||||
|
||||
## 4. 成功准则
|
||||
|
||||
- 切换“时间戳”和“存储清理”页面时,浏览器窗口大小保持 `600px` 不变。
|
||||
- 当页面内容超过视口时,右侧显示细长、半透明的滚动条。
|
||||
- 内容加载或 Accordion 展开时,页面水平方向不发生位移。
|
||||
Reference in New Issue
Block a user