Merge branch 'develop' of https://github.com/ljquan/aitu into develop

This commit is contained in:
liquidliang committed 2026-01-21 19:26:49 +08:00
commit beeef8ba2a
6 files changed
+132 -30

No files matched your search

+53
View File
@@ -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)
**场景**: 为面板、卡片或容器定义边界时。
+1 -1
View File
@@ -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": []
}
@@ -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;
}
}
@@ -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({
<div className="media-library-grid__header-spacer" />
<div className="media-library-grid__sort-options">
{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 (
<Tooltip key={opt.value} content={opt.label} placement="top" theme="light" showArrow={false}>
<Tooltip
key={group.id}
content={`${group.label}: ${isAsc ? '正序' : (isDesc ? '逆序' : '默认')}`}
placement="top"
theme="light"
showArrow={false}
>
<div
className={`media-library-grid__filter-option ${isActive ? 'media-library-grid__filter-option--active' : ''}`}
onClick={() => setFilters({ sortBy: opt.value as SortOption })}
onClick={handleSortClick}
>
<Icon size={14} strokeWidth={1.5} />
</div>
+1 -1
View File
@@ -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
@@ -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;
}