diff --git a/package.json b/package.json index 686df70..e2a4518 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-slider": "^1.1.2", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.1.5", "@sentry/nextjs": "^8", "@types/qrcode": "^1.5.5", @@ -24,6 +24,9 @@ "@types/uuid": "^9.0.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "embla-carousel-auto-scroll": "^8.3.1", + "embla-carousel-autoplay": "^8.3.1", + "embla-carousel-react": "^8.3.1", "immer": "^10.0.4", "jwt-decode": "^4.0.0", "lodash": "^4.17.21", diff --git a/src/app/(website)/music/[category]/[[...page]]/page.tsx b/src/app/(website)/music/[category]/[[...page]]/page.tsx index ae8dafb..410792c 100644 --- a/src/app/(website)/music/[category]/[[...page]]/page.tsx +++ b/src/app/(website)/music/[category]/[[...page]]/page.tsx @@ -1,8 +1,24 @@ /** 期刊列表 */ import { notFound } from 'next/navigation'; -import { Category, JournalRecommendList, JournalItem, Pagination, RedirectCheck, ButtonFM } from '@/components'; -import { apiSearchCategoryList, apiGetJournalRecommendWithCollect, apiJournalList, apiGetTags } from '@/services'; +import { Carousel } from '@/components/ui/carousel'; + +import { + Category, + JournalRecommendList, + JournalItem, + Pagination, + RedirectCheck, + ButtonFM, + HomeCarousel, +} from '@/components'; +import { + apiSearchCategoryList, + apiGetJournalRecommendWithCollect, + apiJournalList, + apiGetTags, + apiCarousel, +} from '@/services'; export async function generateMetadata({ params: { category } }: { params: { category?: string } }) { /** @@ -14,7 +30,7 @@ export async function generateMetadata({ params: { category } }: { params: { cat if (category === 'all' || !category) return; const categoryList = await getCategoryList(); const categoryInfo: Category | undefined = categoryList.find((item: Category) => item.nameEn === category); // 一级分类 - const tagList = await apiGetTagList(); + const tagList = await getTagList(); const tagInfo: TagInfo | undefined = tagList.find((item: TagInfo) => item.engName === category); // 二级分类 const title = categoryInfo?.nameCh || tagInfo?.name; @@ -24,7 +40,14 @@ export async function generateMetadata({ params: { category } }: { params: { cat return { title, description }; } -const apiGetTagList = async () => { +const getCarousel = async () => { + const result = await apiCarousel(); + if (result.code === 200) { + return result.data.filter((item: Carousel) => item.outerLink); + } +}; + +const getTagList = async () => { const result = await apiGetTags(); if (result.code !== 200) return notFound(); const list: TagInfo[] = result.data.map((item) => { @@ -74,12 +97,13 @@ export default async function Journal({ params }: { params: { category?: string; // 获取期刊分类 const categoryList = await getCategoryList(); - const tagList = await apiGetTagList(); + const tagList = await getTagList(); const categoryInfo: TagInfo | undefined = tagList.find((item: TagInfo) => item.engName === category); // 获取热门推荐、期刊列表 - const [recommendList, journalList] = await Promise.all([ + const [recommendList, journalList, carouselList] = await Promise.all([ getRecommendList(), getJournalList({ category: categoryInfo?.id ?? '', page: Number(page) }), + getCarousel(), ]); return ( @@ -117,7 +141,11 @@ export default async function Journal({ params }: { params: { category?: string; {/* 热门推荐 */}
- {!!recommendList?.length ? :
} + {!!recommendList?.length && } + + {!!carouselList?.length && ( + + )}
); diff --git a/src/components/Comment/MainComment/CommentList.tsx b/src/components/Comment/MainComment/CommentList.tsx index 41689ae..cb599dc 100644 --- a/src/components/Comment/MainComment/CommentList.tsx +++ b/src/components/Comment/MainComment/CommentList.tsx @@ -37,7 +37,7 @@ const CommentList = ({ if (res.code === 200) { const list = res.data.rows; if (list.length < pageSize) setHasMore(false); // 如果 length < pageSize 则不再加载 - const finalList = list.filter((item) => !newCommentList.some((nItem) => item._id === nItem._id)); // 过滤自己新发布的评论 + const finalList = list.filter((item: Comment) => !newCommentList.some((nItem) => item._id === nItem._id)); // 过滤自己新发布的评论 setPage((prePage) => prePage + 1); setCommentList([...commentList, ...finalList]); } diff --git a/src/components/HomeCarousel.tsx b/src/components/HomeCarousel.tsx new file mode 100644 index 0000000..10459e3 --- /dev/null +++ b/src/components/HomeCarousel.tsx @@ -0,0 +1,45 @@ +'use client'; + +import * as React from 'react'; + +import Autoplay from 'embla-carousel-autoplay'; +import Image from 'next/image'; +import Link from 'next/link'; + +import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel'; + +export default function HomeCarousel({ className, list }: { className: string; list: Carousel[] }) { + const plugin = React.useRef(Autoplay({ delay: 2000, stopOnInteraction: false, stopOnMouseEnter: true })); + + return ( + + + {list.map((item, index) => ( + + + + + + ))} + + + + + ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 71f7f67..2836c3b 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -13,6 +13,7 @@ export { default as IconPlayAll } from './Icon/PlayAll'; // Home export { default as Category } from './Category'; export { default as ContributorCard } from './ContributorCard'; +export { default as HomeCarousel } from './HomeCarousel'; // Login export { default as Login } from './Login/Login'; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 8a28123..78cdef0 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,4 +1,4 @@ -// @ts-nocheck +'use client'; import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; @@ -7,7 +7,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/utils'; const buttonVariants = cva( - 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { @@ -33,7 +33,7 @@ const buttonVariants = cva( ); export interface ButtonProps - extends React.ButtonHTMLAttributes, + extends Omit, 'size'>, VariantProps { asChild?: boolean; } diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx new file mode 100644 index 0000000..b9b4a81 --- /dev/null +++ b/src/components/ui/carousel.tsx @@ -0,0 +1,226 @@ +'use client'; + +import * as React from 'react'; +import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'; +import { ArrowLeft, ArrowRight } from 'lucide-react'; + +import { cn } from '@/utils'; +import { Button } from './button'; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const Carousel = React.forwardRef & CarouselProps>( + ({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props }, ref) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api?.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +Carousel.displayName = 'Carousel'; + +const CarouselContent = React.forwardRef>( + ({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); + }, +); +CarouselContent.displayName = 'CarouselContent'; + +const CarouselItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); + }, +); +CarouselItem.displayName = 'CarouselItem'; + +const CarouselPrevious = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); + }, +); +CarouselPrevious.displayName = 'CarouselPrevious'; + +const CarouselNext = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); + }, +); +CarouselNext.displayName = 'CarouselNext'; + +export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext }; diff --git a/src/services/server/journal.ts b/src/services/server/journal.ts index 3fd323c..5ea489d 100644 --- a/src/services/server/journal.ts +++ b/src/services/server/journal.ts @@ -124,3 +124,14 @@ export const apiGetTags: () => Promise> = async () => { const res = await serverHttp.get(request); return res; }; + +export const apiCarousel: () => Promise> = async () => { + const request = new Request('http://localhost/', { + method: 'GET', + headers: { + requestUrl: '/user/index/carousel', + }, + }); + const res = await serverHttp.get(request); + return res; +}; diff --git a/src/types/reqeust.d.ts b/src/types/reqeust.d.ts index dc0dea1..0fc8f82 100644 --- a/src/types/reqeust.d.ts +++ b/src/types/reqeust.d.ts @@ -197,3 +197,15 @@ interface TagInfo { engName: string; nameCh: string; } + +/** + * 收藏类型 0:歌曲,1:期刊,2:关注,3:黑名单 + */ +interface Carousel { + carouselId: number; + imgPath: string; + objectType: number; + outerLink: string; + status: number; + publishTime: string; +} diff --git a/yarn.lock b/yarn.lock index 14cb098..4e3c4ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1476,6 +1476,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-compose-refs@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" + integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== + "@radix-ui/react-context@1.0.1": version "1.0.1" resolved "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" @@ -1646,7 +1651,7 @@ "@radix-ui/react-use-previous" "1.0.1" "@radix-ui/react-use-size" "1.0.1" -"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": +"@radix-ui/react-slot@1.0.2": version "1.0.2" resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== @@ -1654,6 +1659,13 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-slot@^1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" + integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-toast@^1.1.5": version "1.1.5" resolved "https://registry.npmmirror.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6" @@ -3341,6 +3353,34 @@ electron-to-chromium@^1.4.668: resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.702.tgz" integrity sha512-LYLXyEUsZ3nNSwiOWjI88N1PJUAMU2QphQSgGLVkFnb3FxZxNui2Vzi2PaKPgPWbsWbZstZnh6BMf/VQJamjiQ== +embla-carousel-auto-scroll@^8.3.1: + version "8.3.1" + resolved "https://registry.npmmirror.com/embla-carousel-auto-scroll/-/embla-carousel-auto-scroll-8.3.1.tgz#094a09d6c0e5d9f1130197d66432710833d6684b" + integrity sha512-jrLEZLceCdQqTGCf9S4W1/AX4S8BwDmiW3eDKdpFEbhrQ187t7MVRDr9M17ljI7V4DMorExgZzEHnokjGS+0SQ== + +embla-carousel-autoplay@^8.3.1: + version "8.3.1" + resolved "https://registry.npmmirror.com/embla-carousel-autoplay/-/embla-carousel-autoplay-8.3.1.tgz#62b45cf55dc001d82b2ac1829f6b3207e7af35a6" + integrity sha512-L8THF1AJJSQtlNa1wZ6lEKh/CiZssE3TsVFtabQNsS+pc1O1O/YTIYCC3khdQAztGMOBf3WfVDIY/4AIfQj3JQ== + +embla-carousel-react@^8.3.1: + version "8.3.1" + resolved "https://registry.npmmirror.com/embla-carousel-react/-/embla-carousel-react-8.3.1.tgz#f6d91f484b00704411524cf01feb80fdcff7dde2" + integrity sha512-gBY0zM+2ASvKFwRpTIOn2SLifFqOKKap9R/y0iCpJWS3bc8OHVEn2gAThGYl2uq0N+hu9aBiswffL++OYZOmDQ== + dependencies: + embla-carousel "8.3.1" + embla-carousel-reactive-utils "8.3.1" + +embla-carousel-reactive-utils@8.3.1: + version "8.3.1" + resolved "https://registry.npmmirror.com/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.3.1.tgz#8e92407f92f55e5f38ef22d24c018077476f1e11" + integrity sha512-Js6rTTINNGnUGPu7l5kTcheoSbEnP5Ak2iX0G9uOoI8okTNLMzuWlEIpYFd1WP0Sq82FFcLkKM2oiO6jcElZ/Q== + +embla-carousel@8.3.1: + version "8.3.1" + resolved "https://registry.npmmirror.com/embla-carousel/-/embla-carousel-8.3.1.tgz#8f5dcd0dc001977efd2bca54d620b6e6736a1a85" + integrity sha512-DutFjtEO586XptDn4cwvBJwsR/8fMa4jUk5Jk2g+/elKgu8mdn0Z2sx33g4JskvbLc1/6P8Xg4QlfELGJFcP5A== + emoji-regex@^10.3.0: version "10.3.0" resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.3.0.tgz"