create-extension-react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/package.json +54 -0
- package/scripts/create.js +418 -0
- package/scripts/dev.js +26 -0
- package/src/background.ts +9 -0
- package/src/bookmarks/index.module.css +17 -0
- package/src/bookmarks/index.tsx +42 -0
- package/src/bookmarks.html +12 -0
- package/src/components/BookmarksContent/index.module.css +95 -0
- package/src/components/BookmarksContent/index.tsx +100 -0
- package/src/components/ConfigInfo/index.module.css +30 -0
- package/src/components/ConfigInfo/index.tsx +36 -0
- package/src/components/HistoryContent/index.module.css +99 -0
- package/src/components/HistoryContent/index.tsx +86 -0
- package/src/components/NewTabContent/index.module.css +86 -0
- package/src/components/NewTabContent/index.tsx +30 -0
- package/src/components/NewTabHeader/index.module.css +76 -0
- package/src/components/NewTabHeader/index.tsx +14 -0
- package/src/components/PopupContent/index.module.css +46 -0
- package/src/components/PopupContent/index.tsx +29 -0
- package/src/components/PopupHeader/index.module.css +15 -0
- package/src/components/PopupHeader/index.tsx +14 -0
- package/src/components/ThemeButton/index.module.css +202 -0
- package/src/components/ThemeButton/index.tsx +48 -0
- package/src/content.ts +10 -0
- package/src/history/index.module.css +17 -0
- package/src/history/index.tsx +42 -0
- package/src/history.html +12 -0
- package/src/hooks/useStorage.ts +50 -0
- package/src/hooks/useTabs.ts +37 -0
- package/src/hooks/useTheme.ts +27 -0
- package/src/manifest.json +26 -0
- package/src/newtab/index.module.css +17 -0
- package/src/newtab/index.tsx +43 -0
- package/src/newtab.html +12 -0
- package/src/popup/index.module.css +8 -0
- package/src/popup/index.tsx +36 -0
- package/src/popup.html +12 -0
- package/src/store/README.md +176 -0
- package/src/store/chromeStorage.ts +88 -0
- package/src/store/configSlice.ts +40 -0
- package/src/store/hooks.ts +6 -0
- package/src/store/initConfig.ts +78 -0
- package/src/store/storageMiddleware.ts +70 -0
- package/src/store/store.ts +49 -0
- package/src/style.css +107 -0
- package/src/utils/initTheme.ts +40 -0
- package/src/variables.css +146 -0
- package/src/vite-env.d.ts +6 -0
- package/tsconfig.json +22 -0
- package/tsconfig.node.json +10 -0
- package/vite-plugin-fix-extension.ts +81 -0
- package/vite.config.ts +50 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { Provider } from 'react-redux';
|
|
4
|
+
import { PersistGate } from 'redux-persist/integration/react';
|
|
5
|
+
import '../style.css';
|
|
6
|
+
import { store, persistor } from '../store/store';
|
|
7
|
+
import { PopupHeader } from '../components/PopupHeader';
|
|
8
|
+
import { PopupContent } from '../components/PopupContent';
|
|
9
|
+
import { useTheme } from '../hooks/useTheme';
|
|
10
|
+
import styles from './index.module.css';
|
|
11
|
+
|
|
12
|
+
const Popup: React.FC = () => {
|
|
13
|
+
// 使用 useTheme hook 确保主题同步和响应
|
|
14
|
+
useTheme();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={styles.container}>
|
|
18
|
+
<PopupHeader title="React Extension" />
|
|
19
|
+
<PopupContent />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const container = document.getElementById('root');
|
|
25
|
+
if (container) {
|
|
26
|
+
const root = createRoot(container);
|
|
27
|
+
|
|
28
|
+
// 使用 PersistGate 等待持久化数据加载
|
|
29
|
+
root.render(
|
|
30
|
+
<Provider store={store}>
|
|
31
|
+
<PersistGate loading={null} persistor={persistor}>
|
|
32
|
+
<Popup />
|
|
33
|
+
</PersistGate>
|
|
34
|
+
</Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/popup.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>React Extension</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body class="popup">
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./popup/index.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Redux 配置管理系统(持久化版本)
|
|
2
|
+
|
|
3
|
+
这个目录包含了使用 Redux Toolkit 和 redux-persist 实现的配置管理系统,支持自动持久化存储。
|
|
4
|
+
|
|
5
|
+
## 文件结构
|
|
6
|
+
|
|
7
|
+
- `store.ts` - Redux store 配置(包含持久化配置)
|
|
8
|
+
- `configSlice.ts` - 配置状态管理 slice
|
|
9
|
+
- `chromeStorage.ts` - Chrome Storage 适配器,用于 redux-persist
|
|
10
|
+
- `initConfig.ts` - 初始化配置(已废弃,redux-persist 自动处理)
|
|
11
|
+
- `hooks.ts` - 类型化的 Redux hooks
|
|
12
|
+
|
|
13
|
+
## 持久化存储
|
|
14
|
+
|
|
15
|
+
使用 `redux-persist` 实现自动持久化:
|
|
16
|
+
|
|
17
|
+
- **存储引擎**: `chrome.storage.local`(Chrome Extension 环境)
|
|
18
|
+
- **降级方案**: `localStorage`(非 Chrome 环境)
|
|
19
|
+
- **自动同步**: 配置修改后自动保存到存储
|
|
20
|
+
- **自动恢复**: 应用启动时自动从存储中恢复配置
|
|
21
|
+
|
|
22
|
+
## 使用方法
|
|
23
|
+
|
|
24
|
+
### 1. 在组件中使用配置
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useAppSelector, useAppDispatch } from '../store/hooks';
|
|
28
|
+
import { setTheme } from '../store/configSlice';
|
|
29
|
+
import dayjs from 'dayjs';
|
|
30
|
+
|
|
31
|
+
function MyComponent() {
|
|
32
|
+
const dispatch = useAppDispatch();
|
|
33
|
+
const isDarkMode = useAppSelector((state) => state.config.theme);
|
|
34
|
+
const updatedAt = useAppSelector((state) => state.config.updatedAt);
|
|
35
|
+
|
|
36
|
+
const toggleTheme = () => {
|
|
37
|
+
dispatch(setTheme(!isDarkMode));
|
|
38
|
+
// updatedAt 会自动更新,配置会自动保存
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<button onClick={toggleTheme}>
|
|
44
|
+
{isDarkMode ? '切换到亮色' : '切换到暗色'}
|
|
45
|
+
</button>
|
|
46
|
+
<p>最后修改: {dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss')}</p>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. 添加新的配置项
|
|
53
|
+
|
|
54
|
+
在 `configSlice.ts` 中添加新的配置项:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import dayjs from 'dayjs';
|
|
58
|
+
|
|
59
|
+
export interface ConfigState {
|
|
60
|
+
theme: boolean;
|
|
61
|
+
updatedAt: string; // 配置最后修改日期(ISO 8601 格式)
|
|
62
|
+
language: string; // 新增配置项
|
|
63
|
+
fontSize: number; // 新增配置项
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const initialState: ConfigState = {
|
|
67
|
+
theme: false,
|
|
68
|
+
updatedAt: dayjs().toISOString(),
|
|
69
|
+
language: 'zh-CN', // 默认值
|
|
70
|
+
fontSize: 14, // 默认值
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// 添加对应的 reducer
|
|
74
|
+
const configSlice = createSlice({
|
|
75
|
+
name: 'config',
|
|
76
|
+
initialState,
|
|
77
|
+
reducers: {
|
|
78
|
+
setTheme: (state, action: PayloadAction<boolean>) => {
|
|
79
|
+
state.theme = action.payload;
|
|
80
|
+
state.updatedAt = dayjs().toISOString(); // 自动更新修改日期
|
|
81
|
+
},
|
|
82
|
+
setLanguage: (state, action: PayloadAction<string>) => {
|
|
83
|
+
state.language = action.payload;
|
|
84
|
+
state.updatedAt = dayjs().toISOString(); // 自动更新修改日期
|
|
85
|
+
},
|
|
86
|
+
setFontSize: (state, action: PayloadAction<number>) => {
|
|
87
|
+
state.fontSize = action.payload;
|
|
88
|
+
state.updatedAt = dayjs().toISOString(); // 自动更新修改日期
|
|
89
|
+
},
|
|
90
|
+
loadConfig: (state, action: PayloadAction<Partial<ConfigState>>) => {
|
|
91
|
+
return { ...state, ...action.payload };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**注意**:添加新配置项后,redux-persist 会自动处理持久化,无需额外配置。
|
|
98
|
+
|
|
99
|
+
### 3. 清除持久化数据
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { useAppDispatch } from '../store/hooks';
|
|
103
|
+
import { persistor } from '../store/store';
|
|
104
|
+
|
|
105
|
+
function ClearConfigButton() {
|
|
106
|
+
const dispatch = useAppDispatch();
|
|
107
|
+
|
|
108
|
+
const handleClear = () => {
|
|
109
|
+
// 清除所有持久化数据
|
|
110
|
+
persistor.purge();
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return <button onClick={handleClear}>清除配置</button>;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 工作原理
|
|
118
|
+
|
|
119
|
+
1. **初始化**:
|
|
120
|
+
- Redux store 创建时,redux-persist 会自动从 `chrome.storage.local` 加载配置
|
|
121
|
+
- `PersistGate` 组件会等待数据加载完成后再渲染子组件
|
|
122
|
+
|
|
123
|
+
2. **状态更新**:
|
|
124
|
+
- 当配置通过 Redux action 更新时,redux-persist 会自动保存到存储
|
|
125
|
+
- 无需手动调用存储 API
|
|
126
|
+
|
|
127
|
+
3. **存储结构**:
|
|
128
|
+
- redux-persist 会将整个 state 序列化为 JSON 存储在 `persist:root` 键下
|
|
129
|
+
- 格式:`{ _persist: { version, rehydrated }, config: { theme, updatedAt } }`
|
|
130
|
+
|
|
131
|
+
4. **双重存储**:
|
|
132
|
+
- `chrome.storage.local`:主要存储(Chrome Extension 环境)
|
|
133
|
+
- `localStorage`:降级方案(非 Chrome 环境)
|
|
134
|
+
|
|
135
|
+
## 配置字段说明
|
|
136
|
+
|
|
137
|
+
### updatedAt(修改日期)
|
|
138
|
+
|
|
139
|
+
- **类型**: `string` (ISO 8601 格式)
|
|
140
|
+
- **说明**: 记录配置最后修改的时间
|
|
141
|
+
- **自动更新**: 当任何配置项修改时,`updatedAt` 会自动更新为当前时间
|
|
142
|
+
- **使用示例**:
|
|
143
|
+
```tsx
|
|
144
|
+
import dayjs from 'dayjs';
|
|
145
|
+
const updatedAt = useAppSelector((state) => state.config.updatedAt);
|
|
146
|
+
const formattedDate = dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 优势
|
|
150
|
+
|
|
151
|
+
- ✅ 自动持久化:无需手动管理存储
|
|
152
|
+
- ✅ 自动恢复:应用启动时自动加载配置
|
|
153
|
+
- ✅ 类型安全:完整的 TypeScript 类型支持
|
|
154
|
+
- ✅ 易于扩展:添加新配置项无需修改持久化逻辑
|
|
155
|
+
- ✅ 零闪烁:HTML 初始化脚本确保主题立即应用
|
|
156
|
+
- ✅ Chrome Storage 支持:专为 Chrome Extension 优化
|
|
157
|
+
- ✅ 降级方案:非 Chrome 环境自动使用 localStorage
|
|
158
|
+
|
|
159
|
+
## 迁移说明
|
|
160
|
+
|
|
161
|
+
如果你之前使用了 `initConfig` 和 `storageMiddleware`:
|
|
162
|
+
|
|
163
|
+
- ✅ **已移除**: `storageMiddleware`(redux-persist 自动处理)
|
|
164
|
+
- ✅ **已移除**: `initConfig` 调用(PersistGate 自动处理)
|
|
165
|
+
- ✅ **保留**: HTML 初始化脚本(用于避免白色闪烁)
|
|
166
|
+
|
|
167
|
+
## 调试
|
|
168
|
+
|
|
169
|
+
查看持久化数据:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
// 在 Chrome DevTools Console 中
|
|
173
|
+
chrome.storage.local.get('persist:root', (data) => {
|
|
174
|
+
console.log(JSON.parse(data['persist:root']));
|
|
175
|
+
});
|
|
176
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Storage } from 'redux-persist';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chrome Storage 适配器,用于 redux-persist
|
|
5
|
+
* 将 redux-persist 的存储接口适配到 chrome.storage.local
|
|
6
|
+
*/
|
|
7
|
+
export const chromeStorage: Storage = {
|
|
8
|
+
getItem: (key: string): Promise<string | null> => {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
11
|
+
chrome.storage.local.get([key], (result) => {
|
|
12
|
+
if (chrome.runtime?.lastError) {
|
|
13
|
+
console.error('Chrome storage getItem error:', chrome.runtime.lastError);
|
|
14
|
+
resolve(null);
|
|
15
|
+
} else {
|
|
16
|
+
const value = result[key] || null;
|
|
17
|
+
if (value) {
|
|
18
|
+
console.log(`[redux-persist] Loaded config from chrome.storage.local: ${key}`);
|
|
19
|
+
} else {
|
|
20
|
+
console.log(`[redux-persist] No config found in chrome.storage.local: ${key}`);
|
|
21
|
+
}
|
|
22
|
+
resolve(value);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
} else {
|
|
26
|
+
// 降级到 localStorage
|
|
27
|
+
try {
|
|
28
|
+
const value = localStorage.getItem(key);
|
|
29
|
+
if (value) {
|
|
30
|
+
console.log(`[redux-persist] Loaded config from localStorage: ${key}`);
|
|
31
|
+
}
|
|
32
|
+
resolve(value);
|
|
33
|
+
} catch (e) {
|
|
34
|
+
resolve(null);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
setItem: (key: string, value: string): Promise<void> => {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
43
|
+
chrome.storage.local.set({ [key]: value }, () => {
|
|
44
|
+
if (chrome.runtime?.lastError) {
|
|
45
|
+
console.error('Chrome storage setItem error:', chrome.runtime.lastError);
|
|
46
|
+
reject(chrome.runtime.lastError);
|
|
47
|
+
} else {
|
|
48
|
+
// 调试:确认数据已保存
|
|
49
|
+
console.log(`[redux-persist] Saved config to chrome.storage.local: ${key}`);
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
// 降级到 localStorage
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem(key, value);
|
|
57
|
+
console.log(`[redux-persist] Saved config to localStorage: ${key}`);
|
|
58
|
+
resolve();
|
|
59
|
+
} catch (e) {
|
|
60
|
+
reject(e);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
removeItem: (key: string): Promise<void> => {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
69
|
+
chrome.storage.local.remove([key], () => {
|
|
70
|
+
if (chrome.runtime?.lastError) {
|
|
71
|
+
console.error('Chrome storage removeItem error:', chrome.runtime.lastError);
|
|
72
|
+
reject(chrome.runtime.lastError);
|
|
73
|
+
} else {
|
|
74
|
+
resolve();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
// 降级到 localStorage
|
|
79
|
+
try {
|
|
80
|
+
localStorage.removeItem(key);
|
|
81
|
+
resolve();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
reject(e);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
|
|
4
|
+
export interface ConfigState {
|
|
5
|
+
theme: boolean; // true = dark, false = light
|
|
6
|
+
updatedAt: string; // 配置最后修改日期(ISO 8601 格式)
|
|
7
|
+
// 可以在这里添加其他配置项
|
|
8
|
+
// example: language: string;
|
|
9
|
+
// example: fontSize: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const initialState: ConfigState = {
|
|
13
|
+
theme: false, // 默认亮色主题
|
|
14
|
+
updatedAt: dayjs().toISOString(), // 初始化时设置为当前时间
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const configSlice = createSlice({
|
|
18
|
+
name: 'config',
|
|
19
|
+
initialState,
|
|
20
|
+
reducers: {
|
|
21
|
+
setTheme: (state, action: PayloadAction<boolean>) => {
|
|
22
|
+
state.theme = action.payload;
|
|
23
|
+
state.updatedAt = dayjs().toISOString(); // 更新修改日期
|
|
24
|
+
// 调试:确认主题已更新
|
|
25
|
+
console.log('[configSlice] setTheme called:', action.payload, 'updatedAt:', state.updatedAt);
|
|
26
|
+
},
|
|
27
|
+
// 从存储中加载配置(不更新修改日期)
|
|
28
|
+
loadConfig: (state, action: PayloadAction<Partial<ConfigState>>) => {
|
|
29
|
+
return { ...state, ...action.payload };
|
|
30
|
+
},
|
|
31
|
+
// 可以添加其他配置的 reducer
|
|
32
|
+
// setLanguage: (state, action: PayloadAction<string>) => {
|
|
33
|
+
// state.language = action.payload;
|
|
34
|
+
// state.updatedAt = dayjs().toISOString();
|
|
35
|
+
// },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const { setTheme, loadConfig } = configSlice.actions;
|
|
40
|
+
export default configSlice.reducer;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
|
|
2
|
+
import type { RootState, AppDispatch } from './store';
|
|
3
|
+
|
|
4
|
+
// 使用类型化的 hooks,避免每次使用时都要指定类型
|
|
5
|
+
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
6
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { store } from './store';
|
|
2
|
+
import { loadConfig } from './configSlice';
|
|
3
|
+
import type { ConfigState } from './configSlice';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 初始化配置:从 chrome.storage 或 localStorage 加载配置
|
|
8
|
+
* 应该在应用启动时调用
|
|
9
|
+
*/
|
|
10
|
+
export async function initConfig(): Promise<void> {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
// 优先从 localStorage 同步读取(最快)
|
|
13
|
+
let config: Partial<ConfigState> = {};
|
|
14
|
+
try {
|
|
15
|
+
const cachedTheme = localStorage.getItem('theme');
|
|
16
|
+
if (cachedTheme !== null) {
|
|
17
|
+
config.theme = JSON.parse(cachedTheme) === true;
|
|
18
|
+
}
|
|
19
|
+
const cachedUpdatedAt = localStorage.getItem('updatedAt');
|
|
20
|
+
if (cachedUpdatedAt !== null) {
|
|
21
|
+
config.updatedAt = JSON.parse(cachedUpdatedAt);
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// localStorage 不可用
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 如果有缓存,先应用缓存配置
|
|
28
|
+
if (config.theme !== undefined || config.updatedAt !== undefined) {
|
|
29
|
+
store.dispatch(loadConfig(config));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 异步从 chrome.storage 读取并更新(确保数据一致性)
|
|
33
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
34
|
+
chrome.storage.local.get(['theme', 'updatedAt'], (result) => {
|
|
35
|
+
const loadedConfig: Partial<ConfigState> = {};
|
|
36
|
+
|
|
37
|
+
if (result.theme !== undefined) {
|
|
38
|
+
loadedConfig.theme = result.theme === true;
|
|
39
|
+
}
|
|
40
|
+
if (result.updatedAt !== undefined) {
|
|
41
|
+
// 验证日期格式
|
|
42
|
+
const parsedDate = dayjs(result.updatedAt);
|
|
43
|
+
loadedConfig.updatedAt = parsedDate.isValid()
|
|
44
|
+
? parsedDate.toISOString()
|
|
45
|
+
: dayjs().toISOString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 更新 store
|
|
49
|
+
if (Object.keys(loadedConfig).length > 0) {
|
|
50
|
+
store.dispatch(loadConfig(loadedConfig));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 同步到 localStorage
|
|
54
|
+
if (loadedConfig.theme !== undefined) {
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem('theme', JSON.stringify(loadedConfig.theme));
|
|
57
|
+
} catch (e) {
|
|
58
|
+
// localStorage 不可用
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (loadedConfig.updatedAt !== undefined) {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem(
|
|
64
|
+
'updatedAt',
|
|
65
|
+
JSON.stringify(loadedConfig.updatedAt)
|
|
66
|
+
);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// localStorage 不可用
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
resolve();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Middleware } from '@reduxjs/toolkit';
|
|
2
|
+
import { RootState } from './store';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redux middleware: 当配置修改后异步同步到 chrome.storage
|
|
7
|
+
* 使用防抖机制,避免频繁写入
|
|
8
|
+
*/
|
|
9
|
+
let syncTimer: ReturnType<typeof setTimeout> | null = null;
|
|
10
|
+
const SYNC_DELAY = 300; // 防抖延迟时间(毫秒)
|
|
11
|
+
|
|
12
|
+
export const storageMiddleware: Middleware<{}, RootState> =
|
|
13
|
+
(store) => (next) => (action) => {
|
|
14
|
+
// 执行 action
|
|
15
|
+
const result = next(action);
|
|
16
|
+
|
|
17
|
+
// 检查是否是配置相关的 action
|
|
18
|
+
if (action.type?.startsWith('config/')) {
|
|
19
|
+
// 清除之前的定时器
|
|
20
|
+
if (syncTimer) {
|
|
21
|
+
clearTimeout(syncTimer);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 设置新的定时器,延迟同步到 chrome.storage
|
|
25
|
+
syncTimer = setTimeout(() => {
|
|
26
|
+
const state = store.getState();
|
|
27
|
+
const config = state.config;
|
|
28
|
+
|
|
29
|
+
// 确保 updatedAt 是最新的
|
|
30
|
+
const configToSave = {
|
|
31
|
+
...config,
|
|
32
|
+
updatedAt: dayjs().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 异步同步到 chrome.storage
|
|
36
|
+
if (typeof chrome !== 'undefined' && chrome.storage?.local) {
|
|
37
|
+
chrome.storage.local.set(
|
|
38
|
+
{
|
|
39
|
+
theme: configToSave.theme,
|
|
40
|
+
updatedAt: configToSave.updatedAt,
|
|
41
|
+
// 可以添加其他配置项
|
|
42
|
+
// language: configToSave.language,
|
|
43
|
+
},
|
|
44
|
+
() => {
|
|
45
|
+
if (chrome.runtime?.lastError) {
|
|
46
|
+
console.error(
|
|
47
|
+
'Failed to sync config to chrome.storage:',
|
|
48
|
+
chrome.runtime.lastError
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// 同时同步到 localStorage(用于快速读取)
|
|
55
|
+
try {
|
|
56
|
+
localStorage.setItem('theme', JSON.stringify(configToSave.theme));
|
|
57
|
+
localStorage.setItem(
|
|
58
|
+
'updatedAt',
|
|
59
|
+
JSON.stringify(configToSave.updatedAt)
|
|
60
|
+
);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// localStorage 可能不可用
|
|
63
|
+
console.warn('Failed to sync to localStorage:', e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, SYNC_DELAY);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
2
|
+
import {
|
|
3
|
+
persistStore,
|
|
4
|
+
persistReducer,
|
|
5
|
+
FLUSH,
|
|
6
|
+
REHYDRATE,
|
|
7
|
+
PAUSE,
|
|
8
|
+
PERSIST,
|
|
9
|
+
PURGE,
|
|
10
|
+
REGISTER,
|
|
11
|
+
} from 'redux-persist';
|
|
12
|
+
import configReducer from './configSlice';
|
|
13
|
+
import { chromeStorage } from './chromeStorage';
|
|
14
|
+
|
|
15
|
+
// 配置持久化
|
|
16
|
+
// 注意:persistReducer 直接包装 configReducer,所以 key 应该是 'config'
|
|
17
|
+
// 不需要 whitelist,因为只持久化这一个 slice
|
|
18
|
+
const persistConfig = {
|
|
19
|
+
key: 'config', // 存储键名
|
|
20
|
+
storage: chromeStorage,
|
|
21
|
+
// 可选:配置版本号,用于迁移
|
|
22
|
+
// version: 1,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// 创建持久化的 reducer
|
|
26
|
+
const persistedReducer = persistReducer(persistConfig, configReducer);
|
|
27
|
+
|
|
28
|
+
export const store = configureStore({
|
|
29
|
+
reducer: {
|
|
30
|
+
config: persistedReducer,
|
|
31
|
+
},
|
|
32
|
+
middleware: (getDefaultMiddleware) =>
|
|
33
|
+
getDefaultMiddleware({
|
|
34
|
+
serializableCheck: {
|
|
35
|
+
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const persistor = persistStore(store);
|
|
41
|
+
|
|
42
|
+
// 调试:监听持久化状态
|
|
43
|
+
persistor.subscribe(() => {
|
|
44
|
+
const state = store.getState();
|
|
45
|
+
console.log('[redux-persist] Current config state:', state.config);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
49
|
+
export type AppDispatch = typeof store.dispatch;
|
package/src/style.css
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* 导入颜色变量 */
|
|
2
|
+
@import './variables.css';
|
|
3
|
+
|
|
4
|
+
/* 全局样式 */
|
|
5
|
+
* {
|
|
6
|
+
margin: 0;
|
|
7
|
+
padding: 0;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* HTML 根元素样式 */
|
|
12
|
+
html {
|
|
13
|
+
color-scheme: light dark;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
html[data-theme='dark'] {
|
|
17
|
+
color-scheme: dark;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
html[data-theme='light'] {
|
|
21
|
+
color-scheme: light;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
26
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
27
|
+
sans-serif;
|
|
28
|
+
-webkit-font-smoothing: antialiased;
|
|
29
|
+
-moz-osx-font-smoothing: grayscale;
|
|
30
|
+
transition: background-color 0.3s ease, color 0.3s ease;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Popup 特定全局样式 */
|
|
34
|
+
body.popup {
|
|
35
|
+
width: 350px;
|
|
36
|
+
min-height: 400px;
|
|
37
|
+
background-color: var(--bg-primary);
|
|
38
|
+
color: var(--text-primary);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
html[data-theme='dark'] body.popup {
|
|
42
|
+
background-color: #1a1a1a;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
html[data-theme='light'] body.popup {
|
|
46
|
+
background-color: #ffffff;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* NewTab 特定全局样式 */
|
|
50
|
+
body.newtab {
|
|
51
|
+
min-height: 100vh;
|
|
52
|
+
background-color: var(--bg-primary);
|
|
53
|
+
color: var(--text-primary);
|
|
54
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* History 特定全局样式 */
|
|
58
|
+
body.history {
|
|
59
|
+
min-height: 100vh;
|
|
60
|
+
background-color: var(--bg-primary);
|
|
61
|
+
color: var(--text-primary);
|
|
62
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Bookmarks 特定全局样式 */
|
|
66
|
+
body.bookmarks {
|
|
67
|
+
min-height: 100vh;
|
|
68
|
+
background-color: var(--bg-primary);
|
|
69
|
+
color: var(--text-primary);
|
|
70
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* 亮色主题背景渐变 */
|
|
74
|
+
html[data-theme='light'] body.newtab {
|
|
75
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
76
|
+
color: var(--text-primary, #333333);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* 亮色主题背景渐变 */
|
|
80
|
+
html[data-theme='light'] body.history {
|
|
81
|
+
background: linear-gradient(135deg, #6b8dd6 0%, #8e37d7 100%);
|
|
82
|
+
color: var(--text-primary, #333333);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* 亮色主题背景渐变 */
|
|
86
|
+
html[data-theme='light'] body.bookmarks {
|
|
87
|
+
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
|
|
88
|
+
color: var(--text-primary, #333333);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* 暗色主题背景渐变 */
|
|
92
|
+
html[data-theme='dark'] body.newtab {
|
|
93
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
94
|
+
color: var(--text-primary, #ffffff);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* 暗色主题背景渐变 */
|
|
98
|
+
html[data-theme='dark'] body.history {
|
|
99
|
+
background: linear-gradient(135deg, #121827 0%, #1f2a44 100%);
|
|
100
|
+
color: var(--text-primary, #ffffff);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* 暗色主题背景渐变 */
|
|
104
|
+
html[data-theme='dark'] body.bookmarks {
|
|
105
|
+
background: linear-gradient(135deg, #0f2027 0%, #203a43 100%);
|
|
106
|
+
color: var(--text-primary, #ffffff);
|
|
107
|
+
}
|