Develop (#5)
* feat: enhance StorageCleanerPage with accordion for storage options and immediate preference saving * feat: add reusable Button component and update StorageCleanerPage and TimestampPage to use it * feat: add StorageCleanerConfirm component for confirmation dialog in StorageCleanerPage * feat: implement fixed height and minimal scrollbar for Popup layout
This commit is contained in:
+48
-21
@@ -1,18 +1,4 @@
|
||||
/* 隐藏页面滚动条 */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* 隐藏body滚动条但允许滚动 */
|
||||
body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 极简滚动条变量 */
|
||||
:root {
|
||||
/* 统一圆角变量 */
|
||||
--radius: 8px;
|
||||
@@ -25,16 +11,54 @@ body {
|
||||
/* 统一按钮文字颜色变量 */
|
||||
--btn-text: #333333;
|
||||
--border: 1px solid #e0e0e0;
|
||||
|
||||
/* 滚动条变量 */
|
||||
--sb-width: 6px;
|
||||
--sb-thumb-color: rgba(0, 0, 0, 0.1);
|
||||
--sb-thumb-hover: rgba(0, 0, 0, 0.2);
|
||||
--sb-track-color: transparent;
|
||||
}
|
||||
|
||||
/* 强制固定视口尺寸 */
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
overflow: hidden; /* 禁用外层滚动 */
|
||||
}
|
||||
|
||||
/* 全局极简滚动条定制 */
|
||||
*::-webkit-scrollbar {
|
||||
width: var(--sb-width);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--sb-track-color);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--sb-thumb-color);
|
||||
border-radius: 10px;
|
||||
background-clip: content-box;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sb-thumb-hover);
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 400px;
|
||||
min-height: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden; /* 内部由 flex 子项控制滚动 */
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
@@ -52,9 +76,12 @@ body {
|
||||
.nav-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
gap: 8px;
|
||||
flex-shrink: 0; /* 禁止导航栏被挤压缩放 */
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 导航按钮 */
|
||||
|
||||
@@ -93,8 +93,20 @@ function App() {
|
||||
</button>
|
||||
</Box>
|
||||
</Box>
|
||||
{currentPage === 'timestamp' && <TimestampPage />}
|
||||
{currentPage === 'storageCleaner' && <StorageCleanerPage />}
|
||||
|
||||
{/* 统一滚动容器 */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
scrollbarGutter: 'stable',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{currentPage === 'timestamp' && <TimestampPage />}
|
||||
{currentPage === 'storageCleaner' && <StorageCleanerPage />}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Typography,
|
||||
Box,
|
||||
Checkbox,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
Alert,
|
||||
Snackbar,
|
||||
@@ -16,8 +15,14 @@ import {
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import Button from '@/components/Button';
|
||||
import StorageCleanerConfirm from '@/components/StorageCleanerConfirm';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import type { StorageCleanerOptions, CleaningResult, StorageCleanerPreferences } from 'types/storage';
|
||||
import type {
|
||||
StorageCleanerOptions,
|
||||
CleaningResult,
|
||||
StorageCleanerPreferences,
|
||||
} from 'types/storage';
|
||||
import {
|
||||
getCurrentTab,
|
||||
isRestrictedUrl,
|
||||
@@ -70,10 +75,7 @@ export default function StorageCleanerPage() {
|
||||
setDomain(new URL(tab.url).hostname);
|
||||
|
||||
// Load user preferences
|
||||
const prefs = await storageUtil.get(
|
||||
'storageCleaner/preferences',
|
||||
DEFAULT_PREFERENCES,
|
||||
);
|
||||
const prefs = await storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES);
|
||||
setAutoRefresh(prefs?.autoRefresh ?? DEFAULT_PREFERENCES.autoRefresh);
|
||||
setOptions(prefs?.selectedTypes ?? DEFAULT_PREFERENCES.selectedTypes);
|
||||
};
|
||||
@@ -84,10 +86,7 @@ export default function StorageCleanerPage() {
|
||||
const handleAutoRefreshChange = useCallback(async (checked: boolean) => {
|
||||
setAutoRefresh(checked);
|
||||
// Save preference immediately
|
||||
const prefs = await storageUtil.get(
|
||||
'storageCleaner/preferences',
|
||||
DEFAULT_PREFERENCES,
|
||||
);
|
||||
const prefs = await storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES);
|
||||
await storageUtil.set('storageCleaner/preferences', {
|
||||
...(prefs || DEFAULT_PREFERENCES),
|
||||
autoRefresh: checked,
|
||||
@@ -98,13 +97,12 @@ export default function StorageCleanerPage() {
|
||||
setOptions((prev) => {
|
||||
const newOptions = { ...prev, [key]: !prev[key] };
|
||||
// Save options immediately
|
||||
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES)
|
||||
.then(prefs => {
|
||||
storageUtil.set('storageCleaner/preferences', {
|
||||
...(prefs || DEFAULT_PREFERENCES),
|
||||
selectedTypes: newOptions,
|
||||
});
|
||||
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES).then((prefs) => {
|
||||
storageUtil.set('storageCleaner/preferences', {
|
||||
...(prefs || DEFAULT_PREFERENCES),
|
||||
selectedTypes: newOptions,
|
||||
});
|
||||
});
|
||||
return newOptions;
|
||||
});
|
||||
}, []);
|
||||
@@ -122,12 +120,9 @@ export default function StorageCleanerPage() {
|
||||
serviceWorkers: checked,
|
||||
};
|
||||
setOptions(newOptions);
|
||||
|
||||
|
||||
// Save options immediately
|
||||
const prefs = await storageUtil.get(
|
||||
'storageCleaner/preferences',
|
||||
DEFAULT_PREFERENCES,
|
||||
);
|
||||
const prefs = await storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES);
|
||||
await storageUtil.set('storageCleaner/preferences', {
|
||||
...(prefs || DEFAULT_PREFERENCES),
|
||||
selectedTypes: newOptions,
|
||||
@@ -193,9 +188,6 @@ export default function StorageCleanerPage() {
|
||||
<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>
|
||||
@@ -327,10 +319,8 @@ export default function StorageCleanerPage() {
|
||||
variant="contained"
|
||||
onClick={() => setShowConfirm(true)}
|
||||
sx={{
|
||||
py: 1.6, borderRadius: 3, fontSize: '1rem', fontWeight: 600, textTransform: 'none',
|
||||
bgcolor: 'primary.main', transition: 'all 0.2s',
|
||||
'&:hover': { bgcolor: 'primary.dark', transform: 'translateY(-1px)' },
|
||||
'&:active': { transform: 'translateY(0)' }
|
||||
bgcolor: 'primary.main',
|
||||
'&:hover': { bgcolor: 'primary.dark' },
|
||||
}}
|
||||
disabled={loading}
|
||||
fullWidth
|
||||
@@ -362,60 +352,12 @@ export default function StorageCleanerPage() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
<StorageCleanerConfirm
|
||||
open={showConfirm}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
onConfirm={handleClean}
|
||||
options={options}
|
||||
/>
|
||||
|
||||
{/* Snackbar */}
|
||||
<Snackbar
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import dayjs from '@/utils/dayjs';
|
||||
import {
|
||||
Button,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
@@ -21,6 +20,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import Button from '@/components/Button';
|
||||
|
||||
// ================= 常量配置 =================
|
||||
const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss';
|
||||
@@ -324,12 +324,6 @@ export default function TimestampPage() {
|
||||
<Button
|
||||
key={m} fullWidth disableRipple
|
||||
onClick={() => { setMode(m); setError(''); setResult(''); }}
|
||||
sx={{
|
||||
position: 'relative', zIndex: 1, borderRadius: 3, py: 1, textTransform: 'none',
|
||||
fontSize: '0.875rem', fontWeight: 500, transition: 'color 0.2s',
|
||||
color: mode === m ? 'text.primary' : 'text.disabled',
|
||||
'&:hover': { bgcolor: 'transparent', color: mode === m ? 'text.primary' : 'text.secondary' },
|
||||
}}
|
||||
>
|
||||
{m === 'ts2dt' ? '时间戳 → 日期' : '日期 → 时间戳'}
|
||||
</Button>
|
||||
@@ -384,12 +378,6 @@ export default function TimestampPage() {
|
||||
<Button
|
||||
fullWidth variant="contained" disableElevation disableRipple
|
||||
onClick={convert}
|
||||
sx={{
|
||||
py: 1.6, borderRadius: 3, fontSize: '1rem', fontWeight: 600, textTransform: 'none',
|
||||
bgcolor: 'primary.main', transition: 'all 0.2s',
|
||||
'&:hover': { bgcolor: 'primary.dark', transform: 'translateY(-1px)' },
|
||||
'&:active': { transform: 'translateY(0)' }
|
||||
}}
|
||||
>
|
||||
立即转换
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user