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:
@@ -0,0 +1,32 @@
|
|||||||
|
import { Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material';
|
||||||
|
|
||||||
|
export type ButtonProps = MuiButtonProps;
|
||||||
|
|
||||||
|
export function Button({ sx = [], ...props }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<MuiButton
|
||||||
|
disableElevation
|
||||||
|
disableRipple
|
||||||
|
{...props}
|
||||||
|
sx={[
|
||||||
|
{
|
||||||
|
py: 1.6,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
},
|
||||||
|
'&:active': {
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(Array.isArray(sx) ? sx : [sx]),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
} from '@mui/material';
|
||||||
|
import type { StorageCleanerOptions } from '@/types/storage';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
|
export interface StorageCleanerConfirmProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
options: StorageCleanerOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StorageCleanerConfirm({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
options,
|
||||||
|
}: StorageCleanerConfirmProps) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
fullWidth
|
||||||
|
maxWidth="xs"
|
||||||
|
slotProps={{
|
||||||
|
paper: {
|
||||||
|
sx: {
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundImage: 'none',
|
||||||
|
boxShadow: '0 8px 32px rgba(0,0,0,0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ textAlign: 'center', pb: 1, pt: 3, fontWeight: 700 }}>
|
||||||
|
确认清理
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ textAlign: 'center', pb: 2 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
将清理以下存储类型:
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'grey.50',
|
||||||
|
borderRadius: 3,
|
||||||
|
p: 2,
|
||||||
|
mb: 2,
|
||||||
|
display: 'inline-block',
|
||||||
|
textAlign: 'left',
|
||||||
|
minWidth: '60%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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">
|
||||||
|
此操作不可撤销。
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ p: 3, pt: 1, gap: 1 }}>
|
||||||
|
<Button variant="outlined" onClick={onClose} fullWidth>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" color="error" onClick={onConfirm} fullWidth>
|
||||||
|
确认清理
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StorageCleanerConfirm;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 实施计划:修复 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。
|
||||||
|
- 在“存储清理”页面展开所有选项,观察右侧是否出现极细滚动条,且不会挤压内容。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# 设计规范: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 展开时,页面水平方向不发生位移。
|
||||||
+48
-21
@@ -1,18 +1,4 @@
|
|||||||
/* 隐藏页面滚动条 */
|
/* 极简滚动条变量 */
|
||||||
html,
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 隐藏body滚动条但允许滚动 */
|
|
||||||
body {
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* 统一圆角变量 */
|
/* 统一圆角变量 */
|
||||||
--radius: 8px;
|
--radius: 8px;
|
||||||
@@ -25,16 +11,54 @@ body {
|
|||||||
/* 统一按钮文字颜色变量 */
|
/* 统一按钮文字颜色变量 */
|
||||||
--btn-text: #333333;
|
--btn-text: #333333;
|
||||||
--border: 1px solid #e0e0e0;
|
--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 {
|
.app {
|
||||||
width: 400px;
|
display: flex;
|
||||||
min-height: 100%;
|
flex-direction: column;
|
||||||
margin: 0 auto;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
overflow-x: hidden;
|
overflow: hidden; /* 内部由 flex 子项控制滚动 */
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
@@ -52,9 +76,12 @@ body {
|
|||||||
.nav-container {
|
.nav-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 16px;
|
padding-top: 16px;
|
||||||
margin-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
gap: 8px;
|
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>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* 统一滚动容器 */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
overflowY: 'auto',
|
||||||
|
scrollbarGutter: 'stable',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{currentPage === 'timestamp' && <TimestampPage />}
|
{currentPage === 'timestamp' && <TimestampPage />}
|
||||||
{currentPage === 'storageCleaner' && <StorageCleanerPage />}
|
{currentPage === 'storageCleaner' && <StorageCleanerPage />}
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Box,
|
Box,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Button,
|
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Alert,
|
Alert,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
@@ -16,8 +15,14 @@ import {
|
|||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import StorageCleanerConfirm from '@/components/StorageCleanerConfirm';
|
||||||
import { storageUtil } from '@/utils/chromeStorage';
|
import { storageUtil } from '@/utils/chromeStorage';
|
||||||
import type { StorageCleanerOptions, CleaningResult, StorageCleanerPreferences } from 'types/storage';
|
import type {
|
||||||
|
StorageCleanerOptions,
|
||||||
|
CleaningResult,
|
||||||
|
StorageCleanerPreferences,
|
||||||
|
} from 'types/storage';
|
||||||
import {
|
import {
|
||||||
getCurrentTab,
|
getCurrentTab,
|
||||||
isRestrictedUrl,
|
isRestrictedUrl,
|
||||||
@@ -70,10 +75,7 @@ export default function StorageCleanerPage() {
|
|||||||
setDomain(new URL(tab.url).hostname);
|
setDomain(new URL(tab.url).hostname);
|
||||||
|
|
||||||
// Load user preferences
|
// Load user preferences
|
||||||
const prefs = await storageUtil.get(
|
const prefs = await storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES);
|
||||||
'storageCleaner/preferences',
|
|
||||||
DEFAULT_PREFERENCES,
|
|
||||||
);
|
|
||||||
setAutoRefresh(prefs?.autoRefresh ?? DEFAULT_PREFERENCES.autoRefresh);
|
setAutoRefresh(prefs?.autoRefresh ?? DEFAULT_PREFERENCES.autoRefresh);
|
||||||
setOptions(prefs?.selectedTypes ?? DEFAULT_PREFERENCES.selectedTypes);
|
setOptions(prefs?.selectedTypes ?? DEFAULT_PREFERENCES.selectedTypes);
|
||||||
};
|
};
|
||||||
@@ -84,10 +86,7 @@ export default function StorageCleanerPage() {
|
|||||||
const handleAutoRefreshChange = useCallback(async (checked: boolean) => {
|
const handleAutoRefreshChange = useCallback(async (checked: boolean) => {
|
||||||
setAutoRefresh(checked);
|
setAutoRefresh(checked);
|
||||||
// Save preference immediately
|
// Save preference immediately
|
||||||
const prefs = await storageUtil.get(
|
const prefs = await storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES);
|
||||||
'storageCleaner/preferences',
|
|
||||||
DEFAULT_PREFERENCES,
|
|
||||||
);
|
|
||||||
await storageUtil.set('storageCleaner/preferences', {
|
await storageUtil.set('storageCleaner/preferences', {
|
||||||
...(prefs || DEFAULT_PREFERENCES),
|
...(prefs || DEFAULT_PREFERENCES),
|
||||||
autoRefresh: checked,
|
autoRefresh: checked,
|
||||||
@@ -98,8 +97,7 @@ export default function StorageCleanerPage() {
|
|||||||
setOptions((prev) => {
|
setOptions((prev) => {
|
||||||
const newOptions = { ...prev, [key]: !prev[key] };
|
const newOptions = { ...prev, [key]: !prev[key] };
|
||||||
// Save options immediately
|
// Save options immediately
|
||||||
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES)
|
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES).then((prefs) => {
|
||||||
.then(prefs => {
|
|
||||||
storageUtil.set('storageCleaner/preferences', {
|
storageUtil.set('storageCleaner/preferences', {
|
||||||
...(prefs || DEFAULT_PREFERENCES),
|
...(prefs || DEFAULT_PREFERENCES),
|
||||||
selectedTypes: newOptions,
|
selectedTypes: newOptions,
|
||||||
@@ -124,10 +122,7 @@ export default function StorageCleanerPage() {
|
|||||||
setOptions(newOptions);
|
setOptions(newOptions);
|
||||||
|
|
||||||
// Save options immediately
|
// Save options immediately
|
||||||
const prefs = await storageUtil.get(
|
const prefs = await storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES);
|
||||||
'storageCleaner/preferences',
|
|
||||||
DEFAULT_PREFERENCES,
|
|
||||||
);
|
|
||||||
await storageUtil.set('storageCleaner/preferences', {
|
await storageUtil.set('storageCleaner/preferences', {
|
||||||
...(prefs || DEFAULT_PREFERENCES),
|
...(prefs || DEFAULT_PREFERENCES),
|
||||||
selectedTypes: newOptions,
|
selectedTypes: newOptions,
|
||||||
@@ -193,9 +188,6 @@ export default function StorageCleanerPage() {
|
|||||||
<Paper sx={{ p: 2, m: 1, borderRadius: 2 }}>
|
<Paper sx={{ p: 2, m: 1, borderRadius: 2 }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box sx={{ textAlign: 'center', mb: 2 }}>
|
<Box sx={{ textAlign: 'center', mb: 2 }}>
|
||||||
<Typography variant="h5" component="h1" sx={{ mb: 1 }}>
|
|
||||||
存储清理
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
当前页面: {domain || '加载中...'}
|
当前页面: {domain || '加载中...'}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -327,10 +319,8 @@ export default function StorageCleanerPage() {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={() => setShowConfirm(true)}
|
onClick={() => setShowConfirm(true)}
|
||||||
sx={{
|
sx={{
|
||||||
py: 1.6, borderRadius: 3, fontSize: '1rem', fontWeight: 600, textTransform: 'none',
|
bgcolor: 'primary.main',
|
||||||
bgcolor: 'primary.main', transition: 'all 0.2s',
|
'&:hover': { bgcolor: 'primary.dark' },
|
||||||
'&:hover': { bgcolor: 'primary.dark', transform: 'translateY(-1px)' },
|
|
||||||
'&:active': { transform: 'translateY(0)' }
|
|
||||||
}}
|
}}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
fullWidth
|
fullWidth
|
||||||
@@ -362,60 +352,12 @@ export default function StorageCleanerPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Confirmation Dialog */}
|
{/* Confirmation Dialog */}
|
||||||
{showConfirm && (
|
<StorageCleanerConfirm
|
||||||
<Paper
|
open={showConfirm}
|
||||||
sx={{
|
onClose={() => setShowConfirm(false)}
|
||||||
position: 'absolute',
|
onConfirm={handleClean}
|
||||||
top: 0,
|
options={options}
|
||||||
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 */}
|
||||||
<Snackbar
|
<Snackbar
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import dayjs from '@/utils/dayjs';
|
import dayjs from '@/utils/dayjs';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -21,6 +20,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|||||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
|
||||||
// ================= 常量配置 =================
|
// ================= 常量配置 =================
|
||||||
const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss';
|
const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss';
|
||||||
@@ -324,12 +324,6 @@ export default function TimestampPage() {
|
|||||||
<Button
|
<Button
|
||||||
key={m} fullWidth disableRipple
|
key={m} fullWidth disableRipple
|
||||||
onClick={() => { setMode(m); setError(''); setResult(''); }}
|
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' ? '时间戳 → 日期' : '日期 → 时间戳'}
|
{m === 'ts2dt' ? '时间戳 → 日期' : '日期 → 时间戳'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -384,12 +378,6 @@ export default function TimestampPage() {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth variant="contained" disableElevation disableRipple
|
fullWidth variant="contained" disableElevation disableRipple
|
||||||
onClick={convert}
|
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>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user