From aed3e4e49d8418d4580b616dbc90f585e5fc96f6 Mon Sep 17 00:00:00 2001
From: mackt <1033530438@qq.com>
Date: Wed, 30 Oct 2024 23:47:05 +0800
Subject: [PATCH] feat: Carousel
---
package.json | 5 +-
.../music/[category]/[[...page]]/page.tsx | 42 +++-
.../Comment/MainComment/CommentList.tsx | 2 +-
src/components/HomeCarousel.tsx | 45 ++++
src/components/index.ts | 1 +
src/components/ui/button.tsx | 6 +-
src/components/ui/carousel.tsx | 226 ++++++++++++++++++
src/services/server/journal.ts | 11 +
src/types/reqeust.d.ts | 12 +
yarn.lock | 42 +++-
10 files changed, 379 insertions(+), 13 deletions(-)
create mode 100644 src/components/HomeCarousel.tsx
create mode 100644 src/components/ui/carousel.tsx
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"