diff --git a/CLAUDE.md b/CLAUDE.md index d2dd7d13..04bbf7ef 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3935,6 +3935,59 @@ const allCategories = useMemo(() => { **原因**: 项目中的 `z-index` 有严格的分层规范(参见 `docs/Z_INDEX_GUIDE.md`)。工具栏位于 2000 层,而抽屉应该位于 4000 层及以上。硬编码低层级会破坏预留宽度的视觉预期。 +### 交互规范: 三段式循环排序模式 + +**场景**: 实现包含正序、逆序且需要支持恢复默认状态的排序按钮时。 + +❌ **错误示例**: +使用多个独立按钮分别代表正序和逆序,或者简单的二段式切换(无法方便地回到默认排序)。 + +✅ **正确示例**: +使用单个按钮循环切换:`正序 -> 逆序 -> 恢复默认排序(如日期降序)`。 +```typescript +const handleSortClick = () => { + if (currentSort === group.options.asc) { + setFilters({ sortBy: group.options.desc }); // 切换到逆序 + } else if (currentSort === group.options.desc) { + setFilters({ sortBy: 'DATE_DESC' }); // 恢复默认 + } else { + setFilters({ sortBy: group.options.asc }); // 切换到正序 + } +}; +``` + +**原因**: 这种模式在节省 UI 空间的同时,能让用户在有限的点击次数内触达所有排序状态,且逻辑闭环。 + +### 样式规范: 筛选岛(Island)组件的间距与对齐 + +**场景**: 在紧凑的水平排列筛选组中显示图标和计数标签时。 + +❌ **错误示例**: +```scss +.filter-option { + width: 32px; // 固定宽度在数字变长时会溢出 + .count { + position: absolute; // 绝对定位容易导致与图标重叠 + right: 2px; + } +} +``` + +✅ **正确示例**: +```scss +.filter-option { + padding: 0 8px; // 使用 padding 适应不同宽度的数字 + display: flex; + align-items: center; + gap: 4px; // 为图标和计数预留固定间距 + .count { + font-size: 11px; // 保持精致感 + } +} +``` + +**原因**: 筛选组通常包含数量反馈,个位数和多位数占用的空间不同。使用弹性布局(Flex + gap)能确保在任何数据状态下 UI 都是对齐且易读的。 + ### UI 设计原则: 以影代框(Shadows Over Borders) **场景**: 为面板、卡片或容器定义边界时。 diff --git a/apps/web/public/version.json b/apps/web/public/version.json index 889f2340..16b34270 100644 --- a/apps/web/public/version.json +++ b/apps/web/public/version.json @@ -1,6 +1,6 @@ { "version": "0.5.38", - "buildTime": "2026-01-21T10:05:43.670Z", + "buildTime": "2026-01-21T10:22:57.427Z", "gitCommit": "unknown", "changelog": [] } \ No newline at end of file diff --git a/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss b/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss index 94efff32..f1c2a065 100644 --- a/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss +++ b/packages/drawnix/src/components/media-library/MediaLibraryGrid.scss @@ -145,15 +145,15 @@ $brand-orange-dark: #E67E22; display: flex; align-items: center; background: #f1f5f9; - padding: 2px; + padding: 3px; border-radius: 8px; - gap: 2px; + gap: 4px; } &__filter-group { display: flex; align-items: center; - gap: 2px; + gap: 4px; } &__filter-separator { @@ -164,8 +164,8 @@ $brand-orange-dark: #E67E22; } &__filter-option { - width: 32px; - height: 28px; + padding: 0 8px; + height: 30px; display: flex; align-items: center; justify-content: center; @@ -175,10 +175,11 @@ $brand-orange-dark: #E67E22; transition: all 0.15s ease; user-select: none; position: relative; + gap: 4px; svg { - width: 14px; - height: 14px; + width: 15px; + height: 15px; flex-shrink: 0; opacity: 0.8; @@ -217,29 +218,25 @@ $brand-orange-dark: #E67E22; } &__filter-count { - position: absolute; - bottom: 2px; - right: 2px; - font-size: 9px; + font-size: 11px; font-weight: 600; color: #94a3b8; line-height: 1; - transform: scale(0.85); - transform-origin: bottom right; letter-spacing: -0.02em; + margin-bottom: -1px; // 对齐图标 } &__sort-options { display: flex; align-items: center; background: #f1f5f9; - padding: 2px; + padding: 3px; border-radius: 8px; - gap: 2px; + gap: 4px; .media-library-grid__filter-option { padding: 0; - width: 28px; + width: 30px; } } diff --git a/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx b/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx index 40c85773..5bdd4397 100644 --- a/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx +++ b/packages/drawnix/src/components/media-library/MediaLibraryGrid.tsx @@ -17,8 +17,10 @@ import { Sparkles, Clock, Calendar, - SortAsc, + ArrowUpAZ, + ArrowDownZA, ArrowDownWideNarrow, + ArrowUpNarrowWide, Minus, Plus } from 'lucide-react'; @@ -88,12 +90,29 @@ const SOURCE_OPTIONS = [ { value: AssetSource.AI_GENERATED, label: 'AI生成', icon: Sparkles, countKey: 'ai' }, ]; -// 排序选项 -const SORT_OPTIONS: { value: SortOption; label: string; icon: any }[] = [ - { value: 'DATE_DESC', label: '最新优先', icon: Clock }, - { value: 'DATE_ASC', label: '最旧优先', icon: Calendar }, - { value: 'NAME_ASC', label: '名称 A-Z', icon: SortAsc }, - { value: 'SIZE_DESC', label: '大小优先', icon: ArrowDownWideNarrow }, +// 排序组定义 +const SORT_GROUPS = [ + { + id: 'DATE', + label: '时间', + options: { asc: 'DATE_ASC' as SortOption, desc: 'DATE_DESC' as SortOption }, + icons: { asc: Calendar, desc: Clock }, + default: 'DATE_DESC' as SortOption + }, + { + id: 'NAME', + label: '名称', + options: { asc: 'NAME_ASC' as SortOption, desc: 'NAME_DESC' as SortOption }, + icons: { asc: ArrowUpAZ, desc: ArrowDownZA }, + default: 'NAME_ASC' as SortOption + }, + { + id: 'SIZE', + label: '大小', + options: { asc: 'SIZE_ASC' as SortOption, desc: 'SIZE_DESC' as SortOption }, + icons: { asc: ArrowUpNarrowWide, desc: ArrowDownWideNarrow }, + default: 'SIZE_DESC' as SortOption + }, ]; export function MediaLibraryGrid({ @@ -456,14 +475,43 @@ export function MediaLibraryGrid({
- {SORT_OPTIONS.map(opt => { - const Icon = opt.icon; - const isActive = (filters.sortBy || 'DATE_DESC') === opt.value; + {SORT_GROUPS.map(group => { + const currentSort = filters.sortBy || 'DATE_DESC'; + const isAsc = currentSort === group.options.asc; + const isDesc = currentSort === group.options.desc; + const isActive = isAsc || isDesc; + const Icon = isAsc ? group.icons.asc : group.icons.desc; + + const handleSortClick = () => { + if (currentSort === group.options.asc) { + // 当前是正序 -> 切换到逆序 + setFilters({ sortBy: group.options.desc }); + } else if (currentSort === group.options.desc) { + // 当前是逆序 + if (currentSort === 'DATE_DESC' && group.id === 'DATE') { + // 如果当前已经是默认的日期逆序,点一下进入日期正序 + setFilters({ sortBy: 'DATE_ASC' }); + } else { + // 否则恢复默认排序 (DATE_DESC) + setFilters({ sortBy: 'DATE_DESC' }); + } + } else { + // 当前不在该组 -> 切换到正序 + setFilters({ sortBy: group.options.asc }); + } + }; + return ( - +
setFilters({ sortBy: opt.value as SortOption })} + onClick={handleSortClick} >
diff --git a/packages/drawnix/src/types/asset.types.ts b/packages/drawnix/src/types/asset.types.ts index a0a9bdcc..e24debc7 100644 --- a/packages/drawnix/src/types/asset.types.ts +++ b/packages/drawnix/src/types/asset.types.ts @@ -117,7 +117,7 @@ export type AssetSourceFilter = 'ALL' | AssetSource; * Sort Option * 排序选项 */ -export type SortOption = 'DATE_DESC' | 'DATE_ASC' | 'NAME_ASC' | 'SIZE_DESC'; +export type SortOption = 'DATE_DESC' | 'DATE_ASC' | 'NAME_ASC' | 'NAME_DESC' | 'SIZE_ASC' | 'SIZE_DESC'; /** * View Mode diff --git a/packages/drawnix/src/utils/asset-utils.ts b/packages/drawnix/src/utils/asset-utils.ts index 4e6948e1..c4c58c54 100644 --- a/packages/drawnix/src/utils/asset-utils.ts +++ b/packages/drawnix/src/utils/asset-utils.ts @@ -99,8 +99,12 @@ export function filterAssets( return a.createdAt - b.createdAt; case 'NAME_ASC': return a.name.localeCompare(b.name); + case 'NAME_DESC': + return b.name.localeCompare(a.name); case 'SIZE_DESC': return (b.size || 0) - (a.size || 0); + case 'SIZE_ASC': + return (a.size || 0) - (b.size || 0); default: return 0; }