Merge branch 'develop' of https://github.com/ljquan/aitu into develop
This commit is contained in:
commit
beeef8ba2a
6 files changed
+132
-30
No files matched your search
@@ -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,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user