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,42 @@
|
|
|
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 { NewTabHeader } from '../components/NewTabHeader';
|
|
8
|
+
import ThemeButton from '../components/ThemeButton';
|
|
9
|
+
import { useTheme } from '../hooks/useTheme';
|
|
10
|
+
import { BookmarksContent } from '../components/BookmarksContent';
|
|
11
|
+
import styles from './index.module.css';
|
|
12
|
+
|
|
13
|
+
const BookmarksApp: React.FC = () => {
|
|
14
|
+
const { isDarkMode, toggleTheme } = useTheme();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={styles.container}>
|
|
18
|
+
<div className={styles.themeButtonWrapper}>
|
|
19
|
+
<ThemeButton isDarkMode={isDarkMode} onChange={toggleTheme} />
|
|
20
|
+
</div>
|
|
21
|
+
<NewTabHeader title="React Extension - Bookmarks" />
|
|
22
|
+
<BookmarksContent />
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const BookmarksPage: React.FC = () => {
|
|
28
|
+
return <BookmarksApp />;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const container = document.getElementById('root');
|
|
32
|
+
if (container) {
|
|
33
|
+
const root = createRoot(container);
|
|
34
|
+
|
|
35
|
+
root.render(
|
|
36
|
+
<Provider store={store}>
|
|
37
|
+
<PersistGate loading={null} persistor={persistor}>
|
|
38
|
+
<BookmarksPage />
|
|
39
|
+
</PersistGate>
|
|
40
|
+
</Provider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -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 - Bookmarks</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body class="bookmarks">
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./bookmarks/index.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
.content {
|
|
2
|
+
width: 100%;
|
|
3
|
+
max-width: 960px;
|
|
4
|
+
margin: 120px auto 40px;
|
|
5
|
+
padding: 0 24px;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 16px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.card {
|
|
12
|
+
background: var(--bg-secondary);
|
|
13
|
+
border-radius: 16px;
|
|
14
|
+
padding: 24px;
|
|
15
|
+
box-shadow: 0 10px 30px var(--shadow-lg);
|
|
16
|
+
backdrop-filter: blur(8px);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.title {
|
|
20
|
+
font-size: 20px;
|
|
21
|
+
margin-bottom: 12px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.summary {
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
color: var(--text-secondary);
|
|
27
|
+
margin-bottom: 12px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.list {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 10px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.item {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: 4px;
|
|
40
|
+
padding: 10px 12px;
|
|
41
|
+
border-radius: 12px;
|
|
42
|
+
background: rgba(255, 255, 255, 0.08);
|
|
43
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.item:hover {
|
|
47
|
+
transform: translateY(-1px);
|
|
48
|
+
box-shadow: 0 6px 16px var(--shadow-md);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.itemTitle {
|
|
52
|
+
font-size: 15px;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
color: var(--text-primary);
|
|
55
|
+
word-break: break-word;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.itemUrl {
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
color: var(--text-secondary);
|
|
61
|
+
word-break: break-all;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.folder {
|
|
65
|
+
font-size: 14px;
|
|
66
|
+
color: var(--text-secondary);
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.empty {
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
color: var(--text-secondary);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.loading {
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
color: var(--text-secondary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.button {
|
|
81
|
+
margin-top: 8px;
|
|
82
|
+
padding: 8px 12px;
|
|
83
|
+
border-radius: 10px;
|
|
84
|
+
border: none;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
background: var(--color-primary);
|
|
87
|
+
color: #ffffff;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.button:hover {
|
|
93
|
+
transform: translateY(-1px);
|
|
94
|
+
box-shadow: 0 6px 16px var(--shadow-md);
|
|
95
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import styles from './index.module.css';
|
|
3
|
+
|
|
4
|
+
type FlatBookmark = {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
depth: number;
|
|
9
|
+
isFolder: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const BookmarksContent: React.FC = () => {
|
|
13
|
+
const [items, setItems] = useState<FlatBookmark[]>([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
chrome.bookmarks.getTree((tree) => {
|
|
18
|
+
const rootChildren = tree[0]?.children ?? [];
|
|
19
|
+
const flat: FlatBookmark[] = [];
|
|
20
|
+
|
|
21
|
+
const walk = (nodes: chrome.bookmarks.BookmarkTreeNode[], depth: number) => {
|
|
22
|
+
nodes.forEach((node) => {
|
|
23
|
+
const isFolder = !node.url;
|
|
24
|
+
flat.push({
|
|
25
|
+
id: node.id,
|
|
26
|
+
title: node.title || (isFolder ? '未命名文件夹' : '未命名书签'),
|
|
27
|
+
url: node.url,
|
|
28
|
+
depth,
|
|
29
|
+
isFolder,
|
|
30
|
+
});
|
|
31
|
+
if (node.children && node.children.length > 0) {
|
|
32
|
+
walk(node.children, depth + 1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
walk(rootChildren, 0);
|
|
38
|
+
setItems(flat);
|
|
39
|
+
setLoading(false);
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const summary = useMemo(() => {
|
|
44
|
+
if (loading) {
|
|
45
|
+
return '正在读取书签...';
|
|
46
|
+
}
|
|
47
|
+
if (items.length === 0) {
|
|
48
|
+
return '暂无书签数据。';
|
|
49
|
+
}
|
|
50
|
+
return `当前共读取 ${items.length} 条书签与文件夹。`;
|
|
51
|
+
}, [items.length, loading]);
|
|
52
|
+
|
|
53
|
+
const openItem = (url?: string) => {
|
|
54
|
+
if (!url) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
chrome.tabs.create({ url });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<main className={styles.content}>
|
|
62
|
+
<section className={styles.card}>
|
|
63
|
+
<h2 className={styles.title}>书签概览</h2>
|
|
64
|
+
<p className={styles.summary}>{summary}</p>
|
|
65
|
+
{loading ? (
|
|
66
|
+
<div className={styles.loading}>加载中...</div>
|
|
67
|
+
) : (
|
|
68
|
+
<div className={styles.list}>
|
|
69
|
+
{items.map((item) => (
|
|
70
|
+
<div
|
|
71
|
+
key={item.id}
|
|
72
|
+
className={styles.item}
|
|
73
|
+
style={{ marginLeft: item.depth * 12 }}
|
|
74
|
+
>
|
|
75
|
+
{item.isFolder ? (
|
|
76
|
+
<div className={styles.folder}>📁 {item.title}</div>
|
|
77
|
+
) : (
|
|
78
|
+
<>
|
|
79
|
+
<div className={styles.itemTitle}>{item.title}</div>
|
|
80
|
+
{item.url && (
|
|
81
|
+
<div className={styles.itemUrl}>{item.url}</div>
|
|
82
|
+
)}
|
|
83
|
+
</>
|
|
84
|
+
)}
|
|
85
|
+
{!item.isFolder && item.url && (
|
|
86
|
+
<button
|
|
87
|
+
className={styles.button}
|
|
88
|
+
onClick={() => openItem(item.url)}
|
|
89
|
+
>
|
|
90
|
+
打开书签
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</section>
|
|
98
|
+
</main>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
padding: var(--spacing-md);
|
|
3
|
+
background-color: var(--bg-secondary);
|
|
4
|
+
border-radius: var(--radius-md);
|
|
5
|
+
border: 1px solid var(--border-color);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.infoItem {
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
align-items: center;
|
|
12
|
+
padding: var(--spacing-sm) 0;
|
|
13
|
+
border-bottom: 1px solid var(--border-color-light);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.infoItem:last-child {
|
|
17
|
+
border-bottom: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.label {
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
color: var(--text-secondary);
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.value {
|
|
27
|
+
color: var(--text-primary);
|
|
28
|
+
font-size: 14px;
|
|
29
|
+
font-family: 'Courier New', monospace;
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useAppSelector } from '../../store/hooks';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import styles from './index.module.css';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 配置信息组件 - 展示配置的最后修改时间
|
|
8
|
+
* 这是一个示例组件,展示如何使用 updatedAt 字段
|
|
9
|
+
*/
|
|
10
|
+
export const ConfigInfo: React.FC = () => {
|
|
11
|
+
const updatedAt = useAppSelector((state) => state.config.updatedAt);
|
|
12
|
+
const isDarkMode = useAppSelector((state) => state.config.theme);
|
|
13
|
+
|
|
14
|
+
// 格式化日期显示
|
|
15
|
+
const formattedDate = dayjs(updatedAt).format('YYYY-MM-DD HH:mm:ss');
|
|
16
|
+
const relativeTime = dayjs(updatedAt).fromNow();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={styles.container}>
|
|
20
|
+
<div className={styles.infoItem}>
|
|
21
|
+
<span className={styles.label}>当前主题:</span>
|
|
22
|
+
<span className={styles.value}>{isDarkMode ? '暗色' : '亮色'}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div className={styles.infoItem}>
|
|
25
|
+
<span className={styles.label}>最后修改:</span>
|
|
26
|
+
<span className={styles.value} title={formattedDate}>
|
|
27
|
+
{relativeTime}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div className={styles.infoItem}>
|
|
31
|
+
<span className={styles.label}>修改时间:</span>
|
|
32
|
+
<span className={styles.value}>{formattedDate}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
.content {
|
|
2
|
+
width: 100%;
|
|
3
|
+
max-width: 960px;
|
|
4
|
+
margin: 120px auto 40px;
|
|
5
|
+
padding: 0 24px;
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 16px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.card {
|
|
12
|
+
background: var(--bg-secondary);
|
|
13
|
+
border-radius: 16px;
|
|
14
|
+
padding: 24px;
|
|
15
|
+
box-shadow: 0 10px 30px var(--shadow-lg);
|
|
16
|
+
backdrop-filter: blur(8px);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.title {
|
|
20
|
+
font-size: 20px;
|
|
21
|
+
margin-bottom: 12px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.summary {
|
|
25
|
+
font-size: 14px;
|
|
26
|
+
color: var(--text-secondary);
|
|
27
|
+
margin-bottom: 12px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.list {
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.item {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: 4px;
|
|
40
|
+
padding: 12px 14px;
|
|
41
|
+
border-radius: 12px;
|
|
42
|
+
background: rgba(255, 255, 255, 0.08);
|
|
43
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.item:hover {
|
|
47
|
+
transform: translateY(-1px);
|
|
48
|
+
box-shadow: 0 6px 16px var(--shadow-md);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.itemTitle {
|
|
52
|
+
font-size: 15px;
|
|
53
|
+
font-weight: 600;
|
|
54
|
+
color: var(--text-primary);
|
|
55
|
+
word-break: break-word;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.itemUrl {
|
|
59
|
+
font-size: 13px;
|
|
60
|
+
color: var(--text-secondary);
|
|
61
|
+
word-break: break-all;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.itemMeta {
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
color: var(--text-tertiary);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.empty {
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
color: var(--text-secondary);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.loading {
|
|
75
|
+
font-size: 14px;
|
|
76
|
+
color: var(--text-secondary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.action {
|
|
80
|
+
margin-top: 8px;
|
|
81
|
+
display: flex;
|
|
82
|
+
gap: 8px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.button {
|
|
86
|
+
padding: 8px 12px;
|
|
87
|
+
border-radius: 10px;
|
|
88
|
+
border: none;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
background: var(--color-primary);
|
|
91
|
+
color: #ffffff;
|
|
92
|
+
font-weight: 600;
|
|
93
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.button:hover {
|
|
97
|
+
transform: translateY(-1px);
|
|
98
|
+
box-shadow: 0 6px 16px var(--shadow-md);
|
|
99
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
import styles from './index.module.css';
|
|
4
|
+
|
|
5
|
+
const HISTORY_DAYS = 7;
|
|
6
|
+
const MAX_RESULTS = 20;
|
|
7
|
+
|
|
8
|
+
export const HistoryContent: React.FC = () => {
|
|
9
|
+
const [items, setItems] = useState<chrome.history.HistoryItem[]>([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const startTime = Date.now() - HISTORY_DAYS * 24 * 60 * 60 * 1000;
|
|
14
|
+
chrome.history.search(
|
|
15
|
+
{
|
|
16
|
+
text: '',
|
|
17
|
+
startTime,
|
|
18
|
+
maxResults: MAX_RESULTS,
|
|
19
|
+
},
|
|
20
|
+
(results) => {
|
|
21
|
+
const sorted = [...results].sort(
|
|
22
|
+
(a, b) => (b.lastVisitTime ?? 0) - (a.lastVisitTime ?? 0)
|
|
23
|
+
);
|
|
24
|
+
setItems(sorted);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const summary = useMemo(() => {
|
|
31
|
+
if (loading) {
|
|
32
|
+
return '正在加载最近的浏览记录...';
|
|
33
|
+
}
|
|
34
|
+
if (items.length === 0) {
|
|
35
|
+
return `最近 ${HISTORY_DAYS} 天没有历史记录。`;
|
|
36
|
+
}
|
|
37
|
+
return `最近 ${HISTORY_DAYS} 天访问记录(最多 ${MAX_RESULTS} 条)。`;
|
|
38
|
+
}, [items.length, loading]);
|
|
39
|
+
|
|
40
|
+
const openItem = (url?: string) => {
|
|
41
|
+
if (!url) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
chrome.tabs.create({ url });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<main className={styles.content}>
|
|
49
|
+
<section className={styles.card}>
|
|
50
|
+
<h2 className={styles.title}>浏览历史</h2>
|
|
51
|
+
<p className={styles.summary}>{summary}</p>
|
|
52
|
+
{loading ? (
|
|
53
|
+
<div className={styles.loading}>加载中...</div>
|
|
54
|
+
) : (
|
|
55
|
+
<div className={styles.list}>
|
|
56
|
+
{items.map((item) => (
|
|
57
|
+
<div key={item.id} className={styles.item}>
|
|
58
|
+
<div className={styles.itemTitle}>
|
|
59
|
+
{item.title || item.url || '未命名页面'}
|
|
60
|
+
</div>
|
|
61
|
+
{item.url && (
|
|
62
|
+
<div className={styles.itemUrl}>{item.url}</div>
|
|
63
|
+
)}
|
|
64
|
+
<div className={styles.itemMeta}>
|
|
65
|
+
{item.lastVisitTime
|
|
66
|
+
? dayjs(item.lastVisitTime).format('YYYY-MM-DD HH:mm')
|
|
67
|
+
: '未知访问时间'}
|
|
68
|
+
</div>
|
|
69
|
+
{item.url && (
|
|
70
|
+
<div className={styles.action}>
|
|
71
|
+
<button
|
|
72
|
+
className={styles.button}
|
|
73
|
+
onClick={() => openItem(item.url)}
|
|
74
|
+
>
|
|
75
|
+
打开页面
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
</section>
|
|
84
|
+
</main>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
.content {
|
|
2
|
+
flex: 1;
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
padding: var(--spacing-2xl) var(--spacing-lg);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.card {
|
|
10
|
+
background: var(--bg-primary);
|
|
11
|
+
border-radius: var(--radius-xl);
|
|
12
|
+
padding: var(--spacing-2xl);
|
|
13
|
+
max-width: 600px;
|
|
14
|
+
width: 100%;
|
|
15
|
+
box-shadow: 0 20px 60px var(--shadow-xl);
|
|
16
|
+
border: 1px solid var(--border-color-light);
|
|
17
|
+
transition: background-color 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.card h2 {
|
|
21
|
+
font-size: 28px;
|
|
22
|
+
color: var(--text-primary);
|
|
23
|
+
margin-bottom: var(--spacing-md);
|
|
24
|
+
margin-top: 0;
|
|
25
|
+
transition: color 0.3s ease;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.card p {
|
|
29
|
+
font-size: 16px;
|
|
30
|
+
color: var(--text-secondary);
|
|
31
|
+
line-height: 1.6;
|
|
32
|
+
margin-bottom: var(--spacing-lg);
|
|
33
|
+
margin-top: 0;
|
|
34
|
+
transition: color 0.3s ease;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.counter {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
gap: var(--spacing-md);
|
|
41
|
+
align-items: center;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.counter p {
|
|
45
|
+
font-size: 18px;
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
color: var(--color-primary);
|
|
48
|
+
margin: 0;
|
|
49
|
+
transition: color 0.3s ease;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[data-theme='dark'] .counter p {
|
|
53
|
+
color: var(--color-primary-light);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.button {
|
|
57
|
+
background: var(--button-bg-primary);
|
|
58
|
+
color: var(--text-inverse);
|
|
59
|
+
border: none;
|
|
60
|
+
padding: var(--spacing-md) var(--spacing-xl);
|
|
61
|
+
border-radius: var(--radius-md);
|
|
62
|
+
font-size: 16px;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
transition: transform 0.2s, box-shadow 0.2s, opacity 0.3s, background 0.3s ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.button:hover {
|
|
69
|
+
transform: translateY(-2px);
|
|
70
|
+
box-shadow: 0 4px 12px var(--shadow-primary);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.button:active {
|
|
74
|
+
transform: translateY(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.loading {
|
|
78
|
+
text-align: center;
|
|
79
|
+
color: var(--text-inverse);
|
|
80
|
+
font-size: 18px;
|
|
81
|
+
transition: color 0.3s ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
[data-theme='dark'] .loading {
|
|
85
|
+
color: var(--text-primary);
|
|
86
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useStorage } from '../../hooks/useStorage';
|
|
3
|
+
import styles from './index.module.css';
|
|
4
|
+
|
|
5
|
+
export const NewTabContent: React.FC = () => {
|
|
6
|
+
const [count, setCount, loading] = useStorage<number>('newtab_count', 0);
|
|
7
|
+
|
|
8
|
+
const handleIncrement = () => {
|
|
9
|
+
setCount(count + 1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
if (loading) {
|
|
13
|
+
return <div className={styles.loading}>加载中...</div>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<main className={styles.content}>
|
|
18
|
+
<div className={styles.card}>
|
|
19
|
+
<h2>欢迎使用新标签页</h2>
|
|
20
|
+
<p>这是一个使用 React + Vite 构建的新标签页</p>
|
|
21
|
+
<div className={styles.counter}>
|
|
22
|
+
<p>访问次数: {count}</p>
|
|
23
|
+
<button onClick={handleIncrement} className={styles.button}>
|
|
24
|
+
增加计数
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
.header {
|
|
2
|
+
background: linear-gradient(
|
|
3
|
+
135deg,
|
|
4
|
+
rgba(255, 255, 255, 0.5) 0%,
|
|
5
|
+
rgba(255, 255, 255, 0.3) 100%
|
|
6
|
+
);
|
|
7
|
+
backdrop-filter: blur(20px) saturate(180%);
|
|
8
|
+
padding: var(--spacing-xl);
|
|
9
|
+
text-align: center;
|
|
10
|
+
color: var(--text-inverse);
|
|
11
|
+
border-bottom: 2px solid rgba(255, 255, 255, 0.4);
|
|
12
|
+
transition: background 0.3s ease, border-color 0.3s ease;
|
|
13
|
+
box-shadow:
|
|
14
|
+
0 4px 20px rgba(0, 0, 0, 0.25),
|
|
15
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
16
|
+
inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
|
17
|
+
position: relative;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.header::before {
|
|
22
|
+
content: '';
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: 0;
|
|
25
|
+
left: 0;
|
|
26
|
+
right: 0;
|
|
27
|
+
bottom: 0;
|
|
28
|
+
background: linear-gradient(
|
|
29
|
+
to bottom,
|
|
30
|
+
rgba(255, 255, 255, 0.2) 0%,
|
|
31
|
+
transparent 50%,
|
|
32
|
+
rgba(0, 0, 0, 0.05) 100%
|
|
33
|
+
);
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-theme='dark'] .header {
|
|
38
|
+
background: linear-gradient(
|
|
39
|
+
135deg,
|
|
40
|
+
rgba(0, 0, 0, 0.6) 0%,
|
|
41
|
+
rgba(0, 0, 0, 0.4) 100%
|
|
42
|
+
);
|
|
43
|
+
border-bottom-color: rgba(255, 255, 255, 0.2);
|
|
44
|
+
box-shadow:
|
|
45
|
+
0 4px 20px rgba(0, 0, 0, 0.5),
|
|
46
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
|
47
|
+
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-theme='dark'] .header::before {
|
|
51
|
+
background: linear-gradient(
|
|
52
|
+
to bottom,
|
|
53
|
+
rgba(255, 255, 255, 0.08) 0%,
|
|
54
|
+
transparent 50%,
|
|
55
|
+
rgba(0, 0, 0, 0.1) 100%
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.header h1 {
|
|
60
|
+
font-size: 32px;
|
|
61
|
+
font-weight: 700;
|
|
62
|
+
margin: 0;
|
|
63
|
+
color: #ffffff;
|
|
64
|
+
text-shadow: var(--text-shadow-header);
|
|
65
|
+
letter-spacing: 1.2px;
|
|
66
|
+
position: relative;
|
|
67
|
+
z-index: 1;
|
|
68
|
+
-webkit-font-smoothing: antialiased;
|
|
69
|
+
-moz-osx-font-smoothing: grayscale;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[data-theme='dark'] .header h1 {
|
|
73
|
+
text-shadow: var(--text-shadow-header-dark);
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: #ffffff;
|
|
76
|
+
}
|