mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-08 22:36:37 +08:00
优化
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState, type DragEvent } from 'react'
|
||||||
import { Layers, Plus, Settings2, X } from 'lucide-react'
|
import { GripVertical, Layers, Plus, Settings2, X } from 'lucide-react'
|
||||||
|
|
||||||
import { MAX_LIBRARY_TAGS } from '../utils/libraryTags'
|
import { ALL_TAG_ID, MAX_LIBRARY_TAGS } from '../utils/libraryTags'
|
||||||
import type { LibraryTagTab } from '../utils/libraryTags'
|
import type { LibraryTagTab } from '../utils/libraryTags'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 媒体库标签栏:固定「全部」+ 每个标签一个页签,选中后只展示该标签下的媒体库。
|
* 媒体库标签栏:固定「全部」+ 每个标签一个页签,选中后只展示该标签下的媒体库。
|
||||||
* 只是管理员/普通用户都能用的展示层过滤,不改动媒体库本身。
|
* 只是管理员/普通用户都能用的展示层过滤,不改动媒体库本身。
|
||||||
|
* 传入 onReorder 时标签页签可拖拽排序:拖拽只作用于「全部」之后的标签,
|
||||||
|
* 「全部」永远固定在第一位,不参与拖动。
|
||||||
*/
|
*/
|
||||||
export function LibraryTagBar({
|
export function LibraryTagBar({
|
||||||
tabs,
|
tabs,
|
||||||
@@ -14,6 +16,7 @@ export function LibraryTagBar({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onCreate,
|
onCreate,
|
||||||
onManage,
|
onManage,
|
||||||
|
onReorder,
|
||||||
manageLabel = '管理标签',
|
manageLabel = '管理标签',
|
||||||
showCreate = true,
|
showCreate = true,
|
||||||
busy = false,
|
busy = false,
|
||||||
@@ -23,6 +26,8 @@ export function LibraryTagBar({
|
|||||||
onSelect: (tagId: string) => void
|
onSelect: (tagId: string) => void
|
||||||
onCreate?: (name: string) => void
|
onCreate?: (name: string) => void
|
||||||
onManage?: () => void
|
onManage?: () => void
|
||||||
|
/** 拖拽排序的落点:收到的是拖拽后的标签名顺序(不含「全部」)。 */
|
||||||
|
onReorder?: (orderedNames: string[]) => void | Promise<void>
|
||||||
manageLabel?: string
|
manageLabel?: string
|
||||||
showCreate?: boolean
|
showCreate?: boolean
|
||||||
busy?: boolean
|
busy?: boolean
|
||||||
@@ -32,6 +37,8 @@ export function LibraryTagBar({
|
|||||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||||
// 同一帧内 blur 与 Enter 都可能触发提交:用一个标记保证只提交一次。
|
// 同一帧内 blur 与 Enter 都可能触发提交:用一个标记保证只提交一次。
|
||||||
const submittedRef = useRef(false)
|
const submittedRef = useRef(false)
|
||||||
|
const [draggingId, setDraggingId] = useState<string | null>(null)
|
||||||
|
const [dragOverId, setDragOverId] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (creating) {
|
if (creating) {
|
||||||
@@ -42,6 +49,25 @@ export function LibraryTagBar({
|
|||||||
|
|
||||||
const hasTags = useMemo(() => tabs.some((tab) => !tab.isAll), [tabs])
|
const hasTags = useMemo(() => tabs.some((tab) => !tab.isAll), [tabs])
|
||||||
|
|
||||||
|
/** 拖拽结束后计算落点顺序并回调。 */
|
||||||
|
const handleDrop = (e: DragEvent, overTabId: string) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const fromId = draggingId ?? e.dataTransfer.getData('text/plain')
|
||||||
|
setDraggingId(null)
|
||||||
|
setDragOverId(null)
|
||||||
|
if (!onReorder || !fromId || fromId === ALL_TAG_ID || fromId === overTabId) return
|
||||||
|
const names = tabs
|
||||||
|
.filter((tab) => !tab.isAll)
|
||||||
|
.map((tab) => tab.id)
|
||||||
|
const fromIndex = names.indexOf(fromId)
|
||||||
|
const overIndex = names.indexOf(overTabId)
|
||||||
|
if (fromIndex < 0 || overIndex < 0) return
|
||||||
|
const next = [...names]
|
||||||
|
const [moved] = next.splice(fromIndex, 1)
|
||||||
|
next.splice(overIndex, 0, moved)
|
||||||
|
void onReorder(next)
|
||||||
|
}
|
||||||
|
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (submittedRef.current) return
|
if (submittedRef.current) return
|
||||||
submittedRef.current = true
|
submittedRef.current = true
|
||||||
@@ -63,24 +89,64 @@ export function LibraryTagBar({
|
|||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const active = tab.id === selectedTagId
|
const active = tab.id === selectedTagId
|
||||||
|
const draggable = !tab.isAll && Boolean(onReorder)
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelect(tab.id)}
|
onClick={() => onSelect(tab.id)}
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
className={`inline-flex items-center gap-1.5 rounded-xl border px-3 py-1.5 text-xs font-bold transition-colors ${
|
draggable={draggable}
|
||||||
active
|
onDragStart={(e) => {
|
||||||
? 'border-brand-500/60 bg-brand-500/10 text-brand-600'
|
if (!draggable) return
|
||||||
: 'border-[var(--app-border)] bg-[var(--app-panel-soft)] text-[var(--app-subtle)] hover:border-brand-500/40 hover:text-[var(--app-text)]'
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
e.dataTransfer.setData('text/plain', tab.id)
|
||||||
|
setDraggingId(tab.id)
|
||||||
|
setDragOverId(null)
|
||||||
|
}}
|
||||||
|
onDragOver={(e) => {
|
||||||
|
if (draggingId === tab.id || tab.isAll) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
if (dragOverId !== tab.id) setDragOverId(tab.id)
|
||||||
|
}}
|
||||||
|
onDragLeave={() => {
|
||||||
|
setDragOverId((prev) => (prev === tab.id ? null : prev))
|
||||||
|
}}
|
||||||
|
onDrop={(e) => handleDrop(e, tab.id)}
|
||||||
|
onDragEnd={() => {
|
||||||
|
setDraggingId(null)
|
||||||
|
setDragOverId(null)
|
||||||
|
}}
|
||||||
|
className={`group inline-flex cursor-default items-center gap-1.5 rounded-xl border px-3 py-1.5 text-xs font-bold transition-colors ${
|
||||||
|
draggingId === tab.id
|
||||||
|
? 'opacity-40'
|
||||||
|
: dragOverId === tab.id
|
||||||
|
? 'border-brand-500 bg-brand-500/10 text-brand-600'
|
||||||
|
: active
|
||||||
|
? 'border-brand-500/60 bg-brand-500/10 text-brand-600'
|
||||||
|
: 'border-[var(--app-border)] bg-[var(--app-panel-soft)] text-[var(--app-subtle)] hover:border-brand-500/40 hover:text-[var(--app-text)]'
|
||||||
}`}
|
}`}
|
||||||
title={tab.isAll ? '显示全部媒体库' : `只看「${tab.name}」标签下的媒体库`}
|
title={
|
||||||
|
tab.isAll
|
||||||
|
? '显示全部媒体库'
|
||||||
|
: draggable
|
||||||
|
? `只看「${tab.name}」标签下的媒体库(拖拽可调整顺序)`
|
||||||
|
: `只看「${tab.name}」标签下的媒体库`
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{tab.isAll && <Layers size={13} />}
|
{tab.isAll && <Layers size={13} />}
|
||||||
<span className="max-w-[8rem] truncate">{tab.name}</span>
|
<span className="max-w-[8rem] truncate">{tab.name}</span>
|
||||||
<span className={`font-mono text-[10px] ${active ? 'text-brand-500' : 'text-[var(--app-muted)]'}`}>
|
<span className={`font-mono text-[10px] ${active ? 'text-brand-500' : 'text-[var(--app-muted)]'}`}>
|
||||||
{tab.count}
|
{tab.count}
|
||||||
</span>
|
</span>
|
||||||
|
{draggable && (
|
||||||
|
<GripVertical
|
||||||
|
size={12}
|
||||||
|
className="shrink-0 cursor-grab text-[var(--app-muted)] opacity-0 transition-opacity group-hover:opacity-100 active:cursor-grabbing"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
normalizeTagName,
|
normalizeTagName,
|
||||||
readCachedLibraryTags,
|
readCachedLibraryTags,
|
||||||
readSelectedTagId,
|
readSelectedTagId,
|
||||||
|
reorderLibraryTags,
|
||||||
resolveSelectedTagId,
|
resolveSelectedTagId,
|
||||||
saveLibraryTags,
|
saveLibraryTags,
|
||||||
writeCachedLibraryTags,
|
writeCachedLibraryTags,
|
||||||
@@ -32,6 +33,8 @@ export type UseLibraryTagsResult = {
|
|||||||
setSelectedTagId: (tagId: string) => void
|
setSelectedTagId: (tagId: string) => void
|
||||||
createTag: (name: string) => Promise<string>
|
createTag: (name: string) => Promise<string>
|
||||||
renameTag: (from: string, to: string) => Promise<void>
|
renameTag: (from: string, to: string) => Promise<void>
|
||||||
|
/** 按给定标签名顺序重排标签栏(拖拽排序的落点)。 */
|
||||||
|
reorderTags: (orderedNames: string[]) => Promise<void>
|
||||||
removeTag: (name: string) => Promise<void>
|
removeTag: (name: string) => Promise<void>
|
||||||
/** 覆盖某个标签下的媒体库集合(保持传入顺序)。 */
|
/** 覆盖某个标签下的媒体库集合(保持传入顺序)。 */
|
||||||
setTagLibraries: (name: string, libraryIds: string[]) => Promise<void>
|
setTagLibraries: (name: string, libraryIds: string[]) => Promise<void>
|
||||||
@@ -103,13 +106,16 @@ export function useLibraryTags(): UseLibraryTagsResult {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 选中的标签可能被删除,落回「全部」以免标签栏空白。
|
// 选中的标签可能被删除,落回「全部」以免标签栏空白。
|
||||||
|
// 注意:标签首次加载完成前 tags 为空集合,此时不能据此判定标签已被删除,
|
||||||
|
// 否则从子页面返回时,持久化的选中标签会被误重置为「全部」。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (loading) return
|
||||||
const resolved = resolveSelectedTagId(tags, selectedTagId)
|
const resolved = resolveSelectedTagId(tags, selectedTagId)
|
||||||
if (resolved !== selectedTagId) {
|
if (resolved !== selectedTagId) {
|
||||||
selectedRef.current = resolved
|
selectedRef.current = resolved
|
||||||
setSelectedTagIdState(resolved)
|
setSelectedTagIdState(resolved)
|
||||||
}
|
}
|
||||||
}, [tags, selectedTagId])
|
}, [loading, tags, selectedTagId])
|
||||||
|
|
||||||
const setSelectedTagId = useCallback((tagId: string) => {
|
const setSelectedTagId = useCallback((tagId: string) => {
|
||||||
const next = tagId && tagId !== ALL_TAG_ID ? tagId : ALL_TAG_ID
|
const next = tagId && tagId !== ALL_TAG_ID ? tagId : ALL_TAG_ID
|
||||||
@@ -193,6 +199,13 @@ export function useLibraryTags(): UseLibraryTagsResult {
|
|||||||
[mutate, setSelectedTagId],
|
[mutate, setSelectedTagId],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const reorderTags = useCallback(
|
||||||
|
async (orderedNames: string[]) => {
|
||||||
|
await mutate((current) => reorderLibraryTags(current, orderedNames))
|
||||||
|
},
|
||||||
|
[mutate],
|
||||||
|
)
|
||||||
|
|
||||||
const removeTag = useCallback(
|
const removeTag = useCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
await mutate((current) => current.filter((tag) => tag.name !== name))
|
await mutate((current) => current.filter((tag) => tag.name !== name))
|
||||||
@@ -239,6 +252,7 @@ export function useLibraryTags(): UseLibraryTagsResult {
|
|||||||
setSelectedTagId,
|
setSelectedTagId,
|
||||||
createTag,
|
createTag,
|
||||||
renameTag,
|
renameTag,
|
||||||
|
reorderTags,
|
||||||
removeTag,
|
removeTag,
|
||||||
setTagLibraries,
|
setTagLibraries,
|
||||||
assignLibrary,
|
assignLibrary,
|
||||||
|
|||||||
@@ -340,6 +340,9 @@ export function HomePage() {
|
|||||||
onCreate={(name) => {
|
onCreate={(name) => {
|
||||||
void libraryTags.createTag(name)
|
void libraryTags.createTag(name)
|
||||||
}}
|
}}
|
||||||
|
onReorder={(names) => {
|
||||||
|
void libraryTags.reorderTags(names)
|
||||||
|
}}
|
||||||
onManage={() => setLibraryTagsOpen(true)}
|
onManage={() => setLibraryTagsOpen(true)}
|
||||||
busy={libraryTags.saving}
|
busy={libraryTags.saving}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ import { partitionPreviewIDs } from '../utils/remoteEmby'
|
|||||||
|
|
||||||
export function LibrariesPage() {
|
export function LibrariesPage() {
|
||||||
const isAdmin = useAuthStore((state) => state.user?.role === 'admin')
|
const isAdmin = useAuthStore((state) => state.user?.role === 'admin')
|
||||||
const [libraries, setLibraries] = useState<Library[]>([])
|
// 会话缓存命中时首屏即用缓存渲染(避免先闪一帧占位再被撑高),
|
||||||
|
// 这样 useScrollMemory 的布局期恢复才能在返回列表时立刻落到原位置。
|
||||||
|
const [libraries, setLibraries] = useState<Library[]>(() => peekLibraries() ?? [])
|
||||||
const [libraryData, setLibraryData] = useState<Record<string, { cards: SeriesCard[]; total: number }>>({})
|
const [libraryData, setLibraryData] = useState<Record<string, { cards: SeriesCard[]; total: number }>>({})
|
||||||
const { pinnedIds, loading: pinnedLoading, togglePin } = usePinnedLibraries()
|
const { pinnedIds, togglePin } = usePinnedLibraries()
|
||||||
const libraryTags = useLibraryTags()
|
const libraryTags = useLibraryTags()
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(() => peekLibraries() === null)
|
||||||
const [repairing, setRepairing] = useState(false)
|
const [repairing, setRepairing] = useState(false)
|
||||||
const [repairEpisodeArtwork, setRepairEpisodeArtwork] = useEpisodeArtworkPreference()
|
const [repairEpisodeArtwork, setRepairEpisodeArtwork] = useEpisodeArtworkPreference()
|
||||||
const [repairMsg, setRepairMsg] = useState('')
|
const [repairMsg, setRepairMsg] = useState('')
|
||||||
@@ -184,7 +186,7 @@ export function LibrariesPage() {
|
|||||||
[taggedPreviews],
|
[taggedPreviews],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (loading || pinnedLoading) {
|
if (loading) {
|
||||||
return <p className="px-2 py-8 text-sm text-sand-500">媒体库加载中…</p>
|
return <p className="px-2 py-8 text-sm text-sand-500">媒体库加载中…</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +214,9 @@ export function LibrariesPage() {
|
|||||||
onCreate={(name) => {
|
onCreate={(name) => {
|
||||||
void libraryTags.createTag(name)
|
void libraryTags.createTag(name)
|
||||||
}}
|
}}
|
||||||
|
onReorder={(names) => {
|
||||||
|
void libraryTags.reorderTags(names)
|
||||||
|
}}
|
||||||
onManage={() => setLibraryTagsOpen(true)}
|
onManage={() => setLibraryTagsOpen(true)}
|
||||||
busy={libraryTags.saving}
|
busy={libraryTags.saving}
|
||||||
/>
|
/>
|
||||||
@@ -248,19 +253,17 @@ export function LibrariesPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按选中标签过滤媒体库预览(ALL_TAG_ID 时原样返回,保留置顶/列表排序)。 */
|
/**
|
||||||
|
* 按选中标签过滤媒体库预览:复用共享的成员筛选(保持输入顺序),
|
||||||
|
* 这样排序下拉(含倒序)在标签页与「全部」页行为一致。
|
||||||
|
*/
|
||||||
function filterPreviewsByTag(previews: LibraryPreview[], tags: LibraryTag[], tagId: string): LibraryPreview[] {
|
function filterPreviewsByTag(previews: LibraryPreview[], tags: LibraryTag[], tagId: string): LibraryPreview[] {
|
||||||
if (!tagId || tagId === ALL_TAG_ID) return previews
|
if (!tagId || tagId === ALL_TAG_ID) return previews
|
||||||
const ordered = filterLibrariesByTag(
|
|
||||||
previews.map((preview) => ({ id: preview.library.id })),
|
|
||||||
tags,
|
|
||||||
tagId,
|
|
||||||
)
|
|
||||||
const byId = new Map(previews.map((preview) => [preview.library.id, preview]))
|
const byId = new Map(previews.map((preview) => [preview.library.id, preview]))
|
||||||
const out: LibraryPreview[] = []
|
const kept: LibraryPreview[] = []
|
||||||
for (const item of ordered) {
|
for (const library of filterLibrariesByTag(previews.map((preview) => preview.library), tags, tagId)) {
|
||||||
const preview = byId.get(item.id)
|
const preview = byId.get(library.id)
|
||||||
if (preview) out.push(preview)
|
if (preview) kept.push(preview)
|
||||||
}
|
}
|
||||||
return out
|
return kept
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
filterLibrariesForTagging,
|
filterLibrariesForTagging,
|
||||||
normalizeLibraryTags,
|
normalizeLibraryTags,
|
||||||
normalizeTagName,
|
normalizeTagName,
|
||||||
|
reorderLibraryTags,
|
||||||
resolveSelectedTagId,
|
resolveSelectedTagId,
|
||||||
tagNameError,
|
tagNameError,
|
||||||
} from './libraryTags.ts'
|
} from './libraryTags.ts'
|
||||||
@@ -49,10 +50,10 @@ check('tag tab keeps creation order', tabs[1].name === '动画' && tabs[2].name
|
|||||||
check('tag count ignores unavailable libraries', tabs[1].count === 1)
|
check('tag count ignores unavailable libraries', tabs[1].count === 1)
|
||||||
|
|
||||||
check(
|
check(
|
||||||
'filtering keeps tag order and drops unknown ids',
|
'filtering drops unknown ids and keeps input order',
|
||||||
filterLibrariesByTag(libs, [{ name: '动画', library_ids: ['lib-c', 'missing', 'lib-a'] }], '动画')
|
filterLibrariesByTag(libs, [{ name: '动画', library_ids: ['lib-c', 'missing', 'lib-a'] }], '动画')
|
||||||
.map((lib) => lib.id)
|
.map((lib) => lib.id)
|
||||||
.join(',') === 'lib-c,lib-a',
|
.join(',') === 'lib-a,lib-c',
|
||||||
)
|
)
|
||||||
check('全部 returns every library in original order', filterLibrariesByTag(libs, [], ALL_TAG_ID).length === 3)
|
check('全部 returns every library in original order', filterLibrariesByTag(libs, [], ALL_TAG_ID).length === 3)
|
||||||
check('unknown tag yields no libraries', filterLibrariesByTag(libs, [], 'nope').length === 0)
|
check('unknown tag yields no libraries', filterLibrariesByTag(libs, [], 'nope').length === 0)
|
||||||
@@ -135,4 +136,31 @@ check(
|
|||||||
tagNameError('动画', [{ name: '动画', library_ids: [] }], '动画') === '',
|
tagNameError('动画', [{ name: '动画', library_ids: [] }], '动画') === '',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const reorderable = [
|
||||||
|
{ name: '动画', library_ids: [] },
|
||||||
|
{ name: '电影', library_ids: [] },
|
||||||
|
{ name: '音乐', library_ids: [] },
|
||||||
|
]
|
||||||
|
check(
|
||||||
|
'reorder moves a tag to the requested slot',
|
||||||
|
reorderLibraryTags(reorderable, ['电影', '动画', '音乐']).map((tag) => tag.name).join(',') === '电影,动画,音乐',
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
'reorder can move a tag to the front',
|
||||||
|
reorderLibraryTags(reorderable, ['音乐', '动画', '电影']).map((tag) => tag.name).join(',') === '音乐,动画,电影',
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
'names missing from the order keep relative order at the end',
|
||||||
|
reorderLibraryTags(reorderable, ['电影']).map((tag) => tag.name).join(',') === '电影,动画,音乐',
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
'empty order leaves tags untouched',
|
||||||
|
reorderLibraryTags(reorderable, []).map((tag) => tag.name).join(',') === '动画,电影,音乐',
|
||||||
|
)
|
||||||
|
check('reorder ignores extra unknown names', reorderLibraryTags(reorderable, ['不存在', '音乐']).map((tag) => tag.name).join(',') === '音乐,动画,电影')
|
||||||
|
check(
|
||||||
|
'reordering with fewer than two tags is a no-op',
|
||||||
|
reorderLibraryTags([{ name: '动画', library_ids: [] }], []).map((tag) => tag.name).join(',') === '动画',
|
||||||
|
)
|
||||||
|
|
||||||
console.log('libraryTags.test.ts ok')
|
console.log('libraryTags.test.ts ok')
|
||||||
|
|||||||
@@ -164,8 +164,9 @@ export function isLibraryInTag(tags: LibraryTag[], tagId: string, libraryId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按标签过滤媒体库:`tagId` 为「全部」时原样返回(保持原有置顶/手动排序),
|
* 按标签过滤媒体库:`tagId` 为「全部」时原样返回。
|
||||||
* 否则只保留该标签下的库,并保持标签里记录的顺序。
|
* 只做成员筛选、保持输入顺序,展示顺序(置顶、排序下拉等)由调用方决定;
|
||||||
|
* 标签里记录的 library_ids 顺序只表示归属,不作为展示排序。
|
||||||
*/
|
*/
|
||||||
export function filterLibrariesByTag<T extends { id: string }>(
|
export function filterLibrariesByTag<T extends { id: string }>(
|
||||||
libraries: T[],
|
libraries: T[],
|
||||||
@@ -173,15 +174,9 @@ export function filterLibrariesByTag<T extends { id: string }>(
|
|||||||
tagId: string,
|
tagId: string,
|
||||||
): T[] {
|
): T[] {
|
||||||
if (!tagId || tagId === ALL_TAG_ID) return libraries
|
if (!tagId || tagId === ALL_TAG_ID) return libraries
|
||||||
const ids = tagLibraryIds(tags, tagId)
|
const ids = new Set(tagLibraryIds(tags, tagId))
|
||||||
if (ids.length === 0) return []
|
if (ids.size === 0) return []
|
||||||
const byId = new Map(libraries.map((lib) => [lib.id, lib]))
|
return libraries.filter((lib) => ids.has(lib.id))
|
||||||
const out: T[] = []
|
|
||||||
for (const id of ids) {
|
|
||||||
const lib = byId.get(id)
|
|
||||||
if (lib) out.push(lib)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成标签栏模型:首项固定为「全部」,其后按标签顺序追加。 */
|
/** 生成标签栏模型:首项固定为「全部」,其后按标签顺序追加。 */
|
||||||
@@ -202,6 +197,22 @@ export function buildLibraryTagTabs(libraries: Library[], tags: LibraryTag[]): L
|
|||||||
return tabs
|
return tabs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按给定标签名顺序重排标签栏:`orderedNames` 是期望的标签名顺序(通常来自拖拽后的顺序)。
|
||||||
|
* 未出现在顺序里的标签(例如新建、旧数据并发写入)保持原相对顺序,追加到末尾,绝不丢失。
|
||||||
|
*/
|
||||||
|
export function reorderLibraryTags(tags: LibraryTag[], orderedNames: string[]): LibraryTag[] {
|
||||||
|
if (tags.length < 2) return tags
|
||||||
|
const rank = new Map(orderedNames.map((name, index) => [name, index]))
|
||||||
|
const ranked: LibraryTag[] = []
|
||||||
|
const rest: LibraryTag[] = []
|
||||||
|
for (const tag of tags) {
|
||||||
|
;(rank.has(tag.name) ? ranked : rest).push(tag)
|
||||||
|
}
|
||||||
|
ranked.sort((a, b) => (rank.get(a.name) ?? 0) - (rank.get(b.name) ?? 0))
|
||||||
|
return [...ranked, ...rest]
|
||||||
|
}
|
||||||
|
|
||||||
/** 选中的标签是否仍存在;被删除或被过滤掉时回落到「全部」。 */
|
/** 选中的标签是否仍存在;被删除或被过滤掉时回落到「全部」。 */
|
||||||
export function resolveSelectedTagId(tags: LibraryTag[], selectedId: string): string {
|
export function resolveSelectedTagId(tags: LibraryTag[], selectedId: string): string {
|
||||||
if (!selectedId || selectedId === ALL_TAG_ID) return ALL_TAG_ID
|
if (!selectedId || selectedId === ALL_TAG_ID) return ALL_TAG_ID
|
||||||
|
|||||||
Reference in New Issue
Block a user