refactor(StorageCleaner): improve style consistency and checkbox alignment

- Group storage options, auto-refresh toggle, and clean button into unified card
- Remove card wrappers from StorageOptionsGrid and AutoRefreshToggle (now sections within parent card)
- Align OptionItem and Select All checkboxes with consistent px-3.5 padding
- Distinguish indeterminate vs checked checkbox states visually
- Standardize spacing (space-y-4), add select-none to page root
- Fix import path for StorageCleanerConfirm to relative ./
This commit is contained in:
雨霖铃
2026-05-28 23:43:02 +08:00
parent 4f7e402dba
commit 77168e2652
4 changed files with 58 additions and 57 deletions
@@ -1,8 +1,8 @@
/**
* 自动刷新开关组件
* 自动刷新开关
*
* 用于 StorageCleaner 页面,控制是否自动刷新存储数据列表。
* 以卡片形式展示,左侧为标签文本,右侧为 shadcn/ui Switch 开关。
* 用于 StorageCleaner 页面的操作区域内(非独立卡片),
* 控制清理后是否自动刷新页面。左侧为标签文本,右侧为 shadcn/ui Switch 开关。
*
* @example
* ```tsx
@@ -48,15 +48,14 @@ export default function AutoRefreshToggle({
return (
<div
className={cn(
'w-full p-4 rounded-xl border border-border bg-card text-card-foreground shadow-sm flex justify-between items-center',
'w-full px-3.5 py-3 border-t border-border flex justify-between items-center',
className,
)}
{...props}
>
{/* Label 与 Switch 通过 htmlFor + id 关联,支持点击文字触发开关 */}
<Label
htmlFor="auto-refresh-switch"
className="text-sm font-bold text-foreground cursor-pointer select-none tracking-tight"
className="text-xs font-bold text-muted-foreground/90 cursor-pointer select-none tracking-wide uppercase"
>
{t('storageCleaner:autoRefresh')}
</Label>
+12 -9
View File
@@ -28,32 +28,35 @@ export default function OptionItem({
<div
onClick={onChange}
className={cn(
'flex justify-between items-center py-2.5 px-3.5 rounded-xl border cursor-pointer select-none',
'flex justify-between items-center py-2.5 px-3.5 rounded-lg border cursor-pointer select-none transition-colors',
checked
? 'bg-primary/5 border-primary/30 shadow-sm'
: 'bg-transparent border-transparent hover:bg-muted/70',
: 'bg-transparent border-transparent hover:bg-muted/50',
className,
)}
{...props}
>
{/* 左侧数据区域 */}
<div className="flex-1 min-w-0 mr-4">
<div className="flex-1 min-w-0 mr-3">
<span
className={cn(
'block text-xs font-semibold leading-tight truncate',
checked ? 'text-foreground font-bold' : 'text-foreground/80',
'block text-xs font-semibold leading-tight truncate transition-colors',
checked ? 'text-foreground' : 'text-foreground/75',
)}
>
{t(labelKey)}
</span>
{/* 底部容量大小或计数标识 */}
{size !== undefined && size > 0 ? (
<span className="block text-[10px] font-mono font-medium text-muted-foreground/80 mt-0.5 tabular-nums">
<span
className={cn(
'block text-[10px] font-mono font-medium mt-0.5 tabular-nums transition-colors',
checked ? 'text-primary/70' : 'text-muted-foreground/70',
)}
>
{isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)}
</span>
) : (
<span className="block text-[10px] font-medium text-muted-foreground/60 mt-0.5 italic">
<span className="block text-[10px] font-medium text-muted-foreground/50 mt-0.5 italic">
{t('storageCleaner:noData')}
</span>
)}
@@ -41,15 +41,9 @@ export default function StorageOptionsGrid({
};
return (
<div
className={cn(
'w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden',
className,
)}
{...props}
>
<div className="p-3">
<div className="grid grid-cols-2 gap-2.5 items-stretch">
<div className={cn('w-full overflow-hidden', className)} {...props}>
<div className="px-0 pt-3.5 pb-2">
<div className="grid grid-cols-2 gap-2 items-stretch">
{optionKeys.map(({ key, isCount }) => (
<OptionItem
key={key}
@@ -65,9 +59,9 @@ export default function StorageOptionsGrid({
<div
onClick={handleToggleAll}
className="border-t border-border flex justify-between items-center px-4 py-2.5 bg-muted/20 hover:bg-muted/50 cursor-pointer select-none"
className="border-t border-border flex justify-between items-center px-3.5 py-2.5 bg-muted/20 hover:bg-muted/40 cursor-pointer select-none transition-colors"
>
<Label className="text-xs font-bold text-muted-foreground/90 cursor-pointer">
<Label className="text-xs font-bold text-muted-foreground/90 cursor-pointer tracking-wide uppercase">
{t('storageCleaner:selectAll')}
</Label>
@@ -75,7 +69,7 @@ export default function StorageOptionsGrid({
checked={allSelected ? true : someSelected ? 'indeterminate' : false}
onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => onSelectAll(checked === true)}
className="h-4 w-4 shrink-0 rounded border-input data-[state=checked]:bg-primary data-[state=indeterminate]:bg-primary"
className="h-4 w-4 shrink-0 rounded border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-muted-foreground/40"
/>
</div>
</div>
+35 -30
View File
@@ -1,6 +1,6 @@
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import StorageCleanerConfirm from '@/pages/StorageCleaner/StorageCleanerConfirm';
import StorageCleanerConfirm from './StorageCleanerConfirm';
import { useStorageCleaner } from './useStorageCleaner';
import StorageOptionsGrid from './StorageOptionsGrid';
import AutoRefreshToggle from './AutoRefreshToggle';
@@ -47,37 +47,42 @@ export default function Index() {
}
return (
<div className="p-4 w-full flex flex-col space-y-3.5">
<StorageOptionsGrid
options={options}
sizes={sizes}
allSelected={allSelected}
someSelected={someSelected}
onOptionChange={handleOptionChange}
onSelectAll={handleSelectAll}
/>
<div className="p-4 w-full flex flex-col space-y-4 select-none">
{/* 操作区域卡片 */}
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
<StorageOptionsGrid
options={options}
sizes={sizes}
allSelected={allSelected}
someSelected={someSelected}
onOptionChange={handleOptionChange}
onSelectAll={handleSelectAll}
/>
<AutoRefreshToggle
reloadAfterClean={reloadAfterClean}
onChange={handleReloadAfterCleanChange}
/>
<AutoRefreshToggle
reloadAfterClean={reloadAfterClean}
onChange={handleReloadAfterCleanChange}
/>
<Button
variant="destructive"
size="default"
onClick={() => setShowConfirm(true)}
disabled={isButtonDisabled}
className="w-full h-10 font-bold shadow-sm text-sm tracking-wide"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('storageCleaner:cleaning')}
</>
) : (
t('storageCleaner:cleanNow')
)}
</Button>
<div className="px-3.5 pb-3.5 pt-1">
<Button
variant="destructive"
size="default"
onClick={() => setShowConfirm(true)}
disabled={isButtonDisabled}
className="w-full h-10 font-bold shadow-sm text-sm tracking-wide"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t('storageCleaner:cleaning')}
</>
) : (
t('storageCleaner:cleanNow')
)}
</Button>
</div>
</div>
<CleaningResult result={result} />