mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 00:44:41 +08:00
feat: add DoubanCustomSelector
This commit is contained in:
@@ -1 +1 @@
|
||||
20250731130730
|
||||
20250731215708
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable no-console,react-hooks/exhaustive-deps */
|
||||
/* eslint-disable no-console,react-hooks/exhaustive-deps,@typescript-eslint/no-explicit-any */
|
||||
|
||||
'use client';
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getDoubanCategories, getDoubanList } from '@/lib/douban.client';
|
||||
import { DoubanItem, DoubanResult } from '@/lib/types';
|
||||
|
||||
import DoubanCardSkeleton from '@/components/DoubanCardSkeleton';
|
||||
import DoubanCustomSelector from '@/components/DoubanCustomSelector';
|
||||
import DoubanSelector from '@/components/DoubanSelector';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import VideoCard from '@/components/VideoCard';
|
||||
@@ -27,9 +28,11 @@ function DoubanPageClient() {
|
||||
const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const type = searchParams.get('type') || 'movie';
|
||||
const tag = searchParams.get('tag') || '';
|
||||
const custom = searchParams.get('custom') === 'true';
|
||||
const name = searchParams.get('name') || '';
|
||||
|
||||
// 获取 runtimeConfig 中的自定义分类数据
|
||||
const [customCategories, setCustomCategories] = useState<
|
||||
Array<{ name: string; type: 'movie' | 'tv'; query: string }>
|
||||
>([]);
|
||||
|
||||
// 选择器状态 - 完全独立,不依赖URL参数
|
||||
const [primarySelection, setPrimarySelection] = useState<string>(() => {
|
||||
@@ -42,6 +45,14 @@ function DoubanPageClient() {
|
||||
return '全部';
|
||||
});
|
||||
|
||||
// 获取自定义分类数据
|
||||
useEffect(() => {
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||
setCustomCategories(runtimeConfig.CUSTOM_CATEGORIES);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化时标记选择器为准备好状态
|
||||
useEffect(() => {
|
||||
// 短暂延迟确保初始状态设置完成
|
||||
@@ -56,23 +67,42 @@ function DoubanPageClient() {
|
||||
useEffect(() => {
|
||||
setSelectorsReady(false);
|
||||
setLoading(true); // 立即显示loading状态
|
||||
}, [type, tag]);
|
||||
}, [type]);
|
||||
|
||||
// 当type变化时重置选择器状态
|
||||
useEffect(() => {
|
||||
// 批量更新选择器状态
|
||||
if (type === 'movie') {
|
||||
setPrimarySelection('热门');
|
||||
setSecondarySelection('全部');
|
||||
} else if (type === 'tv') {
|
||||
setPrimarySelection('');
|
||||
setSecondarySelection('tv');
|
||||
} else if (type === 'show') {
|
||||
setPrimarySelection('');
|
||||
setSecondarySelection('show');
|
||||
if (type === 'custom' && customCategories.length > 0) {
|
||||
// 自定义分类模式:根据 customCategories 设置初始状态
|
||||
const types = Array.from(
|
||||
new Set(customCategories.map((cat) => cat.type))
|
||||
);
|
||||
if (types.length > 0) {
|
||||
const firstType = types[0];
|
||||
setPrimarySelection(firstType);
|
||||
|
||||
// 设置第一个分类的 query 作为二级选择
|
||||
const firstCategory = customCategories.find(
|
||||
(cat) => cat.type === firstType
|
||||
);
|
||||
if (firstCategory) {
|
||||
setSecondarySelection(firstCategory.query);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setPrimarySelection('');
|
||||
setSecondarySelection('全部');
|
||||
// 原有逻辑
|
||||
if (type === 'movie') {
|
||||
setPrimarySelection('热门');
|
||||
setSecondarySelection('全部');
|
||||
} else if (type === 'tv') {
|
||||
setPrimarySelection('');
|
||||
setSecondarySelection('tv');
|
||||
} else if (type === 'show') {
|
||||
setPrimarySelection('');
|
||||
setSecondarySelection('show');
|
||||
} else {
|
||||
setPrimarySelection('');
|
||||
setSecondarySelection('全部');
|
||||
}
|
||||
}
|
||||
|
||||
// 使用短暂延迟确保状态更新完成后标记选择器准备好
|
||||
@@ -81,7 +111,7 @@ function DoubanPageClient() {
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [type, tag, custom]);
|
||||
}, [type, customCategories]);
|
||||
|
||||
// 生成骨架屏数据
|
||||
const skeletonData = Array.from({ length: 25 }, (_, index) => index);
|
||||
@@ -117,13 +147,24 @@ function DoubanPageClient() {
|
||||
try {
|
||||
setLoading(true);
|
||||
let data: DoubanResult;
|
||||
if (custom) {
|
||||
data = await getDoubanList({
|
||||
tag,
|
||||
type,
|
||||
pageLimit: 25,
|
||||
pageStart: 0,
|
||||
});
|
||||
|
||||
if (type === 'custom') {
|
||||
// 自定义分类模式:根据选中的一级和二级选项获取对应的分类
|
||||
const selectedCategory = customCategories.find(
|
||||
(cat) =>
|
||||
cat.type === primarySelection && cat.query === secondarySelection
|
||||
);
|
||||
|
||||
if (selectedCategory) {
|
||||
data = await getDoubanList({
|
||||
tag: selectedCategory.query,
|
||||
type: selectedCategory.type,
|
||||
pageLimit: 25,
|
||||
pageStart: 0,
|
||||
});
|
||||
} else {
|
||||
throw new Error('没有找到对应的分类');
|
||||
}
|
||||
} else {
|
||||
data = await getDoubanCategories(getRequestParams(0));
|
||||
}
|
||||
@@ -140,17 +181,16 @@ function DoubanPageClient() {
|
||||
}
|
||||
}, [
|
||||
type,
|
||||
tag,
|
||||
custom,
|
||||
primarySelection,
|
||||
secondarySelection,
|
||||
getRequestParams,
|
||||
customCategories,
|
||||
]);
|
||||
|
||||
// 只在选择器准备好后才加载数据
|
||||
useEffect(() => {
|
||||
// 只有在选择器准备好时才开始加载
|
||||
if (!selectorsReady && !custom) {
|
||||
if (!selectorsReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,8 +219,6 @@ function DoubanPageClient() {
|
||||
}, [
|
||||
selectorsReady,
|
||||
type,
|
||||
tag,
|
||||
custom,
|
||||
primarySelection,
|
||||
secondarySelection,
|
||||
loadInitialData,
|
||||
@@ -194,13 +232,24 @@ function DoubanPageClient() {
|
||||
setIsLoadingMore(true);
|
||||
|
||||
let data: DoubanResult;
|
||||
if (custom) {
|
||||
data = await getDoubanList({
|
||||
tag,
|
||||
type,
|
||||
pageLimit: 25,
|
||||
pageStart: currentPage * 25,
|
||||
});
|
||||
if (type === 'custom') {
|
||||
// 自定义分类模式:根据选中的一级和二级选项获取对应的分类
|
||||
const selectedCategory = customCategories.find(
|
||||
(cat) =>
|
||||
cat.type === primarySelection &&
|
||||
cat.query === secondarySelection
|
||||
);
|
||||
|
||||
if (selectedCategory) {
|
||||
data = await getDoubanList({
|
||||
tag: selectedCategory.query,
|
||||
type: selectedCategory.type,
|
||||
pageLimit: 25,
|
||||
pageStart: currentPage * 25,
|
||||
});
|
||||
} else {
|
||||
throw new Error('没有找到对应的分类');
|
||||
}
|
||||
} else {
|
||||
data = await getDoubanCategories(
|
||||
getRequestParams(currentPage * 25)
|
||||
@@ -222,7 +271,13 @@ function DoubanPageClient() {
|
||||
|
||||
fetchMoreData();
|
||||
}
|
||||
}, [currentPage, type, tag, custom, primarySelection, secondarySelection]);
|
||||
}, [
|
||||
currentPage,
|
||||
type,
|
||||
primarySelection,
|
||||
secondarySelection,
|
||||
customCategories,
|
||||
]);
|
||||
|
||||
// 设置滚动监听
|
||||
useEffect(() => {
|
||||
@@ -261,10 +316,25 @@ function DoubanPageClient() {
|
||||
// 只有当值真正改变时才设置loading状态
|
||||
if (value !== primarySelection) {
|
||||
setLoading(true);
|
||||
setPrimarySelection(value);
|
||||
|
||||
// 如果是自定义分类模式,同时更新一级和二级选择器
|
||||
if (type === 'custom' && customCategories.length > 0) {
|
||||
const firstCategory = customCategories.find(
|
||||
(cat) => cat.type === value
|
||||
);
|
||||
if (firstCategory) {
|
||||
// 批量更新状态,避免多次触发数据加载
|
||||
setPrimarySelection(value);
|
||||
setSecondarySelection(firstCategory.query);
|
||||
} else {
|
||||
setPrimarySelection(value);
|
||||
}
|
||||
} else {
|
||||
setPrimarySelection(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
[primarySelection]
|
||||
[primarySelection, type, customCategories]
|
||||
);
|
||||
|
||||
const handleSecondaryChange = useCallback(
|
||||
@@ -280,19 +350,18 @@ function DoubanPageClient() {
|
||||
|
||||
const getPageTitle = () => {
|
||||
// 根据 type 生成标题
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (custom) {
|
||||
return tag;
|
||||
}
|
||||
return type === 'movie' ? '电影' : type === 'tv' ? '电视剧' : '综艺';
|
||||
return type === 'movie'
|
||||
? '电影'
|
||||
: type === 'tv'
|
||||
? '电视剧'
|
||||
: type === 'show'
|
||||
? '综艺'
|
||||
: '自定义';
|
||||
};
|
||||
|
||||
const getActivePath = () => {
|
||||
const params = new URLSearchParams();
|
||||
if (type) params.set('type', type);
|
||||
if (tag) params.set('tag', tag);
|
||||
|
||||
const queryString = params.toString();
|
||||
const activePath = `/douban${queryString ? `?${queryString}` : ''}`;
|
||||
@@ -314,8 +383,8 @@ function DoubanPageClient() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 选择器组件 - custom 模式下不显示 */}
|
||||
{!custom && (
|
||||
{/* 选择器组件 */}
|
||||
{type !== 'custom' ? (
|
||||
<div className='bg-white/60 dark:bg-gray-800/40 rounded-2xl p-4 sm:p-6 border border-gray-200/30 dark:border-gray-700/30 backdrop-blur-sm'>
|
||||
<DoubanSelector
|
||||
type={type as 'movie' | 'tv' | 'show'}
|
||||
@@ -325,14 +394,24 @@ function DoubanPageClient() {
|
||||
onSecondaryChange={handleSecondaryChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='bg-white/60 dark:bg-gray-800/40 rounded-2xl p-4 sm:p-6 border border-gray-200/30 dark:border-gray-700/30 backdrop-blur-sm'>
|
||||
<DoubanCustomSelector
|
||||
customCategories={customCategories}
|
||||
primarySelection={primarySelection}
|
||||
secondarySelection={secondarySelection}
|
||||
onPrimaryChange={handlePrimaryChange}
|
||||
onSecondaryChange={handleSecondaryChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 内容展示区域 */}
|
||||
<div className='max-w-[95%] mx-auto mt-8 overflow-visible'>
|
||||
{/* 内容网格 */}
|
||||
<div className='grid grid-cols-3 gap-x-2 gap-y-12 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fit,minmax(160px,1fr))] sm:gap-x-8 sm:gap-y-20'>
|
||||
{loading || (!selectorsReady && !custom)
|
||||
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-12 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] sm:gap-x-8 sm:gap-y-20'>
|
||||
{loading || !selectorsReady
|
||||
? // 显示骨架屏
|
||||
skeletonData.map((index) => <DoubanCardSkeleton key={index} />)
|
||||
: // 显示实际数据
|
||||
|
||||
256
src/components/DoubanCustomSelector.tsx
Normal file
256
src/components/DoubanCustomSelector.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface CustomCategory {
|
||||
name: string;
|
||||
type: 'movie' | 'tv';
|
||||
query: string;
|
||||
}
|
||||
|
||||
interface DoubanCustomSelectorProps {
|
||||
customCategories: CustomCategory[];
|
||||
primarySelection?: string;
|
||||
secondarySelection?: string;
|
||||
onPrimaryChange: (value: string) => void;
|
||||
onSecondaryChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const DoubanCustomSelector: React.FC<DoubanCustomSelectorProps> = ({
|
||||
customCategories,
|
||||
primarySelection,
|
||||
secondarySelection,
|
||||
onPrimaryChange,
|
||||
onSecondaryChange,
|
||||
}) => {
|
||||
// 为不同的选择器创建独立的refs和状态
|
||||
const primaryContainerRef = useRef<HTMLDivElement>(null);
|
||||
const primaryButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const [primaryIndicatorStyle, setPrimaryIndicatorStyle] = useState<{
|
||||
left: number;
|
||||
width: number;
|
||||
}>({ left: 0, width: 0 });
|
||||
|
||||
const secondaryContainerRef = useRef<HTMLDivElement>(null);
|
||||
const secondaryButtonRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
||||
const [secondaryIndicatorStyle, setSecondaryIndicatorStyle] = useState<{
|
||||
left: number;
|
||||
width: number;
|
||||
}>({ left: 0, width: 0 });
|
||||
|
||||
// 根据 customCategories 生成一级选择器选项(按 type 分组)
|
||||
const primaryOptions = React.useMemo(() => {
|
||||
const types = Array.from(new Set(customCategories.map((cat) => cat.type)));
|
||||
return types.map((type) => ({
|
||||
label: type === 'movie' ? '电影' : '剧集',
|
||||
value: type,
|
||||
}));
|
||||
}, [customCategories]);
|
||||
|
||||
// 根据选中的一级选项生成二级选择器选项
|
||||
const secondaryOptions = React.useMemo(() => {
|
||||
if (!primarySelection) return [];
|
||||
return customCategories
|
||||
.filter((cat) => cat.type === primarySelection)
|
||||
.map((cat) => ({
|
||||
label: cat.name || cat.query,
|
||||
value: cat.query,
|
||||
}));
|
||||
}, [customCategories, primarySelection]);
|
||||
|
||||
// 更新指示器位置的通用函数
|
||||
const updateIndicatorPosition = (
|
||||
activeIndex: number,
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
buttonRefs: React.MutableRefObject<(HTMLButtonElement | null)[]>,
|
||||
setIndicatorStyle: React.Dispatch<
|
||||
React.SetStateAction<{ left: number; width: number }>
|
||||
>
|
||||
) => {
|
||||
if (
|
||||
activeIndex >= 0 &&
|
||||
buttonRefs.current[activeIndex] &&
|
||||
containerRef.current
|
||||
) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
const button = buttonRefs.current[activeIndex];
|
||||
const container = containerRef.current;
|
||||
if (button && container) {
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
if (buttonRect.width > 0) {
|
||||
setIndicatorStyle({
|
||||
left: buttonRect.left - containerRect.left,
|
||||
width: buttonRect.width,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时立即计算初始位置
|
||||
useEffect(() => {
|
||||
// 主选择器初始位置
|
||||
if (primaryOptions.length > 0) {
|
||||
const activeIndex = primaryOptions.findIndex(
|
||||
(opt) => opt.value === (primarySelection || primaryOptions[0].value)
|
||||
);
|
||||
updateIndicatorPosition(
|
||||
activeIndex,
|
||||
primaryContainerRef,
|
||||
primaryButtonRefs,
|
||||
setPrimaryIndicatorStyle
|
||||
);
|
||||
}
|
||||
|
||||
// 副选择器初始位置
|
||||
if (secondaryOptions.length > 0) {
|
||||
const activeIndex = secondaryOptions.findIndex(
|
||||
(opt) => opt.value === (secondarySelection || secondaryOptions[0].value)
|
||||
);
|
||||
updateIndicatorPosition(
|
||||
activeIndex,
|
||||
secondaryContainerRef,
|
||||
secondaryButtonRefs,
|
||||
setSecondaryIndicatorStyle
|
||||
);
|
||||
}
|
||||
}, [primaryOptions, secondaryOptions]); // 当选项变化时重新计算
|
||||
|
||||
// 监听主选择器变化
|
||||
useEffect(() => {
|
||||
if (primaryOptions.length > 0) {
|
||||
const activeIndex = primaryOptions.findIndex(
|
||||
(opt) => opt.value === primarySelection
|
||||
);
|
||||
const cleanup = updateIndicatorPosition(
|
||||
activeIndex,
|
||||
primaryContainerRef,
|
||||
primaryButtonRefs,
|
||||
setPrimaryIndicatorStyle
|
||||
);
|
||||
return cleanup;
|
||||
}
|
||||
}, [primarySelection, primaryOptions]);
|
||||
|
||||
// 监听副选择器变化
|
||||
useEffect(() => {
|
||||
if (secondaryOptions.length > 0) {
|
||||
const activeIndex = secondaryOptions.findIndex(
|
||||
(opt) => opt.value === secondarySelection
|
||||
);
|
||||
const cleanup = updateIndicatorPosition(
|
||||
activeIndex,
|
||||
secondaryContainerRef,
|
||||
secondaryButtonRefs,
|
||||
setSecondaryIndicatorStyle
|
||||
);
|
||||
return cleanup;
|
||||
}
|
||||
}, [secondarySelection, secondaryOptions]);
|
||||
|
||||
// 渲染胶囊式选择器
|
||||
const renderCapsuleSelector = (
|
||||
options: { label: string; value: string }[],
|
||||
activeValue: string | undefined,
|
||||
onChange: (value: string) => void,
|
||||
isPrimary = false
|
||||
) => {
|
||||
const containerRef = isPrimary
|
||||
? primaryContainerRef
|
||||
: secondaryContainerRef;
|
||||
const buttonRefs = isPrimary ? primaryButtonRefs : secondaryButtonRefs;
|
||||
const indicatorStyle = isPrimary
|
||||
? primaryIndicatorStyle
|
||||
: secondaryIndicatorStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='relative inline-flex bg-gray-200/60 rounded-full p-0.5 sm:p-1 dark:bg-gray-700/60 backdrop-blur-sm'
|
||||
>
|
||||
{/* 滑动的白色背景指示器 */}
|
||||
{indicatorStyle.width > 0 && (
|
||||
<div
|
||||
className='absolute top-0.5 bottom-0.5 sm:top-1 sm:bottom-1 bg-white dark:bg-gray-500 rounded-full shadow-sm transition-all duration-300 ease-out'
|
||||
style={{
|
||||
left: `${indicatorStyle.left}px`,
|
||||
width: `${indicatorStyle.width}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{options.map((option, index) => {
|
||||
const isActive = activeValue === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
ref={(el) => {
|
||||
buttonRefs.current[index] = el;
|
||||
}}
|
||||
onClick={() => onChange(option.value)}
|
||||
className={`relative z-10 px-2 py-1 sm:px-4 sm:py-2 text-xs sm:text-sm font-medium rounded-full transition-all duration-200 whitespace-nowrap ${
|
||||
isActive
|
||||
? 'text-gray-900 dark:text-gray-100 cursor-default'
|
||||
: 'text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 如果没有自定义分类,则不渲染任何内容
|
||||
if (!customCategories || customCategories.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-4 sm:space-y-6'>
|
||||
{/* 两级选择器包装 */}
|
||||
<div className='space-y-3 sm:space-y-4'>
|
||||
{/* 一级选择器 */}
|
||||
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
||||
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
||||
类型
|
||||
</span>
|
||||
<div className='overflow-x-auto'>
|
||||
{renderCapsuleSelector(
|
||||
primaryOptions,
|
||||
primarySelection || primaryOptions[0]?.value,
|
||||
onPrimaryChange,
|
||||
true
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 二级选择器 */}
|
||||
{secondaryOptions.length > 0 && (
|
||||
<div className='flex flex-col sm:flex-row sm:items-center gap-2'>
|
||||
<span className='text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 min-w-[48px]'>
|
||||
片单
|
||||
</span>
|
||||
<div className='overflow-x-auto'>
|
||||
{renderCapsuleSelector(
|
||||
secondaryOptions,
|
||||
secondarySelection || secondaryOptions[0]?.value,
|
||||
onSecondaryChange,
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoubanCustomSelector;
|
||||
@@ -42,23 +42,20 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES) {
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||
setNavItems((prevItems) => [
|
||||
...prevItems,
|
||||
...runtimeConfig.CUSTOM_CATEGORIES.map((category: any) => ({
|
||||
{
|
||||
icon: Star,
|
||||
label: category.name || category.query,
|
||||
href: `/douban?type=${category.type}&tag=${category.query}${
|
||||
category.name ? `&name=${category.name}` : ''
|
||||
}&custom=true`,
|
||||
})),
|
||||
label: '自定义',
|
||||
href: '/douban?type=custom',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const isActive = (href: string) => {
|
||||
const typeMatch = href.match(/type=([^&]+)/)?.[1];
|
||||
const tagMatch = href.match(/tag=([^&]+)/)?.[1];
|
||||
|
||||
// 解码URL以进行正确的比较
|
||||
const decodedActive = decodeURIComponent(currentActive);
|
||||
@@ -67,9 +64,7 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
||||
return (
|
||||
decodedActive === decodedItemHref ||
|
||||
(decodedActive.startsWith('/douban') &&
|
||||
decodedActive.includes(`type=${typeMatch}`) &&
|
||||
tagMatch &&
|
||||
decodedActive.includes(`tag=${tagMatch}`))
|
||||
decodedActive.includes(`type=${typeMatch}`))
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -144,16 +144,14 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES) {
|
||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
||||
setMenuItems((prevItems) => [
|
||||
...prevItems,
|
||||
...runtimeConfig.CUSTOM_CATEGORIES.map((category: any) => ({
|
||||
{
|
||||
icon: Star,
|
||||
label: category.name || category.query,
|
||||
href: `/douban?type=${category.type}&tag=${category.query}${
|
||||
category.name ? `&name=${category.name}` : ''
|
||||
}&custom=true`,
|
||||
})),
|
||||
label: '自定义',
|
||||
href: '/douban?type=custom',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, []);
|
||||
@@ -242,7 +240,6 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
{menuItems.map((item) => {
|
||||
// 检查当前路径是否匹配这个菜单项
|
||||
const typeMatch = item.href.match(/type=([^&]+)/)?.[1];
|
||||
const tagMatch = item.href.match(/tag=([^&]+)/)?.[1];
|
||||
|
||||
// 解码URL以进行正确的比较
|
||||
const decodedActive = decodeURIComponent(active);
|
||||
@@ -251,9 +248,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
||||
const isActive =
|
||||
decodedActive === decodedItemHref ||
|
||||
(decodedActive.startsWith('/douban') &&
|
||||
decodedActive.includes(`type=${typeMatch}`) &&
|
||||
tagMatch &&
|
||||
decodedActive.includes(`tag=${tagMatch}`));
|
||||
decodedActive.includes(`type=${typeMatch}`));
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Link
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
const CURRENT_VERSION = '20250731130730';
|
||||
const CURRENT_VERSION = '20250731215708';
|
||||
|
||||
// 版本检查结果枚举
|
||||
export enum UpdateStatus {
|
||||
|
||||
Reference in New Issue
Block a user