diff --git a/apps/web/src/app/HeroSection.tsx b/apps/web/src/app/HeroSection.tsx new file mode 100755 index 0000000..f4c21fd --- /dev/null +++ b/apps/web/src/app/HeroSection.tsx @@ -0,0 +1,157 @@ +import React, { + useRef, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { Carousel, Typography } from "antd"; +import { + TeamOutlined, + BookOutlined, + StarOutlined, + LeftOutlined, + RightOutlined, + EyeOutlined, +} from "@ant-design/icons"; +import type { CarouselRef } from "antd/es/carousel"; +import { useAppConfig } from "@nice/client"; +import { useNavigate } from "react-router-dom"; + +interface PlatformStat { + icon: React.ReactNode; + value: number; + label: string; +} + +const HeroSection = () => { + const carouselRef = useRef(null); + const { statistics, slides, slideLinks = [] } = useAppConfig(); + const [countStatistics, setCountStatistics] = useState(4); + const navigator = useNavigate() + const platformStats: PlatformStat[] = useMemo(() => { + return [ + { + icon: , + value: statistics.staffs, + label: "注册学员", + }, + { + icon: , + value: statistics.courses, + label: "精品课程", + }, + { + icon: , + value: statistics.lectures, + label: "课程章节", + }, + { + icon: , + value: statistics.reads, + label: "播放次数", + }, + ]; + }, [statistics]); + const handlePrev = useCallback(() => { + carouselRef.current?.prev(); + }, []); + + const handleNext = useCallback(() => { + carouselRef.current?.next(); + }, []); + + const countNonZeroValues = (statistics: Record): number => { + return Object.values(statistics).filter((value) => value !== 0).length; + }; + + useEffect(() => { + const count = countNonZeroValues(statistics); + console.log(count); + setCountStatistics(count); + }, [statistics]); + return ( +
+
+ + {Array.isArray(slides) ? ( + slides.map((item, index) => ( +
{ + if(slideLinks?.[index])window.open(slideLinks?.[index],"_blank") + }} + > +
+ {/*
*/} +
+ + {/* Content Container */} +
+
+ )) + ) : ( +
+ )} + + + {/* Navigation Buttons */} + + +
+ + {/* Stats Container */} + {countStatistics > 1 && ( +
+
+ {platformStats.map((stat, index) => { + return stat.value ? ( +
+
+ {stat.icon} +
+
+ {stat.value} +
+
+ {stat.label} +
+
+ ) : null; + })} +
+
+ )} +
+ ); +}; + +export default HeroSection; diff --git a/apps/web/src/app/admin/base-setting/page.tsx b/apps/web/src/app/admin/base-setting/page.tsx index b6c74d2..821a1aa 100644 --- a/apps/web/src/app/admin/base-setting/page.tsx +++ b/apps/web/src/app/admin/base-setting/page.tsx @@ -7,6 +7,8 @@ import { useForm } from "antd/es/form/Form"; import { api } from "@nice/client"; import AdminHeader from "@web/src/components/layout/admin/AdminHeader"; import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; +import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader"; +import CarouselUrlInput from "@web/src/components/common/uploader/CarouselUrlInput"; export default function BaseSettingPage() { const { update, baseSetting } = useAppConfig(); @@ -114,6 +116,20 @@ export default function BaseSettingPage() { +
+ + + +
+
+ + + +
diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx index d4077f3..33246c8 100644 --- a/apps/web/src/components/common/uploader/AvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -12,6 +12,8 @@ export interface AvatarUploaderProps { onChange?: (value: string) => void; compressed?: boolean; style?: React.CSSProperties; // 添加style属性 + successText?: string; + showCover?: boolean; } interface UploadingFile { @@ -31,12 +33,14 @@ const AvatarUploader: React.FC = ({ className, placeholder = "点击上传", style, // 解构style属性 + successText = "上传成功", + showCover = true, }) => { const { handleFileUpload, uploadProgress } = useTusUpload(); const [file, setFile] = useState(null); const avatarRef = useRef(null); const [previewUrl, setPreviewUrl] = useState(value || ""); - + const [imageSrc, setImageSrc] = useState(value); const [compressedUrl, setCompressedUrl] = useState(value || ""); const [url, setUrl] = useState(value || ""); const [uploading, setUploading] = useState(false); @@ -44,7 +48,11 @@ const AvatarUploader: React.FC = ({ // 在组件中定义 key 状态 const [avatarKey, setAvatarKey] = useState(0); const { token } = theme.useToken(); - + useEffect(() => { + if (!previewUrl || previewUrl?.length < 1) { + setPreviewUrl(value || ""); + } + }, [value]); const handleChange = async (event: React.ChangeEvent) => { const selectedFile = event.target.files?.[0]; if (!selectedFile) return; @@ -88,12 +96,12 @@ const AvatarUploader: React.FC = ({ // 使用 resolved 的最新值调用 onChange // 强制刷新 Avatar 组件 setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载 - onChange?.(uploadedUrl); console.log(uploadedUrl); - toast.success("头像上传成功"); + onChange?.(uploadedUrl); + toast.success(successText); } catch (error) { console.error("上传错误:", error); - toast.error("头像上传失败"); + toast.error("上传失败"); setFile((prev) => ({ ...prev!, status: "error" })); } finally { setUploading(false); @@ -120,12 +128,20 @@ const AvatarUploader: React.FC = ({ accept="image/*" style={{ display: "none" }} /> - {previewUrl ? ( + {(previewUrl && showCover) ? ( { + if (value && previewUrl && imageSrc === value) { + // 当原始图片(value)加载失败时,切换到 previewUrl + setImageSrc(previewUrl); + return true; // 阻止默认的 fallback 行为,让它尝试新设置的 src + } + return false; // 如果 previewUrl 也失败了,显示默认头像 + }} className="w-full h-full object-cover" /> ) : ( diff --git a/apps/web/src/components/common/uploader/CarouselUrlInput.tsx b/apps/web/src/components/common/uploader/CarouselUrlInput.tsx new file mode 100644 index 0000000..81cc57b --- /dev/null +++ b/apps/web/src/components/common/uploader/CarouselUrlInput.tsx @@ -0,0 +1,53 @@ +import { Button, Input } from "antd"; +import { useEffect, useState } from "react"; + +export default function CarouselUrlInput( + { value, onChange } + : { + value?: string[]; + onChange?: (value: string[]) => void; + }) { + const [url, setUrl] = useState(""); + const [urls, setUrls] = useState(value || []); + const handleChange = (e) => { + if (e.target.value !== "") setUrl(e.target.value); + }; + const handleDelete = (index) => { + setUrls((prevList) => { + // 创建一个新数组并移除指定索引的元素 + const newList = [...prevList]; + newList.splice(index, 1); + return newList; + }); + }; + useEffect(() => { + if (value) { + setUrls(value) + } + }, [value]) + useEffect(() => { + onChange?.(urls); + }, [urls]); + return ( + <> +
+ + +
+
+
    + {urls.map((item, index) => ( +
  • + + {/* {item} */} + +
  • + ))} +
+
+ + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx new file mode 100755 index 0000000..bf324ea --- /dev/null +++ b/apps/web/src/components/common/uploader/MultiAvatarUploader.tsx @@ -0,0 +1,100 @@ +import React, { useEffect, useState } from "react"; +import { Upload, Progress, Button, Image, Form } from "antd"; +import { DeleteOutlined } from "@ant-design/icons"; +import AvatarUploader from "./AvatarUploader"; +import { isEqual } from "lodash"; + +interface MultiAvatarUploaderProps { + value?: string[]; + onChange?: (value: string[]) => void; + className?: string; + placeholder?: string; + style?: React.CSSProperties; +} + +export function MultiAvatarUploader({ + value, + onChange, + className, + style, + placeholder = "点击上传", +}: MultiAvatarUploaderProps) { + const [imageList, setImageList] = useState(value || []); + const [previewImage, setPreviewImage] = useState(""); + useEffect(() => { + if (!isEqual(value, imageList)) { + setImageList(value || []); + } + }, [value]); + useEffect(() => { + onChange?.(imageList); + }, [imageList]); + return ( + <> +
+ {(imageList || [])?.map((image, index) => { + return ( +
+ + setPreviewImage( + visible ? image || "" : "" + ), + }}> +
+ ); + })} +
+
+ { + console.log(value); + setImageList([...imageList, value]); + }}> +
+ + ); +} +export default MultiAvatarUploader; diff --git a/apps/web/src/components/layout/main/TopPic.tsx b/apps/web/src/components/layout/main/TopPic.tsx index 9f47513..28a88a2 100644 --- a/apps/web/src/components/layout/main/TopPic.tsx +++ b/apps/web/src/components/layout/main/TopPic.tsx @@ -1,15 +1,17 @@ +import HeroSection from "@web/src/app/HeroSection"; import usePublicImage from "@web/src/hooks/usePublicImage"; - +// export default function TopPic() { const { imageUrl } = usePublicImage("logo.png"); return (
- Banner + /> */} +
); }