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 页面,控制是否自动刷新存储数据列表。 * 用于 StorageCleaner 页面的操作区域内(非独立卡片),
* 以卡片形式展示,左侧为标签文本,右侧为 shadcn/ui Switch 开关。 * 控制清理后是否自动刷新页面。左侧为标签文本,右侧为 shadcn/ui Switch 开关。
* *
* @example * @example
* ```tsx * ```tsx
@@ -48,15 +48,14 @@ export default function AutoRefreshToggle({
return ( return (
<div <div
className={cn( 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, className,
)} )}
{...props} {...props}
> >
{/* Label 与 Switch 通过 htmlFor + id 关联,支持点击文字触发开关 */}
<Label <Label
htmlFor="auto-refresh-switch" 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')} {t('storageCleaner:autoRefresh')}
</Label> </Label>
+12 -9
View File
@@ -28,32 +28,35 @@ export default function OptionItem({
<div <div
onClick={onChange} onClick={onChange}
className={cn( 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 checked
? 'bg-primary/5 border-primary/30 shadow-sm' ? '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, className,
)} )}
{...props} {...props}
> >
{/* 左侧数据区域 */} <div className="flex-1 min-w-0 mr-3">
<div className="flex-1 min-w-0 mr-4">
<span <span
className={cn( className={cn(
'block text-xs font-semibold leading-tight truncate', 'block text-xs font-semibold leading-tight truncate transition-colors',
checked ? 'text-foreground font-bold' : 'text-foreground/80', checked ? 'text-foreground' : 'text-foreground/75',
)} )}
> >
{t(labelKey)} {t(labelKey)}
</span> </span>
{/* 底部容量大小或计数标识 */}
{size !== undefined && size > 0 ? ( {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)} {isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatSize(size)}
</span> </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')} {t('storageCleaner:noData')}
</span> </span>
)} )}
@@ -41,15 +41,9 @@ export default function StorageOptionsGrid({
}; };
return ( return (
<div <div className={cn('w-full overflow-hidden', className)} {...props}>
className={cn( <div className="px-0 pt-3.5 pb-2">
'w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden', <div className="grid grid-cols-2 gap-2 items-stretch">
className,
)}
{...props}
>
<div className="p-3">
<div className="grid grid-cols-2 gap-2.5 items-stretch">
{optionKeys.map(({ key, isCount }) => ( {optionKeys.map(({ key, isCount }) => (
<OptionItem <OptionItem
key={key} key={key}
@@ -65,9 +59,9 @@ export default function StorageOptionsGrid({
<div <div
onClick={handleToggleAll} 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')} {t('storageCleaner:selectAll')}
</Label> </Label>
@@ -75,7 +69,7 @@ export default function StorageOptionsGrid({
checked={allSelected ? true : someSelected ? 'indeterminate' : false} checked={allSelected ? true : someSelected ? 'indeterminate' : false}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onCheckedChange={(checked) => onSelectAll(checked === true)} 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>
</div> </div>
+35 -30
View File
@@ -1,6 +1,6 @@
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import StorageCleanerConfirm from '@/pages/StorageCleaner/StorageCleanerConfirm'; import StorageCleanerConfirm from './StorageCleanerConfirm';
import { useStorageCleaner } from './useStorageCleaner'; import { useStorageCleaner } from './useStorageCleaner';
import StorageOptionsGrid from './StorageOptionsGrid'; import StorageOptionsGrid from './StorageOptionsGrid';
import AutoRefreshToggle from './AutoRefreshToggle'; import AutoRefreshToggle from './AutoRefreshToggle';
@@ -47,37 +47,42 @@ export default function Index() {
} }
return ( return (
<div className="p-4 w-full flex flex-col space-y-3.5"> <div className="p-4 w-full flex flex-col space-y-4 select-none">
<StorageOptionsGrid {/* 操作区域卡片 */}
options={options} <div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden">
sizes={sizes} <StorageOptionsGrid
allSelected={allSelected} options={options}
someSelected={someSelected} sizes={sizes}
onOptionChange={handleOptionChange} allSelected={allSelected}
onSelectAll={handleSelectAll} someSelected={someSelected}
/> onOptionChange={handleOptionChange}
onSelectAll={handleSelectAll}
/>
<AutoRefreshToggle <AutoRefreshToggle
reloadAfterClean={reloadAfterClean} reloadAfterClean={reloadAfterClean}
onChange={handleReloadAfterCleanChange} onChange={handleReloadAfterCleanChange}
/> />
<Button <div className="px-3.5 pb-3.5 pt-1">
variant="destructive" <Button
size="default" variant="destructive"
onClick={() => setShowConfirm(true)} size="default"
disabled={isButtonDisabled} onClick={() => setShowConfirm(true)}
className="w-full h-10 font-bold shadow-sm text-sm tracking-wide" disabled={isButtonDisabled}
> className="w-full h-10 font-bold shadow-sm text-sm tracking-wide"
{loading ? ( >
<> {loading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <>
{t('storageCleaner:cleaning')} <Loader2 className="mr-2 h-4 w-4 animate-spin" />
</> {t('storageCleaner:cleaning')}
) : ( </>
t('storageCleaner:cleanNow') ) : (
)} t('storageCleaner:cleanNow')
</Button> )}
</Button>
</div>
</div>
<CleaningResult result={result} /> <CleaningResult result={result} />