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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user