- 播放上下文:作品链接带上来源(搜索、标签、作者、hashtag),上一条/下一条 在该列表内移动;当前作品不在列表中时退回全部作品的发布时间顺序 - 顶部提示标签显示当前列表与位置,点击打开侧栏列表,✕ 退出 - 侧栏改为「评论 | TA的作品 | 搜索结果/#hashtag」标签页,作品封面网格可就地切换 - 作者模式:打开「TA的作品」进入,关闭侧栏不退出,点提示 ✕ 后回到原来的列表 - 从搜索结果进入或点 hashtag 时自动打开对应列表 - 描述中的 hashtag 可点击,按完整 hashtag 精确匹配(#丰川祥子 不含 #丰川祥子cos) - 新增 /api/playlist;搜索 SQL 抽到 lib/search-query.ts 与播放页共用,保证顺序一致 - 移动端:底部抽屉、点作品后收起,媒体区支持上下滑切换 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import { X } from "lucide-react";
|
|
import { memo, type ReactNode } from "react";
|
|
import type { SidePanelTab } from "../hooks/useSidePanelState";
|
|
|
|
export type SidePanelTabConfig = {
|
|
key: SidePanelTab;
|
|
label: string;
|
|
count: number;
|
|
content: ReactNode;
|
|
};
|
|
|
|
interface SidePanelProps {
|
|
open: boolean;
|
|
mounted: boolean;
|
|
tab: SidePanelTab;
|
|
tabs: SidePanelTabConfig[];
|
|
onTabChange: (tab: SidePanelTab) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
// 右侧(竖屏为底部抽屉)的侧栏外壳:评论 / TA的作品 / 搜索结果(或 hashtag)几个标签页
|
|
export const SidePanel = memo(function SidePanel({
|
|
open,
|
|
mounted,
|
|
tab,
|
|
tabs,
|
|
onTabChange,
|
|
onClose,
|
|
}: SidePanelProps) {
|
|
const visibility = open
|
|
? "translate-y-0 pointer-events-auto landscape:w-[min(420px,36vw)] landscape:border-l landscape:border-white/10"
|
|
: "translate-y-full pointer-events-none landscape:w-0 landscape:border-l-0";
|
|
// 竖屏时作品网格不需要那么高,给视频多留点位置
|
|
const portraitHeight =
|
|
tab === "comments" ? "h-[min(80vh,88dvh)]" : "h-[min(62vh,64dvh)]";
|
|
|
|
return (
|
|
<aside
|
|
className={`
|
|
z-30 flex flex-col bg-[rgba(22,22,22,0.92)] text-white overflow-hidden
|
|
fixed inset-x-0 bottom-0 w-full ${portraitHeight} border-t border-white/10 rounded-t-2xl
|
|
landscape:relative landscape:inset-auto landscape:h-full landscape:translate-y-0 landscape:border-t-0 landscape:rounded-none
|
|
${mounted ? "transition-transform landscape:transition-[width] duration-200 ease-out" : ""}
|
|
${visibility}
|
|
`}
|
|
>
|
|
<div className="flex items-center gap-1 px-3 border-b border-white/10 shrink-0">
|
|
<div
|
|
className="flex min-w-0 flex-1 items-center gap-5 overflow-x-auto"
|
|
role="tablist"
|
|
>
|
|
{tabs.map((t) => (
|
|
<button
|
|
key={t.key}
|
|
type="button"
|
|
role="tab"
|
|
aria-selected={tab === t.key}
|
|
onClick={() => onTabChange(t.key)}
|
|
className={`relative shrink-0 max-w-[10em] truncate py-3 text-[15px] transition-colors cursor-pointer ${
|
|
tab === t.key
|
|
? "font-semibold text-white"
|
|
: "text-white/55 hover:text-white/80"
|
|
}`}
|
|
>
|
|
{t.label}
|
|
{t.count > 0 && (
|
|
<span className="ml-1 text-xs font-normal text-white/50">
|
|
{t.count}
|
|
</span>
|
|
)}
|
|
{tab === t.key && (
|
|
<span className="absolute inset-x-0 bottom-0 h-0.5 rounded-full bg-rose-400" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="w-8 h-8 shrink-0 inline-flex items-center justify-center rounded-full bg-white/15 text-white/90 border border-white/20 hover:bg-white/25 transition-colors cursor-pointer"
|
|
onClick={onClose}
|
|
aria-label="关闭侧栏"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
{tabs.map((t) => (
|
|
<div
|
|
key={t.key}
|
|
className={tab === t.key ? "flex min-h-0 flex-1 flex-col" : "hidden"}
|
|
>
|
|
{t.content}
|
|
</div>
|
|
))}
|
|
</aside>
|
|
);
|
|
});
|